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
151 lines
3.2 KiB
Go
151 lines
3.2 KiB
Go
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)
|
|
}
|