diff --git a/.gitignore b/.gitignore index d37f75b..75684e7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ ts3j/ bin/ vendor/ .gemini/ +dist/ \ No newline at end of file diff --git a/cmd/tui/main.go b/cmd/tui/main.go index 78fc9af..ecfdf41 100644 --- a/cmd/tui/main.go +++ b/cmd/tui/main.go @@ -22,12 +22,21 @@ func debugLog(format string, args ...any) { } func main() { + // Define flags serverAddr := flag.String("server", "127.0.0.1:9987", "TeamSpeak 3 Server Address") nickname := flag.String("nickname", "TUI-User", "Your nickname") debug := flag.Bool("debug", true, "Enable debug logging to file (default true)") flag.Parse() - // Disable log output completely to prevent TUI corruption (stdout is reserved for UI) + // Panic Recovery + defer func() { + if r := recover(); r != nil { + fmt.Fprintf(os.Stderr, "Panic recovered: %v\n", r) + os.Exit(1) + } + }() + + // Disable log output initially log.SetOutput(io.Discard) // Enable debug file logging if requested @@ -41,6 +50,9 @@ func main() { debugLog("TUI Debug started at %s", timestamp) // Redirect standard log output to debug file initially log.SetOutput(debugFile) + } else { + fmt.Fprintf(os.Stderr, "Failed to create debug log: %v\n", err) + log.SetOutput(os.Stderr) // Fallback to stderr } } diff --git a/cmd/tui/model.go b/cmd/tui/model.go index 7ea3020..0bee33f 100644 --- a/cmd/tui/model.go +++ b/cmd/tui/model.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "regexp" "sort" "strings" "time" @@ -36,6 +37,7 @@ type ChannelNode struct { Users []UserNode Expanded bool Selected bool + Depth int } // UserNode represents a user in a channel @@ -362,41 +364,70 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { type logMsg string func (m *Model) updateChannelList(channels []*ts3client.Channel) { - // Sort channels by ID for stable ordering - sortedChannels := make([]*ts3client.Channel, len(channels)) - copy(sortedChannels, channels) - sort.Slice(sortedChannels, func(i, j int) bool { - return sortedChannels[i].ID < sortedChannels[j].ID - }) - - m.channels = make([]ChannelNode, 0, len(sortedChannels)) - for _, ch := range sortedChannels { - node := ChannelNode{ - ID: ch.ID, - Name: ch.Name, - Users: []UserNode{}, - Expanded: true, + // Build adjacency map: ParentID -> PreviousID(Order) -> Channel + levelMap := make(map[uint64]map[uint64]*ts3client.Channel) + for _, ch := range channels { + if levelMap[ch.ParentID] == nil { + levelMap[ch.ParentID] = make(map[uint64]*ts3client.Channel) } - - // Get users in this channel - for _, cl := range m.client.GetClients() { - if cl.ChannelID == ch.ID { - node.Users = append(node.Users, UserNode{ - ID: cl.ID, - Nickname: cl.Nickname, - IsMe: cl.ID == m.selfID, - Talking: m.talkingClients[cl.ID], - }) - } - } - - // Sort users by ID for stable ordering - sort.Slice(node.Users, func(i, j int) bool { - return node.Users[i].ID < node.Users[j].ID - }) - - m.channels = append(m.channels, node) + levelMap[ch.ParentID][ch.Order] = ch } + + var sortedNodes []ChannelNode + + // Recursive function to flatten the tree in order + var visit func(parentID uint64, depth int) + visit = func(parentID uint64, depth int) { + prevID := uint64(0) + for { + ch, ok := levelMap[parentID][prevID] + if !ok { + break // End of list for this parent + } + + // Create node + node := ChannelNode{ + ID: ch.ID, + Name: ch.Name, + Users: []UserNode{}, + Expanded: true, + Depth: depth, + } + + // Get users in this channel + for _, cl := range m.client.GetClients() { + if cl.ChannelID == ch.ID { + node.Users = append(node.Users, UserNode{ + ID: cl.ID, + Nickname: cl.Nickname, + IsMe: cl.ID == m.selfID, + Talking: m.talkingClients[cl.ID], + }) + } + } + + // Sort users by ID for stable ordering + sort.Slice(node.Users, func(i, j int) bool { + return node.Users[i].ID < node.Users[j].ID + }) + + sortedNodes = append(sortedNodes, node) + + // Recursively visit children + visit(ch.ID, depth+1) + + // Move to next sibling + prevID = ch.ID + } + } + + // Start from root (ParentID = 0) + visit(0, 0) + + // In case there are orphaned channels (shouldn't happen on standard server), standard logic ignores them. + // We'll stick to valid tree. + + m.channels = sortedNodes } func (m *Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { @@ -772,6 +803,9 @@ func (m *Model) renderStatusBar() string { return headerStyle.Render(status) } +// Regex for TeamSpeak spacers: [spacer0], [cspacer], [*spacer], etc. +var spacerRegex = regexp.MustCompile(`^\[([*cZr]?spacer[\w\d]*)\](.*)`) + func (m *Model) renderChannels() string { if len(m.channels) == 0 { return "No channels..." @@ -782,9 +816,11 @@ func (m *Model) renderChannels() string { lines = append(lines, "") for i, ch := range m.channels { - prefix := " " + // Indentation based on depth + indent := strings.Repeat(" ", ch.Depth) + prefix := indent + " " if i == m.selectedIdx { - prefix = "► " + prefix = indent + "► " } style := lipgloss.NewStyle() @@ -792,7 +828,56 @@ func (m *Model) renderChannels() string { style = style.Bold(true).Foreground(lipgloss.Color("212")) } - lines = append(lines, style.Render(prefix+ch.Name)) + // Check for spacer + displayName := ch.Name + matches := spacerRegex.FindStringSubmatch(ch.Name) + if len(matches) > 0 { + tag := matches[1] // e.g. "spacer0", "cspacer", "*spacer" + content := matches[2] // e.g. "---", "Section" + + // Calculate effective width: width/4 - 6 (border+padding) + width := (m.width / 4) - 6 + if width < 0 { + width = 0 + } + + if strings.HasPrefix(tag, "*") { + // Repeat content + if len(content) > 0 { + count := width / len(content) + if count > 0 { + displayName = strings.Repeat(content, count+1)[:width] + } + } else { + displayName = strings.Repeat("-", width) + } + } else if strings.HasPrefix(tag, "c") { + // Center content + if len(content) > len(displayName) { // If parsed content is safer? + // No, just align content + gap := (width - len(content)) / 2 + if gap > 0 { + displayName = strings.Repeat(" ", gap) + content + } else { + displayName = content + } + } else { + // Fallback if measurement is tricky, just show content + // Actually, let's try to center strictly + gap := (width - len(content)) / 2 + if gap > 0 { + displayName = strings.Repeat(" ", gap) + content + } else { + displayName = content + } + } + } else { + // Standard spacer (left align) + displayName = content + } + } + + lines = append(lines, style.Render(prefix+displayName)) // Show users in channel for _, user := range ch.Users { diff --git a/headers.ps1 b/headers.ps1 new file mode 100644 index 0000000..e3a9d2e --- /dev/null +++ b/headers.ps1 @@ -0,0 +1,3 @@ +$env:PATH = "D:\esto_al_path\msys64\mingw64\bin;$env:PATH" +$env:PKG_CONFIG_PATH = "D:\esto_al_path\msys64\mingw64\lib\pkgconfig" + diff --git a/internal/client/commands.go b/internal/client/commands.go index 1051fab..9104d55 100644 --- a/internal/client/commands.go +++ b/internal/client/commands.go @@ -251,6 +251,9 @@ func (c *Client) processCommand(data []byte, pkt *protocol.Packet) error { // Parse Commands (possibly multiple piped items) commands := protocol.ParseCommands([]byte(cmdStr)) + // State for piped commands (TS3 optimization omits repeated keys like ctid) + var lastCtid uint64 + for _, command := range commands { cmd := command.Name args := command.Params @@ -371,8 +374,9 @@ func (c *Client) processCommand(data []byte, pkt *protocol.Packet) error { clientID = uint16(id) } if ctid, ok := args["ctid"]; ok { - fmt.Sscanf(ctid, "%d", &channelID) + fmt.Sscanf(ctid, "%d", &lastCtid) } + channelID = lastCtid log.Printf("Client entered: %s (ID=%d)", nick, clientID) c.emitEvent("client_enter", map[string]any{ "clientID": clientID, diff --git a/tui.ps1 b/tui.ps1 index f645984..167a55a 100644 --- a/tui.ps1 +++ b/tui.ps1 @@ -9,4 +9,5 @@ $env:PKG_CONFIG_PATH = "D:\esto_al_path\msys64\mingw64\lib\pkgconfig" $env:XAI_API_KEY = "xai-TyecBoTLlFNL0Qxwnb0eRainG8hKTpJGtnCziMhm1tTyB1FrLpZm0gHNYA9qqqX21JsXStN1f9DseLdJ" # go run ./cmd/voicebot --server localhost:9987 --nickname Adam --voice Rex --greeting " " --room "test" -go run ./cmd/tui --server ts.vlazaro.es:9987 --nickname Adam \ No newline at end of file +# go run ./cmd/tui --server ts.vlazaro.es:9987 --nickname Adam +go build -o tui_debug.exe ./cmd/tui \ No newline at end of file