fix: resolve missing users in piped commands and refine TUI distribution
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
157
cmd/tui/model.go
157
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 {
|
||||
|
||||
Reference in New Issue
Block a user