fix: resolve missing users in piped commands and refine TUI distribution
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ ts3j/
|
|||||||
bin/
|
bin/
|
||||||
vendor/
|
vendor/
|
||||||
.gemini/
|
.gemini/
|
||||||
|
dist/
|
||||||
@@ -22,12 +22,21 @@ func debugLog(format string, args ...any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// Define flags
|
||||||
serverAddr := flag.String("server", "127.0.0.1:9987", "TeamSpeak 3 Server Address")
|
serverAddr := flag.String("server", "127.0.0.1:9987", "TeamSpeak 3 Server Address")
|
||||||
nickname := flag.String("nickname", "TUI-User", "Your nickname")
|
nickname := flag.String("nickname", "TUI-User", "Your nickname")
|
||||||
debug := flag.Bool("debug", true, "Enable debug logging to file (default true)")
|
debug := flag.Bool("debug", true, "Enable debug logging to file (default true)")
|
||||||
flag.Parse()
|
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)
|
log.SetOutput(io.Discard)
|
||||||
|
|
||||||
// Enable debug file logging if requested
|
// Enable debug file logging if requested
|
||||||
@@ -41,6 +50,9 @@ func main() {
|
|||||||
debugLog("TUI Debug started at %s", timestamp)
|
debugLog("TUI Debug started at %s", timestamp)
|
||||||
// Redirect standard log output to debug file initially
|
// Redirect standard log output to debug file initially
|
||||||
log.SetOutput(debugFile)
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -36,6 +37,7 @@ type ChannelNode struct {
|
|||||||
Users []UserNode
|
Users []UserNode
|
||||||
Expanded bool
|
Expanded bool
|
||||||
Selected bool
|
Selected bool
|
||||||
|
Depth int
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserNode represents a user in a channel
|
// 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
|
type logMsg string
|
||||||
|
|
||||||
func (m *Model) updateChannelList(channels []*ts3client.Channel) {
|
func (m *Model) updateChannelList(channels []*ts3client.Channel) {
|
||||||
// Sort channels by ID for stable ordering
|
// Build adjacency map: ParentID -> PreviousID(Order) -> Channel
|
||||||
sortedChannels := make([]*ts3client.Channel, len(channels))
|
levelMap := make(map[uint64]map[uint64]*ts3client.Channel)
|
||||||
copy(sortedChannels, channels)
|
for _, ch := range channels {
|
||||||
sort.Slice(sortedChannels, func(i, j int) bool {
|
if levelMap[ch.ParentID] == nil {
|
||||||
return sortedChannels[i].ID < sortedChannels[j].ID
|
levelMap[ch.ParentID] = make(map[uint64]*ts3client.Channel)
|
||||||
})
|
|
||||||
|
|
||||||
m.channels = make([]ChannelNode, 0, len(sortedChannels))
|
|
||||||
for _, ch := range sortedChannels {
|
|
||||||
node := ChannelNode{
|
|
||||||
ID: ch.ID,
|
|
||||||
Name: ch.Name,
|
|
||||||
Users: []UserNode{},
|
|
||||||
Expanded: true,
|
|
||||||
}
|
}
|
||||||
|
levelMap[ch.ParentID][ch.Order] = ch
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func (m *Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
@@ -772,6 +803,9 @@ func (m *Model) renderStatusBar() string {
|
|||||||
return headerStyle.Render(status)
|
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 {
|
func (m *Model) renderChannels() string {
|
||||||
if len(m.channels) == 0 {
|
if len(m.channels) == 0 {
|
||||||
return "No channels..."
|
return "No channels..."
|
||||||
@@ -782,9 +816,11 @@ func (m *Model) renderChannels() string {
|
|||||||
lines = append(lines, "")
|
lines = append(lines, "")
|
||||||
|
|
||||||
for i, ch := range m.channels {
|
for i, ch := range m.channels {
|
||||||
prefix := " "
|
// Indentation based on depth
|
||||||
|
indent := strings.Repeat(" ", ch.Depth)
|
||||||
|
prefix := indent + " "
|
||||||
if i == m.selectedIdx {
|
if i == m.selectedIdx {
|
||||||
prefix = "► "
|
prefix = indent + "► "
|
||||||
}
|
}
|
||||||
|
|
||||||
style := lipgloss.NewStyle()
|
style := lipgloss.NewStyle()
|
||||||
@@ -792,7 +828,56 @@ func (m *Model) renderChannels() string {
|
|||||||
style = style.Bold(true).Foreground(lipgloss.Color("212"))
|
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
|
// Show users in channel
|
||||||
for _, user := range ch.Users {
|
for _, user := range ch.Users {
|
||||||
|
|||||||
3
headers.ps1
Normal file
3
headers.ps1
Normal 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"
|
||||||
|
|
||||||
@@ -251,6 +251,9 @@ func (c *Client) processCommand(data []byte, pkt *protocol.Packet) error {
|
|||||||
// Parse Commands (possibly multiple piped items)
|
// Parse Commands (possibly multiple piped items)
|
||||||
commands := protocol.ParseCommands([]byte(cmdStr))
|
commands := protocol.ParseCommands([]byte(cmdStr))
|
||||||
|
|
||||||
|
// State for piped commands (TS3 optimization omits repeated keys like ctid)
|
||||||
|
var lastCtid uint64
|
||||||
|
|
||||||
for _, command := range commands {
|
for _, command := range commands {
|
||||||
cmd := command.Name
|
cmd := command.Name
|
||||||
args := command.Params
|
args := command.Params
|
||||||
@@ -371,8 +374,9 @@ func (c *Client) processCommand(data []byte, pkt *protocol.Packet) error {
|
|||||||
clientID = uint16(id)
|
clientID = uint16(id)
|
||||||
}
|
}
|
||||||
if ctid, ok := args["ctid"]; ok {
|
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)
|
log.Printf("Client entered: %s (ID=%d)", nick, clientID)
|
||||||
c.emitEvent("client_enter", map[string]any{
|
c.emitEvent("client_enter", map[string]any{
|
||||||
"clientID": clientID,
|
"clientID": clientID,
|
||||||
|
|||||||
3
tui.ps1
3
tui.ps1
@@ -9,4 +9,5 @@ $env:PKG_CONFIG_PATH = "D:\esto_al_path\msys64\mingw64\lib\pkgconfig"
|
|||||||
$env:XAI_API_KEY = "xai-TyecBoTLlFNL0Qxwnb0eRainG8hKTpJGtnCziMhm1tTyB1FrLpZm0gHNYA9qqqX21JsXStN1f9DseLdJ"
|
$env:XAI_API_KEY = "xai-TyecBoTLlFNL0Qxwnb0eRainG8hKTpJGtnCziMhm1tTyB1FrLpZm0gHNYA9qqqX21JsXStN1f9DseLdJ"
|
||||||
# go run ./cmd/voicebot --server localhost:9987 --nickname Adam --voice Rex --greeting " " --room "test"
|
# 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
|
||||||
Reference in New Issue
Block a user