feat: TeamSpeak TUI client with Bubble Tea
- Created TUI client in cmd/tui - Implemented channel navigation and user display - Fixed serverInfo initialization in ts3client - Added real-time log panel in TUI - Ordered channels by ID for stability
This commit is contained in:
53
cmd/tui/main.go
Normal file
53
cmd/tui/main.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
// debugLog writes to a debug file
|
||||||
|
var debugFile *os.File
|
||||||
|
|
||||||
|
func debugLog(format string, args ...any) {
|
||||||
|
if debugFile != nil {
|
||||||
|
fmt.Fprintf(debugFile, format+"\n", args...)
|
||||||
|
debugFile.Sync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
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", false, "Enable debug logging to tui-debug.log")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// Disable log output completely to prevent TUI corruption
|
||||||
|
log.SetOutput(io.Discard)
|
||||||
|
|
||||||
|
// Enable debug file logging if requested
|
||||||
|
if *debug {
|
||||||
|
var err error
|
||||||
|
debugFile, err = os.Create("tui-debug.log")
|
||||||
|
if err == nil {
|
||||||
|
defer debugFile.Close()
|
||||||
|
debugLog("TUI Debug started")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the TUI model
|
||||||
|
m := NewModel(*serverAddr, *nickname)
|
||||||
|
|
||||||
|
// Create Bubble Tea program
|
||||||
|
p := tea.NewProgram(m, tea.WithAltScreen())
|
||||||
|
|
||||||
|
// Run
|
||||||
|
if _, err := p.Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
476
cmd/tui/model.go
Normal file
476
cmd/tui/model.go
Normal file
@@ -0,0 +1,476 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go-ts/pkg/ts3client"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Focus indicates which panel has focus
|
||||||
|
type Focus int
|
||||||
|
|
||||||
|
const (
|
||||||
|
FocusChannels Focus = iota
|
||||||
|
FocusChat
|
||||||
|
FocusInput
|
||||||
|
)
|
||||||
|
|
||||||
|
// ChatMessage represents a message in the chat
|
||||||
|
type ChatMessage struct {
|
||||||
|
Time time.Time
|
||||||
|
Sender string
|
||||||
|
Content string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChannelNode represents a channel in the tree
|
||||||
|
type ChannelNode struct {
|
||||||
|
ID uint64
|
||||||
|
Name string
|
||||||
|
Users []UserNode
|
||||||
|
Expanded bool
|
||||||
|
Selected bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserNode represents a user in a channel
|
||||||
|
type UserNode struct {
|
||||||
|
ID uint16
|
||||||
|
Nickname string
|
||||||
|
Talking bool
|
||||||
|
Muted bool
|
||||||
|
IsMe bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Model is the Bubble Tea model for our TUI
|
||||||
|
type Model struct {
|
||||||
|
// Connection
|
||||||
|
serverAddr string
|
||||||
|
nickname string
|
||||||
|
client *ts3client.Client
|
||||||
|
connected bool
|
||||||
|
connecting bool
|
||||||
|
serverName string
|
||||||
|
selfID uint16
|
||||||
|
ping int
|
||||||
|
|
||||||
|
// UI State
|
||||||
|
focus Focus
|
||||||
|
width, height int
|
||||||
|
channels []ChannelNode
|
||||||
|
selectedIdx int
|
||||||
|
chatMessages []ChatMessage
|
||||||
|
logMessages []string // Debug logs shown in chat panel
|
||||||
|
inputText string
|
||||||
|
inputActive bool
|
||||||
|
|
||||||
|
// Error handling
|
||||||
|
lastError string
|
||||||
|
}
|
||||||
|
|
||||||
|
// addLog adds a message to the log panel
|
||||||
|
func (m *Model) addLog(format string, args ...any) {
|
||||||
|
msg := fmt.Sprintf(format, args...)
|
||||||
|
m.logMessages = append(m.logMessages, msg)
|
||||||
|
// Keep last 50 messages
|
||||||
|
if len(m.logMessages) > 50 {
|
||||||
|
m.logMessages = m.logMessages[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewModel creates a new TUI model
|
||||||
|
func NewModel(serverAddr, nickname string) *Model {
|
||||||
|
return &Model{
|
||||||
|
serverAddr: serverAddr,
|
||||||
|
nickname: nickname,
|
||||||
|
focus: FocusChannels,
|
||||||
|
channels: []ChannelNode{},
|
||||||
|
chatMessages: []ChatMessage{},
|
||||||
|
logMessages: []string{"Starting..."},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init is called when the program starts
|
||||||
|
func (m *Model) Init() tea.Cmd {
|
||||||
|
return tea.Batch(
|
||||||
|
m.connectToServer(),
|
||||||
|
tea.SetWindowTitle("TeamSpeak TUI"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TS3Event wraps events from the TS3 client
|
||||||
|
type TS3Event struct {
|
||||||
|
Type string
|
||||||
|
Data map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
// connectToServer initiates connection to TeamSpeak
|
||||||
|
func (m *Model) connectToServer() tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
m.connecting = true
|
||||||
|
|
||||||
|
client := ts3client.New(m.serverAddr, ts3client.Config{
|
||||||
|
Nickname: m.nickname,
|
||||||
|
})
|
||||||
|
|
||||||
|
return ts3ClientMsg{client: client}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ts3ClientMsg struct {
|
||||||
|
client *ts3client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type connectedMsg struct {
|
||||||
|
clientID uint16
|
||||||
|
serverName string
|
||||||
|
}
|
||||||
|
|
||||||
|
type channelListMsg struct {
|
||||||
|
channels []*ts3client.Channel
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientEnterMsg struct {
|
||||||
|
clientID uint16
|
||||||
|
nickname string
|
||||||
|
channelID uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientLeftMsg struct {
|
||||||
|
clientID uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
type chatMsg struct {
|
||||||
|
senderID uint16
|
||||||
|
senderName string
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
type errorMsg struct {
|
||||||
|
err string
|
||||||
|
}
|
||||||
|
|
||||||
|
type tickMsg time.Time
|
||||||
|
|
||||||
|
// Update handles messages and user input
|
||||||
|
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
m.width = msg.Width
|
||||||
|
m.height = msg.Height
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case tea.KeyMsg:
|
||||||
|
return m.handleKeyPress(msg)
|
||||||
|
|
||||||
|
case ts3ClientMsg:
|
||||||
|
m.client = msg.client
|
||||||
|
m.connecting = true
|
||||||
|
|
||||||
|
// Connect asynchronously
|
||||||
|
m.client.ConnectAsync()
|
||||||
|
|
||||||
|
// Set up a ticker to poll for state changes
|
||||||
|
return m, tea.Tick(500*time.Millisecond, func(t time.Time) tea.Msg {
|
||||||
|
return tickMsg(t)
|
||||||
|
})
|
||||||
|
|
||||||
|
case tickMsg:
|
||||||
|
// Poll client state
|
||||||
|
if m.client != nil {
|
||||||
|
// Check if connected
|
||||||
|
if !m.connected {
|
||||||
|
info := m.client.GetSelfInfo()
|
||||||
|
serverInfo := m.client.GetServerInfo()
|
||||||
|
m.addLog("Tick: selfInfo=%v, serverInfo=%v", info != nil, serverInfo != nil)
|
||||||
|
if info != nil && serverInfo != nil {
|
||||||
|
m.connected = true
|
||||||
|
m.selfID = info.ClientID
|
||||||
|
m.serverName = serverInfo.Name
|
||||||
|
m.addLog("Connected! ClientID=%d, Server=%s", m.selfID, m.serverName)
|
||||||
|
|
||||||
|
// Get channels
|
||||||
|
channels := m.client.GetChannels()
|
||||||
|
m.addLog("Got %d channels", len(channels))
|
||||||
|
m.updateChannelList(channels)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Update channel list periodically
|
||||||
|
channels := m.client.GetChannels()
|
||||||
|
if len(channels) != len(m.channels) {
|
||||||
|
m.addLog("Channel update: got %d channels (had %d)", len(channels), len(m.channels))
|
||||||
|
}
|
||||||
|
m.updateChannelList(channels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue ticking
|
||||||
|
return m, tea.Tick(500*time.Millisecond, func(t time.Time) tea.Msg {
|
||||||
|
return tickMsg(t)
|
||||||
|
})
|
||||||
|
|
||||||
|
case connectedMsg:
|
||||||
|
m.connected = true
|
||||||
|
m.connecting = false
|
||||||
|
m.selfID = msg.clientID
|
||||||
|
m.serverName = msg.serverName
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case channelListMsg:
|
||||||
|
m.updateChannelList(msg.channels)
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case errorMsg:
|
||||||
|
m.lastError = msg.err
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.channels = append(m.channels, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
|
// Global keys
|
||||||
|
switch msg.String() {
|
||||||
|
case "ctrl+c", "q":
|
||||||
|
if m.client != nil {
|
||||||
|
m.client.Disconnect()
|
||||||
|
}
|
||||||
|
return m, tea.Quit
|
||||||
|
|
||||||
|
case "tab":
|
||||||
|
// Cycle focus
|
||||||
|
m.focus = (m.focus + 1) % 3
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case "m":
|
||||||
|
// Toggle mute (would need to implement)
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus-specific keys
|
||||||
|
switch m.focus {
|
||||||
|
case FocusChannels:
|
||||||
|
return m.handleChannelKeys(msg)
|
||||||
|
case FocusInput:
|
||||||
|
return m.handleInputKeys(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) handleChannelKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg.String() {
|
||||||
|
case "up", "k":
|
||||||
|
if m.selectedIdx > 0 {
|
||||||
|
m.selectedIdx--
|
||||||
|
}
|
||||||
|
case "down", "j":
|
||||||
|
if m.selectedIdx < len(m.channels)-1 {
|
||||||
|
m.selectedIdx++
|
||||||
|
}
|
||||||
|
case "enter":
|
||||||
|
// Join selected channel
|
||||||
|
if m.selectedIdx < len(m.channels) && m.client != nil {
|
||||||
|
ch := m.channels[m.selectedIdx]
|
||||||
|
m.client.JoinChannel(ch.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) handleInputKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg.String() {
|
||||||
|
case "enter":
|
||||||
|
if m.inputText != "" && m.client != nil {
|
||||||
|
m.client.SendChannelMessage(m.inputText)
|
||||||
|
m.inputText = ""
|
||||||
|
}
|
||||||
|
case "esc":
|
||||||
|
m.focus = FocusChannels
|
||||||
|
m.inputText = ""
|
||||||
|
case "backspace":
|
||||||
|
if len(m.inputText) > 0 {
|
||||||
|
m.inputText = m.inputText[:len(m.inputText)-1]
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Add character to input
|
||||||
|
if len(msg.String()) == 1 {
|
||||||
|
m.inputText += msg.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// View renders the UI
|
||||||
|
func (m *Model) View() string {
|
||||||
|
if m.width == 0 {
|
||||||
|
return "Loading..."
|
||||||
|
}
|
||||||
|
|
||||||
|
// Styles
|
||||||
|
headerStyle := lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(lipgloss.Color("229")).
|
||||||
|
Background(lipgloss.Color("57")).
|
||||||
|
Padding(0, 1).
|
||||||
|
Width(m.width)
|
||||||
|
|
||||||
|
channelPanelStyle := lipgloss.NewStyle().
|
||||||
|
Border(lipgloss.RoundedBorder()).
|
||||||
|
BorderForeground(lipgloss.Color("63")).
|
||||||
|
Padding(0, 1).
|
||||||
|
Width(m.width/3 - 2).
|
||||||
|
Height(m.height - 6)
|
||||||
|
|
||||||
|
chatPanelStyle := lipgloss.NewStyle().
|
||||||
|
Border(lipgloss.RoundedBorder()).
|
||||||
|
BorderForeground(lipgloss.Color("63")).
|
||||||
|
Padding(0, 1).
|
||||||
|
Width(m.width*2/3 - 2).
|
||||||
|
Height(m.height - 6)
|
||||||
|
|
||||||
|
inputStyle := lipgloss.NewStyle().
|
||||||
|
Border(lipgloss.NormalBorder()).
|
||||||
|
BorderForeground(lipgloss.Color("63")).
|
||||||
|
Padding(0, 1).
|
||||||
|
Width(m.width - 4)
|
||||||
|
|
||||||
|
// Header
|
||||||
|
status := "Connecting..."
|
||||||
|
if m.connected {
|
||||||
|
status = fmt.Sprintf("Server: %s │ You: %s (ID: %d)", m.serverName, m.nickname, m.selfID)
|
||||||
|
}
|
||||||
|
header := headerStyle.Render(status)
|
||||||
|
|
||||||
|
// Channel panel
|
||||||
|
channelContent := m.renderChannels()
|
||||||
|
if m.focus == FocusChannels {
|
||||||
|
channelPanelStyle = channelPanelStyle.BorderForeground(lipgloss.Color("212"))
|
||||||
|
}
|
||||||
|
channelPanel := channelPanelStyle.Render(channelContent)
|
||||||
|
|
||||||
|
// Chat panel
|
||||||
|
chatContent := m.renderChat()
|
||||||
|
if m.focus == FocusChat {
|
||||||
|
chatPanelStyle = chatPanelStyle.BorderForeground(lipgloss.Color("212"))
|
||||||
|
}
|
||||||
|
chatPanel := chatPanelStyle.Render(chatContent)
|
||||||
|
|
||||||
|
// Input
|
||||||
|
inputContent := "> " + m.inputText
|
||||||
|
if m.focus == FocusInput {
|
||||||
|
inputStyle = inputStyle.BorderForeground(lipgloss.Color("212"))
|
||||||
|
inputContent += "█"
|
||||||
|
}
|
||||||
|
input := inputStyle.Render(inputContent)
|
||||||
|
|
||||||
|
// Footer help
|
||||||
|
help := lipgloss.NewStyle().Faint(true).Render("↑↓ navigate │ Enter join │ Tab switch │ q quit")
|
||||||
|
|
||||||
|
// Combine panels
|
||||||
|
panels := lipgloss.JoinHorizontal(lipgloss.Top, channelPanel, chatPanel)
|
||||||
|
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Left,
|
||||||
|
header,
|
||||||
|
panels,
|
||||||
|
input,
|
||||||
|
help,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) renderChannels() string {
|
||||||
|
if len(m.channels) == 0 {
|
||||||
|
return "No channels..."
|
||||||
|
}
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
lines = append(lines, lipgloss.NewStyle().Bold(true).Render("CHANNELS"))
|
||||||
|
lines = append(lines, "")
|
||||||
|
|
||||||
|
for i, ch := range m.channels {
|
||||||
|
prefix := " "
|
||||||
|
if i == m.selectedIdx {
|
||||||
|
prefix = "► "
|
||||||
|
}
|
||||||
|
|
||||||
|
style := lipgloss.NewStyle()
|
||||||
|
if i == m.selectedIdx {
|
||||||
|
style = style.Bold(true).Foreground(lipgloss.Color("212"))
|
||||||
|
}
|
||||||
|
|
||||||
|
lines = append(lines, style.Render(prefix+ch.Name))
|
||||||
|
|
||||||
|
// Show users in channel
|
||||||
|
for _, user := range ch.Users {
|
||||||
|
userPrefix := " └─ "
|
||||||
|
userStyle := lipgloss.NewStyle().Faint(true)
|
||||||
|
if user.IsMe {
|
||||||
|
userStyle = userStyle.Foreground(lipgloss.Color("82"))
|
||||||
|
userPrefix = " └─ ► "
|
||||||
|
}
|
||||||
|
lines = append(lines, userStyle.Render(userPrefix+user.Nickname))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Left, lines...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) renderChat() string {
|
||||||
|
var lines []string
|
||||||
|
lines = append(lines, lipgloss.NewStyle().Bold(true).Render("LOG"))
|
||||||
|
lines = append(lines, "")
|
||||||
|
|
||||||
|
if len(m.logMessages) == 0 {
|
||||||
|
lines = append(lines, lipgloss.NewStyle().Faint(true).Render("No logs yet..."))
|
||||||
|
} else {
|
||||||
|
// Limit to last N messages that fit in the panel
|
||||||
|
maxLines := m.height - 10
|
||||||
|
if maxLines < 5 {
|
||||||
|
maxLines = 5
|
||||||
|
}
|
||||||
|
start := 0
|
||||||
|
if len(m.logMessages) > maxLines {
|
||||||
|
start = len(m.logMessages) - maxLines
|
||||||
|
}
|
||||||
|
for _, msg := range m.logMessages[start:] {
|
||||||
|
lines = append(lines, lipgloss.NewStyle().Faint(true).Render(msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Left, lines...)
|
||||||
|
}
|
||||||
24
go.mod
24
go.mod
@@ -11,4 +11,26 @@ require (
|
|||||||
|
|
||||||
require gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302
|
require gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302
|
||||||
|
|
||||||
require github.com/gorilla/websocket v1.5.3 // indirect
|
require (
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/charmbracelet/bubbles v0.21.0 // indirect
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10 // indirect
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
golang.org/x/sys v0.36.0 // indirect
|
||||||
|
golang.org/x/text v0.3.8 // indirect
|
||||||
|
)
|
||||||
|
|||||||
43
go.sum
43
go.sum
@@ -1,8 +1,51 @@
|
|||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
|
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
||||||
|
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
|
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
|
||||||
|
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||||
github.com/dgryski/go-quicklz v0.0.0-20151014073603-d7042a82d57e h1:MhBotBstN1h/GeA7lx7xstbFB8avummjt+nzOi2cY7Y=
|
github.com/dgryski/go-quicklz v0.0.0-20151014073603-d7042a82d57e h1:MhBotBstN1h/GeA7lx7xstbFB8avummjt+nzOi2cY7Y=
|
||||||
github.com/dgryski/go-quicklz v0.0.0-20151014073603-d7042a82d57e/go.mod h1:XLmYwGWgVzMPLlMmcNcWt3b5ixRabPLstWnPVEDRhzc=
|
github.com/dgryski/go-quicklz v0.0.0-20151014073603-d7042a82d57e/go.mod h1:XLmYwGWgVzMPLlMmcNcWt3b5ixRabPLstWnPVEDRhzc=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||||
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||||
|
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||||
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302 h1:xeVptzkP8BuJhoIjNizd2bRHfq9KB9HfOLZu90T04XM=
|
gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302 h1:xeVptzkP8BuJhoIjNizd2bRHfq9KB9HfOLZu90T04XM=
|
||||||
gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302/go.mod h1:/L5E7a21VWl8DeuCPKxQBdVG5cy+L0MRZ08B1wnqt7g=
|
gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302/go.mod h1:/L5E7a21VWl8DeuCPKxQBdVG5cy+L0MRZ08B1wnqt7g=
|
||||||
|
|||||||
@@ -237,6 +237,7 @@ func (c *Client) handleInternalEvent(eventType string, data map[string]any) {
|
|||||||
serverName = v
|
serverName = v
|
||||||
}
|
}
|
||||||
c.selfInfo = &SelfInfo{ClientID: clientID, Nickname: c.config.Nickname}
|
c.selfInfo = &SelfInfo{ClientID: clientID, Nickname: c.config.Nickname}
|
||||||
|
c.serverInfo = &ServerInfo{Name: serverName}
|
||||||
c.emit(EventConnected, &ConnectedEvent{
|
c.emit(EventConnected, &ConnectedEvent{
|
||||||
ClientID: clientID,
|
ClientID: clientID,
|
||||||
ServerName: serverName,
|
ServerName: serverName,
|
||||||
|
|||||||
BIN
teamspeak-tui.exe
Normal file
BIN
teamspeak-tui.exe
Normal file
Binary file not shown.
12
tui.ps1
Normal file
12
tui.ps1
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Fix UTF-8 encoding for PowerShell
|
||||||
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
chcp 65001 > $null
|
||||||
|
|
||||||
|
$env:PATH = "D:\esto_al_path\msys64\mingw64\bin;$env:PATH"
|
||||||
|
$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 localhost:9987 --nickname Adam
|
||||||
Reference in New Issue
Block a user