detached 36f1195

fix(input): store held shortcuts as set

commit
36f1195f67c9ed7b85c52e6f4f316fc451ad5232
author
brice · · 2026-05-24 19:52 UTC
parent
bd18fcb
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +17 -17
M internal/ui/input/global_shortcuts.go +6 -6
@@ -27,7 +27,7 @@ type GlobalShortcutHandler struct {
2727 ctx context.Context
2828 registered map[KeyBinding]Action
2929 lastDispatchAt map[Action]time.Time
30- heldShortcuts map[globalShortcutHoldKey]time.Time
30+ heldShortcuts map[globalShortcutHoldKey]struct{}
3131 keyReleasedCb func(gtk.EventControllerKey, uint, uint, gdk.ModifierType)
3232 keyReleasedHandlerID uint
3333 // generation is mutated only from the GTK main thread alongside controller replacement.
@@ -60,7 +60,7 @@ func NewGlobalShortcutHandler(
6060 callbacks: make([]gtk.ShortcutFunc, 0),
6161 registered: make(map[KeyBinding]Action),
6262 lastDispatchAt: make(map[Action]time.Time),
63- heldShortcuts: make(map[globalShortcutHoldKey]time.Time),
63+ heldShortcuts: make(map[globalShortcutHoldKey]struct{}),
6464 }
6565
6666 if h.controller == nil {
@@ -240,7 +240,7 @@ func (h *GlobalShortcutHandler) registerShortcut(keyval uint, modifiers gdk.Modi
240240 Msg("global shortcut ignored without current key event")
241241 return true
242242 }
243- if h.suppressHeldShortcut(actionToDispatch, eventInfo, time.Now()) {
243+ if h.suppressHeldShortcut(actionToDispatch, eventInfo) {
244244 log.Trace().
245245 Str("action", string(actionToDispatch)).
246246 Str("shortcut", formatBinding(bindingForLog)).
@@ -467,7 +467,7 @@ func (h *GlobalShortcutHandler) ReloadShortcuts(ctx context.Context, workspace *
467467 h.callbacks = make([]gtk.ShortcutFunc, 0)
468468 h.registered = make(map[KeyBinding]Action)
469469 h.lastDispatchAt = make(map[Action]time.Time)
470- h.heldShortcuts = make(map[globalShortcutHoldKey]time.Time)
470+ h.heldShortcuts = make(map[globalShortcutHoldKey]struct{})
471471
472472 h.registerDefaultGlobalShortcuts(log)
473473
@@ -552,7 +552,7 @@ func (h *GlobalShortcutHandler) isActiveWindowShortcutHandler() bool {
552552 return h != nil && h.controller != nil && h.window != nil && h.window.IsActive()
553553 }
554554
555-func (h *GlobalShortcutHandler) suppressHeldShortcut(action Action, info globalShortcutEventInfo, now time.Time) bool {
555+func (h *GlobalShortcutHandler) suppressHeldShortcut(action Action, info globalShortcutEventInfo) bool {
556556 if h == nil || !isHeldGlobalShortcutSuppressed(action) || !info.hasCurrentEvent {
557557 return false
558558 }
@@ -568,7 +568,7 @@ func (h *GlobalShortcutHandler) suppressHeldShortcut(action Action, info globalS
568568 if _, ok := h.heldShortcuts[key]; ok {
569569 return true
570570 }
571- h.heldShortcuts[key] = now
571+ h.heldShortcuts[key] = struct{}{}
572572 return false
573573 }
574574
@@ -27,7 +27,7 @@ type GlobalShortcutHandler struct {
27 ctx context.Context27 ctx context.Context
28 registered map[KeyBinding]Action28 registered map[KeyBinding]Action
29 lastDispatchAt map[Action]time.Time29 lastDispatchAt map[Action]time.Time
30- heldShortcuts map[globalShortcutHoldKey]time.Time30+ heldShortcuts map[globalShortcutHoldKey]struct{}
31 keyReleasedCb func(gtk.EventControllerKey, uint, uint, gdk.ModifierType)31 keyReleasedCb func(gtk.EventControllerKey, uint, uint, gdk.ModifierType)
32 keyReleasedHandlerID uint32 keyReleasedHandlerID uint
33 // generation is mutated only from the GTK main thread alongside controller replacement.33 // generation is mutated only from the GTK main thread alongside controller replacement.
@@ -60,7 +60,7 @@ func NewGlobalShortcutHandler(
60 callbacks: make([]gtk.ShortcutFunc, 0),60 callbacks: make([]gtk.ShortcutFunc, 0),
61 registered: make(map[KeyBinding]Action),61 registered: make(map[KeyBinding]Action),
62 lastDispatchAt: make(map[Action]time.Time),62 lastDispatchAt: make(map[Action]time.Time),
63- heldShortcuts: make(map[globalShortcutHoldKey]time.Time),63+ heldShortcuts: make(map[globalShortcutHoldKey]struct{}),
64 }64 }
65 65
66 if h.controller == nil {66 if h.controller == nil {
@@ -240,7 +240,7 @@ func (h *GlobalShortcutHandler) registerShortcut(keyval uint, modifiers gdk.Modi
240 Msg("global shortcut ignored without current key event")240 Msg("global shortcut ignored without current key event")
241 return true241 return true
242 }242 }
243- if h.suppressHeldShortcut(actionToDispatch, eventInfo, time.Now()) {243+ if h.suppressHeldShortcut(actionToDispatch, eventInfo) {
244 log.Trace().244 log.Trace().
245 Str("action", string(actionToDispatch)).245 Str("action", string(actionToDispatch)).
246 Str("shortcut", formatBinding(bindingForLog)).246 Str("shortcut", formatBinding(bindingForLog)).
@@ -467,7 +467,7 @@ func (h *GlobalShortcutHandler) ReloadShortcuts(ctx context.Context, workspace *
467 h.callbacks = make([]gtk.ShortcutFunc, 0)467 h.callbacks = make([]gtk.ShortcutFunc, 0)
468 h.registered = make(map[KeyBinding]Action)468 h.registered = make(map[KeyBinding]Action)
469 h.lastDispatchAt = make(map[Action]time.Time)469 h.lastDispatchAt = make(map[Action]time.Time)
470- h.heldShortcuts = make(map[globalShortcutHoldKey]time.Time)470+ h.heldShortcuts = make(map[globalShortcutHoldKey]struct{})
471 471
472 h.registerDefaultGlobalShortcuts(log)472 h.registerDefaultGlobalShortcuts(log)
473 473
@@ -552,7 +552,7 @@ func (h *GlobalShortcutHandler) isActiveWindowShortcutHandler() bool {
552 return h != nil && h.controller != nil && h.window != nil && h.window.IsActive()552 return h != nil && h.controller != nil && h.window != nil && h.window.IsActive()
553 }553 }
554 554
555-func (h *GlobalShortcutHandler) suppressHeldShortcut(action Action, info globalShortcutEventInfo, now time.Time) bool {555+func (h *GlobalShortcutHandler) suppressHeldShortcut(action Action, info globalShortcutEventInfo) bool {
556 if h == nil || !isHeldGlobalShortcutSuppressed(action) || !info.hasCurrentEvent {556 if h == nil || !isHeldGlobalShortcutSuppressed(action) || !info.hasCurrentEvent {
557 return false557 return false
558 }558 }
@@ -568,7 +568,7 @@ func (h *GlobalShortcutHandler) suppressHeldShortcut(action Action, info globalS
568 if _, ok := h.heldShortcuts[key]; ok {568 if _, ok := h.heldShortcuts[key]; ok {
569 return true569 return true
570 }570 }
571- h.heldShortcuts[key] = now571+ h.heldShortcuts[key] = struct{}{}
572 return false572 return false
573 }573 }
574 574
M internal/ui/input/global_shortcuts_test.go +11 -11
@@ -189,48 +189,48 @@ func TestGlobalShortcutHandlerSuppressesActionSwitchLastTab(t *testing.T) {
189189 }
190190
191191 func TestGlobalShortcutHandlerSuppressesHeldGlobalShortcutUntilRelease(t *testing.T) {
192- h := &GlobalShortcutHandler{heldShortcuts: make(map[globalShortcutHoldKey]time.Time)}
192+ h := &GlobalShortcutHandler{heldShortcuts: make(map[globalShortcutHoldKey]struct{})}
193193 info := globalShortcutEventInfo{hasCurrentEvent: true, eventType: gdk.KeyPressValue, eventKeyval: uint('f'), eventKeycode: 41}
194194
195- if h.suppressHeldShortcut(ActionToggleFloatingPane, info, time.Unix(100, 0)) {
195+ if h.suppressHeldShortcut(ActionToggleFloatingPane, info) {
196196 t.Fatal("first held global shortcut dispatch was suppressed")
197197 }
198- if !h.suppressHeldShortcut(ActionToggleFloatingPane, info, time.Unix(101, 0)) {
198+ if !h.suppressHeldShortcut(ActionToggleFloatingPane, info) {
199199 t.Fatal("held global shortcut repeat after cooldown was not suppressed")
200200 }
201201
202202 h.releaseHeldGlobalShortcuts(uint('x'), 0)
203- if !h.suppressHeldShortcut(ActionToggleFloatingPane, info, time.Unix(101, 0)) {
203+ if !h.suppressHeldShortcut(ActionToggleFloatingPane, info) {
204204 t.Fatal("unrelated key release re-armed held global shortcut")
205205 }
206206
207207 h.releaseHeldGlobalShortcuts(uint('f'), 0)
208- if h.suppressHeldShortcut(ActionToggleFloatingPane, info, time.Unix(103, 0)) {
208+ if h.suppressHeldShortcut(ActionToggleFloatingPane, info) {
209209 t.Fatal("matching key release did not re-arm held global shortcut")
210210 }
211211 }
212212
213213 func TestGlobalShortcutHandlerSuppressesHeldModeAction(t *testing.T) {
214- h := &GlobalShortcutHandler{heldShortcuts: make(map[globalShortcutHoldKey]time.Time)}
214+ h := &GlobalShortcutHandler{heldShortcuts: make(map[globalShortcutHoldKey]struct{})}
215215 info := globalShortcutEventInfo{hasCurrentEvent: true, eventType: gdk.KeyPressValue, eventKeyval: uint('p'), eventKeycode: 33}
216216
217- if h.suppressHeldShortcut(ActionEnterPaneMode, info, time.Unix(100, 0)) {
217+ if h.suppressHeldShortcut(ActionEnterPaneMode, info) {
218218 t.Fatal("first mode shortcut dispatch was suppressed")
219219 }
220- if !h.suppressHeldShortcut(ActionEnterPaneMode, info, time.Unix(101, 0)) {
220+ if !h.suppressHeldShortcut(ActionEnterPaneMode, info) {
221221 t.Fatal("held mode shortcut repeat was not suppressed")
222222 }
223223 }
224224
225225 func TestGlobalShortcutHandlerSuppressesHeldFloatingProfileAction(t *testing.T) {
226- h := &GlobalShortcutHandler{heldShortcuts: make(map[globalShortcutHoldKey]time.Time)}
226+ h := &GlobalShortcutHandler{heldShortcuts: make(map[globalShortcutHoldKey]struct{})}
227227 info := globalShortcutEventInfo{hasCurrentEvent: true, eventType: gdk.KeyPressValue, eventKeyval: uint('o'), eventKeycode: 32}
228228 action := NewFloatingProfileAction("work", "https://example.com")
229229
230- if h.suppressHeldShortcut(action, info, time.Unix(100, 0)) {
230+ if h.suppressHeldShortcut(action, info) {
231231 t.Fatal("first floating-profile shortcut dispatch was suppressed")
232232 }
233- if !h.suppressHeldShortcut(action, info, time.Unix(101, 0)) {
233+ if !h.suppressHeldShortcut(action, info) {
234234 t.Fatal("held floating-profile shortcut repeat was not suppressed")
235235 }
236236 }
@@ -189,48 +189,48 @@ func TestGlobalShortcutHandlerSuppressesActionSwitchLastTab(t *testing.T) {
189 }189 }
190 190
191 func TestGlobalShortcutHandlerSuppressesHeldGlobalShortcutUntilRelease(t *testing.T) {191 func TestGlobalShortcutHandlerSuppressesHeldGlobalShortcutUntilRelease(t *testing.T) {
192- h := &GlobalShortcutHandler{heldShortcuts: make(map[globalShortcutHoldKey]time.Time)}192+ h := &GlobalShortcutHandler{heldShortcuts: make(map[globalShortcutHoldKey]struct{})}
193 info := globalShortcutEventInfo{hasCurrentEvent: true, eventType: gdk.KeyPressValue, eventKeyval: uint('f'), eventKeycode: 41}193 info := globalShortcutEventInfo{hasCurrentEvent: true, eventType: gdk.KeyPressValue, eventKeyval: uint('f'), eventKeycode: 41}
194 194
195- if h.suppressHeldShortcut(ActionToggleFloatingPane, info, time.Unix(100, 0)) {195+ if h.suppressHeldShortcut(ActionToggleFloatingPane, info) {
196 t.Fatal("first held global shortcut dispatch was suppressed")196 t.Fatal("first held global shortcut dispatch was suppressed")
197 }197 }
198- if !h.suppressHeldShortcut(ActionToggleFloatingPane, info, time.Unix(101, 0)) {198+ if !h.suppressHeldShortcut(ActionToggleFloatingPane, info) {
199 t.Fatal("held global shortcut repeat after cooldown was not suppressed")199 t.Fatal("held global shortcut repeat after cooldown was not suppressed")
200 }200 }
201 201
202 h.releaseHeldGlobalShortcuts(uint('x'), 0)202 h.releaseHeldGlobalShortcuts(uint('x'), 0)
203- if !h.suppressHeldShortcut(ActionToggleFloatingPane, info, time.Unix(101, 0)) {203+ if !h.suppressHeldShortcut(ActionToggleFloatingPane, info) {
204 t.Fatal("unrelated key release re-armed held global shortcut")204 t.Fatal("unrelated key release re-armed held global shortcut")
205 }205 }
206 206
207 h.releaseHeldGlobalShortcuts(uint('f'), 0)207 h.releaseHeldGlobalShortcuts(uint('f'), 0)
208- if h.suppressHeldShortcut(ActionToggleFloatingPane, info, time.Unix(103, 0)) {208+ if h.suppressHeldShortcut(ActionToggleFloatingPane, info) {
209 t.Fatal("matching key release did not re-arm held global shortcut")209 t.Fatal("matching key release did not re-arm held global shortcut")
210 }210 }
211 }211 }
212 212
213 func TestGlobalShortcutHandlerSuppressesHeldModeAction(t *testing.T) {213 func TestGlobalShortcutHandlerSuppressesHeldModeAction(t *testing.T) {
214- h := &GlobalShortcutHandler{heldShortcuts: make(map[globalShortcutHoldKey]time.Time)}214+ h := &GlobalShortcutHandler{heldShortcuts: make(map[globalShortcutHoldKey]struct{})}
215 info := globalShortcutEventInfo{hasCurrentEvent: true, eventType: gdk.KeyPressValue, eventKeyval: uint('p'), eventKeycode: 33}215 info := globalShortcutEventInfo{hasCurrentEvent: true, eventType: gdk.KeyPressValue, eventKeyval: uint('p'), eventKeycode: 33}
216 216
217- if h.suppressHeldShortcut(ActionEnterPaneMode, info, time.Unix(100, 0)) {217+ if h.suppressHeldShortcut(ActionEnterPaneMode, info) {
218 t.Fatal("first mode shortcut dispatch was suppressed")218 t.Fatal("first mode shortcut dispatch was suppressed")
219 }219 }
220- if !h.suppressHeldShortcut(ActionEnterPaneMode, info, time.Unix(101, 0)) {220+ if !h.suppressHeldShortcut(ActionEnterPaneMode, info) {
221 t.Fatal("held mode shortcut repeat was not suppressed")221 t.Fatal("held mode shortcut repeat was not suppressed")
222 }222 }
223 }223 }
224 224
225 func TestGlobalShortcutHandlerSuppressesHeldFloatingProfileAction(t *testing.T) {225 func TestGlobalShortcutHandlerSuppressesHeldFloatingProfileAction(t *testing.T) {
226- h := &GlobalShortcutHandler{heldShortcuts: make(map[globalShortcutHoldKey]time.Time)}226+ h := &GlobalShortcutHandler{heldShortcuts: make(map[globalShortcutHoldKey]struct{})}
227 info := globalShortcutEventInfo{hasCurrentEvent: true, eventType: gdk.KeyPressValue, eventKeyval: uint('o'), eventKeycode: 32}227 info := globalShortcutEventInfo{hasCurrentEvent: true, eventType: gdk.KeyPressValue, eventKeyval: uint('o'), eventKeycode: 32}
228 action := NewFloatingProfileAction("work", "https://example.com")228 action := NewFloatingProfileAction("work", "https://example.com")
229 229
230- if h.suppressHeldShortcut(action, info, time.Unix(100, 0)) {230+ if h.suppressHeldShortcut(action, info) {
231 t.Fatal("first floating-profile shortcut dispatch was suppressed")231 t.Fatal("first floating-profile shortcut dispatch was suppressed")
232 }232 }
233- if !h.suppressHeldShortcut(action, info, time.Unix(101, 0)) {233+ if !h.suppressHeldShortcut(action, info) {
234 t.Fatal("held floating-profile shortcut repeat was not suppressed")234 t.Fatal("held floating-profile shortcut repeat was not suppressed")
235 }235 }
236 }236 }

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