detached 30cf952

feat(input): add gesture handler for mouse button navigation

commit
30cf952b83be1c884a86487cd2edadf84ce4d465
author
brice · · 2025-12-16 19:01 UTC
committer
brice · · 2025-12-16 19:07 UTC
parent
5af29c5
signature
unsigned
1 file · +121 -0
A internal/ui/input/gesture.go +121 -0
@@ -0,0 +1,121 @@
1+package input
2+
3+import (
4+ "context"
5+ "sync"
6+
7+ "github.com/bnema/dumber/internal/logging"
8+ "github.com/jwijenbergh/puregotk/v4/gtk"
9+)
10+
11+const (
12+ // Mouse button constants for X11/Wayland
13+ mouseButtonBack = 8 // Side button - back
14+ mouseButtonForward = 9 // Side button - forward
15+)
16+
17+// GestureHandler handles mouse button gestures for navigation.
18+// It recognizes mouse buttons 8 (back) and 9 (forward) on WebView widgets.
19+type GestureHandler struct {
20+ clickGesture *gtk.GestureClick
21+
22+ // Action handler callback
23+ onAction ActionHandler
24+
25+ ctx context.Context
26+ mu sync.RWMutex
27+}
28+
29+// NewGestureHandler creates a new gesture handler.
30+func NewGestureHandler(ctx context.Context) *GestureHandler {
31+ log := logging.FromContext(ctx)
32+ log.Debug().Msg("creating gesture handler")
33+
34+ return &GestureHandler{
35+ ctx: ctx,
36+ }
37+}
38+
39+// SetOnAction sets the callback for when navigation actions are triggered.
40+func (h *GestureHandler) SetOnAction(fn ActionHandler) {
41+ h.mu.Lock()
42+ defer h.mu.Unlock()
43+ h.onAction = fn
44+}
45+
46+// AttachTo attaches the gesture handler to a GTK widget.
47+// Typically called with a WebView widget to enable mouse button navigation.
48+func (h *GestureHandler) AttachTo(widget *gtk.Widget) {
49+ log := logging.FromContext(h.ctx)
50+
51+ if widget == nil {
52+ log.Error().Msg("cannot attach gesture handler to nil widget")
53+ return
54+ }
55+
56+ // Create click gesture that listens to all buttons
57+ h.clickGesture = gtk.NewGestureClick()
58+ if h.clickGesture == nil {
59+ log.Error().Msg("failed to create gesture click")
60+ return
61+ }
62+
63+ // Listen to all mouse buttons (not just primary)
64+ h.clickGesture.SetButton(0)
65+
66+ // Connect pressed handler
67+ pressedCb := func(gesture gtk.GestureClick, nPress int, x float64, y float64) {
68+ h.handlePressed(nPress)
69+ }
70+ h.clickGesture.ConnectPressed(&pressedCb)
71+
72+ // Add controller to widget
73+ widget.AddController(&h.clickGesture.EventController)
74+
75+ log.Debug().Msg("gesture handler attached to widget")
76+}
77+
78+// handlePressed processes a mouse button press event.
79+func (h *GestureHandler) handlePressed(nPress int) {
80+ // Only handle single clicks for navigation
81+ if nPress != 1 {
82+ return
83+ }
84+
85+ // Get the button that was pressed
86+ button := h.clickGesture.GetCurrentButton()
87+
88+ var action Action
89+ switch button {
90+ case mouseButtonBack:
91+ action = ActionGoBack
92+ case mouseButtonForward:
93+ action = ActionGoForward
94+ default:
95+ // Not a navigation button, ignore
96+ return
97+ }
98+
99+ // Dispatch action to handler
100+ h.mu.RLock()
101+ handler := h.onAction
102+ h.mu.RUnlock()
103+
104+ if handler != nil {
105+ if err := handler(h.ctx, action); err != nil {
106+ log := logging.FromContext(h.ctx)
107+ log.Error().
108+ Err(err).
109+ Str("action", string(action)).
110+ Uint("button", button).
111+ Msg("gesture action handler error")
112+ }
113+ }
114+}
115+
116+// Detach removes the gesture handler.
117+// Note: GTK handles cleanup when the widget is destroyed,
118+// but we clear our reference here.
119+func (h *GestureHandler) Detach() {
120+ h.clickGesture = nil
121+}
@@ -0,0 +1,121 @@
1+package input
2+
3+import (
4+ "context"
5+ "sync"
6+
7+ "github.com/bnema/dumber/internal/logging"
8+ "github.com/jwijenbergh/puregotk/v4/gtk"
9+)
10+
11+const (
12+ // Mouse button constants for X11/Wayland
13+ mouseButtonBack = 8 // Side button - back
14+ mouseButtonForward = 9 // Side button - forward
15+)
16+
17+// GestureHandler handles mouse button gestures for navigation.
18+// It recognizes mouse buttons 8 (back) and 9 (forward) on WebView widgets.
19+type GestureHandler struct {
20+ clickGesture *gtk.GestureClick
21+
22+ // Action handler callback
23+ onAction ActionHandler
24+
25+ ctx context.Context
26+ mu sync.RWMutex
27+}
28+
29+// NewGestureHandler creates a new gesture handler.
30+func NewGestureHandler(ctx context.Context) *GestureHandler {
31+ log := logging.FromContext(ctx)
32+ log.Debug().Msg("creating gesture handler")
33+
34+ return &GestureHandler{
35+ ctx: ctx,
36+ }
37+}
38+
39+// SetOnAction sets the callback for when navigation actions are triggered.
40+func (h *GestureHandler) SetOnAction(fn ActionHandler) {
41+ h.mu.Lock()
42+ defer h.mu.Unlock()
43+ h.onAction = fn
44+}
45+
46+// AttachTo attaches the gesture handler to a GTK widget.
47+// Typically called with a WebView widget to enable mouse button navigation.
48+func (h *GestureHandler) AttachTo(widget *gtk.Widget) {
49+ log := logging.FromContext(h.ctx)
50+
51+ if widget == nil {
52+ log.Error().Msg("cannot attach gesture handler to nil widget")
53+ return
54+ }
55+
56+ // Create click gesture that listens to all buttons
57+ h.clickGesture = gtk.NewGestureClick()
58+ if h.clickGesture == nil {
59+ log.Error().Msg("failed to create gesture click")
60+ return
61+ }
62+
63+ // Listen to all mouse buttons (not just primary)
64+ h.clickGesture.SetButton(0)
65+
66+ // Connect pressed handler
67+ pressedCb := func(gesture gtk.GestureClick, nPress int, x float64, y float64) {
68+ h.handlePressed(nPress)
69+ }
70+ h.clickGesture.ConnectPressed(&pressedCb)
71+
72+ // Add controller to widget
73+ widget.AddController(&h.clickGesture.EventController)
74+
75+ log.Debug().Msg("gesture handler attached to widget")
76+}
77+
78+// handlePressed processes a mouse button press event.
79+func (h *GestureHandler) handlePressed(nPress int) {
80+ // Only handle single clicks for navigation
81+ if nPress != 1 {
82+ return
83+ }
84+
85+ // Get the button that was pressed
86+ button := h.clickGesture.GetCurrentButton()
87+
88+ var action Action
89+ switch button {
90+ case mouseButtonBack:
91+ action = ActionGoBack
92+ case mouseButtonForward:
93+ action = ActionGoForward
94+ default:
95+ // Not a navigation button, ignore
96+ return
97+ }
98+
99+ // Dispatch action to handler
100+ h.mu.RLock()
101+ handler := h.onAction
102+ h.mu.RUnlock()
103+
104+ if handler != nil {
105+ if err := handler(h.ctx, action); err != nil {
106+ log := logging.FromContext(h.ctx)
107+ log.Error().
108+ Err(err).
109+ Str("action", string(action)).
110+ Uint("button", button).
111+ Msg("gesture action handler error")
112+ }
113+ }
114+}
115+
116+// Detach removes the gesture handler.
117+// Note: GTK handles cleanup when the widget is destroyed,
118+// but we clear our reference here.
119+func (h *GestureHandler) Detach() {
120+ h.clickGesture = nil
121+}

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