detached 6120f51

feat(keyboard): add vim-style navigation and update browser shortcuts

commit
6120f51139a326fab53ff1703e79f3fdfb847524
author
bnema · · 2025-11-13 20:48 UTC
committer
bnema · · 2025-11-13 19:47 UTC
parent
7c1a5d9
signature
unsigned
4 files · +34 -6
M gui/src/injected/modules/controls.ts +5 -6
@@ -26,12 +26,11 @@ export function initializeControls(_config?: ControlsConfig): void {
2626
2727 // Keyboard event handler for global controls
2828 function handleKeyboardEvent(event: KeyboardEvent): void {
29- const { altKey, key } = event;
30- // Note: ctrlKey, metaKey reserved for future use
29+ const { altKey, ctrlKey, key } = event;
3130
3231 try {
33- // Navigation: Alt + Left Arrow (Back)
34- if (altKey && key === "ArrowLeft") {
32+ // Navigation: Ctrl + Left Arrow (Back)
33+ if (ctrlKey && key === "ArrowLeft") {
3534 event.preventDefault();
3635 event.stopPropagation();
3736
@@ -42,8 +41,8 @@ export function initializeControls(_config?: ControlsConfig): void {
4241 return;
4342 }
4443
45- // Navigation: Alt + Right Arrow (Forward)
46- if (altKey && key === "ArrowRight") {
44+ // Navigation: Ctrl + Right Arrow (Forward)
45+ if (ctrlKey && key === "ArrowRight") {
4746 event.preventDefault();
4847 event.stopPropagation();
4948
@@ -26,12 +26,11 @@ export function initializeControls(_config?: ControlsConfig): void {
26 26
27 // Keyboard event handler for global controls27 // Keyboard event handler for global controls
28 function handleKeyboardEvent(event: KeyboardEvent): void {28 function handleKeyboardEvent(event: KeyboardEvent): void {
29- const { altKey, key } = event;29+ const { altKey, ctrlKey, key } = event;
30- // Note: ctrlKey, metaKey reserved for future use
31 30
32 try {31 try {
33- // Navigation: Alt + Left Arrow (Back)32+ // Navigation: Ctrl + Left Arrow (Back)
34- if (altKey && key === "ArrowLeft") {33+ if (ctrlKey && key === "ArrowLeft") {
35 event.preventDefault();34 event.preventDefault();
36 event.stopPropagation();35 event.stopPropagation();
37 36
@@ -42,8 +41,8 @@ export function initializeControls(_config?: ControlsConfig): void {
42 return;41 return;
43 }42 }
44 43
45- // Navigation: Alt + Right Arrow (Forward)44+ // Navigation: Ctrl + Right Arrow (Forward)
46- if (altKey && key === "ArrowRight") {45+ if (ctrlKey && key === "ArrowRight") {
47 event.preventDefault();46 event.preventDefault();
48 event.stopPropagation();47 event.stopPropagation();
49 48
M internal/app/browser/window_shortcuts.go +5 -0
@@ -73,6 +73,11 @@ func (h *WindowShortcutHandler) registerGlobalShortcuts() error {
7373 {"alt+ArrowRight", func() { h.handleWorkspaceNavigation(DirectionRight) }, "Navigate right pane"},
7474 {"alt+ArrowUp", func() { h.handleWorkspaceNavigation(DirectionUp) }, "Navigate up pane"},
7575 {"alt+ArrowDown", func() { h.handleWorkspaceNavigation(DirectionDown) }, "Navigate down pane"},
76+ // Vim-style workspace navigation
77+ {"alt+h", func() { h.handleWorkspaceNavigation(DirectionLeft) }, "Navigate left pane (vim)"},
78+ {"alt+l", func() { h.handleWorkspaceNavigation(DirectionRight) }, "Navigate right pane (vim)"},
79+ {"alt+k", func() { h.handleWorkspaceNavigation(DirectionUp) }, "Navigate up pane (vim)"},
80+ {"alt+j", func() { h.handleWorkspaceNavigation(DirectionDown) }, "Navigate down pane (vim)"},
7681 }
7782
7883 for _, shortcut := range shortcuts {
@@ -73,6 +73,11 @@ func (h *WindowShortcutHandler) registerGlobalShortcuts() error {
73 {"alt+ArrowRight", func() { h.handleWorkspaceNavigation(DirectionRight) }, "Navigate right pane"},73 {"alt+ArrowRight", func() { h.handleWorkspaceNavigation(DirectionRight) }, "Navigate right pane"},
74 {"alt+ArrowUp", func() { h.handleWorkspaceNavigation(DirectionUp) }, "Navigate up pane"},74 {"alt+ArrowUp", func() { h.handleWorkspaceNavigation(DirectionUp) }, "Navigate up pane"},
75 {"alt+ArrowDown", func() { h.handleWorkspaceNavigation(DirectionDown) }, "Navigate down pane"},75 {"alt+ArrowDown", func() { h.handleWorkspaceNavigation(DirectionDown) }, "Navigate down pane"},
76+ // Vim-style workspace navigation
77+ {"alt+h", func() { h.handleWorkspaceNavigation(DirectionLeft) }, "Navigate left pane (vim)"},
78+ {"alt+l", func() { h.handleWorkspaceNavigation(DirectionRight) }, "Navigate right pane (vim)"},
79+ {"alt+k", func() { h.handleWorkspaceNavigation(DirectionUp) }, "Navigate up pane (vim)"},
80+ {"alt+j", func() { h.handleWorkspaceNavigation(DirectionDown) }, "Navigate down pane (vim)"},
76 }81 }
77 82
78 for _, shortcut := range shortcuts {83 for _, shortcut := range shortcuts {
M internal/app/browser/workspace_utils.go +9 -0
@@ -33,15 +33,24 @@ func (wm *WorkspaceManager) ensureGUIInPane(pane *BrowserPane) {
3333 }
3434
3535 // mapDirection maps a direction string to GTK orientation and positioning
36+// The bool return value indicates whether the existing pane should be the first child (start)
37+// In GTK Paned with Vertical orientation: start=top, end=bottom
38+// In GTK Paned with Horizontal orientation: start=left, end=right
3639 func mapDirection(direction string) (gtk.Orientation, bool) {
3740 switch direction {
3841 case DirectionLeft:
42+ // New pane goes to left, existing to right
3943 return gtk.OrientationHorizontal, false
4044 case DirectionUp:
45+ // New pane goes to top (start), existing to bottom (end)
46+ // So existingFirst = false
4147 return gtk.OrientationVertical, false
4248 case DirectionDown:
49+ // New pane goes to bottom (end), existing to top (start)
50+ // So existingFirst = true
4351 return gtk.OrientationVertical, true
4452 default:
53+ // Right: new pane to right, existing to left
4554 return gtk.OrientationHorizontal, true
4655 }
4756 }
@@ -33,15 +33,24 @@ func (wm *WorkspaceManager) ensureGUIInPane(pane *BrowserPane) {
33 }33 }
34 34
35 // mapDirection maps a direction string to GTK orientation and positioning35 // mapDirection maps a direction string to GTK orientation and positioning
36+// The bool return value indicates whether the existing pane should be the first child (start)
37+// In GTK Paned with Vertical orientation: start=top, end=bottom
38+// In GTK Paned with Horizontal orientation: start=left, end=right
36 func mapDirection(direction string) (gtk.Orientation, bool) {39 func mapDirection(direction string) (gtk.Orientation, bool) {
37 switch direction {40 switch direction {
38 case DirectionLeft:41 case DirectionLeft:
42+ // New pane goes to left, existing to right
39 return gtk.OrientationHorizontal, false43 return gtk.OrientationHorizontal, false
40 case DirectionUp:44 case DirectionUp:
45+ // New pane goes to top (start), existing to bottom (end)
46+ // So existingFirst = false
41 return gtk.OrientationVertical, false47 return gtk.OrientationVertical, false
42 case DirectionDown:48 case DirectionDown:
49+ // New pane goes to bottom (end), existing to top (start)
50+ // So existingFirst = true
43 return gtk.OrientationVertical, true51 return gtk.OrientationVertical, true
44 default:52 default:
53+ // Right: new pane to right, existing to left
45 return gtk.OrientationHorizontal, true54 return gtk.OrientationHorizontal, true
46 }55 }
47 }56 }
M pkg/webkit/keyboard_bridge.go +15 -0
@@ -41,6 +41,9 @@ const (
4141 keyW = "w"
4242 keyX = "x"
4343 keyS = "s"
44+ keyH = "h"
45+ keyJ = "j"
46+ keyK = "k"
4447 keyEscape = "escape"
4548 keyArrowLeft = "arrowleft"
4649 keyArrowRight = "arrowright"
@@ -92,6 +95,12 @@ var shortcutActions = map[string]string{
9295 altmod + "+" + keyArrowRight: directionRight,
9396 altmod + "+" + keyArrowDown: directionDown,
9497 altmod + "+" + keyArrowUp: directionUp,
98+
99+ // Vim-style navigation (alt modifier)
100+ altmod + "+" + keyH: directionLeft,
101+ altmod + "+" + keyL: directionRight,
102+ altmod + "+" + keyK: directionUp,
103+ altmod + "+" + keyJ: directionDown,
95104 }
96105
97106 // TODO(#8): Refactor this function to reduce cyclomatic complexity (52 -> <30)
@@ -161,6 +170,12 @@ func (w *WebView) AttachKeyboardBridge() {
161170 keyName = keyW
162171 case 'x', 'X':
163172 keyName = keyX
173+ case 'h', 'H':
174+ keyName = keyH
175+ case 'j', 'J':
176+ keyName = keyJ
177+ case 'k', 'K':
178+ keyName = keyK
164179 case 's', 'S':
165180 keyName = keyS
166181 case gdkKeyLeft:
@@ -41,6 +41,9 @@ const (
41 keyW = "w"41 keyW = "w"
42 keyX = "x"42 keyX = "x"
43 keyS = "s"43 keyS = "s"
44+ keyH = "h"
45+ keyJ = "j"
46+ keyK = "k"
44 keyEscape = "escape"47 keyEscape = "escape"
45 keyArrowLeft = "arrowleft"48 keyArrowLeft = "arrowleft"
46 keyArrowRight = "arrowright"49 keyArrowRight = "arrowright"
@@ -92,6 +95,12 @@ var shortcutActions = map[string]string{
92 altmod + "+" + keyArrowRight: directionRight,95 altmod + "+" + keyArrowRight: directionRight,
93 altmod + "+" + keyArrowDown: directionDown,96 altmod + "+" + keyArrowDown: directionDown,
94 altmod + "+" + keyArrowUp: directionUp,97 altmod + "+" + keyArrowUp: directionUp,
98+
99+ // Vim-style navigation (alt modifier)
100+ altmod + "+" + keyH: directionLeft,
101+ altmod + "+" + keyL: directionRight,
102+ altmod + "+" + keyK: directionUp,
103+ altmod + "+" + keyJ: directionDown,
95 }104 }
96 105
97 // TODO(#8): Refactor this function to reduce cyclomatic complexity (52 -> <30)106 // TODO(#8): Refactor this function to reduce cyclomatic complexity (52 -> <30)
@@ -161,6 +170,12 @@ func (w *WebView) AttachKeyboardBridge() {
161 keyName = keyW170 keyName = keyW
162 case 'x', 'X':171 case 'x', 'X':
163 keyName = keyX172 keyName = keyX
173+ case 'h', 'H':
174+ keyName = keyH
175+ case 'j', 'J':
176+ keyName = keyJ
177+ case 'k', 'K':
178+ keyName = keyK
164 case 's', 'S':179 case 's', 'S':
165 keyName = keyS180 keyName = keyS
166 case gdkKeyLeft:181 case gdkKeyLeft:

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