2a6b3830d5
- 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
181 lines
4.3 KiB
Go
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"))
|
|
}
|