Files
dumpsterChat/cmd/tui/views.go
T
hobokenchicken 2a6b3830d5 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
2026-06-28 17:46:19 -04:00

181 lines
4.3 KiB
Go

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"))
}