detached d0dbbe7

fix(ui): fix omnibox pane switching, Ctrl+L handling, and overlay dimensions

commit
d0dbbe7226e6fcbd581c3753644fdf52a9e01afb
author
bnema · · 2025-12-22 21:06 UTC
parent
4b644f1
signature
unsigned
7 files · +78 -23
M dev/check-docs.go +3 -0
@@ -1,3 +1,6 @@
1+//go:build devtools
2+// +build devtools
3+
14 package main
25
36 import (
@@ -1,3 +1,6 @@
1+//go:build devtools
2+// +build devtools
3+
1 package main4 package main
2 5
3 import (6 import (
M go.mod +4 -7
@@ -3,12 +3,12 @@ module github.com/bnema/dumber
33 go 1.25.3
44
55 require (
6- github.com/bnema/puregotk-webkit v0.0.0-00010101000000-000000000000
6+ github.com/bnema/puregotk-webkit v0.0.7
77 github.com/charmbracelet/bubbles v0.21.0
88 github.com/charmbracelet/bubbletea v1.3.10
99 github.com/charmbracelet/lipgloss v1.1.0
1010 github.com/fsnotify/fsnotify v1.9.0
11- github.com/jwijenbergh/puregotk v0.0.0-20251022075221-eae1610c7d83
11+ github.com/jwijenbergh/puregotk v0.0.0-20251222174431-73930527faa3
1212 github.com/ncruces/go-sqlite3 v0.30.3
1313 github.com/pressly/goose/v3 v3.26.0
1414 github.com/rs/zerolog v1.33.0
@@ -17,6 +17,8 @@ require (
1717 github.com/stretchr/testify v1.11.1
1818 )
1919
20+replace github.com/jwijenbergh/puregotk => github.com/bnema/puregotk v0.0.0-20251222174431-73930527faa3
21+
2022 require (
2123 github.com/atotto/clipboard v0.1.4 // indirect
2224 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
@@ -60,8 +62,3 @@ require (
6062 golang.org/x/text v0.31.0 // indirect
6163 gopkg.in/yaml.v3 v3.0.1 // indirect
6264 )
63-
64-replace (
65- github.com/bnema/puregotk-webkit => ../puregotk-webkit
66- github.com/jwijenbergh/puregotk => ../puregotk
67-)
@@ -3,12 +3,12 @@ module github.com/bnema/dumber
3 go 1.25.33 go 1.25.3
4 4
5 require (5 require (
6- github.com/bnema/puregotk-webkit v0.0.0-00010101000000-0000000000006+ github.com/bnema/puregotk-webkit v0.0.7
7 github.com/charmbracelet/bubbles v0.21.07 github.com/charmbracelet/bubbles v0.21.0
8 github.com/charmbracelet/bubbletea v1.3.108 github.com/charmbracelet/bubbletea v1.3.10
9 github.com/charmbracelet/lipgloss v1.1.09 github.com/charmbracelet/lipgloss v1.1.0
10 github.com/fsnotify/fsnotify v1.9.010 github.com/fsnotify/fsnotify v1.9.0
11- github.com/jwijenbergh/puregotk v0.0.0-20251022075221-eae1610c7d8311+ github.com/jwijenbergh/puregotk v0.0.0-20251222174431-73930527faa3
12 github.com/ncruces/go-sqlite3 v0.30.312 github.com/ncruces/go-sqlite3 v0.30.3
13 github.com/pressly/goose/v3 v3.26.013 github.com/pressly/goose/v3 v3.26.0
14 github.com/rs/zerolog v1.33.014 github.com/rs/zerolog v1.33.0
@@ -17,6 +17,8 @@ require (
17 github.com/stretchr/testify v1.11.117 github.com/stretchr/testify v1.11.1
18 )18 )
19 19
20+replace github.com/jwijenbergh/puregotk => github.com/bnema/puregotk v0.0.0-20251222174431-73930527faa3
21+
20 require (22 require (
21 github.com/atotto/clipboard v0.1.4 // indirect23 github.com/atotto/clipboard v0.1.4 // indirect
22 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect24 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
@@ -60,8 +62,3 @@ require (
60 golang.org/x/text v0.31.0 // indirect62 golang.org/x/text v0.31.0 // indirect
61 gopkg.in/yaml.v3 v3.0.1 // indirect63 gopkg.in/yaml.v3 v3.0.1 // indirect
62 )64 )
63-
64-replace (
65- github.com/bnema/puregotk-webkit => ../puregotk-webkit
66- github.com/jwijenbergh/puregotk => ../puregotk
67-)
M go.sum +4 -0 generated file: collapsed
M internal/ui/component/omnibox.go +40 -5
@@ -107,6 +107,12 @@ type Omnibox struct {
107107 singleRow int // single ListBoxRow natural height
108108 valid bool // whether cache is valid
109109 }
110+
111+ // Callback retention: keep GTK signal callbacks reachable by Go GC.
112+ retainedCallbacks []interface{}
113+
114+ // Used to rate-limit allocation-related logs.
115+ loggedInvalidOverlayDims bool
110116 }
111117
112118 // OmniboxConfig holds configuration for creating an Omnibox.
@@ -194,11 +200,27 @@ func (o *Omnibox) resizeAndCenter(rowCount int) {
194200 parentHeight := o.parentOverlay.GetAllocatedHeight()
195201
196202 if parentWidth <= 0 || parentHeight <= 0 {
197- log := logging.FromContext(o.ctx)
198- log.Error().
199- Int("parent_width", parentWidth).
200- Int("parent_height", parentHeight).
201- Msg("omnibox: invalid parent overlay dimensions")
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)
202224 return
203225 }
204226
@@ -311,6 +333,7 @@ func (o *Omnibox) setupKeyboardHandling() {
311333 keyPressedCb := func(_ gtk.EventControllerKey, keyval uint, _ uint, state gdk.ModifierType) bool {
312334 return o.handleKeyPress(keyval, state)
313335 }
336+ o.retainedCallbacks = append(o.retainedCallbacks, keyPressedCb)
314337 controller.ConnectKeyPressed(&keyPressedCb)
315338
316339 o.outerBox.AddController(&controller.EventController)
@@ -363,11 +386,13 @@ func (o *Omnibox) initHeader() error {
363386 historyClickCb := func(_ gtk.Button) {
364387 o.setViewMode(ViewModeHistory)
365388 }
389+ o.retainedCallbacks = append(o.retainedCallbacks, historyClickCb)
366390 o.historyBtn.ConnectClicked(&historyClickCb)
367391
368392 favoritesClickCb := func(_ gtk.Button) {
369393 o.setViewMode(ViewModeFavorites)
370394 }
395+ o.retainedCallbacks = append(o.retainedCallbacks, favoritesClickCb)
371396 o.favoritesBtn.ConnectClicked(&favoritesClickCb)
372397
373398 o.zoomLabel = gtk.NewLabel(nil)
@@ -424,6 +449,7 @@ func (o *Omnibox) initList() error {
424449 o.mu.Unlock()
425450 }
426451 }
452+ o.retainedCallbacks = append(o.retainedCallbacks, rowSelectedCb)
427453 o.listBox.ConnectRowSelected(&rowSelectedCb)
428454 return nil
429455 }
@@ -448,6 +474,7 @@ func (o *Omnibox) setupEntryChanged() {
448474 changedCb := func(_ gtk.SearchEntry) {
449475 o.onEntryChanged()
450476 }
477+ o.retainedCallbacks = append(o.retainedCallbacks, changedCb)
451478 o.entry.ConnectSearchChanged(&changedCb)
452479 }
453480
@@ -1231,6 +1258,7 @@ func (o *Omnibox) Show(ctx context.Context, query string) {
12311258 o.outerBox.SetMarginTop(marginTop)
12321259
12331260 // Show the omnibox
1261+ o.loggedInvalidOverlayDims = false
12341262 o.outerBox.SetVisible(true)
12351263
12361264 // Trigger initial resize (will be updated when results arrive)
@@ -1359,6 +1387,13 @@ func (o *Omnibox) Destroy() {
13591387 }
13601388 o.debounceMu.Unlock()
13611389
1390+ o.mu.Lock()
1391+ o.visible = false
1392+ o.mu.Unlock()
1393+
1394+ o.parentOverlay = nil
1395+ o.retainedCallbacks = nil
1396+
13621397 if o.outerBox != nil {
13631398 o.outerBox.Unparent()
13641399 o.outerBox = nil
@@ -107,6 +107,12 @@ type Omnibox struct {
107 singleRow int // single ListBoxRow natural height107 singleRow int // single ListBoxRow natural height
108 valid bool // whether cache is valid108 valid bool // whether cache is valid
109 }109 }
110+
111+ // Callback retention: keep GTK signal callbacks reachable by Go GC.
112+ retainedCallbacks []interface{}
113+
114+ // Used to rate-limit allocation-related logs.
115+ loggedInvalidOverlayDims bool
110 }116 }
111 117
112 // OmniboxConfig holds configuration for creating an Omnibox.118 // OmniboxConfig holds configuration for creating an Omnibox.
@@ -194,11 +200,27 @@ func (o *Omnibox) resizeAndCenter(rowCount int) {
194 parentHeight := o.parentOverlay.GetAllocatedHeight()200 parentHeight := o.parentOverlay.GetAllocatedHeight()
195 201
196 if parentWidth <= 0 || parentHeight <= 0 {202 if parentWidth <= 0 || parentHeight <= 0 {
197- log := logging.FromContext(o.ctx)203+ // Overlay may not be allocated yet (e.g., right after adding/switching panes).
198- log.Error().204+ // Retry once in the next main-loop tick and avoid spamming logs.
199- Int("parent_width", parentWidth).205+ if !o.loggedInvalidOverlayDims {
200- Int("parent_height", parentHeight).206+ log := logging.FromContext(o.ctx)
201- Msg("omnibox: invalid parent overlay dimensions")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)
202 return224 return
203 }225 }
204 226
@@ -311,6 +333,7 @@ func (o *Omnibox) setupKeyboardHandling() {
311 keyPressedCb := func(_ gtk.EventControllerKey, keyval uint, _ uint, state gdk.ModifierType) bool {333 keyPressedCb := func(_ gtk.EventControllerKey, keyval uint, _ uint, state gdk.ModifierType) bool {
312 return o.handleKeyPress(keyval, state)334 return o.handleKeyPress(keyval, state)
313 }335 }
336+ o.retainedCallbacks = append(o.retainedCallbacks, keyPressedCb)
314 controller.ConnectKeyPressed(&keyPressedCb)337 controller.ConnectKeyPressed(&keyPressedCb)
315 338
316 o.outerBox.AddController(&controller.EventController)339 o.outerBox.AddController(&controller.EventController)
@@ -363,11 +386,13 @@ func (o *Omnibox) initHeader() error {
363 historyClickCb := func(_ gtk.Button) {386 historyClickCb := func(_ gtk.Button) {
364 o.setViewMode(ViewModeHistory)387 o.setViewMode(ViewModeHistory)
365 }388 }
389+ o.retainedCallbacks = append(o.retainedCallbacks, historyClickCb)
366 o.historyBtn.ConnectClicked(&historyClickCb)390 o.historyBtn.ConnectClicked(&historyClickCb)
367 391
368 favoritesClickCb := func(_ gtk.Button) {392 favoritesClickCb := func(_ gtk.Button) {
369 o.setViewMode(ViewModeFavorites)393 o.setViewMode(ViewModeFavorites)
370 }394 }
395+ o.retainedCallbacks = append(o.retainedCallbacks, favoritesClickCb)
371 o.favoritesBtn.ConnectClicked(&favoritesClickCb)396 o.favoritesBtn.ConnectClicked(&favoritesClickCb)
372 397
373 o.zoomLabel = gtk.NewLabel(nil)398 o.zoomLabel = gtk.NewLabel(nil)
@@ -424,6 +449,7 @@ func (o *Omnibox) initList() error {
424 o.mu.Unlock()449 o.mu.Unlock()
425 }450 }
426 }451 }
452+ o.retainedCallbacks = append(o.retainedCallbacks, rowSelectedCb)
427 o.listBox.ConnectRowSelected(&rowSelectedCb)453 o.listBox.ConnectRowSelected(&rowSelectedCb)
428 return nil454 return nil
429 }455 }
@@ -448,6 +474,7 @@ func (o *Omnibox) setupEntryChanged() {
448 changedCb := func(_ gtk.SearchEntry) {474 changedCb := func(_ gtk.SearchEntry) {
449 o.onEntryChanged()475 o.onEntryChanged()
450 }476 }
477+ o.retainedCallbacks = append(o.retainedCallbacks, changedCb)
451 o.entry.ConnectSearchChanged(&changedCb)478 o.entry.ConnectSearchChanged(&changedCb)
452 }479 }
453 480
@@ -1231,6 +1258,7 @@ func (o *Omnibox) Show(ctx context.Context, query string) {
1231 o.outerBox.SetMarginTop(marginTop)1258 o.outerBox.SetMarginTop(marginTop)
1232 1259
1233 // Show the omnibox1260 // Show the omnibox
1261+ o.loggedInvalidOverlayDims = false
1234 o.outerBox.SetVisible(true)1262 o.outerBox.SetVisible(true)
1235 1263
1236 // Trigger initial resize (will be updated when results arrive)1264 // Trigger initial resize (will be updated when results arrive)
@@ -1359,6 +1387,13 @@ func (o *Omnibox) Destroy() {
1359 }1387 }
1360 o.debounceMu.Unlock()1388 o.debounceMu.Unlock()
1361 1389
1390+ o.mu.Lock()
1391+ o.visible = false
1392+ o.mu.Unlock()
1393+
1394+ o.parentOverlay = nil
1395+ o.retainedCallbacks = nil
1396+
1362 if o.outerBox != nil {1397 if o.outerBox != nil {
1363 o.outerBox.Unparent()1398 o.outerBox.Unparent()
1364 o.outerBox = nil1399 o.outerBox = nil
M internal/ui/input/gesture.go +7 -3
@@ -19,6 +19,9 @@ const (
1919 type GestureHandler struct {
2020 clickGesture *gtk.GestureClick
2121
22+ // Callback retention: must stay reachable by Go GC.
23+ pressedCb func(gtk.GestureClick, int, float64, float64)
24+
2225 // Action handler callback
2326 onAction ActionHandler
2427
@@ -63,11 +66,11 @@ func (h *GestureHandler) AttachTo(widget *gtk.Widget) {
6366 // Listen to all mouse buttons (not just primary)
6467 h.clickGesture.SetButton(0)
6568
66- // Connect pressed handler
67- pressedCb := func(_ gtk.GestureClick, nPress int, _ float64, _ float64) {
69+ // Connect pressed handler (retain callback to prevent GC)
70+ h.pressedCb = func(_ gtk.GestureClick, nPress int, _ float64, _ float64) {
6871 h.handlePressed(nPress)
6972 }
70- h.clickGesture.ConnectPressed(&pressedCb)
73+ h.clickGesture.ConnectPressed(&h.pressedCb)
7174
7275 // Add controller to widget
7376 widget.AddController(&h.clickGesture.EventController)
@@ -118,4 +121,5 @@ func (h *GestureHandler) handlePressed(nPress int) {
118121 // but we clear our reference here.
119122 func (h *GestureHandler) Detach() {
120123 h.clickGesture = nil
124+ h.pressedCb = nil
121125 }
@@ -19,6 +19,9 @@ const (
19 type GestureHandler struct {19 type GestureHandler struct {
20 clickGesture *gtk.GestureClick20 clickGesture *gtk.GestureClick
21 21
22+ // Callback retention: must stay reachable by Go GC.
23+ pressedCb func(gtk.GestureClick, int, float64, float64)
24+
22 // Action handler callback25 // Action handler callback
23 onAction ActionHandler26 onAction ActionHandler
24 27
@@ -63,11 +66,11 @@ func (h *GestureHandler) AttachTo(widget *gtk.Widget) {
63 // Listen to all mouse buttons (not just primary)66 // Listen to all mouse buttons (not just primary)
64 h.clickGesture.SetButton(0)67 h.clickGesture.SetButton(0)
65 68
66- // Connect pressed handler69+ // Connect pressed handler (retain callback to prevent GC)
67- pressedCb := func(_ gtk.GestureClick, nPress int, _ float64, _ float64) {70+ h.pressedCb = func(_ gtk.GestureClick, nPress int, _ float64, _ float64) {
68 h.handlePressed(nPress)71 h.handlePressed(nPress)
69 }72 }
70- h.clickGesture.ConnectPressed(&pressedCb)73+ h.clickGesture.ConnectPressed(&h.pressedCb)
71 74
72 // Add controller to widget75 // Add controller to widget
73 widget.AddController(&h.clickGesture.EventController)76 widget.AddController(&h.clickGesture.EventController)
@@ -118,4 +121,5 @@ func (h *GestureHandler) handlePressed(nPress int) {
118 // but we clear our reference here.121 // but we clear our reference here.
119 func (h *GestureHandler) Detach() {122 func (h *GestureHandler) Detach() {
120 h.clickGesture = nil123 h.clickGesture = nil
124+ h.pressedCb = nil
121 }125 }
M internal/ui/input/hover.go +11 -5
@@ -25,6 +25,10 @@ type HoverHandler struct {
2525 paneID entity.PaneID
2626 onEnter HoverCallback
2727
28+ // Callback retention: must stay reachable by Go GC.
29+ enterCb func(gtk.EventControllerMotion, float64, float64)
30+ leaveCb func(gtk.EventControllerMotion)
31+
2832 timer *time.Timer
2933 timerMu sync.Mutex
3034 canceled bool
@@ -63,17 +67,17 @@ func (h *HoverHandler) AttachTo(widget *gtk.Widget) {
6367 return
6468 }
6569
66- // Connect enter handler with debounce
67- enterCb := func(_ gtk.EventControllerMotion, _ float64, _ float64) {
70+ // Connect enter handler with debounce (retain callbacks to prevent GC)
71+ h.enterCb = func(_ gtk.EventControllerMotion, _ float64, _ float64) {
6872 h.handleEnter()
6973 }
70- h.motionCtrl.ConnectEnter(&enterCb)
74+ h.motionCtrl.ConnectEnter(&h.enterCb)
7175
7276 // Connect leave handler to cancel pending focus
73- leaveCb := func(_ gtk.EventControllerMotion) {
77+ h.leaveCb = func(_ gtk.EventControllerMotion) {
7478 h.handleLeave()
7579 }
76- h.motionCtrl.ConnectLeave(&leaveCb)
80+ h.motionCtrl.ConnectLeave(&h.leaveCb)
7781
7882 // Add controller to widget
7983 widget.AddController(&h.motionCtrl.EventController)
@@ -131,4 +135,6 @@ func (h *HoverHandler) Detach() {
131135 h.timerMu.Unlock()
132136
133137 h.motionCtrl = nil
138+ h.enterCb = nil
139+ h.leaveCb = nil
134140 }
@@ -25,6 +25,10 @@ type HoverHandler struct {
25 paneID entity.PaneID25 paneID entity.PaneID
26 onEnter HoverCallback26 onEnter HoverCallback
27 27
28+ // Callback retention: must stay reachable by Go GC.
29+ enterCb func(gtk.EventControllerMotion, float64, float64)
30+ leaveCb func(gtk.EventControllerMotion)
31+
28 timer *time.Timer32 timer *time.Timer
29 timerMu sync.Mutex33 timerMu sync.Mutex
30 canceled bool34 canceled bool
@@ -63,17 +67,17 @@ func (h *HoverHandler) AttachTo(widget *gtk.Widget) {
63 return67 return
64 }68 }
65 69
66- // Connect enter handler with debounce70+ // Connect enter handler with debounce (retain callbacks to prevent GC)
67- enterCb := func(_ gtk.EventControllerMotion, _ float64, _ float64) {71+ h.enterCb = func(_ gtk.EventControllerMotion, _ float64, _ float64) {
68 h.handleEnter()72 h.handleEnter()
69 }73 }
70- h.motionCtrl.ConnectEnter(&enterCb)74+ h.motionCtrl.ConnectEnter(&h.enterCb)
71 75
72 // Connect leave handler to cancel pending focus76 // Connect leave handler to cancel pending focus
73- leaveCb := func(_ gtk.EventControllerMotion) {77+ h.leaveCb = func(_ gtk.EventControllerMotion) {
74 h.handleLeave()78 h.handleLeave()
75 }79 }
76- h.motionCtrl.ConnectLeave(&leaveCb)80+ h.motionCtrl.ConnectLeave(&h.leaveCb)
77 81
78 // Add controller to widget82 // Add controller to widget
79 widget.AddController(&h.motionCtrl.EventController)83 widget.AddController(&h.motionCtrl.EventController)
@@ -131,4 +135,6 @@ func (h *HoverHandler) Detach() {
131 h.timerMu.Unlock()135 h.timerMu.Unlock()
132 136
133 h.motionCtrl = nil137 h.motionCtrl = nil
138+ h.enterCb = nil
139+ h.leaveCb = nil
134 }140 }
M internal/ui/input/keyboard.go +9 -3
@@ -31,6 +31,9 @@ type KeyboardHandler struct {
3131 // GTK controller (nil until attached)
3232 controller *gtk.EventControllerKey
3333
34+ // Callback retention: must stay reachable by Go GC.
35+ keyPressedCb func(gtk.EventControllerKey, uint, uint, gdk.ModifierType) bool
36+
3437 ctx context.Context
3538 mu sync.RWMutex
3639 }
@@ -98,11 +101,11 @@ func (h *KeyboardHandler) AttachTo(window *gtk.ApplicationWindow) {
98101 // Set capture phase to intercept events before WebView gets them
99102 h.controller.SetPropagationPhase(gtk.PhaseCaptureValue)
100103
101- // Connect key pressed handler
102- keyPressedCb := func(_ gtk.EventControllerKey, keyval uint, _ uint, state gdk.ModifierType) bool {
104+ // Connect key pressed handler (retain callback to prevent GC).
105+ h.keyPressedCb = func(_ gtk.EventControllerKey, keyval uint, _ uint, state gdk.ModifierType) bool {
103106 return h.handleKeyPress(keyval, state)
104107 }
105- h.controller.ConnectKeyPressed(&keyPressedCb)
108+ h.controller.ConnectKeyPressed(&h.keyPressedCb)
106109
107110 // Add controller to window
108111 window.AddController(&h.controller.EventController)
@@ -115,6 +118,7 @@ func (h *KeyboardHandler) AttachTo(window *gtk.ApplicationWindow) {
115118 // but we clear our reference here.
116119 func (h *KeyboardHandler) Detach() {
117120 h.controller = nil
121+ h.keyPressedCb = nil
118122 }
119123
120124 // handleKeyPress processes a key press event.
@@ -124,6 +128,8 @@ func (h *KeyboardHandler) handleKeyPress(keyval uint, state gdk.ModifierType) bo
124128 shouldBypass := h.shouldBypass
125129 h.mu.RUnlock()
126130 if shouldBypass != nil && shouldBypass() {
131+ log := logging.FromContext(h.ctx)
132+ log.Debug().Uint("keyval", keyval).Uint("state", uint(state)).Msg("keyboard handler bypassed")
127133 return false
128134 }
129135
@@ -31,6 +31,9 @@ type KeyboardHandler struct {
31 // GTK controller (nil until attached)31 // GTK controller (nil until attached)
32 controller *gtk.EventControllerKey32 controller *gtk.EventControllerKey
33 33
34+ // Callback retention: must stay reachable by Go GC.
35+ keyPressedCb func(gtk.EventControllerKey, uint, uint, gdk.ModifierType) bool
36+
34 ctx context.Context37 ctx context.Context
35 mu sync.RWMutex38 mu sync.RWMutex
36 }39 }
@@ -98,11 +101,11 @@ func (h *KeyboardHandler) AttachTo(window *gtk.ApplicationWindow) {
98 // Set capture phase to intercept events before WebView gets them101 // Set capture phase to intercept events before WebView gets them
99 h.controller.SetPropagationPhase(gtk.PhaseCaptureValue)102 h.controller.SetPropagationPhase(gtk.PhaseCaptureValue)
100 103
101- // Connect key pressed handler104+ // Connect key pressed handler (retain callback to prevent GC).
102- keyPressedCb := func(_ gtk.EventControllerKey, keyval uint, _ uint, state gdk.ModifierType) bool {105+ h.keyPressedCb = func(_ gtk.EventControllerKey, keyval uint, _ uint, state gdk.ModifierType) bool {
103 return h.handleKeyPress(keyval, state)106 return h.handleKeyPress(keyval, state)
104 }107 }
105- h.controller.ConnectKeyPressed(&keyPressedCb)108+ h.controller.ConnectKeyPressed(&h.keyPressedCb)
106 109
107 // Add controller to window110 // Add controller to window
108 window.AddController(&h.controller.EventController)111 window.AddController(&h.controller.EventController)
@@ -115,6 +118,7 @@ func (h *KeyboardHandler) AttachTo(window *gtk.ApplicationWindow) {
115 // but we clear our reference here.118 // but we clear our reference here.
116 func (h *KeyboardHandler) Detach() {119 func (h *KeyboardHandler) Detach() {
117 h.controller = nil120 h.controller = nil
121+ h.keyPressedCb = nil
118 }122 }
119 123
120 // handleKeyPress processes a key press event.124 // handleKeyPress processes a key press event.
@@ -124,6 +128,8 @@ func (h *KeyboardHandler) handleKeyPress(keyval uint, state gdk.ModifierType) bo
124 shouldBypass := h.shouldBypass128 shouldBypass := h.shouldBypass
125 h.mu.RUnlock()129 h.mu.RUnlock()
126 if shouldBypass != nil && shouldBypass() {130 if shouldBypass != nil && shouldBypass() {
131+ log := logging.FromContext(h.ctx)
132+ log.Debug().Uint("keyval", keyval).Uint("state", uint(state)).Msg("keyboard handler bypassed")
127 return false133 return false
128 }134 }
129 135

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