From 4ddae5d5f6ac3ccdc857054126601c8270fec990 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 2 Oct 2025 10:36:30 -0500 Subject: [PATCH 1/2] Reversible scroll wheel behavior --- USAGE_GUIDE.md | 49 ++++++++++++ cmd/gonzo/app.go | 2 +- cmd/gonzo/main.go | 3 + internal/tui/model.go | 8 +- internal/tui/update.go | 174 +++++++++++++++++++++++++++++++---------- 5 files changed, 191 insertions(+), 45 deletions(-) diff --git a/USAGE_GUIDE.md b/USAGE_GUIDE.md index a5be8a0..d4c45ac 100644 --- a/USAGE_GUIDE.md +++ b/USAGE_GUIDE.md @@ -140,6 +140,7 @@ cat test.log | ./build/gonzo -b, --log-buffer=1000 # Maximum log buffer size -m, --memory-size=10000 # Maximum entries in memory --stop-words strings # Additional stop words to filter from analysis + --reverse-scroll-wheel # Reverse scroll wheel direction (natural scrolling) --config string # Config file (default: ~/.gonzo.yaml) # Version and help @@ -224,6 +225,54 @@ export GONZO_STOP_WORDS="debug info warning error" - Stop words only affect word frequency analysis, not log display or filtering - Changes take effect immediately when logs are processed +## Reverse Scroll Wheel Configuration + +### Overview +By default, Gonzo uses traditional scroll wheel behavior (scroll up = content moves up). If you prefer natural/trackpad-style scrolling (scroll up = content moves down), you can enable reverse scroll wheel mode. + +### Enabling Reverse Scroll + +#### Via Command Line +```bash +# Enable reverse scroll wheel +./build/gonzo -f app.log --reverse-scroll-wheel + +# Works with all input modes +cat logs.json | ./build/gonzo --reverse-scroll-wheel +./build/gonzo --otlp-enabled --reverse-scroll-wheel +``` + +#### Via Configuration File +```yaml +# ~/.config/gonzo/config.yml +reverse-scroll-wheel: true +``` + +#### Via Environment Variable +```bash +# Set the environment variable +export GONZO_REVERSE_SCROLL_WHEEL=true +./build/gonzo -f app.log +``` + +### Behavior + +When reverse scroll wheel is enabled: +- **Scroll wheel up** → Content moves down (viewport scrolls down) +- **Scroll wheel down** → Content moves up (viewport scrolls up) + +This affects all scrollable areas: +- Main dashboard log navigation +- All modal windows (log details, patterns, statistics, etc.) +- Chat viewport scrolling +- Model selection +- Help screen + +### Use Cases +- **MacOS users**: Match trackpad natural scrolling behavior +- **Consistency**: Keep same scroll direction across all applications +- **Personal preference**: Use whichever feels more intuitive to you + ### Supported Integrations - [Victoria Logs Integration](guides/VICTORIA_LOGS_USAGE.md) - Using Gonzo with Victoria Logs API diff --git a/cmd/gonzo/app.go b/cmd/gonzo/app.go index 35eb52d..c2c4c26 100644 --- a/cmd/gonzo/app.go +++ b/cmd/gonzo/app.go @@ -90,7 +90,7 @@ func runApp(cmd *cobra.Command, args []string) error { freqMemory := memory.NewFrequencyMemory(cfg.MemorySize) // Initialize TUI model with components - dashboard := tui.NewDashboardModel(cfg.LogBuffer, cfg.UpdateInterval, cfg.AIModel, textAnalyzer.GetStopWords()) + dashboard := tui.NewDashboardModel(cfg.LogBuffer, cfg.UpdateInterval, cfg.AIModel, textAnalyzer.GetStopWords(), cfg.ReverseScrollWheel) if versionChecker != nil { dashboard.SetVersionChecker(versionChecker) } diff --git a/cmd/gonzo/main.go b/cmd/gonzo/main.go index 483ee17..e542f47 100644 --- a/cmd/gonzo/main.go +++ b/cmd/gonzo/main.go @@ -45,6 +45,7 @@ type Config struct { StopWords []string `mapstructure:"stop-words"` Format string `mapstructure:"format"` DisableVersionCheck bool `mapstructure:"disable-version-check"` + ReverseScrollWheel bool `mapstructure:"reverse-scroll-wheel"` } var ( @@ -156,6 +157,7 @@ func init() { rootCmd.Flags().StringSlice("stop-words", []string{}, "Additional stop words to filter out from analysis (adds to built-in list)") rootCmd.Flags().String("format", "", "Log format to use (auto-detect if not specified). Can be: otlp, json, text, or a custom format name from ~/.config/gonzo/formats/") rootCmd.Flags().Bool("disable-version-check", false, "Disable automatic version checking on startup") + rootCmd.Flags().Bool("reverse-scroll-wheel", false, "Reverse scroll wheel direction (natural scrolling)") // Bind flags to viper viper.BindPFlag("memory-size", rootCmd.Flags().Lookup("memory-size")) @@ -176,6 +178,7 @@ func init() { viper.BindPFlag("stop-words", rootCmd.Flags().Lookup("stop-words")) viper.BindPFlag("format", rootCmd.Flags().Lookup("format")) viper.BindPFlag("disable-version-check", rootCmd.Flags().Lookup("disable-version-check")) + viper.BindPFlag("reverse-scroll-wheel", rootCmd.Flags().Lookup("reverse-scroll-wheel")) // Add version command rootCmd.AddCommand(versionCmd) diff --git a/internal/tui/model.go b/internal/tui/model.go index abe21bd..f037300 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -82,8 +82,9 @@ type DashboardModel struct { servicesBySeverity map[string][]ServiceCount // Top services by severity level // Configuration - maxLogBuffer int - updateInterval time.Duration + maxLogBuffer int + updateInterval time.Duration + reverseScrollWheel bool // Filter filterInput textinput.Model @@ -223,7 +224,7 @@ func initializeDrain3BySeverity() map[string]*Drain3Manager { } // NewDashboardModel creates a new dashboard model with stop words -func NewDashboardModel(maxLogBuffer int, updateInterval time.Duration, aiModel string, stopWords map[string]bool) *DashboardModel { +func NewDashboardModel(maxLogBuffer int, updateInterval time.Duration, aiModel string, stopWords map[string]bool, reverseScrollWheel bool) *DashboardModel { filterInput := textinput.New() filterInput.Placeholder = "Filter logs (regex supported)..." filterInput.CharLimit = 200 @@ -263,6 +264,7 @@ func NewDashboardModel(maxLogBuffer int, updateInterval time.Duration, aiModel s m := &DashboardModel{ maxLogBuffer: maxLogBuffer, updateInterval: updateInterval, + reverseScrollWheel: reverseScrollWheel, filterInput: filterInput, searchInput: searchInput, chatInput: chatInput, diff --git a/internal/tui/update.go b/internal/tui/update.go index 4033482..61f235e 100644 --- a/internal/tui/update.go +++ b/internal/tui/update.go @@ -165,13 +165,21 @@ func (m *DashboardModel) handleMouseEvent(msg tea.MouseMsg) (tea.Model, tea.Cmd) return m.handleMouseClick(msg.X, msg.Y) case tea.MouseButtonWheelUp: - // Scroll wheel up = move selection up (like up arrow) - m.moveSelection(1) + // Scroll wheel up = move selection up (like up arrow), or down if reversed + if m.reverseScrollWheel { + m.moveSelection(-1) + } else { + m.moveSelection(1) + } return m, nil case tea.MouseButtonWheelDown: - // Scroll wheel down = move selection down (like down arrow) - m.moveSelection(-1) + // Scroll wheel down = move selection down (like down arrow), or up if reversed + if m.reverseScrollWheel { + m.moveSelection(1) + } else { + m.moveSelection(-1) + } return m, nil } } @@ -187,13 +195,21 @@ func (m *DashboardModel) handleModalMouseEvent(msg tea.MouseMsg) (tea.Model, tea case tea.MouseActionPress: switch msg.Button { case tea.MouseButtonWheelUp: - // Scroll up in single modal - m.infoViewport.ScrollUp(1) + // Scroll up in single modal, or down if reversed + if m.reverseScrollWheel { + m.infoViewport.ScrollDown(1) + } else { + m.infoViewport.ScrollUp(1) + } return m, nil case tea.MouseButtonWheelDown: - // Scroll down in single modal - m.infoViewport.ScrollDown(1) + // Scroll down in single modal, or up if reversed + if m.reverseScrollWheel { + m.infoViewport.ScrollUp(1) + } else { + m.infoViewport.ScrollDown(1) + } return m, nil } } @@ -210,22 +226,46 @@ func (m *DashboardModel) handleModalMouseEvent(msg tea.MouseMsg) (tea.Model, tea case tea.MouseButtonWheelUp: // Priority: if chat is active, always scroll chat viewport if m.chatActive { - m.chatViewport.ScrollUp(1) + if m.reverseScrollWheel { + m.chatViewport.ScrollDown(1) + } else { + m.chatViewport.ScrollUp(1) + } } else if m.modalActiveSection == "info" { - m.infoViewport.ScrollUp(1) + if m.reverseScrollWheel { + m.infoViewport.ScrollDown(1) + } else { + m.infoViewport.ScrollUp(1) + } } else if m.modalActiveSection == "chat" { - m.chatViewport.ScrollUp(1) + if m.reverseScrollWheel { + m.chatViewport.ScrollDown(1) + } else { + m.chatViewport.ScrollUp(1) + } } return m, nil case tea.MouseButtonWheelDown: // Priority: if chat is active, always scroll chat viewport if m.chatActive { - m.chatViewport.ScrollDown(1) + if m.reverseScrollWheel { + m.chatViewport.ScrollUp(1) + } else { + m.chatViewport.ScrollDown(1) + } } else if m.modalActiveSection == "info" { - m.infoViewport.ScrollDown(1) + if m.reverseScrollWheel { + m.infoViewport.ScrollUp(1) + } else { + m.infoViewport.ScrollDown(1) + } } else if m.modalActiveSection == "chat" { - m.chatViewport.ScrollDown(1) + if m.reverseScrollWheel { + m.chatViewport.ScrollUp(1) + } else { + m.chatViewport.ScrollDown(1) + } } return m, nil } @@ -292,13 +332,21 @@ func (m *DashboardModel) handleModelSelectionMouseEvent(msg tea.MouseMsg) (tea.M case tea.MouseActionPress: switch msg.Button { case tea.MouseButtonWheelUp: - // Scroll up - move selection up by 1 - m.selectedModelIndex = max(0, m.selectedModelIndex-1) + // Scroll up - move selection up by 1, or down if reversed + if m.reverseScrollWheel { + m.selectedModelIndex = min(len(m.availableModelsList)-1, m.selectedModelIndex+1) + } else { + m.selectedModelIndex = max(0, m.selectedModelIndex-1) + } return m, nil - + case tea.MouseButtonWheelDown: - // Scroll down - move selection down by 1 - m.selectedModelIndex = min(len(m.availableModelsList)-1, m.selectedModelIndex+1) + // Scroll down - move selection down by 1, or up if reversed + if m.reverseScrollWheel { + m.selectedModelIndex = max(0, m.selectedModelIndex-1) + } else { + m.selectedModelIndex = min(len(m.availableModelsList)-1, m.selectedModelIndex+1) + } return m, nil } } @@ -312,13 +360,21 @@ func (m *DashboardModel) handleHelpModalMouseEvent(msg tea.MouseMsg) (tea.Model, case tea.MouseActionPress: switch msg.Button { case tea.MouseButtonWheelUp: - // Scroll up in help modal - m.infoViewport.ScrollUp(1) + // Scroll up in help modal, or down if reversed + if m.reverseScrollWheel { + m.infoViewport.ScrollDown(1) + } else { + m.infoViewport.ScrollUp(1) + } return m, nil case tea.MouseButtonWheelDown: - // Scroll down in help modal - m.infoViewport.ScrollDown(1) + // Scroll down in help modal, or up if reversed + if m.reverseScrollWheel { + m.infoViewport.ScrollUp(1) + } else { + m.infoViewport.ScrollDown(1) + } return m, nil } } @@ -332,13 +388,21 @@ func (m *DashboardModel) handlePatternsModalMouseEvent(msg tea.MouseMsg) (tea.Mo case tea.MouseActionPress: switch msg.Button { case tea.MouseButtonWheelUp: - // Scroll up in patterns modal - m.infoViewport.ScrollUp(1) + // Scroll up in patterns modal, or down if reversed + if m.reverseScrollWheel { + m.infoViewport.ScrollDown(1) + } else { + m.infoViewport.ScrollUp(1) + } return m, nil case tea.MouseButtonWheelDown: - // Scroll down in patterns modal - m.infoViewport.ScrollDown(1) + // Scroll down in patterns modal, or up if reversed + if m.reverseScrollWheel { + m.infoViewport.ScrollUp(1) + } else { + m.infoViewport.ScrollDown(1) + } return m, nil } } @@ -352,13 +416,21 @@ func (m *DashboardModel) handleStatsModalMouseEvent(msg tea.MouseMsg) (tea.Model case tea.MouseActionPress: switch msg.Button { case tea.MouseButtonWheelUp: - // Scroll up in statistics modal - m.infoViewport.ScrollUp(1) + // Scroll up in statistics modal, or down if reversed + if m.reverseScrollWheel { + m.infoViewport.ScrollDown(1) + } else { + m.infoViewport.ScrollUp(1) + } return m, nil case tea.MouseButtonWheelDown: - // Scroll down in statistics modal - m.infoViewport.ScrollDown(1) + // Scroll down in statistics modal, or up if reversed + if m.reverseScrollWheel { + m.infoViewport.ScrollUp(1) + } else { + m.infoViewport.ScrollDown(1) + } return m, nil } } @@ -372,13 +444,21 @@ func (m *DashboardModel) handleCountsModalMouseEvent(msg tea.MouseMsg) (tea.Mode case tea.MouseActionPress: switch msg.Button { case tea.MouseButtonWheelUp: - // Scroll up in counts modal - m.infoViewport.ScrollUp(1) + // Scroll up in counts modal, or down if reversed + if m.reverseScrollWheel { + m.infoViewport.ScrollDown(1) + } else { + m.infoViewport.ScrollUp(1) + } return m, nil case tea.MouseButtonWheelDown: - // Scroll down in counts modal - m.infoViewport.ScrollDown(1) + // Scroll down in counts modal, or up if reversed + if m.reverseScrollWheel { + m.infoViewport.ScrollUp(1) + } else { + m.infoViewport.ScrollDown(1) + } return m, nil } } @@ -392,16 +472,28 @@ func (m *DashboardModel) handleLogViewerModalMouseEvent(msg tea.MouseMsg) (tea.M case tea.MouseActionPress: switch msg.Button { case tea.MouseButtonWheelUp: - // Navigate up in log list - if m.selectedLogIndex > 0 { - m.selectedLogIndex-- + // Navigate up in log list, or down if reversed + if m.reverseScrollWheel { + if m.selectedLogIndex < len(m.logEntries)-1 { + m.selectedLogIndex++ + } + } else { + if m.selectedLogIndex > 0 { + m.selectedLogIndex-- + } } return m, nil case tea.MouseButtonWheelDown: - // Navigate down in log list - if m.selectedLogIndex < len(m.logEntries)-1 { - m.selectedLogIndex++ + // Navigate down in log list, or up if reversed + if m.reverseScrollWheel { + if m.selectedLogIndex > 0 { + m.selectedLogIndex-- + } + } else { + if m.selectedLogIndex < len(m.logEntries)-1 { + m.selectedLogIndex++ + } } return m, nil } From c7bfd5ccc0fa78fd073a342686566d4dc319751f Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 3 Oct 2025 10:43:48 -0500 Subject: [PATCH 2/2] fix other scroll action --- internal/tui/update.go | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/internal/tui/update.go b/internal/tui/update.go index 61f235e..7fe781f 100644 --- a/internal/tui/update.go +++ b/internal/tui/update.go @@ -472,28 +472,32 @@ func (m *DashboardModel) handleLogViewerModalMouseEvent(msg tea.MouseMsg) (tea.M case tea.MouseActionPress: switch msg.Button { case tea.MouseButtonWheelUp: - // Navigate up in log list, or down if reversed + // Scroll up in log viewer - should behave like scrolling content up (move to later/newer logs) if m.reverseScrollWheel { - if m.selectedLogIndex < len(m.logEntries)-1 { - m.selectedLogIndex++ - } - } else { + // Reversed: wheel up goes to earlier logs if m.selectedLogIndex > 0 { m.selectedLogIndex-- } + } else { + // Normal: wheel up goes to later logs (like scrolling list up) + if m.selectedLogIndex < len(m.logEntries)-1 { + m.selectedLogIndex++ + } } return m, nil case tea.MouseButtonWheelDown: - // Navigate down in log list, or up if reversed + // Scroll down in log viewer - should behave like scrolling content down (move to earlier/older logs) if m.reverseScrollWheel { - if m.selectedLogIndex > 0 { - m.selectedLogIndex-- - } - } else { + // Reversed: wheel down goes to later logs if m.selectedLogIndex < len(m.logEntries)-1 { m.selectedLogIndex++ } + } else { + // Normal: wheel down goes to earlier logs (like scrolling list down) + if m.selectedLogIndex > 0 { + m.selectedLogIndex-- + } } return m, nil }