v0.23.2 bfd5eb4

hover.go RawHistoryFolder

1package input
2
3import (
4	"context"
5	"sync"
6	"time"
7
8	"github.com/bnema/dumber/internal/domain/entity"
9	"github.com/bnema/dumber/internal/logging"
10	"github.com/jwijenbergh/puregotk/v4/gtk"
11)
12
13const (
14	// HoverFocusDelay is the delay before switching focus on hover.
15	HoverFocusDelay = 150 * time.Millisecond
16)
17
18// HoverCallback is called when a pane should receive focus from hover.
19type HoverCallback func(paneID entity.PaneID)
20
21// HoverHandler handles mouse hover events for focus-follows-mouse behavior.
22// It uses a debounce timer to avoid rapid focus switches.
23type HoverHandler struct {
24	motionCtrl *gtk.EventControllerMotion
25	paneID     entity.PaneID
26	onEnter    HoverCallback
27
28	// Callback retention: must stay reachable by Go GC.
29	enterCb func(gtk.EventControllerMotion, float64, float64)
30	leaveCb func(gtk.EventControllerMotion)
31
32	timer    *time.Timer
33	timerMu  sync.Mutex
34	canceled bool
35
36	ctx context.Context
37}
38
39// NewHoverHandler creates a new hover handler for a specific pane.
40func NewHoverHandler(ctx context.Context, paneID entity.PaneID) *HoverHandler {
41	log := logging.FromContext(ctx)
42	log.Debug().Str("pane_id", string(paneID)).Msg("creating hover handler")
43
44	return &HoverHandler{
45		ctx:    ctx,
46		paneID: paneID,
47	}
48}
49
50// SetOnEnter sets the callback for when the pane should receive focus.
51func (h *HoverHandler) SetOnEnter(fn HoverCallback) {
52	h.onEnter = fn
53}
54
55// AttachTo attaches the hover handler to a GTK widget.
56func (h *HoverHandler) AttachTo(widget *gtk.Widget) {
57	log := logging.FromContext(h.ctx)
58
59	if widget == nil {
60		log.Error().Msg("cannot attach hover handler to nil widget")
61		return
62	}
63
64	h.motionCtrl = gtk.NewEventControllerMotion()
65	if h.motionCtrl == nil {
66		log.Error().Msg("failed to create motion controller")
67		return
68	}
69
70	// Connect enter handler with debounce (retain callbacks to prevent GC)
71	h.enterCb = func(_ gtk.EventControllerMotion, _ float64, _ float64) {
72		h.handleEnter()
73	}
74	h.motionCtrl.ConnectEnter(&h.enterCb)
75
76	// Connect leave handler to cancel pending focus
77	h.leaveCb = func(_ gtk.EventControllerMotion) {
78		h.handleLeave()
79	}
80	h.motionCtrl.ConnectLeave(&h.leaveCb)
81
82	// Add controller to widget
83	widget.AddController(&h.motionCtrl.EventController)
84
85	log.Debug().Str("pane_id", string(h.paneID)).Msg("hover handler attached to widget")
86}
87
88// handleEnter processes the mouse enter event with debouncing.
89func (h *HoverHandler) handleEnter() {
90	h.timerMu.Lock()
91	defer h.timerMu.Unlock()
92
93	// Cancel any existing timer
94	if h.timer != nil {
95		h.timer.Stop()
96	}
97
98	h.canceled = false
99
100	// Start new debounce timer
101	h.timer = time.AfterFunc(HoverFocusDelay, func() {
102		h.timerMu.Lock()
103		canceled := h.canceled
104		h.timerMu.Unlock()
105
106		if canceled {
107			return
108		}
109
110		if h.onEnter != nil {
111			h.onEnter(h.paneID)
112		}
113	})
114}
115
116// handleLeave cancels any pending focus switch.
117func (h *HoverHandler) handleLeave() {
118	h.timerMu.Lock()
119	defer h.timerMu.Unlock()
120
121	h.canceled = true
122	if h.timer != nil {
123		h.timer.Stop()
124		h.timer = nil
125	}
126}
127
128// Detach removes the hover handler.
129func (h *HoverHandler) Detach() {
130	h.timerMu.Lock()
131	if h.timer != nil {
132		h.timer.Stop()
133		h.timer = nil
134	}
135	h.timerMu.Unlock()
136
137	h.motionCtrl = nil
138	h.enterCb = nil
139	h.leaveCb = nil
140}

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