From 1a71302f11edeefd484db5d43dd8a5245cb95460 Mon Sep 17 00:00:00 2001 From: "suhird.singh" Date: Wed, 24 Jun 2026 13:39:16 -0400 Subject: [PATCH] gui: add gotoTopMain/gotoBottomMain navigation handlers Add handlers that scroll the main panel to the top and bottom. gotoTopMain implements the vim-style "gg" double press (gocui has no native key-sequence support) by tracking the last 'g' press timestamp in the gui state, while gotoBottomMain jumps to the final line, honouring the ScrollPastBottom config. The origin math and double-press timing are factored into pure helpers so they can be unit tested. --- pkg/gui/gui.go | 4 ++++ pkg/gui/main_panel.go | 50 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 6ea2ee6c..f3b076c6 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -87,6 +87,10 @@ type guiState struct { // Maintains the state of manual filtering i.e. typing in a substring // to filter on in the current panel. Filter filterState + + // Timestamp of the last 'g' key press in the main panel, used to detect + // the vim-style "gg" double press that jumps to the top. + lastGPressedAt time.Time } type filterState struct { diff --git a/pkg/gui/main_panel.go b/pkg/gui/main_panel.go index 004147be..ebd628b0 100644 --- a/pkg/gui/main_panel.go +++ b/pkg/gui/main_panel.go @@ -2,10 +2,33 @@ package gui import ( "math" + "time" "github.com/jesseduffield/gocui" ) +// gPressTimeout is the window within which two consecutive 'g' presses are +// treated as the vim-style "gg" command to jump to the top of the main panel. +const gPressTimeout = 250 * time.Millisecond + +// isDoublePress reports whether `now` follows `previous` closely enough to be +// considered the second half of a double key press. +func isDoublePress(previous, now time.Time, timeout time.Duration) bool { + return !previous.IsZero() && now.Sub(previous) <= timeout +} + +// gotoBottomOriginY returns the vertical origin that scrolls the main panel to +// its final line. When scrollPastBottom is false the last page of content is +// kept on screen rather than scrolling it off the top. +func gotoBottomOriginY(totalLines, viewHeight int, scrollPastBottom bool) int { + reservedLines := 0 + if !scrollPastBottom { + reservedLines = viewHeight + } + + return int(math.Max(0, float64(totalLines-reservedLines))) +} + func (gui *Gui) scrollUpMain() error { mainView := gui.Views.Main mainView.Autoscroll = false @@ -73,6 +96,33 @@ func (gui *Gui) jumpToTopMain(g *gocui.Gui, v *gocui.View) error { return nil } +// gotoTopMain implements the vim-style "gg" command: the first 'g' arms the +// sequence and the second 'g' pressed within gPressTimeout jumps to the top of +// the main panel. +func (gui *Gui) gotoTopMain(g *gocui.Gui, v *gocui.View) error { + now := time.Now() + if !isDoublePress(gui.State.lastGPressedAt, now, gPressTimeout) { + gui.State.lastGPressedAt = now + return nil + } + + gui.State.lastGPressedAt = time.Time{} + return gui.jumpToTopMain(g, v) +} + +// gotoBottomMain implements the vim-style "G" command, jumping to the bottom of +// the main panel. +func (gui *Gui) gotoBottomMain(g *gocui.Gui, v *gocui.View) error { + mainView := gui.Views.Main + mainView.Autoscroll = false + + _, viewHeight := mainView.Size() + ox, _ := mainView.Origin() + newOy := gotoBottomOriginY(mainView.ViewLinesHeight(), viewHeight, gui.Config.UserConfig.Gui.ScrollPastBottom) + + return mainView.SetOrigin(ox, newOy) +} + func (gui *Gui) onMainTabClick(tabIndex int) error { gui.Log.Warn(tabIndex)