detached 7dee585

fix: restore app shortcuts when WebView has focus

Register all app-reserved shortcuts (Ctrl+P, Ctrl+L, Ctrl+T, etc.) as
GTK global shortcuts so they intercept before WebView DOM handlers.
Switch keyboard controller to capture phase during modal modes so plain
keys (s, h for pane mode) reach the handler before WebView.

Fixes regression from PR #187 where bubble-phase change for dead-key
support caused web pages to steal app shortcuts (e.g. GitHub command
palette on Ctrl+P).

Also fixes thread safety: phase switching uses glib.IdleAdd for GTK
main thread dispatch, and config reads are protected by RLock.
commit
7dee585103f2401500ebbe5e751f62dd1c38b666
author
brice · · 2026-03-13 06:32 UTC
parent
c97e5fd
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +96 -8
M internal/ui/app.go +4 -0
@@ -711,10 +711,14 @@ func (a *App) initKeyboardHandler(ctx context.Context) {
711711 ctx,
712712 a.mainWindow.Window(),
713713 a.deps.Config,
714+ a.keyboardHandler,
714715 func(ctx context.Context, action input.Action) error {
715716 return a.kbDispatcher.Dispatch(ctx, action)
716717 },
717718 )
719+ if a.globalShortcutHandler == nil {
720+ logging.FromContext(ctx).Warn().Msg("failed to create global shortcut handler, some shortcuts may not work when WebView has focus")
721+ }
718722 }
719723
720724 // handleAccentKeyPress handles accent key press events for GTK entry widgets
@@ -711,10 +711,14 @@ func (a *App) initKeyboardHandler(ctx context.Context) {
711 ctx,711 ctx,
712 a.mainWindow.Window(),712 a.mainWindow.Window(),
713 a.deps.Config,713 a.deps.Config,
714+ a.keyboardHandler,
714 func(ctx context.Context, action input.Action) error {715 func(ctx context.Context, action input.Action) error {
715 return a.kbDispatcher.Dispatch(ctx, action)716 return a.kbDispatcher.Dispatch(ctx, action)
716 },717 },
717 )718 )
719+ if a.globalShortcutHandler == nil {
720+ logging.FromContext(ctx).Warn().Msg("failed to create global shortcut handler, some shortcuts may not work when WebView has focus")
721+ }
718 }722 }
719 723
720 // handleAccentKeyPress handles accent key press events for GTK entry widgets724 // handleAccentKeyPress handles accent key press events for GTK entry widgets
M internal/ui/input/global_shortcuts.go +33 -0
@@ -18,6 +18,7 @@ import (
1818 // to intercept shortcuts before they reach the WebView.
1919 type GlobalShortcutHandler struct {
2020 controller *gtk.ShortcutController
21+ kbHandler *KeyboardHandler
2122 onAction ActionHandler
2223 ctx context.Context
2324 registered map[KeyBinding]Action
@@ -32,6 +33,7 @@ func NewGlobalShortcutHandler(
3233 ctx context.Context,
3334 window *gtk.ApplicationWindow,
3435 cfg *config.Config,
36+ kbHandler *KeyboardHandler,
3537 onAction ActionHandler,
3638 ) *GlobalShortcutHandler {
3739 log := logging.FromContext(ctx)
@@ -39,6 +41,7 @@ func NewGlobalShortcutHandler(
3941
4042 h := &GlobalShortcutHandler{
4143 controller: gtk.NewShortcutController(),
44+ kbHandler: kbHandler,
4245 onAction: onAction,
4346 ctx: ctx,
4447 callbacks: make([]gtk.ShortcutFunc, 0),
@@ -140,6 +143,16 @@ func NewGlobalShortcutHandler(
140143 Msg("registered floating profile global shortcut")
141144 }
142145 }
146+
147+ // Register all app-reserved shortcuts (mode activations, Ctrl+L, Ctrl+F, etc.)
148+ // so they work even when WebView has focus.
149+ shortcuts := NewShortcutSet(ctx, cfg)
150+ for binding, action := range shortcuts.Global {
151+ if _, exists := h.registered[binding]; exists {
152+ continue
153+ }
154+ h.registerShortcut(binding.Keyval, gdk.ModifierType(binding.Modifiers), action)
155+ }
143156 }
144157
145158 // Attach to window
@@ -182,6 +195,17 @@ func (h *GlobalShortcutHandler) registerShortcut(keyval uint, modifiers gdk.Modi
182195 Str("action", string(actionToDispatch)).
183196 Msg("global shortcut triggered")
184197
198+ // Mode-enter/exit actions go through KeyboardHandler for modal state
199+ if isModeAction(actionToDispatch) {
200+ if h.kbHandler != nil {
201+ h.kbHandler.DispatchAction(actionToDispatch)
202+ return true
203+ }
204+ log.Warn().
205+ Str("action", string(actionToDispatch)).
206+ Msg("mode action triggered but no keyboard handler available")
207+ }
208+
185209 if h.onAction != nil {
186210 if err := h.onAction(h.ctx, actionToDispatch); err != nil {
187211 log.Error().
@@ -247,3 +271,12 @@ func formatBinding(binding KeyBinding) string {
247271 parts = append(parts, strings.ToLower(keyName))
248272 return strings.Join(parts, "+")
249273 }
274+
275+func isModeAction(action Action) bool {
276+ switch action {
277+ case ActionEnterTabMode, ActionEnterPaneMode, ActionEnterSessionMode, ActionEnterResizeMode, ActionExitMode:
278+ return true
279+ default:
280+ return false
281+ }
282+}
@@ -18,6 +18,7 @@ import (
18 // to intercept shortcuts before they reach the WebView.18 // to intercept shortcuts before they reach the WebView.
19 type GlobalShortcutHandler struct {19 type GlobalShortcutHandler struct {
20 controller *gtk.ShortcutController20 controller *gtk.ShortcutController
21+ kbHandler *KeyboardHandler
21 onAction ActionHandler22 onAction ActionHandler
22 ctx context.Context23 ctx context.Context
23 registered map[KeyBinding]Action24 registered map[KeyBinding]Action
@@ -32,6 +33,7 @@ func NewGlobalShortcutHandler(
32 ctx context.Context,33 ctx context.Context,
33 window *gtk.ApplicationWindow,34 window *gtk.ApplicationWindow,
34 cfg *config.Config,35 cfg *config.Config,
36+ kbHandler *KeyboardHandler,
35 onAction ActionHandler,37 onAction ActionHandler,
36 ) *GlobalShortcutHandler {38 ) *GlobalShortcutHandler {
37 log := logging.FromContext(ctx)39 log := logging.FromContext(ctx)
@@ -39,6 +41,7 @@ func NewGlobalShortcutHandler(
39 41
40 h := &GlobalShortcutHandler{42 h := &GlobalShortcutHandler{
41 controller: gtk.NewShortcutController(),43 controller: gtk.NewShortcutController(),
44+ kbHandler: kbHandler,
42 onAction: onAction,45 onAction: onAction,
43 ctx: ctx,46 ctx: ctx,
44 callbacks: make([]gtk.ShortcutFunc, 0),47 callbacks: make([]gtk.ShortcutFunc, 0),
@@ -140,6 +143,16 @@ func NewGlobalShortcutHandler(
140 Msg("registered floating profile global shortcut")143 Msg("registered floating profile global shortcut")
141 }144 }
142 }145 }
146+
147+ // Register all app-reserved shortcuts (mode activations, Ctrl+L, Ctrl+F, etc.)
148+ // so they work even when WebView has focus.
149+ shortcuts := NewShortcutSet(ctx, cfg)
150+ for binding, action := range shortcuts.Global {
151+ if _, exists := h.registered[binding]; exists {
152+ continue
153+ }
154+ h.registerShortcut(binding.Keyval, gdk.ModifierType(binding.Modifiers), action)
155+ }
143 }156 }
144 157
145 // Attach to window158 // Attach to window
@@ -182,6 +195,17 @@ func (h *GlobalShortcutHandler) registerShortcut(keyval uint, modifiers gdk.Modi
182 Str("action", string(actionToDispatch)).195 Str("action", string(actionToDispatch)).
183 Msg("global shortcut triggered")196 Msg("global shortcut triggered")
184 197
198+ // Mode-enter/exit actions go through KeyboardHandler for modal state
199+ if isModeAction(actionToDispatch) {
200+ if h.kbHandler != nil {
201+ h.kbHandler.DispatchAction(actionToDispatch)
202+ return true
203+ }
204+ log.Warn().
205+ Str("action", string(actionToDispatch)).
206+ Msg("mode action triggered but no keyboard handler available")
207+ }
208+
185 if h.onAction != nil {209 if h.onAction != nil {
186 if err := h.onAction(h.ctx, actionToDispatch); err != nil {210 if err := h.onAction(h.ctx, actionToDispatch); err != nil {
187 log.Error().211 log.Error().
@@ -247,3 +271,12 @@ func formatBinding(binding KeyBinding) string {
247 parts = append(parts, strings.ToLower(keyName))271 parts = append(parts, strings.ToLower(keyName))
248 return strings.Join(parts, "+")272 return strings.Join(parts, "+")
249 }273 }
274+
275+func isModeAction(action Action) bool {
276+ switch action {
277+ case ActionEnterTabMode, ActionEnterPaneMode, ActionEnterSessionMode, ActionEnterResizeMode, ActionExitMode:
278+ return true
279+ default:
280+ return false
281+ }
282+}
M internal/ui/input/keyboard.go +59 -8
@@ -26,6 +26,7 @@ import (
2626 "github.com/bnema/dumber/internal/infrastructure/config"
2727 "github.com/bnema/dumber/internal/logging"
2828 "github.com/jwijenbergh/puregotk/v4/gdk"
29+ "github.com/jwijenbergh/puregotk/v4/glib"
2930 "github.com/jwijenbergh/puregotk/v4/gtk"
3031 "github.com/rs/zerolog"
3132 )
@@ -116,7 +117,36 @@ func (h *KeyboardHandler) ReloadShortcuts(ctx context.Context, cfg *config.Confi
116117
117118 // SetOnModeChange sets the callback for mode changes (for UI updates).
118119 func (h *KeyboardHandler) SetOnModeChange(fn func(from, to Mode)) {
119- h.modal.SetOnModeChange(fn)
120+ h.modal.SetOnModeChange(func(from, to Mode) {
121+ // Switch controller phase: capture during modal, bubble for normal.
122+ // This ensures plain modal keys (s, h, etc.) reach the handler
123+ // before WebView, while normal typing still goes through WebKit IM.
124+ if to == ModeNormal {
125+ h.setControllerPhase(gtk.PhaseBubbleValue)
126+ } else if from == ModeNormal {
127+ h.setControllerPhase(gtk.PhaseCaptureValue)
128+ }
129+ // Forward to app-level callback
130+ if fn != nil {
131+ fn(from, to)
132+ }
133+ })
134+}
135+
136+// setControllerPhase changes the propagation phase of the key controller.
137+// Uses glib.IdleAdd to ensure the GTK call runs on the main thread,
138+// since this may be called from modal timeout goroutines.
139+func (h *KeyboardHandler) setControllerPhase(phase gtk.PropagationPhase) {
140+ if h.controller == nil {
141+ return
142+ }
143+ cb := glib.SourceFunc(func(uintptr) bool {
144+ if h.controller != nil {
145+ h.controller.SetPropagationPhase(phase)
146+ }
147+ return false
148+ })
149+ glib.IdleAdd(&cb, 0)
120150 }
121151
122152 // SetRouteKey sets the callback that determines how each key event should be routed.
@@ -392,17 +422,21 @@ func isResizeAction(action Action) bool {
392422 }
393423
394424 func (h *KeyboardHandler) handleModeAction(action Action) bool {
425+ h.mu.RLock()
426+ cfg := h.cfg
427+ h.mu.RUnlock()
428+
395429 switch action {
396430 case ActionEnterTabMode:
397- timeout := time.Duration(h.cfg.Workspace.TabMode.TimeoutMilliseconds) * time.Millisecond
431+ timeout := time.Duration(cfg.Workspace.TabMode.TimeoutMilliseconds) * time.Millisecond
398432 h.modal.EnterTabMode(h.ctx, timeout)
399433 return true
400434 case ActionEnterPaneMode:
401- timeout := time.Duration(h.cfg.Workspace.PaneMode.TimeoutMilliseconds) * time.Millisecond
435+ timeout := time.Duration(cfg.Workspace.PaneMode.TimeoutMilliseconds) * time.Millisecond
402436 h.modal.EnterPaneMode(h.ctx, timeout)
403437 return true
404438 case ActionEnterSessionMode:
405- timeout := time.Duration(h.cfg.Session.SessionMode.TimeoutMilliseconds) * time.Millisecond
439+ timeout := time.Duration(cfg.Session.SessionMode.TimeoutMilliseconds) * time.Millisecond
406440 h.modal.EnterSessionMode(h.ctx, timeout)
407441 return true
408442 case ActionEnterResizeMode:
@@ -410,7 +444,7 @@ func (h *KeyboardHandler) handleModeAction(action Action) bool {
410444 h.modal.ExitMode(h.ctx)
411445 return true
412446 }
413- timeout := time.Duration(h.cfg.Workspace.ResizeMode.TimeoutMilliseconds) * time.Millisecond
447+ timeout := time.Duration(cfg.Workspace.ResizeMode.TimeoutMilliseconds) * time.Millisecond
414448 h.modal.EnterResizeMode(h.ctx, timeout)
415449 return true
416450 case ActionExitMode:
@@ -424,21 +458,30 @@ func (h *KeyboardHandler) handleModeAction(action Action) bool {
424458 // EnterTabMode programmatically enters tab mode.
425459 // Useful for testing or programmatic mode changes.
426460 func (h *KeyboardHandler) EnterTabMode() {
427- timeout := time.Duration(h.cfg.Workspace.TabMode.TimeoutMilliseconds) * time.Millisecond
461+ h.mu.RLock()
462+ cfg := h.cfg
463+ h.mu.RUnlock()
464+ timeout := time.Duration(cfg.Workspace.TabMode.TimeoutMilliseconds) * time.Millisecond
428465 h.modal.EnterTabMode(h.ctx, timeout)
429466 }
430467
431468 // EnterPaneMode programmatically enters pane mode.
432469 // Useful for testing or programmatic mode changes.
433470 func (h *KeyboardHandler) EnterPaneMode() {
434- timeout := time.Duration(h.cfg.Workspace.PaneMode.TimeoutMilliseconds) * time.Millisecond
471+ h.mu.RLock()
472+ cfg := h.cfg
473+ h.mu.RUnlock()
474+ timeout := time.Duration(cfg.Workspace.PaneMode.TimeoutMilliseconds) * time.Millisecond
435475 h.modal.EnterPaneMode(h.ctx, timeout)
436476 }
437477
438478 // EnterSessionMode programmatically enters session mode.
439479 // Useful for testing or programmatic mode changes.
440480 func (h *KeyboardHandler) EnterSessionMode() {
441- timeout := time.Duration(h.cfg.Session.SessionMode.TimeoutMilliseconds) * time.Millisecond
481+ h.mu.RLock()
482+ cfg := h.cfg
483+ h.mu.RUnlock()
484+ timeout := time.Duration(cfg.Session.SessionMode.TimeoutMilliseconds) * time.Millisecond
442485 h.modal.EnterSessionMode(h.ctx, timeout)
443486 }
444487
@@ -448,6 +491,14 @@ func (h *KeyboardHandler) ExitMode() {
448491 h.modal.ExitMode(h.ctx)
449492 }
450493
494+// DispatchAction processes an action externally triggered (e.g., by a global
495+// shortcut). Mode-enter actions update modal state; other actions are forwarded
496+// to the registered action handler.
497+func (h *KeyboardHandler) DispatchAction(action Action) {
498+ mode := h.modal.Mode()
499+ h.dispatchAction(action, mode)
500+}
501+
451502 // handleKeyRelease processes a key release event for accent detection.
452503 func (h *KeyboardHandler) handleKeyRelease(keyval uint) {
453504 h.mu.RLock()
@@ -26,6 +26,7 @@ import (
26 "github.com/bnema/dumber/internal/infrastructure/config"26 "github.com/bnema/dumber/internal/infrastructure/config"
27 "github.com/bnema/dumber/internal/logging"27 "github.com/bnema/dumber/internal/logging"
28 "github.com/jwijenbergh/puregotk/v4/gdk"28 "github.com/jwijenbergh/puregotk/v4/gdk"
29+ "github.com/jwijenbergh/puregotk/v4/glib"
29 "github.com/jwijenbergh/puregotk/v4/gtk"30 "github.com/jwijenbergh/puregotk/v4/gtk"
30 "github.com/rs/zerolog"31 "github.com/rs/zerolog"
31 )32 )
@@ -116,7 +117,36 @@ func (h *KeyboardHandler) ReloadShortcuts(ctx context.Context, cfg *config.Confi
116 117
117 // SetOnModeChange sets the callback for mode changes (for UI updates).118 // SetOnModeChange sets the callback for mode changes (for UI updates).
118 func (h *KeyboardHandler) SetOnModeChange(fn func(from, to Mode)) {119 func (h *KeyboardHandler) SetOnModeChange(fn func(from, to Mode)) {
119- h.modal.SetOnModeChange(fn)120+ h.modal.SetOnModeChange(func(from, to Mode) {
121+ // Switch controller phase: capture during modal, bubble for normal.
122+ // This ensures plain modal keys (s, h, etc.) reach the handler
123+ // before WebView, while normal typing still goes through WebKit IM.
124+ if to == ModeNormal {
125+ h.setControllerPhase(gtk.PhaseBubbleValue)
126+ } else if from == ModeNormal {
127+ h.setControllerPhase(gtk.PhaseCaptureValue)
128+ }
129+ // Forward to app-level callback
130+ if fn != nil {
131+ fn(from, to)
132+ }
133+ })
134+}
135+
136+// setControllerPhase changes the propagation phase of the key controller.
137+// Uses glib.IdleAdd to ensure the GTK call runs on the main thread,
138+// since this may be called from modal timeout goroutines.
139+func (h *KeyboardHandler) setControllerPhase(phase gtk.PropagationPhase) {
140+ if h.controller == nil {
141+ return
142+ }
143+ cb := glib.SourceFunc(func(uintptr) bool {
144+ if h.controller != nil {
145+ h.controller.SetPropagationPhase(phase)
146+ }
147+ return false
148+ })
149+ glib.IdleAdd(&cb, 0)
120 }150 }
121 151
122 // SetRouteKey sets the callback that determines how each key event should be routed.152 // SetRouteKey sets the callback that determines how each key event should be routed.
@@ -392,17 +422,21 @@ func isResizeAction(action Action) bool {
392 }422 }
393 423
394 func (h *KeyboardHandler) handleModeAction(action Action) bool {424 func (h *KeyboardHandler) handleModeAction(action Action) bool {
425+ h.mu.RLock()
426+ cfg := h.cfg
427+ h.mu.RUnlock()
428+
395 switch action {429 switch action {
396 case ActionEnterTabMode:430 case ActionEnterTabMode:
397- timeout := time.Duration(h.cfg.Workspace.TabMode.TimeoutMilliseconds) * time.Millisecond431+ timeout := time.Duration(cfg.Workspace.TabMode.TimeoutMilliseconds) * time.Millisecond
398 h.modal.EnterTabMode(h.ctx, timeout)432 h.modal.EnterTabMode(h.ctx, timeout)
399 return true433 return true
400 case ActionEnterPaneMode:434 case ActionEnterPaneMode:
401- timeout := time.Duration(h.cfg.Workspace.PaneMode.TimeoutMilliseconds) * time.Millisecond435+ timeout := time.Duration(cfg.Workspace.PaneMode.TimeoutMilliseconds) * time.Millisecond
402 h.modal.EnterPaneMode(h.ctx, timeout)436 h.modal.EnterPaneMode(h.ctx, timeout)
403 return true437 return true
404 case ActionEnterSessionMode:438 case ActionEnterSessionMode:
405- timeout := time.Duration(h.cfg.Session.SessionMode.TimeoutMilliseconds) * time.Millisecond439+ timeout := time.Duration(cfg.Session.SessionMode.TimeoutMilliseconds) * time.Millisecond
406 h.modal.EnterSessionMode(h.ctx, timeout)440 h.modal.EnterSessionMode(h.ctx, timeout)
407 return true441 return true
408 case ActionEnterResizeMode:442 case ActionEnterResizeMode:
@@ -410,7 +444,7 @@ func (h *KeyboardHandler) handleModeAction(action Action) bool {
410 h.modal.ExitMode(h.ctx)444 h.modal.ExitMode(h.ctx)
411 return true445 return true
412 }446 }
413- timeout := time.Duration(h.cfg.Workspace.ResizeMode.TimeoutMilliseconds) * time.Millisecond447+ timeout := time.Duration(cfg.Workspace.ResizeMode.TimeoutMilliseconds) * time.Millisecond
414 h.modal.EnterResizeMode(h.ctx, timeout)448 h.modal.EnterResizeMode(h.ctx, timeout)
415 return true449 return true
416 case ActionExitMode:450 case ActionExitMode:
@@ -424,21 +458,30 @@ func (h *KeyboardHandler) handleModeAction(action Action) bool {
424 // EnterTabMode programmatically enters tab mode.458 // EnterTabMode programmatically enters tab mode.
425 // Useful for testing or programmatic mode changes.459 // Useful for testing or programmatic mode changes.
426 func (h *KeyboardHandler) EnterTabMode() {460 func (h *KeyboardHandler) EnterTabMode() {
427- timeout := time.Duration(h.cfg.Workspace.TabMode.TimeoutMilliseconds) * time.Millisecond461+ h.mu.RLock()
462+ cfg := h.cfg
463+ h.mu.RUnlock()
464+ timeout := time.Duration(cfg.Workspace.TabMode.TimeoutMilliseconds) * time.Millisecond
428 h.modal.EnterTabMode(h.ctx, timeout)465 h.modal.EnterTabMode(h.ctx, timeout)
429 }466 }
430 467
431 // EnterPaneMode programmatically enters pane mode.468 // EnterPaneMode programmatically enters pane mode.
432 // Useful for testing or programmatic mode changes.469 // Useful for testing or programmatic mode changes.
433 func (h *KeyboardHandler) EnterPaneMode() {470 func (h *KeyboardHandler) EnterPaneMode() {
434- timeout := time.Duration(h.cfg.Workspace.PaneMode.TimeoutMilliseconds) * time.Millisecond471+ h.mu.RLock()
472+ cfg := h.cfg
473+ h.mu.RUnlock()
474+ timeout := time.Duration(cfg.Workspace.PaneMode.TimeoutMilliseconds) * time.Millisecond
435 h.modal.EnterPaneMode(h.ctx, timeout)475 h.modal.EnterPaneMode(h.ctx, timeout)
436 }476 }
437 477
438 // EnterSessionMode programmatically enters session mode.478 // EnterSessionMode programmatically enters session mode.
439 // Useful for testing or programmatic mode changes.479 // Useful for testing or programmatic mode changes.
440 func (h *KeyboardHandler) EnterSessionMode() {480 func (h *KeyboardHandler) EnterSessionMode() {
441- timeout := time.Duration(h.cfg.Session.SessionMode.TimeoutMilliseconds) * time.Millisecond481+ h.mu.RLock()
482+ cfg := h.cfg
483+ h.mu.RUnlock()
484+ timeout := time.Duration(cfg.Session.SessionMode.TimeoutMilliseconds) * time.Millisecond
442 h.modal.EnterSessionMode(h.ctx, timeout)485 h.modal.EnterSessionMode(h.ctx, timeout)
443 }486 }
444 487
@@ -448,6 +491,14 @@ func (h *KeyboardHandler) ExitMode() {
448 h.modal.ExitMode(h.ctx)491 h.modal.ExitMode(h.ctx)
449 }492 }
450 493
494+// DispatchAction processes an action externally triggered (e.g., by a global
495+// shortcut). Mode-enter actions update modal state; other actions are forwarded
496+// to the registered action handler.
497+func (h *KeyboardHandler) DispatchAction(action Action) {
498+ mode := h.modal.Mode()
499+ h.dispatchAction(action, mode)
500+}
501+
451 // handleKeyRelease processes a key release event for accent detection.502 // handleKeyRelease processes a key release event for accent detection.
452 func (h *KeyboardHandler) handleKeyRelease(keyval uint) {503 func (h *KeyboardHandler) handleKeyRelease(keyval uint) {
453 h.mu.RLock()504 h.mu.RLock()

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