Phase 5: TUI client (Bubbletea)
- cmd/tui/main.go: entry point with --server, --token, --version flags
- cmd/tui/config.go: config from ~/.config/dumpster/config.yaml or env vars
- cmd/tui/auth.go: login flow, session cookie management
- cmd/tui/api.go: HTTP client for servers, channels, messages, voice
- cmd/tui/model.go: root bubbletea model with state management
- cmd/tui/theme.go: Gruvbox dark lipgloss styles
- cmd/tui/servers.go: server bar with [D] style initials
- cmd/tui/channels.go: channel list with #text and 🔊 voice
- cmd/tui/chat.go: IRC-style message viewport with colored usernames
- cmd/tui/input.go: message input with > _ prompt
- cmd/tui/members.go: member list with ●/◐/○ status
- cmd/tui/ws.go: WebSocket client for real-time events
- cmd/tui/voice.go: voice state tracking and mute toggle
- cmd/tui/keys.go: vim-style keybindings (j/k, g/G, Tab, /, etc)
- cmd/tui/views.go: multi-pane layout rendering
- Makefile: added build-tui and run-tui targets
- go.mod/go.sum: added charmbracelet dependencies
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func renderLayout(m model) string {
|
||||
serverBarWidth := 8
|
||||
channelWidth := 24
|
||||
memberWidth := 20
|
||||
statusHeight := 1
|
||||
|
||||
chatHeight := m.height - statusHeight - 2
|
||||
if chatHeight < 5 {
|
||||
chatHeight = 5
|
||||
}
|
||||
|
||||
chatWidth := m.width - serverBarWidth - channelWidth
|
||||
if m.showMembers {
|
||||
chatWidth -= memberWidth
|
||||
}
|
||||
if chatWidth < 20 {
|
||||
chatWidth = 20
|
||||
}
|
||||
|
||||
// ---- Server bar (left column) ----
|
||||
serverBar := ServerBar(m.servers, m.currentServer, m.hoverServer, m.height-statusHeight)
|
||||
|
||||
// ---- Channel list ----
|
||||
channelList := ChannelList(m.channels, m.currentChannel, m.hoverChannel, channelWidth-2, m.height-statusHeight)
|
||||
|
||||
// Apply focus border to channel list
|
||||
if m.focus == focusChannels {
|
||||
channelList = FocusedBorder.Render(channelList)
|
||||
}
|
||||
|
||||
// ---- Chat pane ----
|
||||
// Channel header
|
||||
ch := ChannelByFlatIndex(m.channels, m.currentChannel)
|
||||
headerText := "# general"
|
||||
if ch != nil {
|
||||
headerText = formatChannelHeader(*ch)
|
||||
}
|
||||
|
||||
header := TitleStyle.
|
||||
Width(chatWidth - 2).
|
||||
Render(headerText)
|
||||
|
||||
// Typing indicator
|
||||
typingBar := ""
|
||||
if m.typing != "" {
|
||||
typingBar = HelpStyle.Render(" " + m.typing + " is typing...")
|
||||
}
|
||||
|
||||
// Voice status
|
||||
voiceBar := ""
|
||||
if m.voice.Connected {
|
||||
voiceBar = VoiceStatusStyle.
|
||||
Width(chatWidth - 2).
|
||||
Render(m.voice.StatusText())
|
||||
}
|
||||
|
||||
// Input
|
||||
inputStyle := InputPaneStyle.Width(chatWidth - 2)
|
||||
inputView := inputStyle.Render("> " + m.input.View())
|
||||
|
||||
// Chat viewport
|
||||
chatPane := lipgloss.JoinVertical(lipgloss.Left,
|
||||
header,
|
||||
m.viewport.View(),
|
||||
)
|
||||
|
||||
if typingBar != "" {
|
||||
chatPane = lipgloss.JoinVertical(lipgloss.Left, chatPane, typingBar)
|
||||
}
|
||||
if voiceBar != "" {
|
||||
chatPane = lipgloss.JoinVertical(lipgloss.Left, chatPane, voiceBar)
|
||||
}
|
||||
|
||||
chatPane = lipgloss.JoinVertical(lipgloss.Left, chatPane, inputView)
|
||||
|
||||
// Focus border for chat
|
||||
if m.focus == focusChat || m.focus == focusInput {
|
||||
chatPane = FocusedBorder.Width(chatWidth).Render(chatPane)
|
||||
}
|
||||
|
||||
// ---- Member list ----
|
||||
var memberView string
|
||||
if m.showMembers {
|
||||
memberView = MemberList(m.user, m.members, memberWidth-2, m.height-statusHeight, true)
|
||||
if m.focus == focusMembers {
|
||||
memberView = FocusedBorder.Render(memberView)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Status bar ----
|
||||
statusParts := []string{}
|
||||
if m.err != "" {
|
||||
statusParts = append(statusParts, ErrorStyle.Render("⚠ "+m.err))
|
||||
} else if m.statusMsg != "" {
|
||||
statusParts = append(statusParts, SuccessStyle.Render(m.statusMsg))
|
||||
} else if m.typing != "" {
|
||||
statusParts = append(statusParts, HelpStyle.Render(m.typing+" is typing..."))
|
||||
}
|
||||
|
||||
// Show current user
|
||||
if m.user.Username != "" {
|
||||
right := UsernameStyle.Render(m.user.Username)
|
||||
statusParts = append(statusParts, lipgloss.PlaceHorizontal(
|
||||
m.width-lipgloss.Width(right)-2, lipgloss.Right, right))
|
||||
}
|
||||
|
||||
statusBar := lipgloss.NewStyle().
|
||||
Background(bgT).
|
||||
Foreground(fg).
|
||||
Width(m.width).
|
||||
Padding(0, 1).
|
||||
Render(strings.Join(statusParts, " "))
|
||||
|
||||
// ---- Help overlay ----
|
||||
if m.showHelp {
|
||||
return renderHelp(m)
|
||||
}
|
||||
|
||||
// ---- Compose layout ----
|
||||
var rows []string
|
||||
|
||||
// Top row: servers | channels | chat | members
|
||||
topRow := lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
serverBar,
|
||||
channelList,
|
||||
chatPane,
|
||||
)
|
||||
if m.showMembers && memberView != "" {
|
||||
topRow = lipgloss.JoinHorizontal(lipgloss.Top, topRow, memberView)
|
||||
}
|
||||
|
||||
rows = append(rows, topRow)
|
||||
rows = append(rows, statusBar)
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, rows...)
|
||||
}
|
||||
|
||||
func renderHelp(m model) string {
|
||||
helpText := []string{
|
||||
"",
|
||||
TitleStyle.Render("🗑 Dumpster Chat — Help"),
|
||||
"",
|
||||
" Navigation",
|
||||
" ──────────",
|
||||
" j/k or ↑/↓ Navigate up/down",
|
||||
" g / G Jump to top/bottom",
|
||||
" Tab Cycle focus between panes",
|
||||
" Enter Select server/channel",
|
||||
"",
|
||||
" Chat",
|
||||
" ──────────",
|
||||
" i Focus message input",
|
||||
" Enter Send message (in input)",
|
||||
" Esc Unfocus input",
|
||||
"",
|
||||
" Panels",
|
||||
" ──────────",
|
||||
" m Toggle member list / mute (in voice)",
|
||||
" ? Toggle this help",
|
||||
" q / Ctrl+C Quit",
|
||||
"",
|
||||
HelpStyle.Render(" Press ? to close"),
|
||||
}
|
||||
|
||||
return lipgloss.NewStyle().
|
||||
Background(bg).
|
||||
Foreground(fg).
|
||||
Width(m.width).
|
||||
Height(m.height).
|
||||
Padding(1, 2).
|
||||
Render(strings.Join(helpText, "\n"))
|
||||
}
|
||||
Reference in New Issue
Block a user