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
117 lines
2.6 KiB
Go
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")
|
|
}
|