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 { | |||
| 26 | 26 | ||
| 27 | 27 | // Keyboard event handler for global controls | |
| 28 | 28 | function handleKeyboardEvent(event: KeyboardEvent): void { | |
| 29 | - | const { altKey, key } = event; | |
| 30 | - | // Note: ctrlKey, metaKey reserved for future use | |
| 29 | + | const { altKey, ctrlKey, key } = event; | |
| 31 | 30 | ||
| 32 | 31 | try { | |
| 33 | - | // Navigation: Alt + Left Arrow (Back) | |
| 34 | - | if (altKey && key === "ArrowLeft") { | |
| 32 | + | // Navigation: Ctrl + Left Arrow (Back) | |
| 33 | + | if (ctrlKey && key === "ArrowLeft") { | |
| 35 | 34 | event.preventDefault(); | |
| 36 | 35 | event.stopPropagation(); | |
| 37 | 36 | ||
| @@ -42,8 +41,8 @@ export function initializeControls(_config?: ControlsConfig): void { | |||
| 42 | 41 | return; | |
| 43 | 42 | } | |
| 44 | 43 | ||
| 45 | - | // Navigation: Alt + Right Arrow (Forward) | |
| 46 | - | if (altKey && key === "ArrowRight") { | |
| 44 | + | // Navigation: Ctrl + Right Arrow (Forward) | |
| 45 | + | if (ctrlKey && key === "ArrowRight") { | |
| 47 | 46 | event.preventDefault(); | |
| 48 | 47 | event.stopPropagation(); | |
| 49 | 48 | ||
| @@ -26,12 +26,11 @@ export function initializeControls(_config?: ControlsConfig): void { | |||||
| 26 | 26 | ||||
| 27 | // Keyboard event handler for global controls | 27 | // 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 { | |||
| 73 | 73 | {"alt+ArrowRight", func() { h.handleWorkspaceNavigation(DirectionRight) }, "Navigate right pane"}, | |
| 74 | 74 | {"alt+ArrowUp", func() { h.handleWorkspaceNavigation(DirectionUp) }, "Navigate up pane"}, | |
| 75 | 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 | 83 | 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) { | |||
| 33 | 33 | } | |
| 34 | 34 | ||
| 35 | 35 | // 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 | 39 | func mapDirection(direction string) (gtk.Orientation, bool) { | |
| 37 | 40 | switch direction { | |
| 38 | 41 | case DirectionLeft: | |
| 42 | + | // New pane goes to left, existing to right | |
| 39 | 43 | return gtk.OrientationHorizontal, false | |
| 40 | 44 | case DirectionUp: | |
| 45 | + | // New pane goes to top (start), existing to bottom (end) | |
| 46 | + | // So existingFirst = false | |
| 41 | 47 | return gtk.OrientationVertical, false | |
| 42 | 48 | case DirectionDown: | |
| 49 | + | // New pane goes to bottom (end), existing to top (start) | |
| 50 | + | // So existingFirst = true | |
| 43 | 51 | return gtk.OrientationVertical, true | |
| 44 | 52 | default: | |
| 53 | + | // Right: new pane to right, existing to left | |
| 45 | 54 | return gtk.OrientationHorizontal, true | |
| 46 | 55 | } | |
| 47 | 56 | } | |
| @@ -33,15 +33,24 @@ func (wm *WorkspaceManager) ensureGUIInPane(pane *BrowserPane) { | |||||
| 33 | } | 33 | } | ||
| 34 | 34 | ||||
| 35 | // mapDirection maps a direction string to GTK orientation and positioning | 35 | // 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, false | 43 | 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, false | 47 | 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, true | 51 | return gtk.OrientationVertical, true | ||
| 44 | default: | 52 | default: | ||
| 53 | + | // Right: new pane to right, existing to left | |||
| 45 | return gtk.OrientationHorizontal, true | 54 | return gtk.OrientationHorizontal, true | ||
| 46 | } | 55 | } | ||
| 47 | } | 56 | } | ||
M pkg/webkit/keyboard_bridge.go +15 -0
| @@ -41,6 +41,9 @@ const ( | |||
| 41 | 41 | keyW = "w" | |
| 42 | 42 | keyX = "x" | |
| 43 | 43 | keyS = "s" | |
| 44 | + | keyH = "h" | |
| 45 | + | keyJ = "j" | |
| 46 | + | keyK = "k" | |
| 44 | 47 | keyEscape = "escape" | |
| 45 | 48 | keyArrowLeft = "arrowleft" | |
| 46 | 49 | keyArrowRight = "arrowright" | |
| @@ -92,6 +95,12 @@ var shortcutActions = map[string]string{ | |||
| 92 | 95 | altmod + "+" + keyArrowRight: directionRight, | |
| 93 | 96 | altmod + "+" + keyArrowDown: directionDown, | |
| 94 | 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 | 106 | // TODO(#8): Refactor this function to reduce cyclomatic complexity (52 -> <30) | |
| @@ -161,6 +170,12 @@ func (w *WebView) AttachKeyboardBridge() { | |||
| 161 | 170 | keyName = keyW | |
| 162 | 171 | case 'x', 'X': | |
| 163 | 172 | keyName = keyX | |
| 173 | + | case 'h', 'H': | |
| 174 | + | keyName = keyH | |
| 175 | + | case 'j', 'J': | |
| 176 | + | keyName = keyJ | |
| 177 | + | case 'k', 'K': | |
| 178 | + | keyName = keyK | |
| 164 | 179 | case 's', 'S': | |
| 165 | 180 | keyName = keyS | |
| 166 | 181 | 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 = keyW | 170 | keyName = keyW | ||
| 162 | case 'x', 'X': | 171 | case 'x', 'X': | ||
| 163 | keyName = keyX | 172 | 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 = keyS | 180 | keyName = keyS | ||
| 166 | case gdkKeyLeft: | 181 | case gdkKeyLeft: | ||