Files
dumpsterChat/cmd/tui/chat.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

117 lines
2.6 KiB
Go

package main
import (
"fmt"
"strings"
"time"
"github.com/charmbracelet/lipgloss"
)
// RenderMessages renders the chat message list for the viewport.
func RenderMessages(messages []Message, width int, currentUser string) string {
if len(messages) == 0 {
return HelpStyle.Render(" No messages yet. Press 'i' to type a message.")
}
var lines []string
for _, msg := range messages {
ts := formatTimestamp(msg.CreatedAt)
// Determine display name
displayName := msg.AuthorName
if msg.DisplayName != nil && *msg.DisplayName != "" {
displayName = *msg.DisplayName
}
// Color the username
userColor := UsernameColor(msg.AuthorName)
styledUser := UsernameStyle.Foreground(userColor).Render(displayName)
// Content
content := msg.Content
// Show edited tag
editedTag := ""
if msg.EditedAt != nil {
editedTag = EditedTag.Render(" (edited)")
}
// IRC-style: [HH:MM] <user> message
tsRendered := TimestampStyle.Render("[" + ts + "]")
prefix := fmt.Sprintf("%s <%s> ", tsRendered, styledUser)
// Word wrap the content
contentWidth := width - lipgloss.Width(prefix) - 2
if contentWidth < 10 {
contentWidth = 40
}
wrappedContent := wordWrap(content, contentWidth)
// First line has the prefix, continuation lines are indented
contentLines := strings.Split(wrappedContent, "\n")
for i, cl := range contentLines {
if i == 0 {
lines = append(lines, prefix+cl+editedTag)
} else {
indent := strings.Repeat(" ", lipgloss.Width(prefix))
lines = append(lines, indent+cl)
}
}
}
return strings.Join(lines, "\n")
}
// formatTimestamp parses the created_at string and returns HH:MM.
func formatTimestamp(ts string) string {
// Try various formats the server might return
formats := []string{
time.RFC3339,
"2006-01-02T15:04:05.999999Z",
"2006-01-02T15:04:05.999999-07:00",
"2006-01-02 15:04:05",
"2006-01-02T15:04:05Z",
}
for _, f := range formats {
if t, err := time.Parse(f, ts); err == nil {
return t.Format("15:04")
}
}
// Fallback: try to extract HH:MM from the string
if len(ts) >= 16 {
return ts[11:16]
}
return "??:??"
}
// wordWrap wraps text to the given width.
func wordWrap(text string, width int) string {
if width <= 0 {
return text
}
words := strings.Fields(text)
if len(words) == 0 {
return ""
}
var lines []string
currentLine := words[0]
for _, word := range words[1:] {
if len(currentLine)+1+len(word) > width {
lines = append(lines, currentLine)
currentLine = word
} else {
currentLine += " " + word
}
}
lines = append(lines, currentLine)
return strings.Join(lines, "\n")
}