v0.27.1 c78ce83

gesture.go RawHistoryFolder

1package input
2
3import (
4	"context"
5	"sync"
6
7	"github.com/bnema/dumber/internal/logging"
8	"github.com/jwijenbergh/puregotk/v4/gtk"
9)
10
11// DirectNavigator allows direct back/forward navigation on a WebView.
12// This is used to call GoBack/GoForward directly from gesture handlers
13// without going through callback chains, preserving user gesture context.
14type DirectNavigator interface {
15	GoBackDirect()
16	GoForwardDirect()
17}
18
19// gtkEventSequenceClaimed matches GTK_EVENT_SEQUENCE_CLAIMED (value 1)
20// Used to tell GTK we've handled the gesture and to stop propagating it.
21const gtkEventSequenceClaimed = gtk.EventSequenceClaimedValue
22
23const (
24	// Mouse button constants for X11/Wayland
25	mouseButtonBack    = 8 // Side button - back
26	mouseButtonForward = 9 // Side button - forward
27)
28
29// GestureHandler handles mouse button gestures for navigation.
30// It recognizes mouse buttons 8 (back) and 9 (forward) on WebView widgets.
31type GestureHandler struct {
32	clickGesture *gtk.GestureClick
33
34	// Callback retention: must stay reachable by Go GC.
35	pressedCb func(gtk.GestureClick, int, float64, float64)
36
37	// Action handler callback (fallback if no direct navigator)
38	onAction ActionHandler
39
40	// Direct navigator for calling GoBack/GoForward without callback chains.
41	// This preserves user gesture context like Epiphany does.
42	navigator DirectNavigator
43
44	ctx context.Context
45	mu  sync.RWMutex
46}
47
48// NewGestureHandler creates a new gesture handler.
49func NewGestureHandler(ctx context.Context) *GestureHandler {
50	log := logging.FromContext(ctx)
51	log.Debug().Msg("creating gesture handler")
52
53	return &GestureHandler{
54		ctx: ctx,
55	}
56}
57
58// SetOnAction sets the callback for when navigation actions are triggered.
59// This is a fallback if no DirectNavigator is set.
60func (h *GestureHandler) SetOnAction(fn ActionHandler) {
61	h.mu.Lock()
62	defer h.mu.Unlock()
63	h.onAction = fn
64}
65
66// SetNavigator sets a direct navigator for back/forward operations.
67// When set, GoBack/GoForward are called directly without callback chains,
68// preserving user gesture context.
69func (h *GestureHandler) SetNavigator(nav DirectNavigator) {
70	h.mu.Lock()
71	defer h.mu.Unlock()
72	h.navigator = nav
73}
74
75// AttachTo attaches the gesture handler to a GTK widget.
76// Typically called with a WebView widget to enable mouse button navigation.
77func (h *GestureHandler) AttachTo(widget *gtk.Widget) {
78	log := logging.FromContext(h.ctx)
79
80	if widget == nil {
81		log.Error().Msg("cannot attach gesture handler to nil widget")
82		return
83	}
84
85	// Create click gesture that listens to all buttons
86	h.clickGesture = gtk.NewGestureClick()
87	if h.clickGesture == nil {
88		log.Error().Msg("failed to create gesture click")
89		return
90	}
91
92	// Listen to all mouse buttons (not just primary)
93	h.clickGesture.SetButton(0)
94
95	// Connect pressed handler (retain callback to prevent GC)
96	h.pressedCb = func(_ gtk.GestureClick, nPress int, _ float64, _ float64) {
97		h.handlePressed(nPress)
98	}
99	h.clickGesture.ConnectPressed(&h.pressedCb)
100
101	// Add controller to widget
102	widget.AddController(&h.clickGesture.EventController)
103
104	log.Debug().Msg("gesture handler attached to widget")
105}
106
107// handlePressed processes a mouse button press event.
108func (h *GestureHandler) handlePressed(nPress int) {
109	// Only handle single clicks for navigation
110	if nPress != 1 {
111		return
112	}
113
114	// Get the button that was pressed
115	button := h.clickGesture.GetCurrentButton()
116
117	log := logging.FromContext(h.ctx)
118
119	// Try direct navigation first (preserves user gesture context)
120	h.mu.RLock()
121	nav := h.navigator
122	handler := h.onAction
123	h.mu.RUnlock()
124
125	handled := false
126
127	switch button {
128	case mouseButtonBack:
129		if nav != nil {
130			log.Debug().Uint("button", button).Msg("gesture: direct go back")
131			nav.GoBackDirect()
132			handled = true
133		} else if handler != nil {
134			if err := handler(h.ctx, ActionGoBack); err != nil {
135				log.Warn().Err(err).Msg("gesture action failed")
136			}
137			handled = true
138		}
139	case mouseButtonForward:
140		if nav != nil {
141			log.Debug().Uint("button", button).Msg("gesture: direct go forward")
142			nav.GoForwardDirect()
143			handled = true
144		} else if handler != nil {
145			if err := handler(h.ctx, ActionGoForward); err != nil {
146				log.Warn().Err(err).Msg("gesture action failed")
147			}
148			handled = true
149		}
150	default:
151		// Not a navigation button, ignore
152		return
153	}
154
155	// Claim the gesture sequence to stop event propagation.
156	// This matches Epiphany's behavior: gtk_gesture_set_state(gesture, GTK_EVENT_SEQUENCE_CLAIMED)
157	// Without this, GTK may continue propagating the event which can interfere with WebKit.
158	if handled {
159		h.clickGesture.SetState(gtkEventSequenceClaimed)
160	}
161}
162
163// Detach removes the gesture handler.
164// Note: GTK handles cleanup when the widget is destroyed,
165// but we clear our reference here.
166func (h *GestureHandler) Detach() {
167	h.clickGesture = nil
168	h.pressedCb = nil
169}

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