detached 04133a4

fix(vim): pass tab focus keys to pages

commit
04133a4fda49219376fe99985268c00c5efc6a20
author
brice · · 2026-08-14 12:09 UTC
parent
4000758
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +38 -2
M internal/ui/input/keyboard.go +6 -2
@@ -613,12 +613,16 @@ func normalizeKeyval(keyval uint) uint {
613613 }
614614
615615 func shouldPassthroughNativeVimModeNavigation(mode Mode, keyval uint, modifiers Modifier) bool {
616- if mode != ModeVim || modifiers != ModNone {
616+ if mode != ModeVim {
617617 return false
618618 }
619619 switch keyval {
620620 case uint(gdk.KEY_Left), uint(gdk.KEY_Right), uint(gdk.KEY_Up), uint(gdk.KEY_Down):
621- return true
621+ return modifiers == ModNone
622+ case uint(gdk.KEY_Tab):
623+ return modifiers == ModNone || modifiers == ModShift
624+ case uint(gdk.KEY_ISO_Left_Tab):
625+ return modifiers == ModNone || modifiers == ModShift
622626 default:
623627 return false
624628 }
@@ -613,12 +613,16 @@ func normalizeKeyval(keyval uint) uint {
613 }613 }
614 614
615 func shouldPassthroughNativeVimModeNavigation(mode Mode, keyval uint, modifiers Modifier) bool {615 func shouldPassthroughNativeVimModeNavigation(mode Mode, keyval uint, modifiers Modifier) bool {
616- if mode != ModeVim || modifiers != ModNone {616+ if mode != ModeVim {
617 return false617 return false
618 }618 }
619 switch keyval {619 switch keyval {
620 case uint(gdk.KEY_Left), uint(gdk.KEY_Right), uint(gdk.KEY_Up), uint(gdk.KEY_Down):620 case uint(gdk.KEY_Left), uint(gdk.KEY_Right), uint(gdk.KEY_Up), uint(gdk.KEY_Down):
621- return true621+ return modifiers == ModNone
622+ case uint(gdk.KEY_Tab):
623+ return modifiers == ModNone || modifiers == ModShift
624+ case uint(gdk.KEY_ISO_Left_Tab):
625+ return modifiers == ModNone || modifiers == ModShift
622 default:626 default:
623 return false627 return false
624 }628 }
M internal/ui/input/keyboard_test.go +32 -0
@@ -884,6 +884,38 @@ func TestHandleKeyPress_VimModeArrowKeysPassThroughNatively(t *testing.T) {
884884 }
885885 }
886886
887+func TestHandleKeyPress_VimModeTabKeysPassThroughNatively(t *testing.T) {
888+ ctx := context.Background()
889+ workspace := newTestWorkspace()
890+ workspace.VimMode = entity.VimModeConfig{ActivationShortcut: "ctrl+y"}
891+
892+ h := NewKeyboardHandler(ctx, workspace, newTestSession())
893+ h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)
894+ if h.Mode() != ModeVim {
895+ t.Fatal("failed to enter vim mode")
896+ }
897+
898+ for _, test := range []struct {
899+ name string
900+ keyval uint
901+ modifiers gdk.ModifierType
902+ }{
903+ {name: "tab", keyval: uint(gdk.KEY_Tab)},
904+ {name: "shift tab", keyval: uint(gdk.KEY_Tab), modifiers: gdk.ShiftMaskValue},
905+ {name: "iso left tab", keyval: uint(gdk.KEY_ISO_Left_Tab), modifiers: gdk.ShiftMaskValue},
906+ } {
907+ t.Run(test.name, func(t *testing.T) {
908+ consumed := h.handleKeyPress(test.keyval, 0, test.modifiers)
909+ if consumed {
910+ t.Fatal("tab key should pass through to native page focus navigation in vim mode")
911+ }
912+ if h.Mode() != ModeVim {
913+ t.Fatal("tab key should keep vim mode active")
914+ }
915+ })
916+ }
917+}
918+
887919 func TestHandleKeyPress_VimModeBlocksGlobalShortcutFallback(t *testing.T) {
888920 ctx := context.Background()
889921 workspace := newTestWorkspace()
@@ -884,6 +884,38 @@ func TestHandleKeyPress_VimModeArrowKeysPassThroughNatively(t *testing.T) {
884 }884 }
885 }885 }
886 886
887+func TestHandleKeyPress_VimModeTabKeysPassThroughNatively(t *testing.T) {
888+ ctx := context.Background()
889+ workspace := newTestWorkspace()
890+ workspace.VimMode = entity.VimModeConfig{ActivationShortcut: "ctrl+y"}
891+
892+ h := NewKeyboardHandler(ctx, workspace, newTestSession())
893+ h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)
894+ if h.Mode() != ModeVim {
895+ t.Fatal("failed to enter vim mode")
896+ }
897+
898+ for _, test := range []struct {
899+ name string
900+ keyval uint
901+ modifiers gdk.ModifierType
902+ }{
903+ {name: "tab", keyval: uint(gdk.KEY_Tab)},
904+ {name: "shift tab", keyval: uint(gdk.KEY_Tab), modifiers: gdk.ShiftMaskValue},
905+ {name: "iso left tab", keyval: uint(gdk.KEY_ISO_Left_Tab), modifiers: gdk.ShiftMaskValue},
906+ } {
907+ t.Run(test.name, func(t *testing.T) {
908+ consumed := h.handleKeyPress(test.keyval, 0, test.modifiers)
909+ if consumed {
910+ t.Fatal("tab key should pass through to native page focus navigation in vim mode")
911+ }
912+ if h.Mode() != ModeVim {
913+ t.Fatal("tab key should keep vim mode active")
914+ }
915+ })
916+ }
917+}
918+
887 func TestHandleKeyPress_VimModeBlocksGlobalShortcutFallback(t *testing.T) {919 func TestHandleKeyPress_VimModeBlocksGlobalShortcutFallback(t *testing.T) {
888 ctx := context.Background()920 ctx := context.Background()
889 workspace := newTestWorkspace()921 workspace := newTestWorkspace()

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