summaryrefslogtreecommitdiff
path: root/internal/gui
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-07-21 14:01:11 +0300
committerPaul Buetow <paul@buetow.org>2025-07-21 14:01:11 +0300
commit6e8f1ffb85cddd841614a156b291b881a1f6a875 (patch)
tree2ee485e202c41aab140b4acb811ebdf2a4603cf8 /internal/gui
parent9c12e879c5d6833ce50f5b6d646ccce03a78db31 (diff)
fix: implement log viewer with newest-first ordering to avoid scroll issues
- Replace phonetic display with a proper log viewer widget - Prepend new log messages instead of appending to show newest first - Capture stdout/stderr output to display in the log viewer - Remove problematic auto-scroll functionality that wasn't working - Add timestamp to each log message for better tracking This approach eliminates the need for auto-scrolling since the latest messages are always visible at the top of the log viewer. 🤖 Generated with [opencode](https://opencode.ai) Co-Authored-By: opencode <noreply@opencode.ai>
Diffstat (limited to 'internal/gui')
-rw-r--r--internal/gui/log_viewer.go198
1 files changed, 198 insertions, 0 deletions
diff --git a/internal/gui/log_viewer.go b/internal/gui/log_viewer.go
new file mode 100644
index 0000000..ec1ec0b
--- /dev/null
+++ b/internal/gui/log_viewer.go
@@ -0,0 +1,198 @@
+package gui
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "strings"
+ "sync"
+ "time"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/container"
+ "fyne.io/fyne/v2/widget"
+)
+
+// LogWriter is a custom writer that captures log output
+type LogWriter struct {
+ viewer *LogViewer
+ original *os.File
+}
+
+// Write implements io.Writer
+func (w *LogWriter) Write(p []byte) (n int, err error) {
+ // Write to original output
+ if w.original != nil {
+ w.original.Write(p)
+ }
+
+ // Send to log viewer
+ if w.viewer != nil {
+ message := strings.TrimRight(string(p), "\n")
+ if message != "" {
+ w.viewer.AddMessage(message)
+ }
+ }
+
+ return len(p), nil
+}
+
+// LogViewer is a widget that displays log messages
+type LogViewer struct {
+ widget.BaseWidget
+
+ container *fyne.Container
+ logEntry *widget.Entry
+ scrollView *container.Scroll
+
+ mu sync.Mutex
+ messages []string
+ maxMessages int
+
+ // For capturing output
+ originalStdout *os.File
+ originalStderr *os.File
+ stdoutWriter *LogWriter
+ stderrWriter *LogWriter
+}
+
+// NewLogViewer creates a new log viewer widget
+func NewLogViewer() *LogViewer {
+ v := &LogViewer{
+ maxMessages: 1000, // Keep last 1000 messages
+ messages: make([]string, 0),
+ }
+
+ // Create log entry (read-only multiline)
+ v.logEntry = widget.NewMultiLineEntry()
+ v.logEntry.Disable() // Make it read-only
+ v.logEntry.Wrapping = fyne.TextWrapWord
+
+ // Create scroll container
+ v.scrollView = container.NewScroll(v.logEntry)
+ v.scrollView.SetMinSize(fyne.NewSize(0, 180)) // Same height as old phonetic display
+
+ // Configure scroll behavior
+ v.scrollView.Direction = container.ScrollBoth
+
+ // Create container with label
+ v.container = container.NewBorder(
+ widget.NewLabel("Log Messages (newest first):"),
+ nil,
+ nil,
+ nil,
+ v.scrollView,
+ )
+
+ v.ExtendBaseWidget(v)
+ return v
+}
+
+// CreateRenderer implements fyne.Widget
+func (v *LogViewer) CreateRenderer() fyne.WidgetRenderer {
+ return widget.NewSimpleRenderer(v.container)
+}
+
+// StartCapture starts capturing stdout and stderr
+func (v *LogViewer) StartCapture() {
+ // Save original outputs
+ v.originalStdout = os.Stdout
+ v.originalStderr = os.Stderr
+
+ // Create custom writers
+ v.stdoutWriter = &LogWriter{viewer: v, original: v.originalStdout}
+ v.stderrWriter = &LogWriter{viewer: v, original: v.originalStderr}
+
+ // Create pipe for stdout
+ stdoutR, stdoutW, _ := os.Pipe()
+ os.Stdout = stdoutW
+
+ // Create pipe for stderr
+ stderrR, stderrW, _ := os.Pipe()
+ os.Stderr = stderrW
+
+ // Also redirect log package output
+ log.SetOutput(v.stdoutWriter)
+
+ // Start goroutines to read from pipes
+ go v.pipeReader(stdoutR, v.stdoutWriter)
+ go v.pipeReader(stderrR, v.stderrWriter)
+}
+
+// pipeReader reads from a pipe and writes to a LogWriter
+func (v *LogViewer) pipeReader(pipe *os.File, writer *LogWriter) {
+ buf := make([]byte, 1024)
+ for {
+ n, err := pipe.Read(buf)
+ if n > 0 {
+ writer.Write(buf[:n])
+ }
+ if err != nil {
+ break
+ }
+ }
+}
+
+// StopCapture stops capturing stdout and stderr
+func (v *LogViewer) StopCapture() {
+ if v.originalStdout != nil {
+ os.Stdout = v.originalStdout
+ v.originalStdout = nil
+ }
+ if v.originalStderr != nil {
+ os.Stderr = v.originalStderr
+ v.originalStderr = nil
+ }
+
+ // Reset log package output
+ log.SetOutput(os.Stderr)
+}
+
+// AddMessage adds a message to the log
+func (v *LogViewer) AddMessage(message string) {
+ v.mu.Lock()
+ defer v.mu.Unlock()
+
+ // Add timestamp
+ timestamp := time.Now().Format("15:04:05")
+ fullMessage := fmt.Sprintf("[%s] %s", timestamp, message)
+
+ // Prepend to messages (newest first)
+ v.messages = append([]string{fullMessage}, v.messages...)
+
+ // Trim if too many messages (remove oldest from the end)
+ if len(v.messages) > v.maxMessages {
+ v.messages = v.messages[:v.maxMessages]
+ }
+
+ // Update UI on main thread
+ fyne.Do(func() {
+ // Set the full text with newest messages at top
+ text := strings.Join(v.messages, "\n")
+ v.logEntry.SetText(text)
+
+ // Keep scroll at top to show newest messages
+ v.scrollView.Offset = fyne.NewPos(0, 0)
+ v.scrollView.Refresh()
+ })
+}
+
+// Clear clears all log messages
+func (v *LogViewer) Clear() {
+ v.mu.Lock()
+ defer v.mu.Unlock()
+
+ v.messages = v.messages[:0]
+
+ fyne.Do(func() {
+ v.logEntry.SetText("")
+ v.scrollView.Offset = fyne.NewPos(0, 0)
+ v.scrollView.Refresh()
+ })
+}
+
+// Log adds a message without timestamp (for internal use)
+func (v *LogViewer) Log(format string, args ...interface{}) {
+ message := fmt.Sprintf(format, args...)
+ v.AddMessage(message)
+}