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,150 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ChannelList renders the channel sidebar for a server.
|
||||
func ChannelList(channels []Channel, activeIdx, hoverIdx, width, height int) string {
|
||||
if len(channels) == 0 {
|
||||
return ChannelListStyle.
|
||||
Width(width).
|
||||
Height(height).
|
||||
Render(HelpStyle.Render(" No channels"))
|
||||
}
|
||||
|
||||
// Group channels by category
|
||||
type catGroup struct {
|
||||
name string
|
||||
channels []Channel
|
||||
}
|
||||
catOrder := make([]string, 0)
|
||||
catMap := make(map[string]*catGroup)
|
||||
|
||||
for _, ch := range channels {
|
||||
cat := ch.Category
|
||||
if cat == "" {
|
||||
cat = "general"
|
||||
}
|
||||
if _, ok := catMap[cat]; !ok {
|
||||
catMap[cat] = &catGroup{name: cat}
|
||||
catOrder = append(catOrder, cat)
|
||||
}
|
||||
catMap[cat].channels = append(catMap[cat].channels, ch)
|
||||
}
|
||||
|
||||
// Stable sort categories
|
||||
sort.Strings(catOrder)
|
||||
|
||||
// Build flat list for index mapping
|
||||
var lines []string
|
||||
flatIdx := 0
|
||||
|
||||
for _, catName := range catOrder {
|
||||
group := catMap[catName]
|
||||
lines = append(lines, CategoryStyle.Render(strings.ToUpper(group.name)))
|
||||
|
||||
for _, ch := range group.channels {
|
||||
prefix := " "
|
||||
if ch.Type == "voice" {
|
||||
prefix = VoiceChannelPrefix.Render("🔊 ")
|
||||
} else {
|
||||
prefix = TextChannelPrefix.Render("# ")
|
||||
}
|
||||
|
||||
style := ChannelInactive
|
||||
if flatIdx == activeIdx {
|
||||
style = ChannelActive
|
||||
} else if flatIdx == hoverIdx {
|
||||
style = ChannelHover
|
||||
}
|
||||
|
||||
label := ch.Name
|
||||
if len(label) > width-6 {
|
||||
label = label[:width-9] + "..."
|
||||
}
|
||||
lines = append(lines, style.Render(prefix+label))
|
||||
flatIdx++
|
||||
}
|
||||
}
|
||||
|
||||
return ChannelListStyle.
|
||||
Width(width).
|
||||
Height(height).
|
||||
Render(strings.Join(lines, "\n"))
|
||||
}
|
||||
|
||||
// ChannelFlatIndex converts a (category, channel index in category) to flat index.
|
||||
func ChannelFlatIndex(channels []Channel, targetID string) int {
|
||||
catOrder := getCatOrder(channels)
|
||||
flatIdx := 0
|
||||
for _, catName := range catOrder {
|
||||
for _, ch := range groupByCategory(channels)[catName] {
|
||||
if ch.ID == targetID {
|
||||
return flatIdx
|
||||
}
|
||||
flatIdx++
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ChannelByFlatIndex returns the channel at a flat index in the grouped view.
|
||||
func ChannelByFlatIndex(channels []Channel, flatIdx int) *Channel {
|
||||
catOrder := getCatOrder(channels)
|
||||
idx := 0
|
||||
for _, catName := range catOrder {
|
||||
for _, ch := range groupByCategory(channels)[catName] {
|
||||
if idx == flatIdx {
|
||||
return &ch
|
||||
}
|
||||
idx++
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func groupByCategory(channels []Channel) map[string][]Channel {
|
||||
m := make(map[string][]Channel)
|
||||
for _, ch := range channels {
|
||||
cat := ch.Category
|
||||
if cat == "" {
|
||||
cat = "general"
|
||||
}
|
||||
m[cat] = append(m[cat], ch)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func getCatOrder(channels []Channel) []string {
|
||||
seen := make(map[string]bool)
|
||||
var order []string
|
||||
for _, ch := range channels {
|
||||
cat := ch.Category
|
||||
if cat == "" {
|
||||
cat = "general"
|
||||
}
|
||||
if !seen[cat] {
|
||||
seen[cat] = true
|
||||
order = append(order, cat)
|
||||
}
|
||||
}
|
||||
sort.Strings(order)
|
||||
return order
|
||||
}
|
||||
|
||||
// ChannelCount returns total number of channels (for navigation limits).
|
||||
func ChannelCount(channels []Channel) int {
|
||||
return len(channels)
|
||||
}
|
||||
|
||||
// formatChannelHeader renders the channel header for the chat pane.
|
||||
func formatChannelHeader(ch Channel) string {
|
||||
icon := "#"
|
||||
if ch.Type == "voice" {
|
||||
icon = "🔊"
|
||||
}
|
||||
return fmt.Sprintf("%s %s", icon, ch.Name)
|
||||
}
|
||||
Reference in New Issue
Block a user