detached b2baf19

fix(ui): improve omnibox pane handling and Ctrl+L toggle

commit
b2baf192033375f30f9137f22e55735ab3b49bd1
author
bnema · · 2025-12-22 21:52 UTC
parent
f95a7fa
signature
unsigned
2 files · +70 -57
M internal/ui/component/omnibox.go +47 -51
@@ -20,12 +20,13 @@ import (
2020 )
2121
2222 const (
23- omniboxWidthPct = 0.8 // 80% of parent window width
24- omniboxMaxWidth = 800 // Maximum width in pixels
25- omniboxHeightPct = 0.6 // 60% of parent window height
26- omniboxMaxResults = 10
27- omniboxTopMarginPct = 0.2 // 20% from top
28- debounceDelayMs = 150
23+ omniboxWidthPct = 0.8 // 80% of parent window width
24+ omniboxMaxWidth = 800 // Maximum width in pixels
25+ omniboxFallbackHeight = 600 // Fallback height when overlay not yet allocated
26+ omniboxHeightPct = 0.6 // 60% of parent window height
27+ omniboxMaxResults = 10
28+ omniboxTopMarginPct = 0.2 // 20% from top
29+ debounceDelayMs = 150
2930 )
3031
3132 // ViewMode distinguishes history search from favorites display.
@@ -110,9 +111,6 @@ type Omnibox struct {
110111
111112 // Callback retention: keep GTK signal callbacks reachable by Go GC.
112113 retainedCallbacks []interface{}
113-
114- // Used to rate-limit allocation-related logs.
115- loggedInvalidOverlayDims bool
116114 }
117115
118116 // OmniboxConfig holds configuration for creating an Omnibox.
@@ -195,33 +193,10 @@ func (o *Omnibox) resizeAndCenter(rowCount int) {
195193 return
196194 }
197195
198- // Use GetAllocatedWidth/Height for actual rendered size
196+ // Use GetAllocatedWidth for actual rendered size (height not needed here)
199197 parentWidth := o.parentOverlay.GetAllocatedWidth()
200- parentHeight := o.parentOverlay.GetAllocatedHeight()
201-
202- if parentWidth <= 0 || parentHeight <= 0 {
203- // Overlay may not be allocated yet (e.g., right after adding/switching panes).
204- // Retry once in the next main-loop tick and avoid spamming logs.
205- if !o.loggedInvalidOverlayDims {
206- log := logging.FromContext(o.ctx)
207- log.Error().
208- Int("parent_width", parentWidth).
209- Int("parent_height", parentHeight).
210- Msg("omnibox: invalid parent overlay dimensions")
211- o.loggedInvalidOverlayDims = true
212- }
213-
214- var cb glib.SourceFunc = func(uintptr) bool {
215- o.mu.RLock()
216- visible := o.visible
217- o.mu.RUnlock()
218- if visible {
219- o.resizeAndCenter(rowCount)
220- }
221- return false
222- }
223- glib.IdleAdd(&cb, 0)
224- return
198+ if parentWidth <= 0 {
199+ parentWidth = omniboxMaxWidth
225200 }
226201
227202 width := int(float64(parentWidth) * omniboxWidthPct)
@@ -493,6 +468,13 @@ func (o *Omnibox) handleKeyPress(keyval uint, state gdk.ModifierType) bool {
493468 o.Hide(o.ctx)
494469 return true
495470
471+ case uint(gdk.KEY_l), uint(gdk.KEY_L):
472+ // Ctrl+L toggles omnibox (hides when visible)
473+ if ctrl {
474+ o.Hide(o.ctx)
475+ return true
476+ }
477+
496478 case uint(gdk.KEY_Return), uint(gdk.KEY_KP_Enter):
497479 o.navigateToSelected()
498480 return true
@@ -532,24 +514,31 @@ func (o *Omnibox) handleKeyPress(keyval uint, state gdk.ModifierType) bool {
532514 }
533515 return false // Let entry handle 'y' for typing
534516
517+ default:
518+ return o.handleCtrlNumberShortcut(keyval, ctrl)
519+ }
520+
521+ return false // Let entry handle the key
522+}
523+
524+// handleCtrlNumberShortcut handles Ctrl+1-9 and Ctrl+0 for quick navigation.
525+func (o *Omnibox) handleCtrlNumberShortcut(keyval uint, ctrl bool) bool {
526+ if !ctrl {
527+ return false
528+ }
529+
530+ const tenthItemIndex = 9
531+ switch keyval {
535532 case uint(gdk.KEY_1), uint(gdk.KEY_2), uint(gdk.KEY_3), uint(gdk.KEY_4), uint(gdk.KEY_5),
536533 uint(gdk.KEY_6), uint(gdk.KEY_7), uint(gdk.KEY_8), uint(gdk.KEY_9):
537- if ctrl {
538- const firstShortcutKey = uint(gdk.KEY_1)
539- index := int(keyval - firstShortcutKey) // 0-8
540- o.selectAndNavigate(index)
541- return true
542- }
543-
534+ index := int(keyval - uint(gdk.KEY_1))
535+ o.selectAndNavigate(index)
536+ return true
544537 case uint(gdk.KEY_0):
545- if ctrl {
546- const tenthItemIndex = 9
547- o.selectAndNavigate(tenthItemIndex)
548- return true
549- }
538+ o.selectAndNavigate(tenthItemIndex)
539+ return true
550540 }
551-
552- return false // Let entry handle the key
541+ return false
553542 }
554543
555544 // onEntryChanged handles text input changes with debouncing.
@@ -1245,9 +1234,17 @@ func (o *Omnibox) Show(ctx context.Context, query string) {
12451234 o.scrolledWin.SetMaxContentHeight(0)
12461235 }
12471236
1248- // Set width and vertical positioning (20% from top)
1237+ // Get parent dimensions with fallback if not yet allocated
12491238 parentWidth := o.parentOverlay.GetAllocatedWidth()
12501239 parentHeight := o.parentOverlay.GetAllocatedHeight()
1240+ if parentWidth <= 0 {
1241+ parentWidth = omniboxMaxWidth
1242+ }
1243+ if parentHeight <= 0 {
1244+ parentHeight = omniboxFallbackHeight
1245+ }
1246+
1247+ // Set width and vertical positioning (20% from top)
12511248 width := int(float64(parentWidth) * omniboxWidthPct)
12521249 if width > omniboxMaxWidth {
12531250 width = omniboxMaxWidth
@@ -1258,7 +1255,6 @@ func (o *Omnibox) Show(ctx context.Context, query string) {
12581255 o.outerBox.SetMarginTop(marginTop)
12591256
12601257 // Show the omnibox
1261- o.loggedInvalidOverlayDims = false
12621258 o.outerBox.SetVisible(true)
12631259
12641260 // Trigger initial resize (will be updated when results arrive)
@@ -20,12 +20,13 @@ import (
20 )20 )
21 21
22 const (22 const (
23- omniboxWidthPct = 0.8 // 80% of parent window width23+ omniboxWidthPct = 0.8 // 80% of parent window width
24- omniboxMaxWidth = 800 // Maximum width in pixels24+ omniboxMaxWidth = 800 // Maximum width in pixels
25- omniboxHeightPct = 0.6 // 60% of parent window height25+ omniboxFallbackHeight = 600 // Fallback height when overlay not yet allocated
26- omniboxMaxResults = 1026+ omniboxHeightPct = 0.6 // 60% of parent window height
27- omniboxTopMarginPct = 0.2 // 20% from top27+ omniboxMaxResults = 10
28- debounceDelayMs = 15028+ omniboxTopMarginPct = 0.2 // 20% from top
29+ debounceDelayMs = 150
29 )30 )
30 31
31 // ViewMode distinguishes history search from favorites display.32 // ViewMode distinguishes history search from favorites display.
@@ -110,9 +111,6 @@ type Omnibox struct {
110 111
111 // Callback retention: keep GTK signal callbacks reachable by Go GC.112 // Callback retention: keep GTK signal callbacks reachable by Go GC.
112 retainedCallbacks []interface{}113 retainedCallbacks []interface{}
113-
114- // Used to rate-limit allocation-related logs.
115- loggedInvalidOverlayDims bool
116 }114 }
117 115
118 // OmniboxConfig holds configuration for creating an Omnibox.116 // OmniboxConfig holds configuration for creating an Omnibox.
@@ -195,33 +193,10 @@ func (o *Omnibox) resizeAndCenter(rowCount int) {
195 return193 return
196 }194 }
197 195
198- // Use GetAllocatedWidth/Height for actual rendered size196+ // Use GetAllocatedWidth for actual rendered size (height not needed here)
199 parentWidth := o.parentOverlay.GetAllocatedWidth()197 parentWidth := o.parentOverlay.GetAllocatedWidth()
200- parentHeight := o.parentOverlay.GetAllocatedHeight()198+ if parentWidth <= 0 {
201-199+ parentWidth = omniboxMaxWidth
202- if parentWidth <= 0 || parentHeight <= 0 {
203- // Overlay may not be allocated yet (e.g., right after adding/switching panes).
204- // Retry once in the next main-loop tick and avoid spamming logs.
205- if !o.loggedInvalidOverlayDims {
206- log := logging.FromContext(o.ctx)
207- log.Error().
208- Int("parent_width", parentWidth).
209- Int("parent_height", parentHeight).
210- Msg("omnibox: invalid parent overlay dimensions")
211- o.loggedInvalidOverlayDims = true
212- }
213-
214- var cb glib.SourceFunc = func(uintptr) bool {
215- o.mu.RLock()
216- visible := o.visible
217- o.mu.RUnlock()
218- if visible {
219- o.resizeAndCenter(rowCount)
220- }
221- return false
222- }
223- glib.IdleAdd(&cb, 0)
224- return
225 }200 }
226 201
227 width := int(float64(parentWidth) * omniboxWidthPct)202 width := int(float64(parentWidth) * omniboxWidthPct)
@@ -493,6 +468,13 @@ func (o *Omnibox) handleKeyPress(keyval uint, state gdk.ModifierType) bool {
493 o.Hide(o.ctx)468 o.Hide(o.ctx)
494 return true469 return true
495 470
471+ case uint(gdk.KEY_l), uint(gdk.KEY_L):
472+ // Ctrl+L toggles omnibox (hides when visible)
473+ if ctrl {
474+ o.Hide(o.ctx)
475+ return true
476+ }
477+
496 case uint(gdk.KEY_Return), uint(gdk.KEY_KP_Enter):478 case uint(gdk.KEY_Return), uint(gdk.KEY_KP_Enter):
497 o.navigateToSelected()479 o.navigateToSelected()
498 return true480 return true
@@ -532,24 +514,31 @@ func (o *Omnibox) handleKeyPress(keyval uint, state gdk.ModifierType) bool {
532 }514 }
533 return false // Let entry handle 'y' for typing515 return false // Let entry handle 'y' for typing
534 516
517+ default:
518+ return o.handleCtrlNumberShortcut(keyval, ctrl)
519+ }
520+
521+ return false // Let entry handle the key
522+}
523+
524+// handleCtrlNumberShortcut handles Ctrl+1-9 and Ctrl+0 for quick navigation.
525+func (o *Omnibox) handleCtrlNumberShortcut(keyval uint, ctrl bool) bool {
526+ if !ctrl {
527+ return false
528+ }
529+
530+ const tenthItemIndex = 9
531+ switch keyval {
535 case uint(gdk.KEY_1), uint(gdk.KEY_2), uint(gdk.KEY_3), uint(gdk.KEY_4), uint(gdk.KEY_5),532 case uint(gdk.KEY_1), uint(gdk.KEY_2), uint(gdk.KEY_3), uint(gdk.KEY_4), uint(gdk.KEY_5),
536 uint(gdk.KEY_6), uint(gdk.KEY_7), uint(gdk.KEY_8), uint(gdk.KEY_9):533 uint(gdk.KEY_6), uint(gdk.KEY_7), uint(gdk.KEY_8), uint(gdk.KEY_9):
537- if ctrl {534+ index := int(keyval - uint(gdk.KEY_1))
538- const firstShortcutKey = uint(gdk.KEY_1)535+ o.selectAndNavigate(index)
539- index := int(keyval - firstShortcutKey) // 0-8536+ return true
540- o.selectAndNavigate(index)
541- return true
542- }
543-
544 case uint(gdk.KEY_0):537 case uint(gdk.KEY_0):
545- if ctrl {538+ o.selectAndNavigate(tenthItemIndex)
546- const tenthItemIndex = 9539+ return true
547- o.selectAndNavigate(tenthItemIndex)
548- return true
549- }
550 }540 }
551-541+ return false
552- return false // Let entry handle the key
553 }542 }
554 543
555 // onEntryChanged handles text input changes with debouncing.544 // onEntryChanged handles text input changes with debouncing.
@@ -1245,9 +1234,17 @@ func (o *Omnibox) Show(ctx context.Context, query string) {
1245 o.scrolledWin.SetMaxContentHeight(0)1234 o.scrolledWin.SetMaxContentHeight(0)
1246 }1235 }
1247 1236
1248- // Set width and vertical positioning (20% from top)1237+ // Get parent dimensions with fallback if not yet allocated
1249 parentWidth := o.parentOverlay.GetAllocatedWidth()1238 parentWidth := o.parentOverlay.GetAllocatedWidth()
1250 parentHeight := o.parentOverlay.GetAllocatedHeight()1239 parentHeight := o.parentOverlay.GetAllocatedHeight()
1240+ if parentWidth <= 0 {
1241+ parentWidth = omniboxMaxWidth
1242+ }
1243+ if parentHeight <= 0 {
1244+ parentHeight = omniboxFallbackHeight
1245+ }
1246+
1247+ // Set width and vertical positioning (20% from top)
1251 width := int(float64(parentWidth) * omniboxWidthPct)1248 width := int(float64(parentWidth) * omniboxWidthPct)
1252 if width > omniboxMaxWidth {1249 if width > omniboxMaxWidth {
1253 width = omniboxMaxWidth1250 width = omniboxMaxWidth
@@ -1258,7 +1255,6 @@ func (o *Omnibox) Show(ctx context.Context, query string) {
1258 o.outerBox.SetMarginTop(marginTop)1255 o.outerBox.SetMarginTop(marginTop)
1259 1256
1260 // Show the omnibox1257 // Show the omnibox
1261- o.loggedInvalidOverlayDims = false
1262 o.outerBox.SetVisible(true)1258 o.outerBox.SetVisible(true)
1263 1259
1264 // Trigger initial resize (will be updated when results arrive)1260 // Trigger initial resize (will be updated when results arrive)
M internal/ui/component/workspace_view.go +23 -6
@@ -188,8 +188,16 @@ func (wv *WorkspaceView) SetActivePaneID(paneID entity.PaneID) error {
188188 func (wv *WorkspaceView) setActivePaneIDInternal(paneID entity.PaneID) error {
189189 currentActiveID := wv.getActivePaneIDInternal()
190190
191+ wv.logger.Debug().
192+ Str("from_pane", string(currentActiveID)).
193+ Str("to_pane", string(paneID)).
194+ Bool("omnibox_visible", wv.omnibox != nil).
195+ Str("omnibox_pane", string(wv.omniboxPaneID)).
196+ Msg("active pane changing")
197+
191198 // Destroy omnibox if active pane is changing
192199 if currentActiveID != paneID && wv.omnibox != nil {
200+ wv.logger.Debug().Msg("destroying omnibox due to pane change")
193201 wv.hideOmniboxInternal()
194202 }
195203 // Destroy find bar if active pane is changing
@@ -478,12 +486,6 @@ func (wv *WorkspaceView) ShowOmnibox(ctx context.Context, query string) {
478486 wv.mu.Lock()
479487 defer wv.mu.Unlock()
480488
481- // If omnibox already exists, just show it
482- if wv.omnibox != nil {
483- wv.omnibox.Show(ctx, query)
484- return
485- }
486-
487489 // Get the active pane view
488490 activePaneID := wv.getActivePaneIDInternal()
489491 pv := wv.paneViews[activePaneID]
@@ -492,6 +494,21 @@ func (wv *WorkspaceView) ShowOmnibox(ctx context.Context, query string) {
492494 return
493495 }
494496
497+ // If omnibox exists but on different pane, destroy and recreate on active pane
498+ if wv.omnibox != nil {
499+ if wv.omniboxPaneID != activePaneID {
500+ wv.logger.Debug().
501+ Str("old_pane", string(wv.omniboxPaneID)).
502+ Str("new_pane", string(activePaneID)).
503+ Msg("omnibox pane changed, recreating")
504+ wv.hideOmniboxInternal()
505+ } else {
506+ wv.logger.Debug().Str("pane", string(activePaneID)).Msg("showing existing omnibox")
507+ wv.omnibox.Show(ctx, query)
508+ return
509+ }
510+ }
511+
495512 // Create omnibox with pane-specific toast callback
496513 cfg := wv.omniboxCfg
497514 cfg.OnToast = func(message string) {
@@ -188,8 +188,16 @@ func (wv *WorkspaceView) SetActivePaneID(paneID entity.PaneID) error {
188 func (wv *WorkspaceView) setActivePaneIDInternal(paneID entity.PaneID) error {188 func (wv *WorkspaceView) setActivePaneIDInternal(paneID entity.PaneID) error {
189 currentActiveID := wv.getActivePaneIDInternal()189 currentActiveID := wv.getActivePaneIDInternal()
190 190
191+ wv.logger.Debug().
192+ Str("from_pane", string(currentActiveID)).
193+ Str("to_pane", string(paneID)).
194+ Bool("omnibox_visible", wv.omnibox != nil).
195+ Str("omnibox_pane", string(wv.omniboxPaneID)).
196+ Msg("active pane changing")
197+
191 // Destroy omnibox if active pane is changing198 // Destroy omnibox if active pane is changing
192 if currentActiveID != paneID && wv.omnibox != nil {199 if currentActiveID != paneID && wv.omnibox != nil {
200+ wv.logger.Debug().Msg("destroying omnibox due to pane change")
193 wv.hideOmniboxInternal()201 wv.hideOmniboxInternal()
194 }202 }
195 // Destroy find bar if active pane is changing203 // Destroy find bar if active pane is changing
@@ -478,12 +486,6 @@ func (wv *WorkspaceView) ShowOmnibox(ctx context.Context, query string) {
478 wv.mu.Lock()486 wv.mu.Lock()
479 defer wv.mu.Unlock()487 defer wv.mu.Unlock()
480 488
481- // If omnibox already exists, just show it
482- if wv.omnibox != nil {
483- wv.omnibox.Show(ctx, query)
484- return
485- }
486-
487 // Get the active pane view489 // Get the active pane view
488 activePaneID := wv.getActivePaneIDInternal()490 activePaneID := wv.getActivePaneIDInternal()
489 pv := wv.paneViews[activePaneID]491 pv := wv.paneViews[activePaneID]
@@ -492,6 +494,21 @@ func (wv *WorkspaceView) ShowOmnibox(ctx context.Context, query string) {
492 return494 return
493 }495 }
494 496
497+ // If omnibox exists but on different pane, destroy and recreate on active pane
498+ if wv.omnibox != nil {
499+ if wv.omniboxPaneID != activePaneID {
500+ wv.logger.Debug().
501+ Str("old_pane", string(wv.omniboxPaneID)).
502+ Str("new_pane", string(activePaneID)).
503+ Msg("omnibox pane changed, recreating")
504+ wv.hideOmniboxInternal()
505+ } else {
506+ wv.logger.Debug().Str("pane", string(activePaneID)).Msg("showing existing omnibox")
507+ wv.omnibox.Show(ctx, query)
508+ return
509+ }
510+ }
511+
495 // Create omnibox with pane-specific toast callback512 // Create omnibox with pane-specific toast callback
496 cfg := wv.omniboxCfg513 cfg := wv.omniboxCfg
497 cfg.OnToast = func(message string) {514 cfg.OnToast = func(message string) {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close