Fix audio quality with per-user mixing buffer and prevent TUI layout break on log overflow

This commit is contained in:
Jose Luis Montañes Ojados
2026-01-16 22:33:35 +01:00
parent f83f525600
commit 9675f3764c
4 changed files with 177 additions and 129 deletions

View File

@@ -27,7 +27,7 @@ func main() {
debug := flag.Bool("debug", true, "Enable debug logging to file (default true)")
flag.Parse()
// Disable log output completely to prevent TUI corruption
// Disable log output completely to prevent TUI corruption (stdout is reserved for UI)
log.SetOutput(io.Discard)
// Enable debug file logging if requested
@@ -39,6 +39,8 @@ func main() {
if err == nil {
defer debugFile.Close()
debugLog("TUI Debug started at %s", timestamp)
// Redirect standard log output to debug file initially
log.SetOutput(debugFile)
}
}
@@ -51,16 +53,19 @@ func main() {
// Pass program reference to model for event handlers
m.SetProgram(p)
// Set up log capture
// Set up log capture for UI display
r, w, _ := os.Pipe()
if debugFile != nil {
// Log to both UI (pipe) and Debug File
log.SetOutput(io.MultiWriter(w, debugFile))
} else {
// Log to UI only
log.SetOutput(w)
}
// Make sure logs have timestamp removed (TUI adds it if needed, or we keep it)
log.SetFlags(log.Ltime) // Just time
// Goroutine to capture logs and send them to the UI
go func() {
buf := make([]byte, 1024)
for {
@@ -71,7 +76,6 @@ func main() {
if n > 0 {
lines := string(buf[:n])
// Split by newline and send each line
// Simple split, might need better buffering for partial lines but OK for debug
p.Send(logMsg(lines))
}
}

View File

@@ -66,7 +66,6 @@ type Model struct {
selectedIdx int
chatMessages []ChatMessage
logMessages []string // Debug logs shown in chat panel
logFullscreen bool // Toggle fullscreen log view
inputText string
inputActive bool
@@ -86,6 +85,7 @@ type Model struct {
// Program reference for sending messages from event handlers
program *tea.Program
showLog bool
}
// addLog adds a message to the log panel
@@ -213,7 +213,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Handle incoming audio - play through speakers
m.client.On(ts3client.EventAudio, func(e *ts3client.AudioEvent) {
if m.audioPlayer != nil {
m.audioPlayer.PlayPCM(e.PCM)
m.audioPlayer.PlayPCM(e.SenderID, e.PCM)
}
})
}
@@ -449,6 +449,13 @@ func (m *Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
}
return m, nil
}
case "l", "L":
// Toggle Log/Chat view
if m.focus != FocusInput {
m.showLog = !m.showLog
return m, nil
}
}
// Focus-specific keys
@@ -464,10 +471,6 @@ func (m *Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
}
func (m *Model) handleChatKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "f":
m.logFullscreen = !m.logFullscreen
}
return m, nil
}
@@ -524,18 +527,6 @@ func (m *Model) View() string {
return "Loading..."
}
// Fullscreen Log Mode
if m.logFullscreen {
style := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("212")). // Active color
Padding(0, 1).
Width(m.width - 2).
Height(m.height - 2)
return style.Render(m.renderChat())
}
// Styles
// Layout: header(1) + panels + input(3) + help(1) = header + panels + 4
// panels should be height - 5 (1 for header, 3 for input with border, 1 for help)
@@ -571,12 +562,60 @@ func (m *Model) View() string {
}
channelPanel := channelPanelStyle.Render(channelContent)
// Chat panel
chatContent := m.renderChat()
if m.focus == FocusChat {
chatPanelStyle = chatPanelStyle.BorderForeground(lipgloss.Color("212"))
// Right panel (Chat or Log)
var rightPanel string
if m.showLog {
// Log View
// Use chatPanelStyle default but add focus logic
logStyle := chatPanelStyle.Copy()
if m.focus == FocusChat {
logStyle = logStyle.BorderForeground(lipgloss.Color("212"))
}
// Limit to last N messages log to fit panel
// Panel height is m.height - 7, padding reduces it more
maxLines := m.height - 11
if maxLines < 1 {
maxLines = 1
}
start := 0
if len(m.logMessages) > maxLines {
start = len(m.logMessages) - maxLines
}
// Calculate available width for text
textWidth := (m.width * 2 / 3) - 6
if textWidth < 10 {
textWidth = 10
}
textStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
var lines []string
for _, msg := range m.logMessages[start:] {
// Sanitize: remove newlines/returns to prevent double spacing
msg = strings.ReplaceAll(msg, "\n", " ")
msg = strings.ReplaceAll(msg, "\r", "")
// Truncate simplisticly to prevent wrapping issues if too long
if len(msg) > textWidth {
msg = msg[:textWidth-3] + "..."
}
lines = append(lines, textStyle.Render(msg))
}
rightPanel = logStyle.Render(strings.Join(lines, "\n"))
} else {
// Chat View
chatContent := m.renderChat()
if m.focus == FocusChat {
chatPanelStyle = chatPanelStyle.BorderForeground(lipgloss.Color("212"))
}
rightPanel = chatPanelStyle.Render(chatContent)
}
chatPanel := chatPanelStyle.Render(chatContent)
// Input
inputContent := "> " + m.inputText
@@ -587,10 +626,14 @@ func (m *Model) View() string {
input := inputStyle.Render(inputContent)
// Footer help
help := lipgloss.NewStyle().Faint(true).Render("↑↓ navigate │ Enter join │ Tab switch │ V talk │ M mute │ +/- vol │ q quit")
logHelp := "L log"
if m.showLog {
logHelp = "L chat"
}
help := lipgloss.NewStyle().Faint(true).Render(fmt.Sprintf("↑↓ navigate │ Enter join │ Tab switch │ %s │ V talk │ M mute │ +/- vol │ q quit", logHelp))
// Combine panels
panels := lipgloss.JoinHorizontal(lipgloss.Top, channelPanel, chatPanel)
panels := lipgloss.JoinHorizontal(lipgloss.Top, channelPanel, rightPanel)
return lipgloss.JoinVertical(lipgloss.Left,
header,
@@ -721,43 +764,36 @@ func (m *Model) renderChannels() string {
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..."))
if len(m.chatMessages) == 0 {
lines = append(lines, lipgloss.NewStyle().Faint(true).Render("No chat messages yet..."))
} else {
// Limit to last N messages that fit in the panel
maxLines := m.height - 10
if maxLines < 5 {
maxLines = 5
// Limit messages to fit panel
// Panel height is roughly m.height - 7
maxLines := m.height - 9
if maxLines < 1 {
maxLines = 1
}
start := 0
if len(m.logMessages) > maxLines {
start = len(m.logMessages) - maxLines
if len(m.chatMessages) > maxLines {
start = len(m.chatMessages) - maxLines
}
panelWidth := (m.width / 2) - 4
if m.logFullscreen {
panelWidth = m.width - 6
}
if panelWidth < 10 {
panelWidth = 10
}
for _, msg := range m.chatMessages[start:] {
prefix := msg.Time.Format("15:04") + " " + msg.Sender + ": "
content := msg.Content
for _, msg := range m.logMessages[start:] {
// Truncate if too long to prevent wrapping breaking the layout
displayMsg := msg
if len(displayMsg) > panelWidth {
displayMsg = displayMsg[:panelWidth-3] + "..."
}
// Replace newlines just in case
displayMsg = strings.ReplaceAll(displayMsg, "\n", " ")
lines = append(lines, lipgloss.NewStyle().Faint(true).Render(displayMsg))
// Simple wrapping logic could go here, but for now simple truncation/display
line := lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Render(prefix) + content
lines = append(lines, line)
}
}
// Fill remaining lines with empty space to maintain stable layout
for len(lines) < m.height-9 {
lines = append(lines, "")
}
return lipgloss.JoinVertical(lipgloss.Left, lines...)
}