detached 3d9e595

feat(shortcuts): add Alt+1-9, Alt+0, and Alt+Tab for direct tab switching

- Add ActionSwitchTabIndex1-10 actions for direct tab access by position
- Add ActionSwitchLastTab action for Alt+Tab style switching
- Wire Alt+1-9 and Alt+0 keybindings to switch tabs by index
- Wire Alt+Tab and Shift+Alt+Tab to switch to last active tab
- Dispatch new actions through KeyboardDispatcher to TabCoordinator
commit
3d9e595763b2c3129c323a53397d8761b5567dbe
author
brice · · 2025-12-18 06:44 UTC
parent
5690117
signature
unsigned
2 files · +54 -5
M internal/ui/dispatcher/keyboard.go +22 -0
@@ -59,6 +59,28 @@ func (d *KeyboardDispatcher) Dispatch(ctx context.Context, action input.Action)
5959 return d.tabCoord.SwitchNext(ctx)
6060 case input.ActionPreviousTab:
6161 return d.tabCoord.SwitchPrev(ctx)
62+ case input.ActionSwitchLastTab:
63+ return d.tabCoord.SwitchToLastActive(ctx)
64+ case input.ActionSwitchTabIndex1:
65+ return d.tabCoord.SwitchByIndex(ctx, 0)
66+ case input.ActionSwitchTabIndex2:
67+ return d.tabCoord.SwitchByIndex(ctx, 1)
68+ case input.ActionSwitchTabIndex3:
69+ return d.tabCoord.SwitchByIndex(ctx, 2)
70+ case input.ActionSwitchTabIndex4:
71+ return d.tabCoord.SwitchByIndex(ctx, 3)
72+ case input.ActionSwitchTabIndex5:
73+ return d.tabCoord.SwitchByIndex(ctx, 4)
74+ case input.ActionSwitchTabIndex6:
75+ return d.tabCoord.SwitchByIndex(ctx, 5)
76+ case input.ActionSwitchTabIndex7:
77+ return d.tabCoord.SwitchByIndex(ctx, 6)
78+ case input.ActionSwitchTabIndex8:
79+ return d.tabCoord.SwitchByIndex(ctx, 7)
80+ case input.ActionSwitchTabIndex9:
81+ return d.tabCoord.SwitchByIndex(ctx, 8)
82+ case input.ActionSwitchTabIndex10:
83+ return d.tabCoord.SwitchByIndex(ctx, 9)
6284
6385 // Pane actions
6486 case input.ActionSplitRight:
@@ -59,6 +59,28 @@ func (d *KeyboardDispatcher) Dispatch(ctx context.Context, action input.Action)
59 return d.tabCoord.SwitchNext(ctx)59 return d.tabCoord.SwitchNext(ctx)
60 case input.ActionPreviousTab:60 case input.ActionPreviousTab:
61 return d.tabCoord.SwitchPrev(ctx)61 return d.tabCoord.SwitchPrev(ctx)
62+ case input.ActionSwitchLastTab:
63+ return d.tabCoord.SwitchToLastActive(ctx)
64+ case input.ActionSwitchTabIndex1:
65+ return d.tabCoord.SwitchByIndex(ctx, 0)
66+ case input.ActionSwitchTabIndex2:
67+ return d.tabCoord.SwitchByIndex(ctx, 1)
68+ case input.ActionSwitchTabIndex3:
69+ return d.tabCoord.SwitchByIndex(ctx, 2)
70+ case input.ActionSwitchTabIndex4:
71+ return d.tabCoord.SwitchByIndex(ctx, 3)
72+ case input.ActionSwitchTabIndex5:
73+ return d.tabCoord.SwitchByIndex(ctx, 4)
74+ case input.ActionSwitchTabIndex6:
75+ return d.tabCoord.SwitchByIndex(ctx, 5)
76+ case input.ActionSwitchTabIndex7:
77+ return d.tabCoord.SwitchByIndex(ctx, 6)
78+ case input.ActionSwitchTabIndex8:
79+ return d.tabCoord.SwitchByIndex(ctx, 7)
80+ case input.ActionSwitchTabIndex9:
81+ return d.tabCoord.SwitchByIndex(ctx, 8)
82+ case input.ActionSwitchTabIndex10:
83+ return d.tabCoord.SwitchByIndex(ctx, 9)
62 84
63 // Pane actions85 // Pane actions
64 case input.ActionSplitRight:86 case input.ActionSplitRight:
M internal/ui/input/shortcuts.go +32 -5
@@ -44,11 +44,22 @@ const (
4444 ActionExitMode Action = "exit_mode"
4545
4646 // Tab actions (global and modal)
47- ActionNewTab Action = "new_tab"
48- ActionCloseTab Action = "close_tab"
49- ActionNextTab Action = "next_tab"
50- ActionPreviousTab Action = "previous_tab"
51- ActionRenameTab Action = "rename_tab"
47+ ActionNewTab Action = "new_tab"
48+ ActionCloseTab Action = "close_tab"
49+ ActionNextTab Action = "next_tab"
50+ ActionPreviousTab Action = "previous_tab"
51+ ActionRenameTab Action = "rename_tab"
52+ ActionSwitchLastTab Action = "switch_last_tab" // Alt+Tab style switching
53+ ActionSwitchTabIndex1 Action = "switch_tab_1"
54+ ActionSwitchTabIndex2 Action = "switch_tab_2"
55+ ActionSwitchTabIndex3 Action = "switch_tab_3"
56+ ActionSwitchTabIndex4 Action = "switch_tab_4"
57+ ActionSwitchTabIndex5 Action = "switch_tab_5"
58+ ActionSwitchTabIndex6 Action = "switch_tab_6"
59+ ActionSwitchTabIndex7 Action = "switch_tab_7"
60+ ActionSwitchTabIndex8 Action = "switch_tab_8"
61+ ActionSwitchTabIndex9 Action = "switch_tab_9"
62+ ActionSwitchTabIndex10 Action = "switch_tab_10"
5263
5364 // Pane actions (modal)
5465 ActionSplitRight Action = "split_right"
@@ -182,6 +193,22 @@ func (s *ShortcutSet) buildGlobalShortcuts(ctx context.Context, cfg *config.Work
182193 // Also support Alt+Arrow keys for pane navigation (except Left/Right used for back/forward)
183194 s.Global[KeyBinding{uint(gdk.KEY_Up), ModAlt}] = ActionFocusUp
184195 s.Global[KeyBinding{uint(gdk.KEY_Down), ModAlt}] = ActionFocusDown
196+
197+ // Direct tab switching (Alt+1-9, Alt+0 for tab 10)
198+ s.Global[KeyBinding{uint(gdk.KEY_1), ModAlt}] = ActionSwitchTabIndex1
199+ s.Global[KeyBinding{uint(gdk.KEY_2), ModAlt}] = ActionSwitchTabIndex2
200+ s.Global[KeyBinding{uint(gdk.KEY_3), ModAlt}] = ActionSwitchTabIndex3
201+ s.Global[KeyBinding{uint(gdk.KEY_4), ModAlt}] = ActionSwitchTabIndex4
202+ s.Global[KeyBinding{uint(gdk.KEY_5), ModAlt}] = ActionSwitchTabIndex5
203+ s.Global[KeyBinding{uint(gdk.KEY_6), ModAlt}] = ActionSwitchTabIndex6
204+ s.Global[KeyBinding{uint(gdk.KEY_7), ModAlt}] = ActionSwitchTabIndex7
205+ s.Global[KeyBinding{uint(gdk.KEY_8), ModAlt}] = ActionSwitchTabIndex8
206+ s.Global[KeyBinding{uint(gdk.KEY_9), ModAlt}] = ActionSwitchTabIndex9
207+ s.Global[KeyBinding{uint(gdk.KEY_0), ModAlt}] = ActionSwitchTabIndex10
208+
209+ // Alt+Tab style switching (switch to last active tab)
210+ s.Global[KeyBinding{uint(gdk.KEY_Tab), ModAlt}] = ActionSwitchLastTab
211+ s.Global[KeyBinding{uint(gdk.KEY_Tab), ModAlt | ModShift}] = ActionSwitchLastTab
185212 }
186213
187214 // buildTabModeShortcuts populates tab mode shortcuts from config.
@@ -44,11 +44,22 @@ const (
44 ActionExitMode Action = "exit_mode"44 ActionExitMode Action = "exit_mode"
45 45
46 // Tab actions (global and modal)46 // Tab actions (global and modal)
47- ActionNewTab Action = "new_tab"47+ ActionNewTab Action = "new_tab"
48- ActionCloseTab Action = "close_tab"48+ ActionCloseTab Action = "close_tab"
49- ActionNextTab Action = "next_tab"49+ ActionNextTab Action = "next_tab"
50- ActionPreviousTab Action = "previous_tab"50+ ActionPreviousTab Action = "previous_tab"
51- ActionRenameTab Action = "rename_tab"51+ ActionRenameTab Action = "rename_tab"
52+ ActionSwitchLastTab Action = "switch_last_tab" // Alt+Tab style switching
53+ ActionSwitchTabIndex1 Action = "switch_tab_1"
54+ ActionSwitchTabIndex2 Action = "switch_tab_2"
55+ ActionSwitchTabIndex3 Action = "switch_tab_3"
56+ ActionSwitchTabIndex4 Action = "switch_tab_4"
57+ ActionSwitchTabIndex5 Action = "switch_tab_5"
58+ ActionSwitchTabIndex6 Action = "switch_tab_6"
59+ ActionSwitchTabIndex7 Action = "switch_tab_7"
60+ ActionSwitchTabIndex8 Action = "switch_tab_8"
61+ ActionSwitchTabIndex9 Action = "switch_tab_9"
62+ ActionSwitchTabIndex10 Action = "switch_tab_10"
52 63
53 // Pane actions (modal)64 // Pane actions (modal)
54 ActionSplitRight Action = "split_right"65 ActionSplitRight Action = "split_right"
@@ -182,6 +193,22 @@ func (s *ShortcutSet) buildGlobalShortcuts(ctx context.Context, cfg *config.Work
182 // Also support Alt+Arrow keys for pane navigation (except Left/Right used for back/forward)193 // Also support Alt+Arrow keys for pane navigation (except Left/Right used for back/forward)
183 s.Global[KeyBinding{uint(gdk.KEY_Up), ModAlt}] = ActionFocusUp194 s.Global[KeyBinding{uint(gdk.KEY_Up), ModAlt}] = ActionFocusUp
184 s.Global[KeyBinding{uint(gdk.KEY_Down), ModAlt}] = ActionFocusDown195 s.Global[KeyBinding{uint(gdk.KEY_Down), ModAlt}] = ActionFocusDown
196+
197+ // Direct tab switching (Alt+1-9, Alt+0 for tab 10)
198+ s.Global[KeyBinding{uint(gdk.KEY_1), ModAlt}] = ActionSwitchTabIndex1
199+ s.Global[KeyBinding{uint(gdk.KEY_2), ModAlt}] = ActionSwitchTabIndex2
200+ s.Global[KeyBinding{uint(gdk.KEY_3), ModAlt}] = ActionSwitchTabIndex3
201+ s.Global[KeyBinding{uint(gdk.KEY_4), ModAlt}] = ActionSwitchTabIndex4
202+ s.Global[KeyBinding{uint(gdk.KEY_5), ModAlt}] = ActionSwitchTabIndex5
203+ s.Global[KeyBinding{uint(gdk.KEY_6), ModAlt}] = ActionSwitchTabIndex6
204+ s.Global[KeyBinding{uint(gdk.KEY_7), ModAlt}] = ActionSwitchTabIndex7
205+ s.Global[KeyBinding{uint(gdk.KEY_8), ModAlt}] = ActionSwitchTabIndex8
206+ s.Global[KeyBinding{uint(gdk.KEY_9), ModAlt}] = ActionSwitchTabIndex9
207+ s.Global[KeyBinding{uint(gdk.KEY_0), ModAlt}] = ActionSwitchTabIndex10
208+
209+ // Alt+Tab style switching (switch to last active tab)
210+ s.Global[KeyBinding{uint(gdk.KEY_Tab), ModAlt}] = ActionSwitchLastTab
211+ s.Global[KeyBinding{uint(gdk.KEY_Tab), ModAlt | ModShift}] = ActionSwitchLastTab
185 }212 }
186 213
187 // buildTabModeShortcuts populates tab mode shortcuts from config.214 // buildTabModeShortcuts populates tab mode shortcuts from config.

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