fix: resolve missing users in piped commands and refine TUI distribution

This commit is contained in:
Jose Luis Montañes Ojados
2026-01-16 23:54:36 +01:00
parent 4f26d9b430
commit 78e7988db1
6 changed files with 145 additions and 39 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ ts3j/
bin/
vendor/
.gemini/
dist/

View File

@@ -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
}
}

View File

@@ -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 {

3
headers.ps1 Normal file
View File

@@ -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"

View File

@@ -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,

View File

@@ -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
# go run ./cmd/tui --server ts.vlazaro.es:9987 --nickname Adam
go build -o tui_debug.exe ./cmd/tui