detached 9fa7546

Merge pull request #376 from bnema/agent/rename-page-mode-vim-mode

feat(input): rename Page Mode to Vim Mode
commit
9fa7546d026480cca704e42e139bf8e0766032f0
author
bnema · · 2026-08-14 02:42 UTC
committer
GitHub · · 2026-08-14 02:42 UTC
merge of
48cc1e2 + ae52164 · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
52 files · +1789 -1568
M README.md +1 -1
@@ -21,7 +21,7 @@ Dumber uses three layout levels:
21 - **Workspaces** hold a layout of panes.21 - **Workspaces** hold a layout of panes.
22 - **Panes** display web pages and can be split, stacked, moved, resized, or closed.22 - **Panes** display web pages and can be split, stacked, moved, resized, or closed.
23 23
24-Most browser-management actions are exposed through modal keybindings. Enter a mode, run one or more commands, then leave the mode. The default modes cover pane management, tabs, page scrolling, resizing, and session commands.24+Most browser-management actions are exposed through modal keybindings. Enter a mode, run one or more commands, then leave the mode. The default modes cover pane management, tabs, Vim-style scrolling, resizing, and session commands.
25 25
26 The browser chrome stays out of the way by default. Open the omnibox when you want to navigate; otherwise the pane is just the page or web app you are using. This makes Dumber work well as a side pane next to an editor, terminal, or another desktop application.26 The browser chrome stays out of the way by default. Open the omnibox when you want to navigate; otherwise the pane is just the page or web app you are using. This makes Dumber work well as a side pane next to an editor, terminal, or another desktop application.
27 27
M docs/config/index.md +22 -22
@@ -398,57 +398,57 @@ cancel = ["escape"]
398 398
399 > **Note:** Actions are inverted to key→action map in memory for O(1) lookup performance during navigation.399 > **Note:** Actions are inverted to key→action map in memory for O(1) lookup performance during navigation.
400 400
401-### Page Mode401+### Vim Mode
402 402
403 | Key | Type | Default | Description |403 | Key | Type | Default | Description |
404 |-----|------|---------|-------------|404 |-----|------|---------|-------------|
405-| `workspace.page_mode.activation_shortcut` | string | `"ctrl+y"` | Page mode activation key |405+| `workspace.vim_mode.activation_shortcut` | string | `"ctrl+y"` | Vim mode activation key |
406-| `workspace.page_mode.timeout_ms` | int | `0` | Page mode timeout in milliseconds (`0` disables auto-timeout) |406+| `workspace.vim_mode.timeout_ms` | int | `0` | Vim mode timeout in milliseconds (`0` disables auto-timeout) |
407-| `workspace.page_mode.actions` | map | See below | Action→binding mappings |407+| `workspace.vim_mode.actions` | map | See below | Action→binding mappings |
408 408
409-**Default page mode actions:**409+**Default vim mode actions:**
410 410
411 ```toml411 ```toml
412-[workspace.page_mode.actions.page-scroll-left]412+[workspace.vim_mode.actions.vim-scroll-left]
413 keys = ["h"]413 keys = ["h"]
414 desc = "Scroll page left"414 desc = "Scroll page left"
415 415
416-[workspace.page_mode.actions.page-scroll-down]416+[workspace.vim_mode.actions.vim-scroll-down]
417 keys = ["j"]417 keys = ["j"]
418 desc = "Scroll page down"418 desc = "Scroll page down"
419 419
420-[workspace.page_mode.actions.page-scroll-up]420+[workspace.vim_mode.actions.vim-scroll-up]
421 keys = ["k"]421 keys = ["k"]
422 desc = "Scroll page up"422 desc = "Scroll page up"
423 423
424-[workspace.page_mode.actions.page-scroll-right]424+[workspace.vim_mode.actions.vim-scroll-right]
425 keys = ["l"]425 keys = ["l"]
426 desc = "Scroll page right"426 desc = "Scroll page right"
427 427
428-[workspace.page_mode.actions.page-scroll-down-fast]428+[workspace.vim_mode.actions.vim-scroll-down-fast]
429 keys = ["shift+j"]429 keys = ["shift+j"]
430 desc = "Scroll page down fast"430 desc = "Scroll page down fast"
431 431
432-[workspace.page_mode.actions.page-scroll-up-fast]432+[workspace.vim_mode.actions.vim-scroll-up-fast]
433 keys = ["shift+k"]433 keys = ["shift+k"]
434 desc = "Scroll page up fast"434 desc = "Scroll page up fast"
435 435
436-[workspace.page_mode.actions.confirm]436+[workspace.vim_mode.actions.confirm]
437 keys = ["enter"]437 keys = ["enter"]
438 desc = "Confirm action"438 desc = "Confirm action"
439 439
440-[workspace.page_mode.actions.cancel]440+[workspace.vim_mode.actions.cancel]
441 keys = ["escape"]441 keys = ["escape"]
442 desc = "Cancel/exit mode"442 desc = "Cancel/exit mode"
443 ```443 ```
444 444
445 Notes:445 Notes:
446-- Page Mode scrolls the active webpage inside the active pane only.446+- Vim Mode scrolls the active webpage inside the active pane only.
447-- The default `timeout_ms = 0` means Page Mode stays active until you exit it or focus moves into an editable/browser UI context.447+- The default `timeout_ms = 0` means Vim Mode stays active until you exit it or focus moves into an editable/browser UI context.
448-- Arrow keys still use the browser engine's native page-navigation path while Page Mode is active.448+- Arrow keys still use the browser engine's native page-navigation path while Vim Mode is active.
449-- Other app-level shortcuts stay suspended until Page Mode exits, except for the Page Mode toggle itself.449+- Other app-level shortcuts stay suspended until Vim Mode exits, except for the Vim Mode toggle itself.
450-- `workspace.styling.pane_mode_color`, `workspace.styling.transition_duration`, and `workspace.styling.mode_indicator_toaster_enabled` control the pane-local Page Mode visuals.450+- `workspace.styling.pane_mode_color`, `workspace.styling.transition_duration`, and `workspace.styling.mode_indicator_toaster_enabled` control the pane-local Vim Mode visuals.
451-- Scroll execution: CEF and WebKit both use the shared `BuildScrollByJS` resolver when the page is ready. Each step starts under the viewport center, walks up through ancestors that can move in the requested direction, and hands scrolling to the document when a nested container reaches its boundary. The application repeater owns held-key cadence; each engine executes one immediate scroll step per tick. Cross-origin frame contents remain best-effort. CEF may use native precision-wheel input only as a pre-frame fallback before the browser/main frame is ready.451+- Scroll execution: CEF and WebKit both use the shared `BuildPageScrollByJS` resolver when the page is ready. Each step starts under the viewport center, walks up through ancestors that can move in the requested direction, and hands scrolling to the document when a nested container reaches its boundary. The application repeater owns held-key cadence; each engine executes one immediate scroll step per tick. Cross-origin frame contents remain best-effort. CEF may use native precision-wheel input only as a pre-frame fallback before the browser/main frame is ready.
452 452
453 ### Resize Mode453 ### Resize Mode
454 454
@@ -581,11 +581,11 @@ desc = "Open floating pane on GitHub"
581 | `workspace.styling.border_width` | int | `1` | Active pane border width (px) - overlay |581 | `workspace.styling.border_width` | int | `1` | Active pane border width (px) - overlay |
582 | `workspace.styling.border_color` | string | `"@theme_selected_bg_color"` | Active pane border color |582 | `workspace.styling.border_color` | string | `"@theme_selected_bg_color"` | Active pane border color |
583 | `workspace.styling.mode_border_width` | int | `4` | Modal mode border width (px) - applies to all modes |583 | `workspace.styling.mode_border_width` | int | `4` | Modal mode border width (px) - applies to all modes |
584-| `workspace.styling.pane_mode_color` | string | `"#4A90E2"` | Pane mode color (blue); also reused for Page mode pane-local accent, indicator, and pulse |584+| `workspace.styling.pane_mode_color` | string | `"#4A90E2"` | Pane mode color (blue); also reused for Vim mode pane-local accent, indicator, and pulse |
585 | `workspace.styling.tab_mode_color` | string | `"#FFA500"` | Tab mode color (orange) - used for border and toaster |585 | `workspace.styling.tab_mode_color` | string | `"#FFA500"` | Tab mode color (orange) - used for border and toaster |
586 | `workspace.styling.session_mode_color` | string | `"#9B59B6"` | Session mode color (purple) - used for border and toaster |586 | `workspace.styling.session_mode_color` | string | `"#9B59B6"` | Session mode color (purple) - used for border and toaster |
587 | `workspace.styling.resize_mode_color` | string | `"#00D4AA"` | Resize mode color (teal) - used for border and toaster |587 | `workspace.styling.resize_mode_color` | string | `"#00D4AA"` | Resize mode color (teal) - used for border and toaster |
588-| `workspace.styling.mode_indicator_toaster_enabled` | bool | `true` | Show bottom-left mode toaster when modal modes are active; Page Mode keeps a persistent `PAGE MODE` indicator until exit or toggle off |588+| `workspace.styling.mode_indicator_toaster_enabled` | bool | `true` | Show bottom-left mode toaster when modal modes are active; Vim Mode keeps a persistent `VIM MODE` indicator until exit or toggle off |
589 | `workspace.styling.transition_duration` | int | `120` | Border and pulse transition duration (ms) |589 | `workspace.styling.transition_duration` | int | `120` | Border and pulse transition duration (ms) |
590 590
591 **Example:**591 **Example:**
@@ -595,7 +595,7 @@ desc = "Open floating pane on GitHub"
595 border_width = 1595 border_width = 1
596 border_color = "@theme_selected_bg_color"596 border_color = "@theme_selected_bg_color"
597 mode_border_width = 4597 mode_border_width = 4
598-pane_mode_color = "#4A90E2" # Blue for pane mode and Page mode local accent598+pane_mode_color = "#4A90E2" # Blue for pane mode and Vim mode local accent
599 tab_mode_color = "#FFA500" # Orange for tab mode599 tab_mode_color = "#FFA500" # Orange for tab mode
600 session_mode_color = "#9B59B6" # Purple for session mode600 session_mode_color = "#9B59B6" # Purple for session mode
601 resize_mode_color = "#00D4AA" # Teal for resize mode601 resize_mode_color = "#00D4AA" # Teal for resize mode
M docs/getting-started.md +8 -8
@@ -16,12 +16,12 @@ Dumber uses modal keybindings inspired by Zellij:
16 |------|------------|---------|16 |------|------------|---------|
17 | Pane Mode | `Ctrl+P` | Split, close, focus panes |17 | Pane Mode | `Ctrl+P` | Split, close, focus panes |
18 | Tab Mode | `Ctrl+T` | Create, close, switch tabs |18 | Tab Mode | `Ctrl+T` | Create, close, switch tabs |
19-| Page Mode | `Ctrl+Y` | Scroll the active webpage with `h/j/k/l` and `Shift+J/K`; arrow keys stay native and other app shortcuts wait until exit |19+| Vim Mode | `Ctrl+Y` | Scroll the active webpage with `h/j/k/l` and `Shift+J/K`; arrow keys stay native and other app shortcuts wait until exit |
20 | Resize Mode | `Ctrl+N` | Resize pane splits |20 | Resize Mode | `Ctrl+N` | Resize pane splits |
21 | Session Mode | `Ctrl+O` | Session management |21 | Session Mode | `Ctrl+O` | Session management |
22 22
23 Press `Escape` or `Enter` to exit any mode.23 Press `Escape` or `Enter` to exit any mode.
24-Page Mode stays local to the active pane and automatically leaves the mode when focus moves into the omnibox, find bar, overlays, or a page editable.24+Vim Mode stays local to the active pane and automatically leaves the mode when focus moves into the omnibox, find bar, overlays, or a page editable.
25 25
26 ## Pane Mode Quick Reference26 ## Pane Mode Quick Reference
27 27
@@ -30,18 +30,18 @@ Page Mode stays local to the active pane and automatically leaves the mode when
30 3. `Shift+arrows` to focus adjacent panes30 3. `Shift+arrows` to focus adjacent panes
31 4. `X` to close current pane31 4. `X` to close current pane
32 32
33-## Page Mode Quick Reference33+## Vim Mode Quick Reference
34 34
35-1. Press `Ctrl+Y` to enter Page Mode35+1. Press `Ctrl+Y` to enter Vim Mode
36 2. Use `h`, `j`, `k`, `l` to scroll left, down, up, or right36 2. Use `h`, `j`, `k`, `l` to scroll left, down, up, or right
37 3. Use `Shift+J` / `Shift+K` for faster vertical jumps37 3. Use `Shift+J` / `Shift+K` for faster vertical jumps
38-4. Arrow keys continue to use the browser engine's native page navigation while Page Mode is active38+4. Arrow keys continue to use the browser engine's native page navigation while Vim Mode is active
39-5. Other app-level shortcuts stay suspended until you leave Page Mode39+5. Other app-level shortcuts stay suspended until you leave Vim Mode
40 6. Press `Escape` or `Enter` to leave the mode40 6. Press `Escape` or `Enter` to leave the mode
41 7. If a page input or editor is already focused, `Ctrl+Y` is passed through instead of stealing the keypress41 7. If a page input or editor is already focused, `Ctrl+Y` is passed through instead of stealing the keypress
42 42
43-> **Engine behavior**: CEF and WebKit execute Page Mode scroll steps with the43+> **Engine behavior**: CEF and WebKit execute Vim Mode scroll steps with the
44-> shared `BuildScrollByJS` resolver (viewport-center start, nested-scroller44+> shared `BuildPageScrollByJS` resolver (viewport-center start, nested-scroller
45 > handoff, document fallback). The application repeater owns held-key cadence;45 > handoff, document fallback). The application repeater owns held-key cadence;
46 > each engine runs one immediate step per tick. Cross-origin frames are46 > each engine runs one immediate step per tick. Cross-origin frames are
47 > best-effort. CEF may use native precision-wheel input only before the browser47 > best-effort. CEF may use native precision-wheel input only before the browser
M docs/reference/configuration.md +3 -3
@@ -100,9 +100,9 @@
100 | `workspace.tab_mode.activation_shortcut` | string | `ctrl+t` | |100 | `workspace.tab_mode.activation_shortcut` | string | `ctrl+t` | |
101 | `workspace.tab_mode.timeout_ms` | int | `3000` | |101 | `workspace.tab_mode.timeout_ms` | int | `3000` | |
102 | `workspace.tab_mode.actions.<action>` | []string | see defaults | tab mode key mappings |102 | `workspace.tab_mode.actions.<action>` | []string | see defaults | tab mode key mappings |
103-| `workspace.page_mode.activation_shortcut` | string | `ctrl+y` | |103+| `workspace.vim_mode.activation_shortcut` | string | `ctrl+y` | |
104-| `workspace.page_mode.timeout_ms` | int | `0` | >= 0 |104+| `workspace.vim_mode.timeout_ms` | int | `0` | >= 0 |
105-| `workspace.page_mode.actions.<action>` | []string | see defaults | page mode key mappings |105+| `workspace.vim_mode.actions.<action>` | object | see defaults | vim mode action object with `keys` ([]string) and `desc` (string) |
106 | `workspace.resize_mode.activation_shortcut` | string | `ctrl+n` | |106 | `workspace.resize_mode.activation_shortcut` | string | `ctrl+n` | |
107 | `workspace.resize_mode.timeout_ms` | int | `3000` | |107 | `workspace.resize_mode.timeout_ms` | int | `3000` | |
108 | `workspace.resize_mode.actions.<action>` | []string | see defaults | resize mode key mappings |108 | `workspace.resize_mode.actions.<action>` | []string | see defaults | resize mode key mappings |
M docs/reference/keybindings.md +12 -12
@@ -8,12 +8,12 @@ Dumber uses modal keybindings inspired by Zellij. Press a mode activation key, t
8 |------|-----|---------|8 |------|-----|---------|
9 | Pane Mode | `Ctrl+P` | Split, close, focus panes |9 | Pane Mode | `Ctrl+P` | Split, close, focus panes |
10 | Tab Mode | `Ctrl+T` | Create, close, switch tabs |10 | Tab Mode | `Ctrl+T` | Create, close, switch tabs |
11-| Page Mode | `Ctrl+Y` | Scroll the active webpage with `h/j/k/l` and `Shift+J/K`; arrow keys stay native and other app shortcuts wait until exit |11+| Vim Mode | `Ctrl+Y` | Scroll the active webpage with `h/j/k/l` and `Shift+J/K`; arrow keys stay native and other app shortcuts wait until exit |
12 | Resize Mode | `Ctrl+N` | Resize pane splits |12 | Resize Mode | `Ctrl+N` | Resize pane splits |
13 | Session Mode | `Ctrl+O` | Session management |13 | Session Mode | `Ctrl+O` | Session management |
14 14
15 Press `Escape` or `Enter` to exit any mode.15 Press `Escape` or `Enter` to exit any mode.
16-Page Mode activation is passed through when the focused webpage element is already editable, so browser-page editors keep their own `Ctrl+Y` behavior.16+Vim Mode activation is passed through when the focused webpage element is already editable, so browser-page editors keep their own `Ctrl+Y` behavior.
17 17
18 Keybinding tables use uppercase letters as visual labels for unshifted letter keys. In config, use lowercase (for example, `["w"]` for Pane Mode eject). Shifted keys are shown with an explicit `Shift+` prefix.18 Keybinding tables use uppercase letters as visual labels for unshifted letter keys. In config, use lowercase (for example, `["w"]` for Pane Mode eject). Shifted keys are shown with an explicit `Shift+` prefix.
19 19
@@ -53,11 +53,11 @@ Keybinding tables use uppercase letters as visual labels for unshifted letter ke
53 | Confirm | `Enter` |53 | Confirm | `Enter` |
54 | Cancel | `Escape` |54 | Cancel | `Escape` |
55 55
56-## Page Mode (`Ctrl+Y`)56+## Vim Mode (`Ctrl+Y`)
57 57
58-Page Mode is an explicit page-scrolling mode for the active pane only. It applies a pane-local accent from `workspace.styling.pane_mode_color`, and when `workspace.styling.mode_indicator_toaster_enabled` is true, shows a persistent bottom-left `PAGE MODE` toaster while the mode is active; the toaster hides on exit, when the mode is toggled off, or when the toaster is disabled in config. The mode exits automatically when focus moves into the omnibox, find bar, overlays, or an editable element inside the page. The default `timeout_ms` is `0`, so Page Mode does not auto-time out unless you configure one. Arrow keys continue to flow through the browser engine's native page-navigation path while Page Mode is active, while other app-level shortcuts stay suspended until you leave the mode.58+Vim Mode is an explicit Vim-style scrolling mode for the active pane only. It applies a pane-local accent from `workspace.styling.pane_mode_color`, and when `workspace.styling.mode_indicator_toaster_enabled` is true, shows a persistent bottom-left `VIM MODE` toaster while the mode is active; the toaster hides on exit, when the mode is toggled off, or when the toaster is disabled in config. The mode exits automatically when focus moves into the omnibox, find bar, overlays, or an editable element inside the page. The default `timeout_ms` is `0`, so Vim Mode does not auto-time out unless you configure one. Arrow keys continue to flow through the browser engine's native page-navigation path while Vim Mode is active, while other app-level shortcuts stay suspended until you leave the mode.
59 59
60-CEF and WebKit execute Page Mode scroll commands (`h/j/k/l`, `Shift+J/K`) with the shared `BuildScrollByJS` resolver. Each step starts under the viewport center, walks up through ancestors that can move in the requested direction, and hands scrolling to the document when a nested container reaches its boundary. Cross-origin frame contents remain best-effort.60+CEF and WebKit execute Vim Mode scroll commands (`h/j/k/l`, `Shift+J/K`) with the shared `BuildPageScrollByJS` resolver. Each step starts under the viewport center, walks up through ancestors that can move in the requested direction, and hands scrolling to the document when a nested container reaches its boundary. Cross-origin frame contents remain best-effort.
61 61
62 | Action | Keys |62 | Action | Keys |
63 |--------|------|63 |--------|------|
@@ -137,26 +137,26 @@ All keybindings can be customized in `~/.config/dumber/config.toml`:
137 split-right = ["arrowright", "r"]137 split-right = ["arrowright", "r"]
138 close-pane = ["x", "q"]138 close-pane = ["x", "q"]
139 139
140-[workspace.page_mode]140+[workspace.vim_mode]
141 activation_shortcut = "ctrl+y"141 activation_shortcut = "ctrl+y"
142 timeout_ms = 0142 timeout_ms = 0
143 143
144-[workspace.page_mode.actions.page-scroll-left]144+[workspace.vim_mode.actions.vim-scroll-left]
145 keys = ["h"]145 keys = ["h"]
146 146
147-[workspace.page_mode.actions.page-scroll-down]147+[workspace.vim_mode.actions.vim-scroll-down]
148 keys = ["j"]148 keys = ["j"]
149 149
150-[workspace.page_mode.actions.page-scroll-up]150+[workspace.vim_mode.actions.vim-scroll-up]
151 keys = ["k"]151 keys = ["k"]
152 152
153-[workspace.page_mode.actions.page-scroll-right]153+[workspace.vim_mode.actions.vim-scroll-right]
154 keys = ["l"]154 keys = ["l"]
155 155
156-[workspace.page_mode.actions.page-scroll-down-fast]156+[workspace.vim_mode.actions.vim-scroll-down-fast]
157 keys = ["shift+j"]157 keys = ["shift+j"]
158 158
159-[workspace.page_mode.actions.page-scroll-up-fast]159+[workspace.vim_mode.actions.vim-scroll-up-fast]
160 keys = ["shift+k"]160 keys = ["shift+k"]
161 161
162 [workspace.shortcuts.actions.close-pane]162 [workspace.shortcuts.actions.close-pane]
M internal/application/port/webview.go +4 -4
@@ -395,7 +395,7 @@ type TextInputTargetProvider interface {
395 TextInputTarget() TextInputTarget395 TextInputTarget() TextInputTarget
396 }396 }
397 397
398-// PageScrollCommand identifies a semantic page-scroll request at the398+// PageScrollCommand identifies a semantic vim-scroll request at the
399 // application boundary without depending on usecase package enums.399 // application boundary without depending on usecase package enums.
400 type PageScrollCommand int400 type PageScrollCommand int
401 401
@@ -408,7 +408,7 @@ const (
408 PageScrollCommandDownFast408 PageScrollCommandDownFast
409 )409 )
410 410
411-// PageScrollRequest carries a semantic page-scroll command identity and its411+// PageScrollRequest carries a semantic vim-scroll command identity and its
412 // fallback pixel deltas. Engines own the execution strategy and may use the412 // fallback pixel deltas. Engines own the execution strategy and may use the
413 // fallback delta when no native mechanism exists for the command.413 // fallback delta when no native mechanism exists for the command.
414 type PageScrollRequest struct {414 type PageScrollRequest struct {
@@ -422,7 +422,7 @@ type PageScrollRequest struct {
422 422
423 // PageScrollable is an optional capability for WebViews that support semantic423 // PageScrollable is an optional capability for WebViews that support semantic
424 // page scrolling. Page scrolling is used by keyboard-driven navigation modes424 // page scrolling. Page scrolling is used by keyboard-driven navigation modes
425-// such as Page mode.425+// such as Vim mode.
426 //426 //
427 // Engines own the execution strategy. When a native mechanism exists for the427 // Engines own the execution strategy. When a native mechanism exists for the
428 // requested command, the engine may ignore the fallback delta. Otherwise it428 // requested command, the engine may ignore the fallback delta. Otherwise it
@@ -432,7 +432,7 @@ type PageScrollable interface {
432 }432 }
433 433
434 // PageScrollCanceler is an optional capability for adapters that queue held434 // PageScrollCanceler is an optional capability for adapters that queue held
435-// page-scroll work. Implementations discard unconsumed continuous deltas while435+// vim-scroll work. Implementations discard unconsumed continuous deltas while
436 // preserving any guaranteed tap already accepted.436 // preserving any guaranteed tap already accepted.
437 type PageScrollCanceler interface {437 type PageScrollCanceler interface {
438 CancelPageScroll(ctx context.Context)438 CancelPageScroll(ctx context.Context)
M internal/application/usecase/keybindings.go +1 -0
@@ -18,6 +18,7 @@ var validModes = map[string]bool{
18 "global": true,18 "global": true,
19 "pane": true,19 "pane": true,
20 "tab": true,20 "tab": true,
21+ "vim": true,
21 "resize": true,22 "resize": true,
22 "session": true,23 "session": true,
23 }24 }
M internal/application/usecase/keybindings_test.go +11 -0
@@ -72,6 +72,17 @@ func TestGetKeybindingsUseCase_Execute(t *testing.T) {
72 }72 }
73 73
74 func TestSetKeybindingUseCase_Execute(t *testing.T) {74 func TestSetKeybindingUseCase_Execute(t *testing.T) {
75+ t.Run("accepts vim mode", func(t *testing.T) {
76+ mockProvider := mocks.NewMockKeybindingsProvider(t)
77+ mockSaver := mocks.NewMockKeybindingsSaver(t)
78+ req := port.SetKeybindingRequest{Mode: "vim", Action: "vim-scroll-down", Keys: []string{"j"}}
79+ mockProvider.EXPECT().CheckConflicts(mock.Anything, req.Mode, req.Action, req.Keys).Return(nil, nil)
80+ mockSaver.EXPECT().SetKeybinding(mock.Anything, req).Return(nil)
81+
82+ _, err := usecase.NewSetKeybindingUseCase(mockProvider, mockSaver).Execute(context.Background(), req)
83+ require.NoError(t, err)
84+ })
85+
75 t.Run("successfully sets keybinding", func(t *testing.T) {86 t.Run("successfully sets keybinding", func(t *testing.T) {
76 // Arrange87 // Arrange
77 mockProvider := mocks.NewMockKeybindingsProvider(t)88 mockProvider := mocks.NewMockKeybindingsProvider(t)
D internal/application/usecase/page_mode_policy.go +0 -73
@@ -1,73 +0,0 @@
1-package usecase
2-
3-// PageModePolicyTransition describes what the UI should do after a Page mode
4-// policy evaluation.
5-type PageModePolicyTransition string
6-
7-const (
8- PageModePolicyTransitionStay PageModePolicyTransition = "stay"
9- PageModePolicyTransitionExit PageModePolicyTransition = "exit"
10- PageModePolicyTransitionBlockActivation PageModePolicyTransition = "block_activation"
11-)
12-
13-// PageModePolicyTrigger identifies the focus or activation event being
14-// evaluated.
15-type PageModePolicyTrigger string
16-
17-const (
18- PageModePolicyTriggerActivationAttempt PageModePolicyTrigger = "activation_attempt"
19- PageModePolicyTriggerOmniboxFocus PageModePolicyTrigger = "omnibox_focus"
20- PageModePolicyTriggerFindBarFocus PageModePolicyTrigger = "find_bar_focus"
21- PageModePolicyTriggerOverlayFocus PageModePolicyTrigger = "overlay_focus"
22- PageModePolicyTriggerPageEditableFocusChanged PageModePolicyTrigger = "page_editable_focus_changed"
23- PageModePolicyTriggerContextChanged PageModePolicyTrigger = "context_changed"
24-)
25-
26-// PageModePolicyInput captures the current Page mode state and the event being
27-// evaluated.
28-type PageModePolicyInput struct {
29- Trigger PageModePolicyTrigger
30- PageModeActive bool
31- PageEditableFocused bool
32- EventInActiveContext bool
33- PreserveOnContextChange bool
34-}
35-
36-// PageModePolicyUseCase evaluates whether Page mode should stay active, exit,
37-// or be blocked from activating.
38-type PageModePolicyUseCase struct{}
39-
40-// NewPageModePolicyUseCase creates a PageModePolicyUseCase.
41-func NewPageModePolicyUseCase() *PageModePolicyUseCase {
42- return &PageModePolicyUseCase{}
43-}
44-
45-// Evaluate decides the Page mode transition for the provided event.
46-func (*PageModePolicyUseCase) Evaluate(input PageModePolicyInput) PageModePolicyTransition {
47- switch input.Trigger {
48- case PageModePolicyTriggerActivationAttempt:
49- if input.PageEditableFocused {
50- return PageModePolicyTransitionBlockActivation
51- }
52- return PageModePolicyTransitionStay
53- case PageModePolicyTriggerOmniboxFocus,
54- PageModePolicyTriggerFindBarFocus,
55- PageModePolicyTriggerOverlayFocus:
56- if input.PageModeActive {
57- return PageModePolicyTransitionExit
58- }
59- return PageModePolicyTransitionStay
60- case PageModePolicyTriggerPageEditableFocusChanged:
61- if input.PageModeActive && input.PageEditableFocused && input.EventInActiveContext {
62- return PageModePolicyTransitionExit
63- }
64- return PageModePolicyTransitionStay
65- case PageModePolicyTriggerContextChanged:
66- if input.PageModeActive && !input.PreserveOnContextChange {
67- return PageModePolicyTransitionExit
68- }
69- return PageModePolicyTransitionStay
70- default:
71- return PageModePolicyTransitionStay
72- }
73-}
D internal/application/usecase/page_mode_policy_test.go +0 -109
@@ -1,109 +0,0 @@
1-package usecase
2-
3-import (
4- "testing"
5-
6- "github.com/stretchr/testify/assert"
7-)
8-
9-func TestPageModePolicyEvaluate(t *testing.T) {
10- tests := []struct {
11- name string
12- input PageModePolicyInput
13- want PageModePolicyTransition
14- }{
15- {
16- name: "activation blocked while page editable focused",
17- input: PageModePolicyInput{
18- Trigger: PageModePolicyTriggerActivationAttempt,
19- PageEditableFocused: true,
20- },
21- want: PageModePolicyTransitionBlockActivation,
22- },
23- {
24- name: "activation stays allowed when page is not editable",
25- input: PageModePolicyInput{
26- Trigger: PageModePolicyTriggerActivationAttempt,
27- PageEditableFocused: false,
28- },
29- want: PageModePolicyTransitionStay,
30- },
31- {
32- name: "active page editable focus exits page mode",
33- input: PageModePolicyInput{
34- Trigger: PageModePolicyTriggerPageEditableFocusChanged,
35- PageModeActive: true,
36- PageEditableFocused: true,
37- EventInActiveContext: true,
38- },
39- want: PageModePolicyTransitionExit,
40- },
41- {
42- name: "background page editable focus does not exit page mode",
43- input: PageModePolicyInput{
44- Trigger: PageModePolicyTriggerPageEditableFocusChanged,
45- PageModeActive: true,
46- PageEditableFocused: true,
47- EventInActiveContext: false,
48- },
49- want: PageModePolicyTransitionStay,
50- },
51- {
52- name: "overlay focus exits page mode",
53- input: PageModePolicyInput{
54- Trigger: PageModePolicyTriggerOverlayFocus,
55- PageModeActive: true,
56- },
57- want: PageModePolicyTransitionExit,
58- },
59- {
60- name: "omnibox focus exits page mode",
61- input: PageModePolicyInput{
62- Trigger: PageModePolicyTriggerOmniboxFocus,
63- PageModeActive: true,
64- },
65- want: PageModePolicyTransitionExit,
66- },
67- {
68- name: "find bar focus exits page mode",
69- input: PageModePolicyInput{
70- Trigger: PageModePolicyTriggerFindBarFocus,
71- PageModeActive: true,
72- },
73- want: PageModePolicyTransitionExit,
74- },
75- {
76- name: "context change preserves page mode when scoped context stays valid",
77- input: PageModePolicyInput{
78- Trigger: PageModePolicyTriggerContextChanged,
79- PageModeActive: true,
80- PreserveOnContextChange: true,
81- },
82- want: PageModePolicyTransitionStay,
83- },
84- {
85- name: "context change exits page mode when scoped context is lost",
86- input: PageModePolicyInput{
87- Trigger: PageModePolicyTriggerContextChanged,
88- PageModeActive: true,
89- PreserveOnContextChange: false,
90- },
91- want: PageModePolicyTransitionExit,
92- },
93- {
94- name: "non page mode focus change stays normal",
95- input: PageModePolicyInput{
96- Trigger: PageModePolicyTriggerOverlayFocus,
97- PageModeActive: false,
98- },
99- want: PageModePolicyTransitionStay,
100- },
101- }
102-
103- uc := NewPageModePolicyUseCase()
104- for _, tt := range tests {
105- t.Run(tt.name, func(t *testing.T) {
106- assert.Equal(t, tt.want, uc.Evaluate(tt.input))
107- })
108- }
109-}
A internal/application/usecase/vim_mode_policy.go +73 -0
@@ -0,0 +1,73 @@
1+package usecase
2+
3+// VimModePolicyTransition describes what the UI should do after a Vim mode
4+// policy evaluation.
5+type VimModePolicyTransition string
6+
7+const (
8+ VimModePolicyTransitionStay VimModePolicyTransition = "stay"
9+ VimModePolicyTransitionExit VimModePolicyTransition = "exit"
10+ VimModePolicyTransitionBlockActivation VimModePolicyTransition = "block_activation"
11+)
12+
13+// VimModePolicyTrigger identifies the focus or activation event being
14+// evaluated.
15+type VimModePolicyTrigger string
16+
17+const (
18+ VimModePolicyTriggerActivationAttempt VimModePolicyTrigger = "activation_attempt"
19+ VimModePolicyTriggerOmniboxFocus VimModePolicyTrigger = "omnibox_focus"
20+ VimModePolicyTriggerFindBarFocus VimModePolicyTrigger = "find_bar_focus"
21+ VimModePolicyTriggerOverlayFocus VimModePolicyTrigger = "overlay_focus"
22+ VimModePolicyTriggerPageEditableFocusChanged VimModePolicyTrigger = "page_editable_focus_changed"
23+ VimModePolicyTriggerContextChanged VimModePolicyTrigger = "context_changed"
24+)
25+
26+// VimModePolicyInput captures the current Vim mode state and the event being
27+// evaluated.
28+type VimModePolicyInput struct {
29+ Trigger VimModePolicyTrigger
30+ VimModeActive bool
31+ PageEditableFocused bool
32+ EventInActiveContext bool
33+ PreserveOnContextChange bool
34+}
35+
36+// VimModePolicyUseCase evaluates whether Vim mode should stay active, exit,
37+// or be blocked from activating.
38+type VimModePolicyUseCase struct{}
39+
40+// NewVimModePolicyUseCase creates a VimModePolicyUseCase.
41+func NewVimModePolicyUseCase() *VimModePolicyUseCase {
42+ return &VimModePolicyUseCase{}
43+}
44+
45+// Evaluate decides the Vim mode transition for the provided event.
46+func (*VimModePolicyUseCase) Evaluate(input VimModePolicyInput) VimModePolicyTransition {
47+ switch input.Trigger {
48+ case VimModePolicyTriggerActivationAttempt:
49+ if input.PageEditableFocused {
50+ return VimModePolicyTransitionBlockActivation
51+ }
52+ return VimModePolicyTransitionStay
53+ case VimModePolicyTriggerOmniboxFocus,
54+ VimModePolicyTriggerFindBarFocus,
55+ VimModePolicyTriggerOverlayFocus:
56+ if input.VimModeActive {
57+ return VimModePolicyTransitionExit
58+ }
59+ return VimModePolicyTransitionStay
60+ case VimModePolicyTriggerPageEditableFocusChanged:
61+ if input.VimModeActive && input.PageEditableFocused && input.EventInActiveContext {
62+ return VimModePolicyTransitionExit
63+ }
64+ return VimModePolicyTransitionStay
65+ case VimModePolicyTriggerContextChanged:
66+ if input.VimModeActive && !input.PreserveOnContextChange {
67+ return VimModePolicyTransitionExit
68+ }
69+ return VimModePolicyTransitionStay
70+ default:
71+ return VimModePolicyTransitionStay
72+ }
73+}
A internal/application/usecase/vim_mode_policy_test.go +109 -0
@@ -0,0 +1,109 @@
1+package usecase
2+
3+import (
4+ "testing"
5+
6+ "github.com/stretchr/testify/assert"
7+)
8+
9+func TestVimModePolicyEvaluate(t *testing.T) {
10+ tests := []struct {
11+ name string
12+ input VimModePolicyInput
13+ want VimModePolicyTransition
14+ }{
15+ {
16+ name: "activation blocked while page editable focused",
17+ input: VimModePolicyInput{
18+ Trigger: VimModePolicyTriggerActivationAttempt,
19+ PageEditableFocused: true,
20+ },
21+ want: VimModePolicyTransitionBlockActivation,
22+ },
23+ {
24+ name: "activation stays allowed when page is not editable",
25+ input: VimModePolicyInput{
26+ Trigger: VimModePolicyTriggerActivationAttempt,
27+ PageEditableFocused: false,
28+ },
29+ want: VimModePolicyTransitionStay,
30+ },
31+ {
32+ name: "active page editable focus exits vim mode",
33+ input: VimModePolicyInput{
34+ Trigger: VimModePolicyTriggerPageEditableFocusChanged,
35+ VimModeActive: true,
36+ PageEditableFocused: true,
37+ EventInActiveContext: true,
38+ },
39+ want: VimModePolicyTransitionExit,
40+ },
41+ {
42+ name: "background page editable focus does not exit vim mode",
43+ input: VimModePolicyInput{
44+ Trigger: VimModePolicyTriggerPageEditableFocusChanged,
45+ VimModeActive: true,
46+ PageEditableFocused: true,
47+ EventInActiveContext: false,
48+ },
49+ want: VimModePolicyTransitionStay,
50+ },
51+ {
52+ name: "overlay focus exits vim mode",
53+ input: VimModePolicyInput{
54+ Trigger: VimModePolicyTriggerOverlayFocus,
55+ VimModeActive: true,
56+ },
57+ want: VimModePolicyTransitionExit,
58+ },
59+ {
60+ name: "omnibox focus exits vim mode",
61+ input: VimModePolicyInput{
62+ Trigger: VimModePolicyTriggerOmniboxFocus,
63+ VimModeActive: true,
64+ },
65+ want: VimModePolicyTransitionExit,
66+ },
67+ {
68+ name: "find bar focus exits vim mode",
69+ input: VimModePolicyInput{
70+ Trigger: VimModePolicyTriggerFindBarFocus,
71+ VimModeActive: true,
72+ },
73+ want: VimModePolicyTransitionExit,
74+ },
75+ {
76+ name: "context change preserves vim mode when scoped context stays valid",
77+ input: VimModePolicyInput{
78+ Trigger: VimModePolicyTriggerContextChanged,
79+ VimModeActive: true,
80+ PreserveOnContextChange: true,
81+ },
82+ want: VimModePolicyTransitionStay,
83+ },
84+ {
85+ name: "context change exits vim mode when scoped context is lost",
86+ input: VimModePolicyInput{
87+ Trigger: VimModePolicyTriggerContextChanged,
88+ VimModeActive: true,
89+ PreserveOnContextChange: false,
90+ },
91+ want: VimModePolicyTransitionExit,
92+ },
93+ {
94+ name: "non vim mode focus change stays normal",
95+ input: VimModePolicyInput{
96+ Trigger: VimModePolicyTriggerOverlayFocus,
97+ VimModeActive: false,
98+ },
99+ want: VimModePolicyTransitionStay,
100+ },
101+ }
102+
103+ uc := NewVimModePolicyUseCase()
104+ for _, tt := range tests {
105+ t.Run(tt.name, func(t *testing.T) {
106+ assert.Equal(t, tt.want, uc.Evaluate(tt.input))
107+ })
108+ }
109+}
M internal/bootstrap/runtime_config.go +1 -1
@@ -140,7 +140,7 @@ func cloneRuntimeSearchShortcuts(in map[string]entity.RuntimeSearchShortcut) map
140 func cloneWorkspaceConfig(in entity.WorkspaceConfig) entity.WorkspaceConfig {140 func cloneWorkspaceConfig(in entity.WorkspaceConfig) entity.WorkspaceConfig {
141 in.PaneMode.Actions = cloneActionBindings(in.PaneMode.Actions)141 in.PaneMode.Actions = cloneActionBindings(in.PaneMode.Actions)
142 in.TabMode.Actions = cloneActionBindings(in.TabMode.Actions)142 in.TabMode.Actions = cloneActionBindings(in.TabMode.Actions)
143- in.PageMode.Actions = cloneActionBindings(in.PageMode.Actions)143+ in.VimMode.Actions = cloneActionBindings(in.VimMode.Actions)
144 in.ResizeMode.Actions = cloneActionBindings(in.ResizeMode.Actions)144 in.ResizeMode.Actions = cloneActionBindings(in.ResizeMode.Actions)
145 in.Shortcuts.Actions = cloneActionBindings(in.Shortcuts.Actions)145 in.Shortcuts.Actions = cloneActionBindings(in.Shortcuts.Actions)
146 in.FloatingPane.Profiles = cloneFloatingPaneProfiles(in.FloatingPane.Profiles)146 in.FloatingPane.Profiles = cloneFloatingPaneProfiles(in.FloatingPane.Profiles)
M internal/bootstrap/runtime_config_test.go +8 -8
@@ -258,8 +258,8 @@ func runtimeConfigWithNestedMutableFields() *config.Config {
258 cfg.Workspace.TabMode.Actions = map[string]entity.ActionBinding{258 cfg.Workspace.TabMode.Actions = map[string]entity.ActionBinding{
259 "next-tab": {Keys: []string{"l"}, Desc: "Next tab"},259 "next-tab": {Keys: []string{"l"}, Desc: "Next tab"},
260 }260 }
261- cfg.Workspace.PageMode.Actions = map[string]entity.ActionBinding{261+ cfg.Workspace.VimMode.Actions = map[string]entity.ActionBinding{
262- "page-scroll-down": {Keys: []string{"j"}, Desc: "Scroll down"},262+ "vim-scroll-down": {Keys: []string{"j"}, Desc: "Scroll down"},
263 }263 }
264 cfg.Workspace.ResizeMode.Actions = map[string]entity.ActionBinding{264 cfg.Workspace.ResizeMode.Actions = map[string]entity.ActionBinding{
265 "grow-right": {Keys: []string{"right"}, Desc: "Grow right"},265 "grow-right": {Keys: []string{"right"}, Desc: "Grow right"},
@@ -283,8 +283,8 @@ func mutateNestedRuntimeConfigSnapshot(snapshot entity.RuntimeConfigSnapshot) {
283 mutateActionBinding(snapshot.UI.Workspace.TabMode.Actions, "next-tab", "mutated-tab")283 mutateActionBinding(snapshot.UI.Workspace.TabMode.Actions, "next-tab", "mutated-tab")
284 snapshot.UI.Workspace.TabMode.Actions["added-tab"] = entity.ActionBinding{Keys: []string{"added"}}284 snapshot.UI.Workspace.TabMode.Actions["added-tab"] = entity.ActionBinding{Keys: []string{"added"}}
285 285
286- mutateActionBinding(snapshot.UI.Workspace.PageMode.Actions, "page-scroll-down", "mutated-page")286+ mutateActionBinding(snapshot.UI.Workspace.VimMode.Actions, "vim-scroll-down", "mutated-page")
287- snapshot.UI.Workspace.PageMode.Actions["added-page"] = entity.ActionBinding{Keys: []string{"added"}}287+ snapshot.UI.Workspace.VimMode.Actions["added-page"] = entity.ActionBinding{Keys: []string{"added"}}
288 288
289 mutateActionBinding(snapshot.UI.Workspace.ResizeMode.Actions, "grow-right", "mutated-resize")289 mutateActionBinding(snapshot.UI.Workspace.ResizeMode.Actions, "grow-right", "mutated-resize")
290 snapshot.UI.Workspace.ResizeMode.Actions["added-resize"] = entity.ActionBinding{Keys: []string{"added"}}290 snapshot.UI.Workspace.ResizeMode.Actions["added-resize"] = entity.ActionBinding{Keys: []string{"added"}}
@@ -316,8 +316,8 @@ func assertNestedRuntimeConfigSnapshotUnchanged(t *testing.T, snapshot entity.Ru
316 assertActionBinding(t, snapshot.UI.Workspace.TabMode.Actions, "next-tab", "l")316 assertActionBinding(t, snapshot.UI.Workspace.TabMode.Actions, "next-tab", "l")
317 assertMapEntryAbsent(t, snapshot.UI.Workspace.TabMode.Actions, "added-tab")317 assertMapEntryAbsent(t, snapshot.UI.Workspace.TabMode.Actions, "added-tab")
318 318
319- assertActionBinding(t, snapshot.UI.Workspace.PageMode.Actions, "page-scroll-down", "j")319+ assertActionBinding(t, snapshot.UI.Workspace.VimMode.Actions, "vim-scroll-down", "j")
320- assertMapEntryAbsent(t, snapshot.UI.Workspace.PageMode.Actions, "added-page")320+ assertMapEntryAbsent(t, snapshot.UI.Workspace.VimMode.Actions, "added-page")
321 321
322 assertActionBinding(t, snapshot.UI.Workspace.ResizeMode.Actions, "grow-right", "right")322 assertActionBinding(t, snapshot.UI.Workspace.ResizeMode.Actions, "grow-right", "right")
323 assertMapEntryAbsent(t, snapshot.UI.Workspace.ResizeMode.Actions, "added-resize")323 assertMapEntryAbsent(t, snapshot.UI.Workspace.ResizeMode.Actions, "added-resize")
@@ -366,8 +366,8 @@ func assertNestedSourceConfigUnchanged(t *testing.T, cfg *config.Config) {
366 assertActionBinding(t, cfg.Workspace.TabMode.Actions, "next-tab", "l")366 assertActionBinding(t, cfg.Workspace.TabMode.Actions, "next-tab", "l")
367 assertMapEntryAbsent(t, cfg.Workspace.TabMode.Actions, "added-tab")367 assertMapEntryAbsent(t, cfg.Workspace.TabMode.Actions, "added-tab")
368 368
369- assertActionBinding(t, cfg.Workspace.PageMode.Actions, "page-scroll-down", "j")369+ assertActionBinding(t, cfg.Workspace.VimMode.Actions, "vim-scroll-down", "j")
370- assertMapEntryAbsent(t, cfg.Workspace.PageMode.Actions, "added-page")370+ assertMapEntryAbsent(t, cfg.Workspace.VimMode.Actions, "added-page")
371 371
372 assertActionBinding(t, cfg.Workspace.ResizeMode.Actions, "grow-right", "right")372 assertActionBinding(t, cfg.Workspace.ResizeMode.Actions, "grow-right", "right")
373 assertMapEntryAbsent(t, cfg.Workspace.ResizeMode.Actions, "added-resize")373 assertMapEntryAbsent(t, cfg.Workspace.ResizeMode.Actions, "added-resize")
M internal/domain/entity/config_types.go +4 -4
@@ -78,15 +78,15 @@ func (t *TabModeConfig) GetKeyBindings() map[string]string {
78 return keyBindingsFromActions(t.Actions)78 return keyBindingsFromActions(t.Actions)
79 }79 }
80 80
81-// PageModeConfig holds page scroll mode shortcut configuration.81+// VimModeConfig holds vim scroll mode shortcut configuration.
82-type PageModeConfig struct {82+type VimModeConfig struct {
83 ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" toml:"activation_shortcut" json:"activation_shortcut"` //nolint:lll // struct tags must stay on one line83 ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" toml:"activation_shortcut" json:"activation_shortcut"` //nolint:lll // struct tags must stay on one line
84 TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" toml:"timeout_ms" json:"timeout_ms"`84 TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" toml:"timeout_ms" json:"timeout_ms"`
85 Actions map[string]ActionBinding `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"`85 Actions map[string]ActionBinding `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"`
86 }86 }
87 87
88 // GetKeyBindings returns a map from key string to action name.88 // GetKeyBindings returns a map from key string to action name.
89-func (p *PageModeConfig) GetKeyBindings() map[string]string {89+func (p *VimModeConfig) GetKeyBindings() map[string]string {
90 return keyBindingsFromActions(p.Actions)90 return keyBindingsFromActions(p.Actions)
91 }91 }
92 92
@@ -240,7 +240,7 @@ type WorkspaceConfig struct {
240 NewPaneURL string `mapstructure:"new_pane_url" yaml:"new_pane_url" toml:"new_pane_url" json:"new_pane_url"`240 NewPaneURL string `mapstructure:"new_pane_url" yaml:"new_pane_url" toml:"new_pane_url" json:"new_pane_url"`
241 PaneMode PaneModeConfig `mapstructure:"pane_mode" yaml:"pane_mode" toml:"pane_mode" json:"pane_mode"`241 PaneMode PaneModeConfig `mapstructure:"pane_mode" yaml:"pane_mode" toml:"pane_mode" json:"pane_mode"`
242 TabMode TabModeConfig `mapstructure:"tab_mode" yaml:"tab_mode" toml:"tab_mode" json:"tab_mode"`242 TabMode TabModeConfig `mapstructure:"tab_mode" yaml:"tab_mode" toml:"tab_mode" json:"tab_mode"`
243- PageMode PageModeConfig `mapstructure:"page_mode" yaml:"page_mode" toml:"page_mode" json:"page_mode"`243+ VimMode VimModeConfig `mapstructure:"vim_mode" yaml:"vim_mode" toml:"vim_mode" json:"vim_mode"`
244 ResizeMode ResizeModeConfig `mapstructure:"resize_mode" yaml:"resize_mode" toml:"resize_mode" json:"resize_mode"`244 ResizeMode ResizeModeConfig `mapstructure:"resize_mode" yaml:"resize_mode" toml:"resize_mode" json:"resize_mode"`
245 Shortcuts GlobalShortcutsConfig `mapstructure:"shortcuts" yaml:"shortcuts" toml:"shortcuts" json:"shortcuts"`245 Shortcuts GlobalShortcutsConfig `mapstructure:"shortcuts" yaml:"shortcuts" toml:"shortcuts" json:"shortcuts"`
246 FloatingPane FloatingPaneConfig `mapstructure:"floating_pane" yaml:"floating_pane" toml:"floating_pane" json:"floating_pane"`246 FloatingPane FloatingPaneConfig `mapstructure:"floating_pane" yaml:"floating_pane" toml:"floating_pane" json:"floating_pane"`
M internal/infrastructure/cef/webview.go +3 -3
@@ -2396,7 +2396,7 @@ func (wv *WebView) takePendingCreate() *pendingBrowserCreate {
2396 }2396 }
2397 2397
2398 // pageScrollWheelEvent builds the CEF mouse wheel event used for keyboard-driven2398 // pageScrollWheelEvent builds the CEF mouse wheel event used for keyboard-driven
2399-// Page Mode scrolling. It targets the center of the current OSR view so the2399+// Vim Mode scrolling. It targets the center of the current OSR view so the
2400 // scroll goes through Chromium's compositor/input pipeline instead of a series2400 // scroll goes through Chromium's compositor/input pipeline instead of a series
2401 // of synthetic key taps.2401 // of synthetic key taps.
2402 func (wv *WebView) pageScrollWheelEvent() purecef.MouseEvent {2402 func (wv *WebView) pageScrollWheelEvent() purecef.MouseEvent {
@@ -2441,7 +2441,7 @@ func (wv *WebView) ScrollPage(ctx context.Context, request port.PageScrollReques
2441 return nil2441 return nil
2442 }2442 }
2443 2443
2444- wv.RunJavaScript(ctx, webutil.BuildScrollByJS(request.FallbackDX, request.FallbackDY))2444+ wv.RunJavaScript(ctx, webutil.BuildPageScrollByJS(request.FallbackDX, request.FallbackDY))
2445 return nil2445 return nil
2446 }2446 }
2447 2447
@@ -2510,7 +2510,7 @@ func (wv *WebView) flushPageScroll() {
2510 }2510 }
2511 q.mu.Unlock()2511 q.mu.Unlock()
2512 if dx, dy := tapDX+heldDX, tapDY+heldDY; (dx != 0 || dy != 0) && !wv.destroyed.Load() {2512 if dx, dy := tapDX+heldDX, tapDY+heldDY; (dx != 0 || dy != 0) && !wv.destroyed.Load() {
2513- wv.executeJavaScriptNow(webutil.BuildScrollByJS(dx, dy))2513+ wv.executeJavaScriptNow(webutil.BuildPageScrollByJS(dx, dy))
2514 }2514 }
2515 q.commitMu.Unlock()2515 q.commitMu.Unlock()
2516 2516
M internal/infrastructure/cef/webview_scroll_test.go +3 -3
@@ -42,7 +42,7 @@ func TestPageScrollWheelDeltas_UseFallbackDeltasWithCEFSign(t *testing.T) {
42 }42 }
43 43
44 // scrollRecorderHost wraps purecef.BrowserHost and records mouse wheel events,44 // scrollRecorderHost wraps purecef.BrowserHost and records mouse wheel events,
45-// allowing tests to verify that Page Mode uses Chromium's native scroll path.45+// allowing tests to verify that Vim Mode uses Chromium's native scroll path.
46 type scrollRecorderHost struct {46 type scrollRecorderHost struct {
47 purecef.BrowserHost // embedded nil — only SendMouseWheelEvent is called in these tests47 purecef.BrowserHost // embedded nil — only SendMouseWheelEvent is called in these tests
48 48
@@ -174,7 +174,7 @@ func TestScrollPage_ReadyBrowser_HorizontalDirectionsUseDOMSigns(t *testing.T) {
174 func TestScrollPage_ReadyBrowser_NestedBoundaryHandsOffToDocument(t *testing.T) {174 func TestScrollPage_ReadyBrowser_NestedBoundaryHandsOffToDocument(t *testing.T) {
175 node, err := exec.LookPath("node")175 node, err := exec.LookPath("node")
176 if err != nil {176 if err != nil {
177- t.Skip("node is required to execute the generated page-scroll helper")177+ t.Skip("node is required to execute the generated vim-scroll helper")
178 }178 }
179 179
180 host := &scrollRecorderHost{}180 host := &scrollRecorderHost{}
@@ -211,7 +211,7 @@ process.stdout.write(inner.scrollTop+','+root.scrollTop);
211 `, pageScrollScript)211 `, pageScrollScript)
212 output, err := exec.Command(node, "-e", harness).CombinedOutput()212 output, err := exec.Command(node, "-e", harness).CombinedOutput()
213 if err != nil {213 if err != nil {
214- t.Fatalf("execute generated page-scroll helper: %v\n%s", err, output)214+ t.Fatalf("execute generated vim-scroll helper: %v\n%s", err, output)
215 }215 }
216 if got := strings.TrimSpace(string(output)); got != "200,80" {216 if got := strings.TrimSpace(string(output)); got != "200,80" {
217 t.Fatalf("nested/document scroll offsets = %s, want 200,80", got)217 t.Fatalf("nested/document scroll offsets = %s, want 200,80", got)
M internal/infrastructure/config/defaults.go +9 -9
@@ -304,18 +304,18 @@ func DefaultConfig() *Config {
304 "cancel": {Keys: []string{"escape"}, Desc: "Cancel/exit mode"},304 "cancel": {Keys: []string{"escape"}, Desc: "Cancel/exit mode"},
305 },305 },
306 },306 },
307- PageMode: PageModeConfig{307+ VimMode: VimModeConfig{
308 ActivationShortcut: defaultPageActivationShortcut,308 ActivationShortcut: defaultPageActivationShortcut,
309 TimeoutMilliseconds: defaultPageTimeoutMilliseconds,309 TimeoutMilliseconds: defaultPageTimeoutMilliseconds,
310 Actions: map[string]ActionBinding{310 Actions: map[string]ActionBinding{
311- "page-scroll-left": {Keys: []string{"h"}, Desc: "Scroll page left"},311+ "vim-scroll-left": {Keys: []string{"h"}, Desc: "Scroll page left"},
312- "page-scroll-down": {Keys: []string{"j"}, Desc: "Scroll page down"},312+ "vim-scroll-down": {Keys: []string{"j"}, Desc: "Scroll page down"},
313- "page-scroll-up": {Keys: []string{"k"}, Desc: "Scroll page up"},313+ "vim-scroll-up": {Keys: []string{"k"}, Desc: "Scroll page up"},
314- "page-scroll-right": {Keys: []string{"l"}, Desc: "Scroll page right"},314+ "vim-scroll-right": {Keys: []string{"l"}, Desc: "Scroll page right"},
315- "page-scroll-down-fast": {Keys: []string{"shift+j"}, Desc: "Scroll page down fast"},315+ "vim-scroll-down-fast": {Keys: []string{"shift+j"}, Desc: "Scroll page down fast"},
316- "page-scroll-up-fast": {Keys: []string{"shift+k"}, Desc: "Scroll page up fast"},316+ "vim-scroll-up-fast": {Keys: []string{"shift+k"}, Desc: "Scroll page up fast"},
317- "confirm": {Keys: []string{"enter"}, Desc: "Confirm action"},317+ "confirm": {Keys: []string{"enter"}, Desc: "Confirm action"},
318- "cancel": {Keys: []string{"escape"}, Desc: "Cancel/exit mode"},318+ "cancel": {Keys: []string{"escape"}, Desc: "Cancel/exit mode"},
319 },319 },
320 },320 },
321 ResizeMode: ResizeModeConfig{321 ResizeMode: ResizeModeConfig{
M internal/infrastructure/config/keybindings_gateway.go +15 -15
@@ -32,7 +32,7 @@ func (g *KeybindingsGateway) GetKeybindings(ctx context.Context) (port.Keybindin
32 g.buildGlobalGroup(cfg, defaults),32 g.buildGlobalGroup(cfg, defaults),
33 g.buildPaneModeGroup(cfg, defaults),33 g.buildPaneModeGroup(cfg, defaults),
34 g.buildTabModeGroup(cfg, defaults),34 g.buildTabModeGroup(cfg, defaults),
35- g.buildPageModeGroup(cfg, defaults),35+ g.buildVimModeGroup(cfg, defaults),
36 g.buildResizeModeGroup(cfg, defaults),36 g.buildResizeModeGroup(cfg, defaults),
37 g.buildSessionModeGroup(cfg, defaults),37 g.buildSessionModeGroup(cfg, defaults),
38 }38 }
@@ -49,7 +49,7 @@ func (g *KeybindingsGateway) GetDefaultKeybindings(ctx context.Context) (port.Ke
49 g.buildGlobalGroup(defaults, defaults),49 g.buildGlobalGroup(defaults, defaults),
50 g.buildPaneModeGroup(defaults, defaults),50 g.buildPaneModeGroup(defaults, defaults),
51 g.buildTabModeGroup(defaults, defaults),51 g.buildTabModeGroup(defaults, defaults),
52- g.buildPageModeGroup(defaults, defaults),52+ g.buildVimModeGroup(defaults, defaults),
53 g.buildResizeModeGroup(defaults, defaults),53 g.buildResizeModeGroup(defaults, defaults),
54 g.buildSessionModeGroup(defaults, defaults),54 g.buildSessionModeGroup(defaults, defaults),
55 }55 }
@@ -110,7 +110,7 @@ func (g *KeybindingsGateway) ResetAllKeybindings(ctx context.Context) error {
110 110
111 cfg.Workspace.PaneMode.Actions = defaults.Workspace.PaneMode.Actions111 cfg.Workspace.PaneMode.Actions = defaults.Workspace.PaneMode.Actions
112 cfg.Workspace.TabMode.Actions = defaults.Workspace.TabMode.Actions112 cfg.Workspace.TabMode.Actions = defaults.Workspace.TabMode.Actions
113- cfg.Workspace.PageMode.Actions = defaults.Workspace.PageMode.Actions113+ cfg.Workspace.VimMode.Actions = defaults.Workspace.VimMode.Actions
114 cfg.Workspace.ResizeMode.Actions = defaults.Workspace.ResizeMode.Actions114 cfg.Workspace.ResizeMode.Actions = defaults.Workspace.ResizeMode.Actions
115 cfg.Workspace.Shortcuts = defaults.Workspace.Shortcuts115 cfg.Workspace.Shortcuts = defaults.Workspace.Shortcuts
116 cfg.Session.SessionMode.Actions = defaults.Session.SessionMode.Actions116 cfg.Session.SessionMode.Actions = defaults.Session.SessionMode.Actions
@@ -147,13 +147,13 @@ func (g *KeybindingsGateway) buildTabModeGroup(cfg, defaults *Config) port.Keybi
147 }147 }
148 }148 }
149 149
150-// buildPageModeGroup builds the page mode group.150+// buildVimModeGroup builds the vim mode group.
151-func (g *KeybindingsGateway) buildPageModeGroup(cfg, defaults *Config) port.KeybindingGroup {151+func (g *KeybindingsGateway) buildVimModeGroup(cfg, defaults *Config) port.KeybindingGroup {
152 return port.KeybindingGroup{152 return port.KeybindingGroup{
153- Mode: "page",153+ Mode: "vim",
154- DisplayName: "Page Mode",154+ DisplayName: "Vim Mode",
155- Bindings: g.buildModeBindings(cfg.Workspace.PageMode.Actions, defaults.Workspace.PageMode.Actions),155+ Bindings: g.buildModeBindings(cfg.Workspace.VimMode.Actions, defaults.Workspace.VimMode.Actions),
156- Activation: cfg.Workspace.PageMode.ActivationShortcut,156+ Activation: cfg.Workspace.VimMode.ActivationShortcut,
157 }157 }
158 }158 }
159 159
@@ -221,10 +221,10 @@ func (*KeybindingsGateway) updateKeybinding(cfg *Config, req port.SetKeybindingR
221 existing.Keys = req.Keys221 existing.Keys = req.Keys
222 cfg.Workspace.TabMode.Actions[req.Action] = existing222 cfg.Workspace.TabMode.Actions[req.Action] = existing
223 }223 }
224- case "page":224+ case "vim":
225- if existing, ok := cfg.Workspace.PageMode.Actions[req.Action]; ok {225+ if existing, ok := cfg.Workspace.VimMode.Actions[req.Action]; ok {
226 existing.Keys = req.Keys226 existing.Keys = req.Keys
227- cfg.Workspace.PageMode.Actions[req.Action] = existing227+ cfg.Workspace.VimMode.Actions[req.Action] = existing
228 }228 }
229 case "resize":229 case "resize":
230 if existing, ok := cfg.Workspace.ResizeMode.Actions[req.Action]; ok {230 if existing, ok := cfg.Workspace.ResizeMode.Actions[req.Action]; ok {
@@ -254,8 +254,8 @@ func (*KeybindingsGateway) getDefaultKeys(defaults *Config, mode, action string)
254 if binding, ok := defaults.Workspace.TabMode.Actions[action]; ok {254 if binding, ok := defaults.Workspace.TabMode.Actions[action]; ok {
255 return binding.Keys255 return binding.Keys
256 }256 }
257- case "page":257+ case "vim":
258- if binding, ok := defaults.Workspace.PageMode.Actions[action]; ok {258+ if binding, ok := defaults.Workspace.VimMode.Actions[action]; ok {
259 return binding.Keys259 return binding.Keys
260 }260 }
261 case "resize":261 case "resize":
@@ -291,7 +291,7 @@ func (*KeybindingsGateway) checkConflicts(cfg *Config, targetMode, targetAction
291 addBindings(modeGlobal, cfg.Workspace.Shortcuts.Actions)291 addBindings(modeGlobal, cfg.Workspace.Shortcuts.Actions)
292 addBindings("pane", cfg.Workspace.PaneMode.Actions)292 addBindings("pane", cfg.Workspace.PaneMode.Actions)
293 addBindings("tab", cfg.Workspace.TabMode.Actions)293 addBindings("tab", cfg.Workspace.TabMode.Actions)
294- addBindings("page", cfg.Workspace.PageMode.Actions)294+ addBindings("vim", cfg.Workspace.VimMode.Actions)
295 addBindings("resize", cfg.Workspace.ResizeMode.Actions)295 addBindings("resize", cfg.Workspace.ResizeMode.Actions)
296 addBindings("session", cfg.Session.SessionMode.Actions)296 addBindings("session", cfg.Session.SessionMode.Actions)
297 297
M internal/infrastructure/config/keybindings_gateway_test.go +47 -47
@@ -37,83 +37,83 @@ func inMemoryManager() *Manager {
37 }37 }
38 }38 }
39 39
40-func TestKeybindingsGateway_PageModeIncludedInGetKeybindings(t *testing.T) {40+func TestKeybindingsGateway_VimModeIncludedInGetKeybindings(t *testing.T) {
41 gw := NewKeybindingsGateway(inMemoryManager())41 gw := NewKeybindingsGateway(inMemoryManager())
42 cfg, err := gw.GetKeybindings(context.Background())42 cfg, err := gw.GetKeybindings(context.Background())
43 require.NoError(t, err)43 require.NoError(t, err)
44 44
45 var pageGroup *port.KeybindingGroup45 var pageGroup *port.KeybindingGroup
46 for i, g := range cfg.Groups {46 for i, g := range cfg.Groups {
47- if g.Mode == "page" {47+ if g.Mode == "vim" {
48 pageGroup = &cfg.Groups[i]48 pageGroup = &cfg.Groups[i]
49 break49 break
50 }50 }
51 }51 }
52- require.NotNil(t, pageGroup, "page mode group must be present in GetKeybindings output")52+ require.NotNil(t, pageGroup, "vim mode group must be present in GetKeybindings output")
53- assert.Equal(t, "Page Mode", pageGroup.DisplayName)53+ assert.Equal(t, "Vim Mode", pageGroup.DisplayName)
54 assert.Equal(t, "ctrl+y", pageGroup.Activation)54 assert.Equal(t, "ctrl+y", pageGroup.Activation)
55 55
56 // Verify all expected actions are present.56 // Verify all expected actions are present.
57 expectedActions := map[string]struct{}{57 expectedActions := map[string]struct{}{
58- "page-scroll-left": {},58+ "vim-scroll-left": {},
59- "page-scroll-down": {},59+ "vim-scroll-down": {},
60- "page-scroll-up": {},60+ "vim-scroll-up": {},
61- "page-scroll-right": {},61+ "vim-scroll-right": {},
62- "page-scroll-down-fast": {},62+ "vim-scroll-down-fast": {},
63- "page-scroll-up-fast": {},63+ "vim-scroll-up-fast": {},
64- "confirm": {},64+ "confirm": {},
65- "cancel": {},65+ "cancel": {},
66 }66 }
67 67
68 for _, entry := range pageGroup.Bindings {68 for _, entry := range pageGroup.Bindings {
69 delete(expectedActions, entry.Action)69 delete(expectedActions, entry.Action)
70 assert.NotEmpty(t, entry.Keys, "action %q has empty keys", entry.Action)70 assert.NotEmpty(t, entry.Keys, "action %q has empty keys", entry.Action)
71- assert.False(t, entry.IsCustom, "default page mode config should not have IsCustom=true")71+ assert.False(t, entry.IsCustom, "default vim mode config should not have IsCustom=true")
72 }72 }
73- assert.Empty(t, expectedActions, "page mode group missing actions: %v", expectedActions)73+ assert.Empty(t, expectedActions, "vim mode group missing actions: %v", expectedActions)
74 74
75 // Must be placed after global, pane, tab groups (consistent ordering).75 // Must be placed after global, pane, tab groups (consistent ordering).
76 groups := cfg.Groups76 groups := cfg.Groups
77 foundIdx := -177 foundIdx := -1
78 for i, g := range groups {78 for i, g := range groups {
79- if g.Mode == "page" {79+ if g.Mode == "vim" {
80 foundIdx = i80 foundIdx = i
81 break81 break
82 }82 }
83 }83 }
84- assert.Greater(t, foundIdx, 2, "page mode group should be after pane/tab groups")84+ assert.Greater(t, foundIdx, 2, "vim mode group should be after pane/tab groups")
85 }85 }
86 86
87-func TestKeybindingsGateway_PageModeIncludedInGetDefaultKeybindings(t *testing.T) {87+func TestKeybindingsGateway_VimModeIncludedInGetDefaultKeybindings(t *testing.T) {
88 gw := NewKeybindingsGateway(inMemoryManager())88 gw := NewKeybindingsGateway(inMemoryManager())
89 cfg, err := gw.GetDefaultKeybindings(context.Background())89 cfg, err := gw.GetDefaultKeybindings(context.Background())
90 require.NoError(t, err)90 require.NoError(t, err)
91 91
92 var pageGroup *port.KeybindingGroup92 var pageGroup *port.KeybindingGroup
93 for i, g := range cfg.Groups {93 for i, g := range cfg.Groups {
94- if g.Mode == "page" {94+ if g.Mode == "vim" {
95 pageGroup = &cfg.Groups[i]95 pageGroup = &cfg.Groups[i]
96 break96 break
97 }97 }
98 }98 }
99- require.NotNil(t, pageGroup, "page mode group must be present in GetDefaultKeybindings output")99+ require.NotNil(t, pageGroup, "vim mode group must be present in GetDefaultKeybindings output")
100- assert.Equal(t, "Page Mode", pageGroup.DisplayName)100+ assert.Equal(t, "Vim Mode", pageGroup.DisplayName)
101 assert.Equal(t, "ctrl+y", pageGroup.Activation)101 assert.Equal(t, "ctrl+y", pageGroup.Activation)
102 102
103 // Verify default keys match actual keys.103 // Verify default keys match actual keys.
104 for _, entry := range pageGroup.Bindings {104 for _, entry := range pageGroup.Bindings {
105 assert.Equal(t, entry.DefaultKeys, entry.Keys,105 assert.Equal(t, entry.DefaultKeys, entry.Keys,
106- "default page mode action %q should have keys == default_keys", entry.Action)106+ "default vim mode action %q should have keys == default_keys", entry.Action)
107 }107 }
108 }108 }
109 109
110-func TestKeybindingsGateway_PageModeSetKeybindingUpdatesFile(t *testing.T) {110+func TestKeybindingsGateway_VimModeSetKeybindingUpdatesFile(t *testing.T) {
111 mgr := newTestKeybindingsManager(t)111 mgr := newTestKeybindingsManager(t)
112 gw := NewKeybindingsGateway(mgr)112 gw := NewKeybindingsGateway(mgr)
113 113
114 err := gw.SetKeybinding(context.Background(), port.SetKeybindingRequest{114 err := gw.SetKeybinding(context.Background(), port.SetKeybindingRequest{
115- Mode: "page",115+ Mode: "vim",
116- Action: "page-scroll-left",116+ Action: "vim-scroll-left",
117 Keys: []string{"a"},117 Keys: []string{"a"},
118 })118 })
119 require.NoError(t, err)119 require.NoError(t, err)
@@ -127,21 +127,21 @@ func TestKeybindingsGateway_PageModeSetKeybindingUpdatesFile(t *testing.T) {
127 assert.NotContains(t, content, `keys = ['h']`)127 assert.NotContains(t, content, `keys = ['h']`)
128 }128 }
129 129
130-func TestKeybindingsGateway_PageModeResetKeybinding(t *testing.T) {130+func TestKeybindingsGateway_VimModeResetKeybinding(t *testing.T) {
131 mgr := newTestKeybindingsManager(t)131 mgr := newTestKeybindingsManager(t)
132 gw := NewKeybindingsGateway(mgr)132 gw := NewKeybindingsGateway(mgr)
133 133
134 // Set a keybinding.134 // Set a keybinding.
135 require.NoError(t, gw.SetKeybinding(context.Background(), port.SetKeybindingRequest{135 require.NoError(t, gw.SetKeybinding(context.Background(), port.SetKeybindingRequest{
136- Mode: "page",136+ Mode: "vim",
137- Action: "page-scroll-left",137+ Action: "vim-scroll-left",
138 Keys: []string{"a"},138 Keys: []string{"a"},
139 }))139 }))
140 140
141 // Reset it.141 // Reset it.
142 err := gw.ResetKeybinding(context.Background(), port.ResetKeybindingRequest{142 err := gw.ResetKeybinding(context.Background(), port.ResetKeybindingRequest{
143- Mode: "page",143+ Mode: "vim",
144- Action: "page-scroll-left",144+ Action: "vim-scroll-left",
145 })145 })
146 require.NoError(t, err)146 require.NoError(t, err)
147 147
@@ -154,21 +154,21 @@ func TestKeybindingsGateway_PageModeResetKeybinding(t *testing.T) {
154 assert.NotContains(t, content, `keys = ['a']`)154 assert.NotContains(t, content, `keys = ['a']`)
155 }155 }
156 156
157-func TestKeybindingsGateway_PageModeIsPartOfResetAll(t *testing.T) {157+func TestKeybindingsGateway_VimModeIsPartOfResetAll(t *testing.T) {
158 mgr := newTestKeybindingsManager(t)158 mgr := newTestKeybindingsManager(t)
159 gw := NewKeybindingsGateway(mgr)159 gw := NewKeybindingsGateway(mgr)
160 160
161- // Change a page mode binding.161+ // Change a vim mode binding.
162 require.NoError(t, gw.SetKeybinding(context.Background(), port.SetKeybindingRequest{162 require.NoError(t, gw.SetKeybinding(context.Background(), port.SetKeybindingRequest{
163- Mode: "page",163+ Mode: "vim",
164- Action: "page-scroll-left",164+ Action: "vim-scroll-left",
165 Keys: []string{"a"},165 Keys: []string{"a"},
166 }))166 }))
167 167
168 // Change another action.168 // Change another action.
169 require.NoError(t, gw.SetKeybinding(context.Background(), port.SetKeybindingRequest{169 require.NoError(t, gw.SetKeybinding(context.Background(), port.SetKeybindingRequest{
170- Mode: "page",170+ Mode: "vim",
171- Action: "page-scroll-down",171+ Action: "vim-scroll-down",
172 Keys: []string{"b"},172 Keys: []string{"b"},
173 }))173 }))
174 174
@@ -181,36 +181,36 @@ func TestKeybindingsGateway_PageModeIsPartOfResetAll(t *testing.T) {
181 data, err := os.ReadFile(configFile)181 data, err := os.ReadFile(configFile)
182 require.NoError(t, err)182 require.NoError(t, err)
183 content := string(data)183 content := string(data)
184- assert.Contains(t, content, `keys = ['h']`, "page-scroll-left should be restored to default")184+ assert.Contains(t, content, `keys = ['h']`, "vim-scroll-left should be restored to default")
185- assert.Contains(t, content, `keys = ['j']`, "page-scroll-down should be restored to default")185+ assert.Contains(t, content, `keys = ['j']`, "vim-scroll-down should be restored to default")
186 assert.NotContains(t, content, "keys = ['a']")186 assert.NotContains(t, content, "keys = ['a']")
187 assert.NotContains(t, content, "keys = ['b']")187 assert.NotContains(t, content, "keys = ['b']")
188 }188 }
189 189
190-func TestKeybindingsGateway_PageModeConflictsWithinSelf(t *testing.T) {190+func TestKeybindingsGateway_VimModeConflictsWithinSelf(t *testing.T) {
191 gw := NewKeybindingsGateway(inMemoryManager())191 gw := NewKeybindingsGateway(inMemoryManager())
192- conflicts, err := gw.CheckConflicts(context.Background(), "page", "custom", []string{"h"})192+ conflicts, err := gw.CheckConflicts(context.Background(), "vim", "custom", []string{"h"})
193 require.NoError(t, err)193 require.NoError(t, err)
194- require.NotEmpty(t, conflicts, "should detect conflict with page-scroll-left which uses 'h'")194+ require.NotEmpty(t, conflicts, "should detect conflict with vim-scroll-left which uses 'h'")
195 195
196 // The conflict should reference the same mode.196 // The conflict should reference the same mode.
197- assert.Equal(t, "page-scroll-left", conflicts[0].ConflictingAction)197+ assert.Equal(t, "vim-scroll-left", conflicts[0].ConflictingAction)
198- assert.Equal(t, "page", conflicts[0].ConflictingMode)198+ assert.Equal(t, "vim", conflicts[0].ConflictingMode)
199 }199 }
200 200
201-func TestKeybindingsGateway_PageModeConflictsWithOtherModes(t *testing.T) {201+func TestKeybindingsGateway_VimModeConflictsWithOtherModes(t *testing.T) {
202 gw := NewKeybindingsGateway(inMemoryManager())202 gw := NewKeybindingsGateway(inMemoryManager())
203- // "enter" is used by page mode's confirm - but also by other modes like pane_mode.203+ // "enter" is used by vim mode's confirm - but also by other modes like pane_mode.
204- conflicts, err := gw.CheckConflicts(context.Background(), "page", "custom", []string{"enter"})204+ conflicts, err := gw.CheckConflicts(context.Background(), "vim", "custom", []string{"enter"})
205 require.NoError(t, err)205 require.NoError(t, err)
206 206
207 // There should be conflicts since "enter" is used in other modes too.207 // There should be conflicts since "enter" is used in other modes too.
208 assert.NotEmpty(t, conflicts, "should detect cross-mode conflict for 'enter'")208 assert.NotEmpty(t, conflicts, "should detect cross-mode conflict for 'enter'")
209 }209 }
210 210
211-func TestKeybindingsGateway_PageModeNoConflictForUnusedKey(t *testing.T) {211+func TestKeybindingsGateway_VimModeNoConflictForUnusedKey(t *testing.T) {
212 gw := NewKeybindingsGateway(inMemoryManager())212 gw := NewKeybindingsGateway(inMemoryManager())
213- conflicts, err := gw.CheckConflicts(context.Background(), "page", "custom", []string{"ctrl+9"})213+ conflicts, err := gw.CheckConflicts(context.Background(), "vim", "custom", []string{"ctrl+9"})
214 require.NoError(t, err)214 require.NoError(t, err)
215 assert.Empty(t, conflicts, "no conflict expected for unused key")215 assert.Empty(t, conflicts, "no conflict expected for unused key")
216 }216 }
M internal/infrastructure/config/loader.go +3 -3
@@ -706,9 +706,9 @@ func (m *Manager) setWorkspaceDefaults(defaults *Config) {
706 m.viper.SetDefault("workspace.tab_mode.activation_shortcut", defaults.Workspace.TabMode.ActivationShortcut)706 m.viper.SetDefault("workspace.tab_mode.activation_shortcut", defaults.Workspace.TabMode.ActivationShortcut)
707 m.viper.SetDefault("workspace.tab_mode.timeout_ms", defaults.Workspace.TabMode.TimeoutMilliseconds)707 m.viper.SetDefault("workspace.tab_mode.timeout_ms", defaults.Workspace.TabMode.TimeoutMilliseconds)
708 m.viper.SetDefault("workspace.tab_mode.actions", defaults.Workspace.TabMode.Actions)708 m.viper.SetDefault("workspace.tab_mode.actions", defaults.Workspace.TabMode.Actions)
709- m.viper.SetDefault("workspace.page_mode.activation_shortcut", defaults.Workspace.PageMode.ActivationShortcut)709+ m.viper.SetDefault("workspace.vim_mode.activation_shortcut", defaults.Workspace.VimMode.ActivationShortcut)
710- m.viper.SetDefault("workspace.page_mode.timeout_ms", defaults.Workspace.PageMode.TimeoutMilliseconds)710+ m.viper.SetDefault("workspace.vim_mode.timeout_ms", defaults.Workspace.VimMode.TimeoutMilliseconds)
711- m.viper.SetDefault("workspace.page_mode.actions", defaults.Workspace.PageMode.Actions)711+ m.viper.SetDefault("workspace.vim_mode.actions", defaults.Workspace.VimMode.Actions)
712 m.viper.SetDefault("workspace.resize_mode.activation_shortcut", defaults.Workspace.ResizeMode.ActivationShortcut)712 m.viper.SetDefault("workspace.resize_mode.activation_shortcut", defaults.Workspace.ResizeMode.ActivationShortcut)
713 m.viper.SetDefault("workspace.resize_mode.timeout_ms", defaults.Workspace.ResizeMode.TimeoutMilliseconds)713 m.viper.SetDefault("workspace.resize_mode.timeout_ms", defaults.Workspace.ResizeMode.TimeoutMilliseconds)
714 m.viper.SetDefault("workspace.resize_mode.step_percent", defaults.Workspace.ResizeMode.StepPercent)714 m.viper.SetDefault("workspace.resize_mode.step_percent", defaults.Workspace.ResizeMode.StepPercent)
M internal/infrastructure/config/migrate.go +91 -1
@@ -123,6 +123,7 @@ func (m *Migrator) DetectChanges() ([]port.KeyChange, error) {
123 m.detectLegacyBrowsingContextRenames(rawUserKeysWithValues),123 m.detectLegacyBrowsingContextRenames(rawUserKeysWithValues),
124 m.detectLegacyFavoritesSidebarRename(userKeysWithValues)...,124 m.detectLegacyFavoritesSidebarRename(userKeysWithValues)...,
125 )125 )
126+ legacyRenames = append(legacyRenames, m.detectLegacyVimModeRenames(rawUserKeysWithValues)...)
126 var changes []port.KeyChange127 var changes []port.KeyChange
127 changes = append(changes, legacyRenames...)128 changes = append(changes, legacyRenames...)
128 129
@@ -302,7 +303,7 @@ func (m *Migrator) defaultActionMaps() []defaultActionMap {
302 {key: "workspace.shortcuts.actions", actions: m.defaultConfig.Workspace.Shortcuts.Actions},303 {key: "workspace.shortcuts.actions", actions: m.defaultConfig.Workspace.Shortcuts.Actions},
303 {key: "workspace.pane_mode.actions", actions: m.defaultConfig.Workspace.PaneMode.Actions},304 {key: "workspace.pane_mode.actions", actions: m.defaultConfig.Workspace.PaneMode.Actions},
304 {key: "workspace.tab_mode.actions", actions: m.defaultConfig.Workspace.TabMode.Actions},305 {key: "workspace.tab_mode.actions", actions: m.defaultConfig.Workspace.TabMode.Actions},
305- {key: "workspace.page_mode.actions", actions: m.defaultConfig.Workspace.PageMode.Actions},306+ {key: "workspace.vim_mode.actions", actions: m.defaultConfig.Workspace.VimMode.Actions},
306 {key: "workspace.resize_mode.actions", actions: m.defaultConfig.Workspace.ResizeMode.Actions},307 {key: "workspace.resize_mode.actions", actions: m.defaultConfig.Workspace.ResizeMode.Actions},
307 {key: "session.session_mode.actions", actions: m.defaultConfig.Session.SessionMode.Actions},308 {key: "session.session_mode.actions", actions: m.defaultConfig.Session.SessionMode.Actions},
308 }309 }
@@ -420,6 +421,7 @@ func (m *Migrator) Migrate() ([]string, error) {
420 transformer.TransformLegacyActions(rawConfig)421 transformer.TransformLegacyActions(rawConfig)
421 transformer.TransformLegacyEngineConfig(rawConfig)422 transformer.TransformLegacyEngineConfig(rawConfig)
422 transformer.TransformLegacyPopupsToBrowsingContexts(rawConfig)423 transformer.TransformLegacyPopupsToBrowsingContexts(rawConfig)
424+ m.migrateLegacyVimMode(rawConfig)
423 425
424 // Build sets of keys to remove and renames to apply426 // Build sets of keys to remove and renames to apply
425 keysToRemove := make(map[string]bool)427 keysToRemove := make(map[string]bool)
@@ -788,6 +790,94 @@ func (m *Migrator) detectLegacyBrowsingContextRenames(rawUserKeys map[string]any
788 return changes790 return changes
789 }791 }
790 792
793+func (m *Migrator) detectLegacyVimModeRenames(rawUserKeys map[string]any) []port.KeyChange {
794+ changes := make([]port.KeyChange, 0, 3)
795+ for oldKey, oldValue := range rawUserKeys {
796+ newKey, ok := legacyVimModeKey(oldKey)
797+ if !ok {
798+ continue
799+ }
800+ if _, hasNew := rawUserKeys[newKey]; hasNew {
801+ continue
802+ }
803+ changes = append(changes, port.KeyChange{
804+ Type: port.KeyChangeRenamed,
805+ OldKey: oldKey,
806+ NewKey: newKey,
807+ OldValue: m.formatValue(oldValue),
808+ NewValue: m.formatValue(m.defaultValueForKey(newKey)),
809+ })
810+ }
811+ return changes
812+}
813+
814+func (*Migrator) migrateLegacyVimMode(rawConfig map[string]any) {
815+ workspace, ok := rawConfig["workspace"].(map[string]any)
816+ if !ok {
817+ return
818+ }
819+ legacyMode, ok := workspace["page_mode"].(map[string]any)
820+ if !ok {
821+ return
822+ }
823+ vimMode, ok := workspace["vim_mode"].(map[string]any)
824+ if !ok {
825+ vimMode = make(map[string]any)
826+ workspace["vim_mode"] = vimMode
827+ }
828+ for key, value := range legacyMode {
829+ if key == "actions" {
830+ continue
831+ }
832+ if _, exists := vimMode[key]; !exists {
833+ vimMode[key] = value
834+ }
835+ }
836+ legacyActions, _ := legacyMode["actions"].(map[string]any)
837+ vimActions, _ := vimMode["actions"].(map[string]any)
838+ if vimActions == nil {
839+ vimActions = make(map[string]any)
840+ vimMode["actions"] = vimActions
841+ }
842+ for oldAction, value := range legacyActions {
843+ newKey, ok := legacyVimModeKey("workspace.page_mode.actions." + oldAction)
844+ if !ok {
845+ continue
846+ }
847+ newAction := strings.TrimPrefix(newKey, "workspace.vim_mode.actions.")
848+ if _, exists := vimActions[newAction]; !exists {
849+ vimActions[newAction] = value
850+ }
851+ }
852+ delete(workspace, "page_mode")
853+}
854+
855+// legacyVimModeKey converts one legacy Page Mode config key to its canonical
856+// Vim Mode key, including action-name normalization.
857+func legacyVimModeKey(oldKey string) (string, bool) {
858+ const oldPrefix = "workspace.page_mode"
859+ const newPrefix = "workspace.vim_mode"
860+
861+ if !strings.HasPrefix(oldKey, oldPrefix+".") {
862+ return "", false
863+ }
864+ newKey := newPrefix + strings.TrimPrefix(oldKey, oldPrefix)
865+ const actionsPrefix = newPrefix + ".actions."
866+ if strings.HasPrefix(newKey, actionsPrefix) {
867+ return actionsPrefix + legacyVimActionName(strings.TrimPrefix(newKey, actionsPrefix)), true
868+ }
869+ return newKey, true
870+}
871+
872+func legacyVimActionName(action string) string {
873+ for _, prefix := range []string{"page-scroll-", "page_scroll_"} {
874+ if strings.HasPrefix(action, prefix) {
875+ return "vim-scroll-" + strings.ReplaceAll(strings.TrimPrefix(action, prefix), "_", "-")
876+ }
877+ }
878+ return action
879+}
880+
791 // flattenMapWithValues recursively flattens a nested map to dot-notation keys with values.881 // flattenMapWithValues recursively flattens a nested map to dot-notation keys with values.
792 func (m *Migrator) flattenMapWithValues(data map[string]any, prefix string, result map[string]any) {882 func (m *Migrator) flattenMapWithValues(data map[string]any, prefix string, result map[string]any) {
793 for k, v := range data {883 for k, v := range data {
M internal/infrastructure/config/migrate_test.go +112 -0
@@ -1175,6 +1175,118 @@ func TestMigrator_Migrate_AppliesLegacyBrowsingContextsRename(t *testing.T) {
1175 assert.NotContains(t, string(migrated), "[workspace.popups]")1175 assert.NotContains(t, string(migrated), "[workspace.popups]")
1176 }1176 }
1177 1177
1178+func TestMigrator_Migrate_RenamesLegacyPageModeToVimMode(t *testing.T) {
1179+ home := t.TempDir()
1180+ t.Setenv("HOME", home)
1181+ t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
1182+ t.Setenv("XDG_DATA_HOME", filepath.Join(home, ".local", "share"))
1183+ t.Setenv("XDG_STATE_HOME", filepath.Join(home, ".local", "state"))
1184+ t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache"))
1185+
1186+ configFile, err := GetConfigFile()
1187+ require.NoError(t, err)
1188+ require.NoError(t, os.MkdirAll(filepath.Dir(configFile), 0o755))
1189+ require.NoError(t, os.WriteFile(configFile, []byte(`
1190+[workspace.page_mode]
1191+activation_shortcut = "ctrl+alt+y"
1192+timeout_ms = 250
1193+
1194+[workspace.page_mode.actions.page-scroll-down]
1195+keys = ["n"]
1196+desc = "Scroll down"
1197+
1198+[workspace.page_mode.actions.page_scroll_up]
1199+keys = ["u"]
1200+desc = "Scroll up"
1201+
1202+[workspace.page_mode.actions.page_scroll_down_fast]
1203+keys = ["d"]
1204+desc = "Scroll down fast"
1205+`), 0o644))
1206+
1207+ migrator := NewMigrator()
1208+ applied, err := migrator.Migrate()
1209+ require.NoError(t, err)
1210+ assert.Contains(t, applied, "workspace.page_mode.activation_shortcut -> workspace.vim_mode.activation_shortcut")
1211+ assert.Contains(t, applied, "workspace.page_mode.timeout_ms -> workspace.vim_mode.timeout_ms")
1212+ assert.Contains(t, applied, "workspace.page_mode.actions -> workspace.vim_mode.actions")
1213+
1214+ migrated, err := os.ReadFile(configFile)
1215+ require.NoError(t, err)
1216+ assert.Contains(t, string(migrated), "[workspace.vim_mode]")
1217+ assert.Contains(t, string(migrated), "activation_shortcut = 'ctrl+alt+y'")
1218+ assert.Contains(t, string(migrated), "[workspace.vim_mode.actions.vim-scroll-down]")
1219+ assert.Contains(t, string(migrated), "[workspace.vim_mode.actions.vim-scroll-up]")
1220+ assert.Contains(t, string(migrated), "[workspace.vim_mode.actions.vim-scroll-down-fast]")
1221+ assert.Contains(t, string(migrated), "keys = ['u']")
1222+ assert.NotContains(t, string(migrated), "page_mode")
1223+ assert.NotContains(t, string(migrated), "page-scroll-down")
1224+}
1225+
1226+func TestMigrator_DetectChanges_CanonicalizesLegacyVimActionNames(t *testing.T) {
1227+ home := t.TempDir()
1228+ t.Setenv("HOME", home)
1229+ t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
1230+ t.Setenv("XDG_DATA_HOME", filepath.Join(home, ".local", "share"))
1231+ t.Setenv("XDG_STATE_HOME", filepath.Join(home, ".local", "state"))
1232+ t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache"))
1233+
1234+ configFile, err := GetConfigFile()
1235+ require.NoError(t, err)
1236+ require.NoError(t, os.MkdirAll(filepath.Dir(configFile), 0o755))
1237+ require.NoError(t, os.WriteFile(configFile, []byte(`
1238+[workspace.page_mode.actions.page_scroll_down_fast]
1239+keys = ["d"]
1240+desc = "Scroll down fast"
1241+`), 0o644))
1242+
1243+ changes, err := NewMigrator().DetectChanges()
1244+ require.NoError(t, err)
1245+
1246+ foundRename := false
1247+ for _, change := range changes {
1248+ if change.Type == port.KeyChangeRenamed &&
1249+ change.OldKey == "workspace.page_mode.actions" &&
1250+ change.NewKey == "workspace.vim_mode.actions" {
1251+ foundRename = true
1252+ }
1253+ }
1254+ assert.True(t, foundRename, "DetectChanges must report the canonical Vim Mode actions key")
1255+ assert.Equal(t, "vim-scroll-down-fast", legacyVimActionName("page_scroll_down_fast"))
1256+}
1257+
1258+func TestMigrator_Migrate_MergesLegacyPageModeActionsIntoExistingVimMode(t *testing.T) {
1259+ home := t.TempDir()
1260+ t.Setenv("HOME", home)
1261+ t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
1262+ t.Setenv("XDG_DATA_HOME", filepath.Join(home, ".local", "share"))
1263+ t.Setenv("XDG_STATE_HOME", filepath.Join(home, ".local", "state"))
1264+ t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache"))
1265+
1266+ configFile, err := GetConfigFile()
1267+ require.NoError(t, err)
1268+ require.NoError(t, os.MkdirAll(filepath.Dir(configFile), 0o755))
1269+ require.NoError(t, os.WriteFile(configFile, []byte(`
1270+[workspace.page_mode.actions.page-scroll-down]
1271+keys = ["n"]
1272+
1273+[workspace.page_mode.actions.page-scroll-up]
1274+keys = ["u"]
1275+
1276+[workspace.vim_mode.actions.vim-scroll-down]
1277+keys = ["j"]
1278+`), 0o644))
1279+
1280+ _, err = NewMigrator().Migrate()
1281+ require.NoError(t, err)
1282+
1283+ migrated, err := os.ReadFile(configFile)
1284+ require.NoError(t, err)
1285+ assert.Contains(t, string(migrated), "[workspace.vim_mode.actions.vim-scroll-down]\n keys = ['j']")
1286+ assert.Contains(t, string(migrated), "[workspace.vim_mode.actions.vim-scroll-up]\n keys = ['u']")
1287+ assert.NotContains(t, string(migrated), "page_mode")
1288+}
1289+
1178 func TestMigrator_DetectChanges_MixedBrowsingContextsDoesNotDoubleReportLegacyRename(t *testing.T) {1290 func TestMigrator_DetectChanges_MixedBrowsingContextsDoesNotDoubleReportLegacyRename(t *testing.T) {
1179 home := t.TempDir()1291 home := t.TempDir()
1180 t.Setenv("HOME", home)1292 t.Setenv("HOME", home)
D internal/infrastructure/config/page_mode_config_test.go +0 -223
@@ -1,223 +0,0 @@
1-package config
2-
3-import (
4- "testing"
5-
6- "github.com/stretchr/testify/assert"
7- "github.com/stretchr/testify/require"
8-)
9-
10-func TestDefaultConfig_PageModeDefaults(t *testing.T) {
11- cfg := DefaultConfig()
12-
13- // PageMode must exist in workspace config.
14- pageMode := cfg.Workspace.PageMode
15- assert.Equal(t, "ctrl+y", pageMode.ActivationShortcut)
16- assert.Equal(t, 0, pageMode.TimeoutMilliseconds)
17-
18- // All required actions must exist with exact key bindings.
19- require.NotNil(t, pageMode.Actions)
20- requireActionBinding(t, pageMode.Actions, "page-scroll-left", []string{"h"})
21- requireActionBinding(t, pageMode.Actions, "page-scroll-down", []string{"j"})
22- requireActionBinding(t, pageMode.Actions, "page-scroll-up", []string{"k"})
23- requireActionBinding(t, pageMode.Actions, "page-scroll-right", []string{"l"})
24- requireActionBinding(t, pageMode.Actions, "page-scroll-down-fast", []string{"shift+j"})
25- requireActionBinding(t, pageMode.Actions, "page-scroll-up-fast", []string{"shift+k"})
26- requireActionBinding(t, pageMode.Actions, "confirm", []string{"enter"})
27- requireActionBinding(t, pageMode.Actions, "cancel", []string{"escape"})
28-}
29-
30-func TestPageModeConfig_GetKeyBindings(t *testing.T) {
31- cfg := DefaultConfig()
32- bindings := cfg.Workspace.PageMode.GetKeyBindings()
33-
34- assert.Equal(t, "page-scroll-left", bindings["h"])
35- assert.Equal(t, "page-scroll-down", bindings["j"])
36- assert.Equal(t, "page-scroll-up", bindings["k"])
37- assert.Equal(t, "page-scroll-right", bindings["l"])
38- assert.Equal(t, "page-scroll-down-fast", bindings["shift+j"])
39- assert.Equal(t, "page-scroll-up-fast", bindings["shift+k"])
40- assert.Equal(t, "confirm", bindings["enter"])
41- assert.Equal(t, "cancel", bindings["escape"])
42- assert.Len(t, bindings, 8)
43-}
44-
45-func TestValidatePageMode_NonNegativeTimeout(t *testing.T) {
46- tests := []struct {
47- name string
48- mutate func(*Config)
49- want string
50- }{
51- {
52- name: "negative timeout fails",
53- mutate: func(cfg *Config) {
54- cfg.Workspace.PageMode.TimeoutMilliseconds = -1
55- },
56- want: "workspace.page_mode.timeout_ms must be non-negative",
57- },
58- {
59- name: "zero timeout ok",
60- mutate: func(_ *Config) {
61- },
62- want: "",
63- },
64- {
65- name: "positive timeout ok",
66- mutate: func(cfg *Config) {
67- cfg.Workspace.PageMode.TimeoutMilliseconds = 5000
68- },
69- want: "",
70- },
71- }
72-
73- for _, tt := range tests {
74- t.Run(tt.name, func(t *testing.T) {
75- cfg := DefaultConfig()
76- tt.mutate(cfg)
77- err := validateConfig(cfg)
78- if tt.want == "" {
79- require.NoError(t, err)
80- } else {
81- require.Error(t, err)
82- assert.Contains(t, err.Error(), tt.want)
83- }
84- })
85- }
86-}
87-
88-func TestValidatePageMode_NilActionsFail(t *testing.T) {
89- cfg := DefaultConfig()
90- cfg.Workspace.PageMode.Actions = nil
91-
92- err := validateConfig(cfg)
93- require.Error(t, err)
94- assert.Contains(t, err.Error(), "workspace.page_mode.actions cannot be empty")
95-}
96-
97-func TestValidatePageMode_DuplicateKeyBindings(t *testing.T) {
98- cfg := DefaultConfig()
99- // Add a duplicate key binding that conflicts with an existing one.
100- action := "custom-action"
101- cfg.Workspace.PageMode.Actions[action] = ActionBinding{
102- Keys: []string{"h"}, // "h" is already used by page-scroll-left
103- Desc: "Custom action",
104- }
105-
106- err := validateConfig(cfg)
107- require.Error(t, err)
108- assert.Contains(t, err.Error(), "duplicate key binding 'h' found in page_mode actions")
109- assert.Contains(t, err.Error(), "page-scroll-left")
110- assert.Contains(t, err.Error(), action)
111-}
112-
113-func TestValidatePageMode_EmptyActionKeysFail(t *testing.T) {
114- cfg := DefaultConfig()
115- // Override an action with no key bindings.
116- cfg.Workspace.PageMode.Actions = map[string]ActionBinding{
117- "page-scroll-left": {Keys: []string{}, Desc: "No keys"},
118- }
119-
120- err := validateConfig(cfg)
121- require.Error(t, err)
122- assert.Contains(t, err.Error(), "workspace.page_mode.actions.page-scroll-left must have at least one key binding")
123-}
124-
125-func TestValidateModalModeActions_RegressionTable(t *testing.T) {
126- tests := []struct {
127- name string
128- modePath string
129- timeout int
130- actions map[string]ActionBinding
131- want string
132- }{
133- {
134- name: "page negative timeout",
135- modePath: "page_mode",
136- timeout: -1,
137- actions: map[string]ActionBinding{"page-scroll-left": {Keys: []string{"h"}}},
138- want: "workspace.page_mode.timeout_ms must be non-negative",
139- },
140- {
141- name: "pane empty actions",
142- modePath: "pane_mode",
143- timeout: 0,
144- actions: nil,
145- want: "workspace.pane_mode.actions cannot be empty",
146- },
147- {
148- name: "tab empty keys",
149- modePath: "tab_mode",
150- timeout: 0,
151- actions: map[string]ActionBinding{"new-tab": {Keys: []string{}}},
152- want: "workspace.tab_mode.actions.new-tab must have at least one key binding",
153- },
154- {
155- name: "page duplicate vim binding",
156- modePath: "page_mode",
157- timeout: 0,
158- actions: map[string]ActionBinding{
159- "page-scroll-left": {Keys: []string{"h"}},
160- "page-scroll-right": {Keys: []string{"h"}},
161- },
162- want: "duplicate key binding 'h' found in page_mode actions",
163- },
164- {
165- name: "page canonical vim set ok",
166- modePath: "page_mode",
167- timeout: 0,
168- actions: map[string]ActionBinding{
169- "page-scroll-left": {Keys: []string{"h"}},
170- "page-scroll-down": {Keys: []string{"j"}},
171- "page-scroll-up": {Keys: []string{"k"}},
172- "page-scroll-right": {Keys: []string{"l"}},
173- "page-scroll-down-fast": {Keys: []string{"shift+j"}},
174- "page-scroll-up-fast": {Keys: []string{"shift+k"}},
175- "confirm": {Keys: []string{"enter"}},
176- "cancel": {Keys: []string{"escape"}},
177- },
178- want: "",
179- },
180- }
181-
182- for _, tt := range tests {
183- t.Run(tt.name, func(t *testing.T) {
184- errs := validateModalModeActions(tt.modePath, tt.timeout, tt.actions)
185- if tt.want == "" {
186- assert.Empty(t, errs)
187- return
188- }
189- require.NotEmpty(t, errs)
190- assert.Contains(t, errs[0], tt.want)
191- })
192- }
193-}
194-
195-func TestSchemaProvider_PageModeKeys(t *testing.T) {
196- provider := &SchemaProvider{}
197- schema := provider.GetSchema()
198-
199- var foundActivation, foundTimeout, foundActions bool
200- for _, key := range schema {
201- switch key.Key {
202- case "workspace.page_mode.activation_shortcut":
203- foundActivation = true
204- assert.Equal(t, "string", key.Type)
205- assert.Equal(t, "ctrl+y", key.Default)
206- assert.Equal(t, SectionWorkspace, key.Section)
207- case "workspace.page_mode.timeout_ms":
208- foundTimeout = true
209- assert.Equal(t, "int", key.Type)
210- assert.Equal(t, "0", key.Default)
211- assert.Equal(t, ">=0", key.Range)
212- assert.Equal(t, SectionWorkspace, key.Section)
213- case "workspace.page_mode.actions.<action>":
214- foundActions = true
215- assert.Equal(t, "[]string", key.Type)
216- assert.Equal(t, SectionWorkspace, key.Section)
217- }
218- }
219-
220- assert.True(t, foundActivation, "missing workspace.page_mode.activation_shortcut schema key")
221- assert.True(t, foundTimeout, "missing workspace.page_mode.timeout_ms schema key")
222- assert.True(t, foundActions, "missing workspace.page_mode.actions.<action> schema key")
223-}
M internal/infrastructure/config/schema.go +2 -2
@@ -24,8 +24,8 @@ type TabModeConfig = entity.TabModeConfig
24 // ResizeModeConfig defines modal behavior for resizing panes (Zellij-style).24 // ResizeModeConfig defines modal behavior for resizing panes (Zellij-style).
25 type ResizeModeConfig = entity.ResizeModeConfig25 type ResizeModeConfig = entity.ResizeModeConfig
26 26
27-// PageModeConfig defines modal behavior for page scrolling.27+// VimModeConfig defines modal behavior for Vim-style scrolling.
28-type PageModeConfig = entity.PageModeConfig28+type VimModeConfig = entity.VimModeConfig
29 29
30 // SessionModeConfig defines modal behavior for session management.30 // SessionModeConfig defines modal behavior for session management.
31 type SessionModeConfig = entity.SessionModeConfig31 type SessionModeConfig = entity.SessionModeConfig
M internal/infrastructure/config/schema_provider.go +10 -10
@@ -472,27 +472,27 @@ func (*SchemaProvider) getWorkspaceKeys(defaults *Config) []entity.ConfigKeyInfo
472 Description: "Key bindings for tab mode actions",472 Description: "Key bindings for tab mode actions",
473 Section: SectionWorkspace,473 Section: SectionWorkspace,
474 },474 },
475- // Page mode475+ // Vim mode
476 {476 {
477- Key: "workspace.page_mode.activation_shortcut",477+ Key: "workspace.vim_mode.activation_shortcut",
478 Type: "string",478 Type: "string",
479- Default: defaults.Workspace.PageMode.ActivationShortcut,479+ Default: defaults.Workspace.VimMode.ActivationShortcut,
480- Description: "Shortcut to enter page scroll mode",480+ Description: "Shortcut to enter vim scroll mode",
481 Section: SectionWorkspace,481 Section: SectionWorkspace,
482 },482 },
483 {483 {
484- Key: "workspace.page_mode.timeout_ms",484+ Key: "workspace.vim_mode.timeout_ms",
485 Type: "int",485 Type: "int",
486- Default: fmt.Sprintf("%d", defaults.Workspace.PageMode.TimeoutMilliseconds),486+ Default: fmt.Sprintf("%d", defaults.Workspace.VimMode.TimeoutMilliseconds),
487- Description: "Page mode timeout in milliseconds (0 = no timeout)",487+ Description: "Vim mode timeout in milliseconds (0 = no timeout)",
488 Range: ">=0",488 Range: ">=0",
489 Section: SectionWorkspace,489 Section: SectionWorkspace,
490 },490 },
491 {491 {
492- Key: "workspace.page_mode.actions.<action>",492+ Key: "workspace.vim_mode.actions.<action>",
493- Type: "[]string",493+ Type: "object",
494 Default: "(see defaults)",494 Default: "(see defaults)",
495- Description: "Key bindings for page mode actions",495+ Description: "Vim mode action object with keys ([]string) and desc (string)",
496 Section: SectionWorkspace,496 Section: SectionWorkspace,
497 },497 },
498 // Resize mode498 // Resize mode
M internal/infrastructure/config/schema_provider_test.go +1 -1
@@ -146,7 +146,7 @@ func TestSchemaProviderCoversViperDefaults(t *testing.T) {
146 "search_shortcuts": "search_shortcuts.<key>",146 "search_shortcuts": "search_shortcuts.<key>",
147 "workspace.pane_mode.actions": "workspace.pane_mode.actions.<action>",147 "workspace.pane_mode.actions": "workspace.pane_mode.actions.<action>",
148 "workspace.tab_mode.actions": "workspace.tab_mode.actions.<action>",148 "workspace.tab_mode.actions": "workspace.tab_mode.actions.<action>",
149- "workspace.page_mode.actions": "workspace.page_mode.actions.<action>",149+ "workspace.vim_mode.actions": "workspace.vim_mode.actions.<action>",
150 "workspace.resize_mode.actions": "workspace.resize_mode.actions.<action>",150 "workspace.resize_mode.actions": "workspace.resize_mode.actions.<action>",
151 "workspace.floating_pane.profiles": "workspace.floating_pane.profiles.<name>",151 "workspace.floating_pane.profiles": "workspace.floating_pane.profiles.<name>",
152 "session.session_mode.actions": "session.session_mode.actions.<action>",152 "session.session_mode.actions": "session.session_mode.actions.<action>",
M internal/infrastructure/config/validation.go +3 -3
@@ -29,7 +29,7 @@ func validateConfig(config *Config) error {
29 validationErrors = append(validationErrors, validatePaneMode(config)...)29 validationErrors = append(validationErrors, validatePaneMode(config)...)
30 validationErrors = append(validationErrors, validateTabBar(config)...)30 validationErrors = append(validationErrors, validateTabBar(config)...)
31 validationErrors = append(validationErrors, validateTabMode(config)...)31 validationErrors = append(validationErrors, validateTabMode(config)...)
32- validationErrors = append(validationErrors, validatePageMode(config)...)32+ validationErrors = append(validationErrors, validateVimMode(config)...)
33 validationErrors = append(validationErrors, validateFloatingPane(config)...)33 validationErrors = append(validationErrors, validateFloatingPane(config)...)
34 validationErrors = append(validationErrors, validateLogging(config)...)34 validationErrors = append(validationErrors, validateLogging(config)...)
35 validationErrors = append(validationErrors, validateWorkspaceNewPaneURL(config)...)35 validationErrors = append(validationErrors, validateWorkspaceNewPaneURL(config)...)
@@ -242,8 +242,8 @@ func validateTabMode(config *Config) []string {
242 return validateModalModeActions("tab_mode", config.Workspace.TabMode.TimeoutMilliseconds, config.Workspace.TabMode.Actions)242 return validateModalModeActions("tab_mode", config.Workspace.TabMode.TimeoutMilliseconds, config.Workspace.TabMode.Actions)
243 }243 }
244 244
245-func validatePageMode(config *Config) []string {245+func validateVimMode(config *Config) []string {
246- return validateModalModeActions("page_mode", config.Workspace.PageMode.TimeoutMilliseconds, config.Workspace.PageMode.Actions)246+ return validateModalModeActions("vim_mode", config.Workspace.VimMode.TimeoutMilliseconds, config.Workspace.VimMode.Actions)
247 }247 }
248 248
249 // validateModalModeActions shared timeout/empty-actions/duplicate-key checks for249 // validateModalModeActions shared timeout/empty-actions/duplicate-key checks for
A internal/infrastructure/config/vim_mode_config_test.go +223 -0
@@ -0,0 +1,223 @@
1+package config
2+
3+import (
4+ "testing"
5+
6+ "github.com/stretchr/testify/assert"
7+ "github.com/stretchr/testify/require"
8+)
9+
10+func TestDefaultConfig_VimModeDefaults(t *testing.T) {
11+ cfg := DefaultConfig()
12+
13+ // VimMode must exist in workspace config.
14+ vimMode := cfg.Workspace.VimMode
15+ assert.Equal(t, "ctrl+y", vimMode.ActivationShortcut)
16+ assert.Equal(t, 0, vimMode.TimeoutMilliseconds)
17+
18+ // All required actions must exist with exact key bindings.
19+ require.NotNil(t, vimMode.Actions)
20+ requireActionBinding(t, vimMode.Actions, "vim-scroll-left", []string{"h"})
21+ requireActionBinding(t, vimMode.Actions, "vim-scroll-down", []string{"j"})
22+ requireActionBinding(t, vimMode.Actions, "vim-scroll-up", []string{"k"})
23+ requireActionBinding(t, vimMode.Actions, "vim-scroll-right", []string{"l"})
24+ requireActionBinding(t, vimMode.Actions, "vim-scroll-down-fast", []string{"shift+j"})
25+ requireActionBinding(t, vimMode.Actions, "vim-scroll-up-fast", []string{"shift+k"})
26+ requireActionBinding(t, vimMode.Actions, "confirm", []string{"enter"})
27+ requireActionBinding(t, vimMode.Actions, "cancel", []string{"escape"})
28+}
29+
30+func TestVimModeConfig_GetKeyBindings(t *testing.T) {
31+ cfg := DefaultConfig()
32+ bindings := cfg.Workspace.VimMode.GetKeyBindings()
33+
34+ assert.Equal(t, "vim-scroll-left", bindings["h"])
35+ assert.Equal(t, "vim-scroll-down", bindings["j"])
36+ assert.Equal(t, "vim-scroll-up", bindings["k"])
37+ assert.Equal(t, "vim-scroll-right", bindings["l"])
38+ assert.Equal(t, "vim-scroll-down-fast", bindings["shift+j"])
39+ assert.Equal(t, "vim-scroll-up-fast", bindings["shift+k"])
40+ assert.Equal(t, "confirm", bindings["enter"])
41+ assert.Equal(t, "cancel", bindings["escape"])
42+ assert.Len(t, bindings, 8)
43+}
44+
45+func TestValidateVimMode_NonNegativeTimeout(t *testing.T) {
46+ tests := []struct {
47+ name string
48+ mutate func(*Config)
49+ want string
50+ }{
51+ {
52+ name: "negative timeout fails",
53+ mutate: func(cfg *Config) {
54+ cfg.Workspace.VimMode.TimeoutMilliseconds = -1
55+ },
56+ want: "workspace.vim_mode.timeout_ms must be non-negative",
57+ },
58+ {
59+ name: "zero timeout ok",
60+ mutate: func(_ *Config) {
61+ },
62+ want: "",
63+ },
64+ {
65+ name: "positive timeout ok",
66+ mutate: func(cfg *Config) {
67+ cfg.Workspace.VimMode.TimeoutMilliseconds = 5000
68+ },
69+ want: "",
70+ },
71+ }
72+
73+ for _, tt := range tests {
74+ t.Run(tt.name, func(t *testing.T) {
75+ cfg := DefaultConfig()
76+ tt.mutate(cfg)
77+ err := validateConfig(cfg)
78+ if tt.want == "" {
79+ require.NoError(t, err)
80+ } else {
81+ require.Error(t, err)
82+ assert.Contains(t, err.Error(), tt.want)
83+ }
84+ })
85+ }
86+}
87+
88+func TestValidateVimMode_NilActionsFail(t *testing.T) {
89+ cfg := DefaultConfig()
90+ cfg.Workspace.VimMode.Actions = nil
91+
92+ err := validateConfig(cfg)
93+ require.Error(t, err)
94+ assert.Contains(t, err.Error(), "workspace.vim_mode.actions cannot be empty")
95+}
96+
97+func TestValidateVimMode_DuplicateKeyBindings(t *testing.T) {
98+ cfg := DefaultConfig()
99+ // Add a duplicate key binding that conflicts with an existing one.
100+ action := "custom-action"
101+ cfg.Workspace.VimMode.Actions[action] = ActionBinding{
102+ Keys: []string{"h"}, // "h" is already used by vim-scroll-left
103+ Desc: "Custom action",
104+ }
105+
106+ err := validateConfig(cfg)
107+ require.Error(t, err)
108+ assert.Contains(t, err.Error(), "duplicate key binding 'h' found in vim_mode actions")
109+ assert.Contains(t, err.Error(), "vim-scroll-left")
110+ assert.Contains(t, err.Error(), action)
111+}
112+
113+func TestValidateVimMode_EmptyActionKeysFail(t *testing.T) {
114+ cfg := DefaultConfig()
115+ // Override an action with no key bindings.
116+ cfg.Workspace.VimMode.Actions = map[string]ActionBinding{
117+ "vim-scroll-left": {Keys: []string{}, Desc: "No keys"},
118+ }
119+
120+ err := validateConfig(cfg)
121+ require.Error(t, err)
122+ assert.Contains(t, err.Error(), "workspace.vim_mode.actions.vim-scroll-left must have at least one key binding")
123+}
124+
125+func TestValidateModalModeActions_RegressionTable(t *testing.T) {
126+ tests := []struct {
127+ name string
128+ modePath string
129+ timeout int
130+ actions map[string]ActionBinding
131+ want string
132+ }{
133+ {
134+ name: "page negative timeout",
135+ modePath: "vim_mode",
136+ timeout: -1,
137+ actions: map[string]ActionBinding{"vim-scroll-left": {Keys: []string{"h"}}},
138+ want: "workspace.vim_mode.timeout_ms must be non-negative",
139+ },
140+ {
141+ name: "pane empty actions",
142+ modePath: "pane_mode",
143+ timeout: 0,
144+ actions: nil,
145+ want: "workspace.pane_mode.actions cannot be empty",
146+ },
147+ {
148+ name: "tab empty keys",
149+ modePath: "tab_mode",
150+ timeout: 0,
151+ actions: map[string]ActionBinding{"new-tab": {Keys: []string{}}},
152+ want: "workspace.tab_mode.actions.new-tab must have at least one key binding",
153+ },
154+ {
155+ name: "page duplicate vim binding",
156+ modePath: "vim_mode",
157+ timeout: 0,
158+ actions: map[string]ActionBinding{
159+ "vim-scroll-left": {Keys: []string{"h"}},
160+ "vim-scroll-right": {Keys: []string{"h"}},
161+ },
162+ want: "duplicate key binding 'h' found in vim_mode actions",
163+ },
164+ {
165+ name: "page canonical vim set ok",
166+ modePath: "vim_mode",
167+ timeout: 0,
168+ actions: map[string]ActionBinding{
169+ "vim-scroll-left": {Keys: []string{"h"}},
170+ "vim-scroll-down": {Keys: []string{"j"}},
171+ "vim-scroll-up": {Keys: []string{"k"}},
172+ "vim-scroll-right": {Keys: []string{"l"}},
173+ "vim-scroll-down-fast": {Keys: []string{"shift+j"}},
174+ "vim-scroll-up-fast": {Keys: []string{"shift+k"}},
175+ "confirm": {Keys: []string{"enter"}},
176+ "cancel": {Keys: []string{"escape"}},
177+ },
178+ want: "",
179+ },
180+ }
181+
182+ for _, tt := range tests {
183+ t.Run(tt.name, func(t *testing.T) {
184+ errs := validateModalModeActions(tt.modePath, tt.timeout, tt.actions)
185+ if tt.want == "" {
186+ assert.Empty(t, errs)
187+ return
188+ }
189+ require.NotEmpty(t, errs)
190+ assert.Contains(t, errs[0], tt.want)
191+ })
192+ }
193+}
194+
195+func TestSchemaProvider_VimModeKeys(t *testing.T) {
196+ provider := &SchemaProvider{}
197+ schema := provider.GetSchema()
198+
199+ var foundActivation, foundTimeout, foundActions bool
200+ for _, key := range schema {
201+ switch key.Key {
202+ case "workspace.vim_mode.activation_shortcut":
203+ foundActivation = true
204+ assert.Equal(t, "string", key.Type)
205+ assert.Equal(t, "ctrl+y", key.Default)
206+ assert.Equal(t, SectionWorkspace, key.Section)
207+ case "workspace.vim_mode.timeout_ms":
208+ foundTimeout = true
209+ assert.Equal(t, "int", key.Type)
210+ assert.Equal(t, "0", key.Default)
211+ assert.Equal(t, ">=0", key.Range)
212+ assert.Equal(t, SectionWorkspace, key.Section)
213+ case "workspace.vim_mode.actions.<action>":
214+ foundActions = true
215+ assert.Equal(t, "object", key.Type)
216+ assert.Equal(t, SectionWorkspace, key.Section)
217+ }
218+ }
219+
220+ assert.True(t, foundActivation, "missing workspace.vim_mode.activation_shortcut schema key")
221+ assert.True(t, foundTimeout, "missing workspace.vim_mode.timeout_ms schema key")
222+ assert.True(t, foundActions, "missing workspace.vim_mode.actions.<action> schema key")
223+}
M internal/infrastructure/webkit/messaging.go +2 -2
@@ -289,9 +289,9 @@ func decodeScriptMessage(valuePtr uintptr, log zerolog.Logger) (Message, bool) {
289 }289 }
290 290
291 // handleAllowlistedBridgeMessage handles the one narrow page-origin hint that291 // handleAllowlistedBridgeMessage handles the one narrow page-origin hint that
292-// is allowed before trusted-page routing: editable focus state for Page mode.292+// is allowed before trusted-page routing: editable focus state for Vim mode.
293 // This is intentionally not a privileged bridge command; the only effect is293 // This is intentionally not a privileged bridge command; the only effect is
294-// updating editable-focus state on the sender WebView so Page mode can block or294+// updating editable-focus state on the sender WebView so Vim mode can block or
295 // exit locally. The per-WebView token is a reuse/staleness guard, not a broad295 // exit locally. The per-WebView token is a reuse/staleness guard, not a broad
296 // authorization boundary for page privileges.296 // authorization boundary for page privileges.
297 func (r *MessageRouter) handleAllowlistedBridgeMessage(senderWV *WebView, msg Message) bool {297 func (r *MessageRouter) handleAllowlistedBridgeMessage(senderWV *WebView, msg Message) bool {
M internal/infrastructure/webkit/webview.go +1 -1
@@ -40,7 +40,7 @@ var _ port.PopupFeatureResolver = (*WebView)(nil)
40 var _ port.OAuthCallbackCapable = (*WebView)(nil)40 var _ port.OAuthCallbackCapable = (*WebView)(nil)
41 var _ port.PageScrollable = (*WebView)(nil)41 var _ port.PageScrollable = (*WebView)(nil)
42 42
43-var buildPageScrollFallbackJS = webutil.BuildScrollByJS43+var buildPageScrollFallbackJS = webutil.BuildPageScrollByJS
44 var runPageScrollFallbackJS = func(ctx context.Context, wv *WebView, script string) {44 var runPageScrollFallbackJS = func(ctx context.Context, wv *WebView, script string) {
45 wv.RunJavaScript(ctx, script)45 wv.RunJavaScript(ctx, script)
46 }46 }
M internal/infrastructure/webutil/pagescroll.go +3 -3
@@ -2,16 +2,16 @@ package webutil
2 2
3 import "fmt"3 import "fmt"
4 4
5-// BuildScrollByJS returns one immediate JavaScript scroll step for the given5+// BuildPageScrollByJS returns one immediate JavaScript scroll step for the given
6 // CSS-pixel delta. It resolves the initial target under the viewport center,6 // CSS-pixel delta. It resolves the initial target under the viewport center,
7 // walks toward scrollable ancestors that can move in the requested direction,7 // walks toward scrollable ancestors that can move in the requested direction,
8 // and hands off to the document when a nested scroller reaches its boundary.8 // and hands off to the document when a nested scroller reaches its boundary.
9 // Cross-origin frame contents remain best-effort because elementFromPoint can9 // Cross-origin frame contents remain best-effort because elementFromPoint can
10 // only return the accessible frame element from the top-level document.10 // only return the accessible frame element from the top-level document.
11 //11 //
12-// The app-level Page Mode repeater owns held-key cadence; this helper performs12+// The app-level Vim Mode repeater owns held-key cadence; this helper performs
13 // no requestAnimationFrame loop of its own.13 // no requestAnimationFrame loop of its own.
14-func BuildScrollByJS(dx, dy int) string {14+func BuildPageScrollByJS(dx, dy int) string {
15 return fmt.Sprintf(`(function(){15 return fmt.Sprintf(`(function(){
16 var dx=%d,dy=%d,doc=document;16 var dx=%d,dy=%d,doc=document;
17 function hasScrollableOverflow(value){17 function hasScrollableOverflow(value){
M internal/infrastructure/webutil/pagescroll_test.go +17 -17
@@ -5,8 +5,8 @@ import (
5 "testing"5 "testing"
6 )6 )
7 7
8-func TestBuildScrollByJS_TargetsViewportCenterInsteadOfActiveElement(t *testing.T) {8+func TestBuildPageScrollByJS_TargetsViewportCenterInsteadOfActiveElement(t *testing.T) {
9- js := BuildScrollByJS(0, 80)9+ js := BuildPageScrollByJS(0, 80)
10 10
11 if !strings.Contains(js, "doc.elementFromPoint(window.innerWidth/2,window.innerHeight/2)") {11 if !strings.Contains(js, "doc.elementFromPoint(window.innerWidth/2,window.innerHeight/2)") {
12 t.Fatalf("page scroll must resolve its initial target from the viewport center, got: %s", js)12 t.Fatalf("page scroll must resolve its initial target from the viewport center, got: %s", js)
@@ -16,7 +16,7 @@ func TestBuildScrollByJS_TargetsViewportCenterInsteadOfActiveElement(t *testing.
16 }16 }
17 }17 }
18 18
19-func TestBuildScrollByJS_ContainsRequiredOperations(t *testing.T) {19+func TestBuildPageScrollByJS_ContainsRequiredOperations(t *testing.T) {
20 tests := []struct {20 tests := []struct {
21 name string21 name string
22 dx int22 dx int
@@ -50,7 +50,7 @@ func TestBuildScrollByJS_ContainsRequiredOperations(t *testing.T) {
50 50
51 for _, tt := range tests {51 for _, tt := range tests {
52 t.Run(tt.name, func(t *testing.T) {52 t.Run(tt.name, func(t *testing.T) {
53- js := BuildScrollByJS(tt.dx, tt.dy)53+ js := BuildPageScrollByJS(tt.dx, tt.dy)
54 for _, check := range required {54 for _, check := range required {
55 for _, snippet := range check.snippets {55 for _, snippet := range check.snippets {
56 if !strings.Contains(js, snippet) {56 if !strings.Contains(js, snippet) {
@@ -66,8 +66,8 @@ func TestBuildScrollByJS_ContainsRequiredOperations(t *testing.T) {
66 }66 }
67 }67 }
68 68
69-func TestBuildScrollByJS_SpecificDelta(t *testing.T) {69+func TestBuildPageScrollByJS_SpecificDelta(t *testing.T) {
70- js := BuildScrollByJS(0, 80)70+ js := BuildPageScrollByJS(0, 80)
71 if !strings.Contains(js, "var dx=0,dy=80") {71 if !strings.Contains(js, "var dx=0,dy=80") {
72 t.Errorf("expected dy=80 in JS variables, got: %s", js)72 t.Errorf("expected dy=80 in JS variables, got: %s", js)
73 }73 }
@@ -75,38 +75,38 @@ func TestBuildScrollByJS_SpecificDelta(t *testing.T) {
75 t.Errorf("expected immediate fallback scroll step without requestAnimationFrame, got: %s", js)75 t.Errorf("expected immediate fallback scroll step without requestAnimationFrame, got: %s", js)
76 }76 }
77 77
78- js = BuildScrollByJS(-80, 0)78+ js = BuildPageScrollByJS(-80, 0)
79 if !strings.Contains(js, "var dx=-80,dy=0") {79 if !strings.Contains(js, "var dx=-80,dy=0") {
80 t.Errorf("expected dx=-80 in JS variables, got: %s", js)80 t.Errorf("expected dx=-80 in JS variables, got: %s", js)
81 }81 }
82 }82 }
83 83
84-func TestBuildScrollByJS_HorizontalAxisCheck(t *testing.T) {84+func TestBuildPageScrollByJS_HorizontalAxisCheck(t *testing.T) {
85- js := BuildScrollByJS(-80, 0)85+ js := BuildPageScrollByJS(-80, 0)
86 if !strings.Contains(js, "clientWidth") || !strings.Contains(js, "scrollWidth") {86 if !strings.Contains(js, "clientWidth") || !strings.Contains(js, "scrollWidth") {
87 t.Error("horizontal scroll must check scrollWidth/clientWidth")87 t.Error("horizontal scroll must check scrollWidth/clientWidth")
88 }88 }
89 }89 }
90 90
91-func TestBuildScrollByJS_VerticalAxisCheck(t *testing.T) {91+func TestBuildPageScrollByJS_VerticalAxisCheck(t *testing.T) {
92- js := BuildScrollByJS(0, 80)92+ js := BuildPageScrollByJS(0, 80)
93 if !strings.Contains(js, "clientHeight") || !strings.Contains(js, "scrollHeight") {93 if !strings.Contains(js, "clientHeight") || !strings.Contains(js, "scrollHeight") {
94 t.Error("vertical scroll must check scrollHeight/clientHeight")94 t.Error("vertical scroll must check scrollHeight/clientHeight")
95 }95 }
96 }96 }
97 97
98-func TestBuildScrollByJS_NegativeDeltaMagnitude(t *testing.T) {98+func TestBuildPageScrollByJS_NegativeDeltaMagnitude(t *testing.T) {
99- js := BuildScrollByJS(0, -320)99+ js := BuildPageScrollByJS(0, -320)
100 if !strings.Contains(js, "var dx=0,dy=-320") {100 if !strings.Contains(js, "var dx=0,dy=-320") {
101 t.Errorf("expected dy=-320 in JS variables, got: %s", js)101 t.Errorf("expected dy=-320 in JS variables, got: %s", js)
102 }102 }
103 }103 }
104 104
105-func TestBuildScrollByJS_SyntacticallyValid(t *testing.T) {105+func TestBuildPageScrollByJS_SyntacticallyValid(t *testing.T) {
106 tests := []struct{ dx, dy int }{{0, 80}, {0, -320}, {-80, 0}, {80, 0}, {0, 0}}106 tests := []struct{ dx, dy int }{{0, 80}, {0, -320}, {-80, 0}, {80, 0}, {0, 0}}
107 107
108 for _, tt := range tests {108 for _, tt := range tests {
109- js := BuildScrollByJS(tt.dx, tt.dy)109+ js := BuildPageScrollByJS(tt.dx, tt.dy)
110 110
111 depth := 0111 depth := 0
112 for _, ch := range js {112 for _, ch := range js {
@@ -136,8 +136,8 @@ func TestBuildScrollByJS_SyntacticallyValid(t *testing.T) {
136 }136 }
137 }137 }
138 138
139-func TestBuildScrollByJS_ZeroDeltaSafe(t *testing.T) {139+func TestBuildPageScrollByJS_ZeroDeltaSafe(t *testing.T) {
140- js := BuildScrollByJS(0, 0)140+ js := BuildPageScrollByJS(0, 0)
141 if !strings.Contains(js, "var dx=0,dy=0") {141 if !strings.Contains(js, "var dx=0,dy=0") {
142 t.Errorf("expected zero deltas in JS variables, got: %s", js)142 t.Errorf("expected zero deltas in JS variables, got: %s", js)
143 }143 }
M internal/ui/app.go +118 -116
@@ -53,11 +53,11 @@ const (
53 floatingSessionIDDefault = "default"53 floatingSessionIDDefault = "default"
54 floatingPaneVisibleClass = "floating-pane-visible"54 floatingPaneVisibleClass = "floating-pane-visible"
55 55
56- // pageModePulseInterval is the minimum interval between page mode pulses.56+ // vimModePulseInterval is the minimum interval between vim mode pulses.
57- // Held Page Mode scrolling now runs on its own smooth repeater, so the pulse57+ // Held Vim Mode scrolling now runs on its own smooth repeater, so the pulse
58 // is intentionally throttled well below scroll cadence to avoid GTK CSS churn58 // is intentionally throttled well below scroll cadence to avoid GTK CSS churn
59 // becoming part of the perceived scroll jank.59 // becoming part of the perceived scroll jank.
60- pageModePulseInterval = 120 * time.Millisecond60+ vimModePulseInterval = 120 * time.Millisecond
61 )61 )
62 62
63 func gtkApplicationFlags() gio.ApplicationFlags {63 func gtkApplicationFlags() gio.ApplicationFlags {
@@ -123,7 +123,7 @@ type App struct {
123 // Focus tracking stays app-global.123 // Focus tracking stays app-global.
124 focusMgr *focus.Manager124 focusMgr *focus.Manager
125 125
126- pageModePolicyUC *usecase.PageModePolicyUseCase126+ vimModePolicyUC *usecase.VimModePolicyUseCase
127 pageEditableFocusByPane map[entity.PaneID]bool127 pageEditableFocusByPane map[entity.PaneID]bool
128 128
129 resizeModeBorderTarget layout.Widget129 resizeModeBorderTarget layout.Widget
@@ -159,11 +159,11 @@ type App struct {
159 // Accent picker for dead keys support159 // Accent picker for dead keys support
160 accentFocusProvider port.FocusedInputProvider160 accentFocusProvider port.FocusedInputProvider
161 161
162- // Page mode pulse debounce - tracks last pulse time to skip rapid162+ // Vim mode pulse debounce - tracks last pulse time to skip rapid
163- // repeats during held-key scroll actions. Reset on page mode exit163+ // repeats during held-key scroll actions. Reset on vim mode exit
164 // via handleModeChange.164 // via handleModeChange.
165- pageModePulseLastTime time.Time165+ vimModePulseLastTime time.Time
166- pageModePulseMu sync.Mutex166+ vimModePulseMu sync.Mutex
167 167
168 // Deferred initialization - runs after first load_started to avoid blocking initial navigation168 // Deferred initialization - runs after first load_started to avoid blocking initial navigation
169 deferredInitOnce sync.Once169 deferredInitOnce sync.Once
@@ -216,7 +216,7 @@ func New(deps *Dependencies) (*App, error) {
216 windowForTab: make(map[entity.TabID]*browserWindow),216 windowForTab: make(map[entity.TabID]*browserWindow),
217 floatingSessions: make(map[floatingSessionKey]*floatingWorkspaceSession),217 floatingSessions: make(map[floatingSessionKey]*floatingWorkspaceSession),
218 browserWindows: make(map[string]*browserWindow),218 browserWindows: make(map[string]*browserWindow),
219- pageModePolicyUC: usecase.NewPageModePolicyUseCase(),219+ vimModePolicyUC: usecase.NewVimModePolicyUseCase(),
220 pageEditableFocusByPane: make(map[entity.PaneID]bool),220 pageEditableFocusByPane: make(map[entity.PaneID]bool),
221 dispatchOnMainThread: func(label string, fn func()) syncdispatch.SyncDispatchResult {221 dispatchOnMainThread: func(label string, fn func()) syncdispatch.SyncDispatchResult {
222 if fn != nil {222 if fn != nil {
@@ -835,7 +835,7 @@ func (a *App) wireSessionManagerShortcut() {
835 }835 }
836 bw.sessionManager.Toggle(ctx)836 bw.sessionManager.Toggle(ctx)
837 if bw.sessionManager.IsVisible() {837 if bw.sessionManager.IsVisible() {
838- a.handlePageModeFocusTrigger(ctx, bw, usecase.PageModePolicyTriggerOverlayFocus)838+ a.handleVimModeFocusTrigger(ctx, bw, usecase.VimModePolicyTriggerOverlayFocus)
839 }839 }
840 return nil840 return nil
841 })841 })
@@ -1088,9 +1088,9 @@ func (a *App) initBrowserWindowInput(ctx context.Context, bw *browserWindow) {
1088 }1088 }
1089 return a.dispatchBrowserWindowAction(actionCtx, bw, action)1089 return a.dispatchBrowserWindowAction(actionCtx, bw, action)
1090 })1090 })
1091- bw.keyboardHandler.SetOnPageScrollLifecycle(func(scrollCtx context.Context, action input.Action, phase input.PageScrollPhase) error {1091+ bw.keyboardHandler.SetOnVimScrollLifecycle(func(scrollCtx context.Context, action input.Action, phase input.VimScrollPhase) error {
1092 a.activateBrowserWindow(bw)1092 a.activateBrowserWindow(bw)
1093- return a.kbDispatcher.DispatchPageScrollLifecycle(scrollCtx, action, phase)1093+ return a.kbDispatcher.DispatchVimScrollLifecycle(scrollCtx, action, phase)
1094 })1094 })
1095 bw.keyboardHandler.SetOnEscape(func(escapeCtx context.Context) bool {1095 bw.keyboardHandler.SetOnEscape(func(escapeCtx context.Context) bool {
1096 a.activateBrowserWindow(bw)1096 a.activateBrowserWindow(bw)
@@ -1126,8 +1126,8 @@ func (a *App) initBrowserWindowInput(ctx context.Context, bw *browserWindow) {
1126 1126
1127 return input.RouteHandleShortcuts1127 return input.RouteHandleShortcuts
1128 })1128 })
1129- bw.keyboardHandler.SetPageModeActivationPassthrough(func() bool {1129+ bw.keyboardHandler.SetVimModeActivationPassthrough(func() bool {
1130- return a.shouldBypassPageModeActivation(bw)1130+ return a.shouldBypassVimModeActivation(bw)
1131 })1131 })
1132 bw.keyboardHandler.SetAccentHandler(a)1132 bw.keyboardHandler.SetAccentHandler(a)
1133 bw.keyboardHandler.AttachTo(bw.mainWindow.Window())1133 bw.keyboardHandler.AttachTo(bw.mainWindow.Window())
@@ -1369,7 +1369,7 @@ func (a *App) initOmniboxConfig(ctx context.Context) {
1369 a.accentFocusProvider.SetFocusedInput(a.deps.NewGTKEntryTarget(entry))1369 a.accentFocusProvider.SetFocusedInput(a.deps.NewGTKEntryTarget(entry))
1370 }1370 }
1371 }1371 }
1372- a.handlePageModeFocusTrigger(ctx, a.lastFocusedBrowserWindow(), usecase.PageModePolicyTriggerOmniboxFocus)1372+ a.handleVimModeFocusTrigger(ctx, a.lastFocusedBrowserWindow(), usecase.VimModePolicyTriggerOmniboxFocus)
1373 },1373 },
1374 OnFocusOut: func() {1374 OnFocusOut: func() {
1375 // When omnibox loses focus, set WebView as the focused input1375 // When omnibox loses focus, set WebView as the focused input
@@ -1408,7 +1408,7 @@ func (a *App) initFindBarConfig(ctx context.Context) {
1408 a.accentFocusProvider.SetFocusedInput(a.deps.NewGTKEntryTarget(entry))1408 a.accentFocusProvider.SetFocusedInput(a.deps.NewGTKEntryTarget(entry))
1409 }1409 }
1410 }1410 }
1411- a.handlePageModeFocusTrigger(ctx, a.lastFocusedBrowserWindow(), usecase.PageModePolicyTriggerFindBarFocus)1411+ a.handleVimModeFocusTrigger(ctx, a.lastFocusedBrowserWindow(), usecase.VimModePolicyTriggerFindBarFocus)
1412 },1412 },
1413 OnFocusOut: func() {1413 OnFocusOut: func() {
1414 // When find bar loses focus, set WebView as the focused input1414 // When find bar loses focus, set WebView as the focused input
@@ -1433,7 +1433,7 @@ func (a *App) ToggleSessionManager(ctx context.Context) {
1433 }1433 }
1434 bw.sessionManager.Toggle(ctx)1434 bw.sessionManager.Toggle(ctx)
1435 if bw.sessionManager.IsVisible() {1435 if bw.sessionManager.IsVisible() {
1436- a.handlePageModeFocusTrigger(ctx, bw, usecase.PageModePolicyTriggerOverlayFocus)1436+ a.handleVimModeFocusTrigger(ctx, bw, usecase.VimModePolicyTriggerOverlayFocus)
1437 }1437 }
1438 }1438 }
1439 1439
@@ -1533,7 +1533,7 @@ func (a *App) HandleMovePaneToTab(ctx context.Context) error {
1533 1533
1534 a.attachTabPickerToActivePane()1534 a.attachTabPickerToActivePane()
1535 bw.tabPicker.Show(ctx, items)1535 bw.tabPicker.Show(ctx, items)
1536- a.handlePageModeFocusTrigger(ctx, bw, usecase.PageModePolicyTriggerOverlayFocus)1536+ a.handleVimModeFocusTrigger(ctx, bw, usecase.VimModePolicyTriggerOverlayFocus)
1537 return nil1537 return nil
1538 }1538 }
1539 1539
@@ -3108,7 +3108,7 @@ func (a *App) initTabCoordinator(ctx context.Context) {
3108 // tracks ActiveTabID and PreviousActiveTabID on bw.tabs. No manual bw state needed.3108 // tracks ActiveTabID and PreviousActiveTabID on bw.tabs. No manual bw state needed.
3109 if bw := a.browserWindowForTabTarget(target); bw != nil {3109 if bw := a.browserWindowForTabTarget(target); bw != nil {
3110 a.activateBrowserWindow(bw)3110 a.activateBrowserWindow(bw)
3111- a.handlePageModeTabSwitch(ctx, bw)3111+ a.handleVimModeTabSwitch(ctx, bw)
3112 }3112 }
3113 a.switchWorkspaceView(ctx, tab.ID)3113 a.switchWorkspaceView(ctx, tab.ID)
3114 })3114 })
@@ -3393,8 +3393,8 @@ func (a *App) withFocusedTabTarget(ctx context.Context, action string, ensure bo
3393 3393
3394 func (a *App) wireKeyboardActions() {3394 func (a *App) wireKeyboardActions() {
3395 a.kbDispatcher.SetOnQuit(a.Quit)3395 a.kbDispatcher.SetOnQuit(a.Quit)
3396- a.kbDispatcher.SetOnPageModePulse(func(ctx context.Context, fast bool) {3396+ a.kbDispatcher.SetOnVimModePulse(func(ctx context.Context, fast bool) {
3397- a.triggerPageModePulse(ctx, fast)3397+ a.triggerVimModePulse(ctx, fast)
3398 })3398 })
3399 a.kbDispatcher.SetOnFindOpen(func(ctx context.Context) error {3399 a.kbDispatcher.SetOnFindOpen(func(ctx context.Context) error {
3400 a.ToggleFindBar(ctx)3400 a.ToggleFindBar(ctx)
@@ -3727,14 +3727,14 @@ func (a *App) updateWindowTitleFromActivePane(tabID entity.TabID) {
3727 a.updateWindowTitle(title, a.ownerOrLastFocusedBrowserWindow(tabID, ""))3727 a.updateWindowTitle(title, a.ownerOrLastFocusedBrowserWindow(tabID, ""))
3728 }3728 }
3729 3729
3730-func (a *App) pageModePolicy() *usecase.PageModePolicyUseCase {3730+func (a *App) vimModePolicy() *usecase.VimModePolicyUseCase {
3731 if a == nil {3731 if a == nil {
3732- return usecase.NewPageModePolicyUseCase()3732+ return usecase.NewVimModePolicyUseCase()
3733 }3733 }
3734- if a.pageModePolicyUC == nil {3734+ if a.vimModePolicyUC == nil {
3735- a.pageModePolicyUC = usecase.NewPageModePolicyUseCase()3735+ a.vimModePolicyUC = usecase.NewVimModePolicyUseCase()
3736 }3736 }
3737- return a.pageModePolicyUC3737+ return a.vimModePolicyUC
3738 }3738 }
3739 3739
3740 func (a *App) ensurePageEditableFocusMap() {3740 func (a *App) ensurePageEditableFocusMap() {
@@ -3766,51 +3766,51 @@ func (a *App) clearPageEditableFocusState(paneID entity.PaneID) {
3766 a.setPageEditableFocused(paneID, false)3766 a.setPageEditableFocused(paneID, false)
3767 }3767 }
3768 3768
3769-func (a *App) pageModeActiveForBrowserWindow(bw *browserWindow) bool {3769+func (a *App) vimModeActiveForBrowserWindow(bw *browserWindow) bool {
3770- return bw != nil && bw.keyboardHandler != nil && bw.keyboardHandler.Mode() == input.ModePage3770+ return bw != nil && bw.keyboardHandler != nil && bw.keyboardHandler.Mode() == input.ModeVim
3771 }3771 }
3772 3772
3773-func (a *App) applyPageModePolicyTransition(_ context.Context, bw *browserWindow, transition usecase.PageModePolicyTransition) {3773+func (a *App) applyVimModePolicyTransition(_ context.Context, bw *browserWindow, transition usecase.VimModePolicyTransition) {
3774- if transition != usecase.PageModePolicyTransitionExit || bw == nil || bw.keyboardHandler == nil {3774+ if transition != usecase.VimModePolicyTransitionExit || bw == nil || bw.keyboardHandler == nil {
3775 return3775 return
3776 }3776 }
3777- if bw.keyboardHandler.Mode() != input.ModePage {3777+ if bw.keyboardHandler.Mode() != input.ModeVim {
3778 return3778 return
3779 }3779 }
3780 bw.keyboardHandler.ExitMode()3780 bw.keyboardHandler.ExitMode()
3781 }3781 }
3782 3782
3783-func (a *App) handlePageModeFocusTrigger(ctx context.Context, bw *browserWindow, trigger usecase.PageModePolicyTrigger) {3783+func (a *App) handleVimModeFocusTrigger(ctx context.Context, bw *browserWindow, trigger usecase.VimModePolicyTrigger) {
3784 if bw == nil {3784 if bw == nil {
3785 return3785 return
3786 }3786 }
3787- transition := a.pageModePolicy().Evaluate(usecase.PageModePolicyInput{3787+ transition := a.vimModePolicy().Evaluate(usecase.VimModePolicyInput{
3788- Trigger: trigger,3788+ Trigger: trigger,
3789- PageModeActive: a.pageModeActiveForBrowserWindow(bw),3789+ VimModeActive: a.vimModeActiveForBrowserWindow(bw),
3790 })3790 })
3791- a.applyPageModePolicyTransition(ctx, bw, transition)3791+ a.applyVimModePolicyTransition(ctx, bw, transition)
3792 }3792 }
3793 3793
3794-func (a *App) handlePageModeTabSwitch(_ context.Context, bw *browserWindow) {3794+func (a *App) handleVimModeTabSwitch(_ context.Context, bw *browserWindow) {
3795- if bw == nil || bw.tabs == nil || !a.pageModeActiveForBrowserWindow(bw) {3795+ if bw == nil || bw.tabs == nil || !a.vimModeActiveForBrowserWindow(bw) {
3796 return3796 return
3797 }3797 }
3798- transition := a.pageModePolicy().Evaluate(usecase.PageModePolicyInput{3798+ transition := a.vimModePolicy().Evaluate(usecase.VimModePolicyInput{
3799- Trigger: usecase.PageModePolicyTriggerContextChanged,3799+ Trigger: usecase.VimModePolicyTriggerContextChanged,
3800- PageModeActive: true,3800+ VimModeActive: true,
3801 PreserveOnContextChange: false,3801 PreserveOnContextChange: false,
3802 })3802 })
3803- if transition != usecase.PageModePolicyTransitionExit {3803+ if transition != usecase.VimModePolicyTransitionExit {
3804 return3804 return
3805 }3805 }
3806 if prevTabID := bw.tabs.PreviousActiveTabID; prevTabID != "" {3806 if prevTabID := bw.tabs.PreviousActiveTabID; prevTabID != "" {
3807 if prevView := a.workspaceViews[prevTabID]; prevView != nil {3807 if prevView := a.workspaceViews[prevTabID]; prevView != nil {
3808- if pv := prevView.GetPaneView(bw.pageModePaneID); pv != nil {3808+ if pv := prevView.GetPaneView(bw.vimModePaneID); pv != nil {
3809- pv.SetPageMode(false)3809+ pv.SetVimMode(false)
3810 }3810 }
3811 }3811 }
3812 }3812 }
3813- bw.pageModePaneID = ""3813+ bw.vimModePaneID = ""
3814 bw.keyboardHandler.ExitMode()3814 bw.keyboardHandler.ExitMode()
3815 }3815 }
3816 3816
@@ -3826,25 +3826,25 @@ func (a *App) handlePageEditableFocusChanged(ctx context.Context, paneID entity.
3826 }3826 }
3827 ws := a.activeWorkspaceForBrowserWindow(bw)3827 ws := a.activeWorkspaceForBrowserWindow(bw)
3828 activeContext := ws != nil && ws.ActivePaneID == paneID && a.lastFocusedBrowserWindow() == bw3828 activeContext := ws != nil && ws.ActivePaneID == paneID && a.lastFocusedBrowserWindow() == bw
3829- transition := a.pageModePolicy().Evaluate(usecase.PageModePolicyInput{3829+ transition := a.vimModePolicy().Evaluate(usecase.VimModePolicyInput{
3830- Trigger: usecase.PageModePolicyTriggerPageEditableFocusChanged,3830+ Trigger: usecase.VimModePolicyTriggerPageEditableFocusChanged,
3831- PageModeActive: a.pageModeActiveForBrowserWindow(bw),3831+ VimModeActive: a.vimModeActiveForBrowserWindow(bw),
3832 PageEditableFocused: editable,3832 PageEditableFocused: editable,
3833 EventInActiveContext: activeContext,3833 EventInActiveContext: activeContext,
3834 })3834 })
3835- a.applyPageModePolicyTransition(ctx, bw, transition)3835+ a.applyVimModePolicyTransition(ctx, bw, transition)
3836 }3836 }
3837 3837
3838-func (a *App) shouldBypassPageModeActivation(bw *browserWindow) bool {3838+func (a *App) shouldBypassVimModeActivation(bw *browserWindow) bool {
3839 ws := a.activeWorkspaceForBrowserWindow(bw)3839 ws := a.activeWorkspaceForBrowserWindow(bw)
3840 if ws == nil {3840 if ws == nil {
3841 return false3841 return false
3842 }3842 }
3843- transition := a.pageModePolicy().Evaluate(usecase.PageModePolicyInput{3843+ transition := a.vimModePolicy().Evaluate(usecase.VimModePolicyInput{
3844- Trigger: usecase.PageModePolicyTriggerActivationAttempt,3844+ Trigger: usecase.VimModePolicyTriggerActivationAttempt,
3845 PageEditableFocused: a.pageEditableFocused(ws.ActivePaneID),3845 PageEditableFocused: a.pageEditableFocused(ws.ActivePaneID),
3846 })3846 })
3847- return transition == usecase.PageModePolicyTransitionBlockActivation3847+ return transition == usecase.VimModePolicyTransitionBlockActivation
3848 }3848 }
3849 3849
3850 // handleModeChange is called when the input mode changes for a specific browser window.3850 // handleModeChange is called when the input mode changes for a specific browser window.
@@ -3860,13 +3860,13 @@ func (a *App) handleModeChange(ctx context.Context, bw *browserWindow, from, to
3860 a.applyResizeModeBorder(ctx, a.activeWorkspace())3860 a.applyResizeModeBorder(ctx, a.activeWorkspace())
3861 }3861 }
3862 3862
3863- // Handle pane-local Page Mode visual ownership.3863+ // Handle pane-local Vim Mode visual ownership.
3864- // Entering Page Mode accents the active pane; leaving removes the accent.3864+ // Entering Vim Mode accents the active pane; leaving removes the accent.
3865- a.handlePageModeOwnership(ctx, bw, to, from)3865+ a.handleVimModeOwnership(ctx, bw, to, from)
3866 3866
3867 // Update global border overlay visibility based on mode.3867 // Update global border overlay visibility based on mode.
3868 // Note: resize mode border is handled per-pane (stack container), not via global overlay.3868 // Note: resize mode border is handled per-pane (stack container), not via global overlay.
3869- // Page mode explicitly skips the global border overlay.3869+ // Vim mode explicitly skips the global border overlay.
3870 if bw != nil && bw.borderMgr != nil {3870 if bw != nil && bw.borderMgr != nil {
3871 bw.borderMgr.OnModeChange(ctx, from, to)3871 bw.borderMgr.OnModeChange(ctx, from, to)
3872 }3872 }
@@ -3875,23 +3875,23 @@ func (a *App) handleModeChange(ctx context.Context, bw *browserWindow, from, to
3875 a.updateModeIndicatorToaster(ctx, bw, to)3875 a.updateModeIndicatorToaster(ctx, bw, to)
3876 }3876 }
3877 3877
3878-// transferPageModeOwnershipToPane transfers the pane-local Page Mode accent3878+// transferVimModeOwnershipToPane transfers the pane-local Vim Mode accent
3879 // from the current owning pane to another pane in the same browser window.3879 // from the current owning pane to another pane in the same browser window.
3880-// The transfer only happens while that window is in Page Mode.3880+// The transfer only happens while that window is in Vim Mode.
3881-func (a *App) transferPageModeOwnershipToPane(ctx context.Context, bw *browserWindow, newPaneID entity.PaneID) {3881+func (a *App) transferVimModeOwnershipToPane(ctx context.Context, bw *browserWindow, newPaneID entity.PaneID) {
3882 if bw == nil {3882 if bw == nil {
3883 return3883 return
3884 }3884 }
3885 3885
3886- // Only transfer if page mode is currently active and the owner is changing.3886+ // Only transfer if vim mode is currently active and the owner is changing.
3887- if bw.pageModePaneID == "" || bw.pageModePaneID == newPaneID {3887+ if bw.vimModePaneID == "" || bw.vimModePaneID == newPaneID {
3888 return3888 return
3889 }3889 }
3890 3890
3891- // Check if this window is actually in page mode.3891+ // Check if this window is actually in vim mode.
3892- if bw.keyboardHandler == nil || bw.keyboardHandler.Mode() != input.ModePage {3892+ if bw.keyboardHandler == nil || bw.keyboardHandler.Mode() != input.ModeVim {
3893- // Page mode not active on this window; just clear stale ownership.3893+ // Vim mode not active on this window; just clear stale ownership.
3894- bw.pageModePaneID = ""3894+ bw.vimModePaneID = ""
3895 return3895 return
3896 }3896 }
3897 3897
@@ -3900,39 +3900,39 @@ func (a *App) transferPageModeOwnershipToPane(ctx context.Context, bw *browserWi
3900 return3900 return
3901 }3901 }
3902 3902
3903- transition := a.pageModePolicy().Evaluate(usecase.PageModePolicyInput{3903+ transition := a.vimModePolicy().Evaluate(usecase.VimModePolicyInput{
3904- Trigger: usecase.PageModePolicyTriggerContextChanged,3904+ Trigger: usecase.VimModePolicyTriggerContextChanged,
3905- PageModeActive: true,3905+ VimModeActive: true,
3906 PreserveOnContextChange: !a.pageEditableFocused(newPaneID),3906 PreserveOnContextChange: !a.pageEditableFocused(newPaneID),
3907 })3907 })
3908- if transition == usecase.PageModePolicyTransitionExit {3908+ if transition == usecase.VimModePolicyTransitionExit {
3909- a.applyPageModePolicyTransition(ctx, bw, transition)3909+ a.applyVimModePolicyTransition(ctx, bw, transition)
3910 return3910 return
3911 }3911 }
3912 3912
3913- oldPaneID := bw.pageModePaneID3913+ oldPaneID := bw.vimModePaneID
3914 3914
3915 // Deactivate the old owning pane.3915 // Deactivate the old owning pane.
3916 if oldPV := wsView.GetPaneView(oldPaneID); oldPV != nil {3916 if oldPV := wsView.GetPaneView(oldPaneID); oldPV != nil {
3917- oldPV.SetPageMode(false)3917+ oldPV.SetVimMode(false)
3918 }3918 }
3919 3919
3920 // Activate the new pane.3920 // Activate the new pane.
3921 if newPV := wsView.GetPaneView(newPaneID); newPV != nil {3921 if newPV := wsView.GetPaneView(newPaneID); newPV != nil {
3922- newPV.SetPageMode(true)3922+ newPV.SetVimMode(true)
3923 }3923 }
3924- bw.pageModePaneID = newPaneID3924+ bw.vimModePaneID = newPaneID
3925 3925
3926 logging.FromContext(ctx).Debug().3926 logging.FromContext(ctx).Debug().
3927 Str("window_id", bw.id).3927 Str("window_id", bw.id).
3928 Str("old_pane_id", string(oldPaneID)).3928 Str("old_pane_id", string(oldPaneID)).
3929 Str("new_pane_id", string(newPaneID)).3929 Str("new_pane_id", string(newPaneID)).
3930- Msg("page mode ownership transferred")3930+ Msg("vim mode ownership transferred")
3931 }3931 }
3932 3932
3933-// handlePageModeOwnership manages the pane-local Page Mode accent and pulse3933+// handleVimModeOwnership manages the pane-local Vim Mode accent and pulse
3934 // owner for a specific browser window when input modes change.3934 // owner for a specific browser window when input modes change.
3935-func (a *App) handlePageModeOwnership(ctx context.Context, bw *browserWindow, to, from input.Mode) {3935+func (a *App) handleVimModeOwnership(ctx context.Context, bw *browserWindow, to, from input.Mode) {
3936 if bw == nil {3936 if bw == nil {
3937 return3937 return
3938 }3938 }
@@ -3941,96 +3941,96 @@ func (a *App) handlePageModeOwnership(ctx context.Context, bw *browserWindow, to
3941 wsView := a.activeWorkspaceViewForBrowserWindow(bw)3941 wsView := a.activeWorkspaceViewForBrowserWindow(bw)
3942 if ws == nil || wsView == nil {3942 if ws == nil || wsView == nil {
3943 // If there's no workspace yet (startup), just track the intent.3943 // If there's no workspace yet (startup), just track the intent.
3944- if to != input.ModePage && from == input.ModePage {3944+ if to != input.ModeVim && from == input.ModeVim {
3945- bw.pageModePaneID = ""3945+ bw.vimModePaneID = ""
3946 }3946 }
3947 return3947 return
3948 }3948 }
3949 3949
3950- if to == input.ModePage && from != input.ModePage {3950+ if to == input.ModeVim && from != input.ModeVim {
3951- // Entering page mode: activate on the current active pane of this window.3951+ // Entering vim mode: activate on the current active pane of this window.
3952 paneID := ws.ActivePaneID3952 paneID := ws.ActivePaneID
3953 if pv := wsView.GetPaneView(paneID); pv != nil {3953 if pv := wsView.GetPaneView(paneID); pv != nil {
3954- pv.SetPageMode(true)3954+ pv.SetVimMode(true)
3955- bw.pageModePaneID = paneID3955+ bw.vimModePaneID = paneID
3956 logging.FromContext(ctx).Debug().3956 logging.FromContext(ctx).Debug().
3957 Str("window_id", bw.id).3957 Str("window_id", bw.id).
3958 Str("pane_id", string(paneID)).3958 Str("pane_id", string(paneID)).
3959- Msg("page mode activated on pane")3959+ Msg("vim mode activated on pane")
3960 }3960 }
3961- } else if from == input.ModePage && to != input.ModePage {3961+ } else if from == input.ModeVim && to != input.ModeVim {
3962- // Leaving Page Mode: deactivate the pane that owns the visual accent.3962+ // Leaving Vim Mode: deactivate the pane that owns the visual accent.
3963- a.clearPageModeOwnership(ctx, bw)3963+ a.clearVimModeOwnership(ctx, bw)
3964 }3964 }
3965 }3965 }
3966 3966
3967-// triggerPageModePulse triggers a pane-local page mode pulse on the3967+// triggerVimModePulse triggers a pane-local vim mode pulse on the
3968 // last-focused browser window's owning pane. This is called by the3968 // last-focused browser window's owning pane. This is called by the
3969 // keyboard dispatcher after a page scroll action. The bw used is the3969 // keyboard dispatcher after a page scroll action. The bw used is the
3970 // window that was active when the action was dispatched (via3970 // window that was active when the action was dispatched (via
3971 // dispatchBrowserWindowAction which calls activateBrowserWindow first).3971 // dispatchBrowserWindowAction which calls activateBrowserWindow first).
3972-// If no pane is currently in page mode, the pulse is a no-op.3972+// If no pane is currently in vim mode, the pulse is a no-op.
3973 //3973 //
3974-// Pulses are debounced at pageModePulseInterval to prevent excessive GTK CSS3974+// Pulses are debounced at vimModePulseInterval to prevent excessive GTK CSS
3975 // class churn during held-key repeats. Smooth scroll cadence is owned by the3975 // class churn during held-key repeats. Smooth scroll cadence is owned by the
3976-// Page Mode repeater and backend scroll path; pulse feedback is deliberately3976+// Vim Mode repeater and backend scroll path; pulse feedback is deliberately
3977 // slower so indicator/overlay animation work does not become part of the3977 // slower so indicator/overlay animation work does not become part of the
3978 // scrolling critical path.3978 // scrolling critical path.
3979-func (a *App) triggerPageModePulse(_ context.Context, fast bool) {3979+func (a *App) triggerVimModePulse(_ context.Context, fast bool) {
3980 // Debounce: skip if called again too soon during a continuous hold.3980 // Debounce: skip if called again too soon during a continuous hold.
3981 // We still serialize the timestamp update so repeated dispatcher calls do3981 // We still serialize the timestamp update so repeated dispatcher calls do
3982 // not restart CSS animations at scroll cadence.3982 // not restart CSS animations at scroll cadence.
3983- a.pageModePulseMu.Lock()3983+ a.vimModePulseMu.Lock()
3984- since := time.Since(a.pageModePulseLastTime)3984+ since := time.Since(a.vimModePulseLastTime)
3985- if since < pageModePulseInterval {3985+ if since < vimModePulseInterval {
3986- a.pageModePulseMu.Unlock()3986+ a.vimModePulseMu.Unlock()
3987 return3987 return
3988 }3988 }
3989- a.pageModePulseLastTime = time.Now()3989+ a.vimModePulseLastTime = time.Now()
3990- a.pageModePulseMu.Unlock()3990+ a.vimModePulseMu.Unlock()
3991 3991
3992 bw := a.lastFocusedBrowserWindow()3992 bw := a.lastFocusedBrowserWindow()
3993- if bw == nil || bw.pageModePaneID == "" {3993+ if bw == nil || bw.vimModePaneID == "" {
3994 return3994 return
3995 }3995 }
3996 wsView := a.activeWorkspaceViewForBrowserWindow(bw)3996 wsView := a.activeWorkspaceViewForBrowserWindow(bw)
3997 if wsView == nil {3997 if wsView == nil {
3998 return3998 return
3999 }3999 }
4000- if pv := wsView.GetPaneView(bw.pageModePaneID); pv != nil {4000+ if pv := wsView.GetPaneView(bw.vimModePaneID); pv != nil {
4001 if fast {4001 if fast {
4002- pv.TriggerPageModePulseFast()4002+ pv.TriggerVimModePulseFast()
4003 } else {4003 } else {
4004- pv.TriggerPageModePulse()4004+ pv.TriggerVimModePulse()
4005 }4005 }
4006 }4006 }
4007 }4007 }
4008 4008
4009-// clearPageModeOwnership deactivates page mode on the owning pane for a4009+// clearVimModeOwnership deactivates vim mode on the owning pane for a
4010 // specific browser window and resets its tracked pane ID.4010 // specific browser window and resets its tracked pane ID.
4011 // Also resets the pulse debounce timer so the first pulse on re-entry4011 // Also resets the pulse debounce timer so the first pulse on re-entry
4012 // is never skipped.4012 // is never skipped.
4013-func (a *App) clearPageModeOwnership(ctx context.Context, bw *browserWindow) {4013+func (a *App) clearVimModeOwnership(ctx context.Context, bw *browserWindow) {
4014- if bw == nil || bw.pageModePaneID == "" {4014+ if bw == nil || bw.vimModePaneID == "" {
4015 return4015 return
4016 }4016 }
4017 4017
4018 wsView := a.activeWorkspaceViewForBrowserWindow(bw)4018 wsView := a.activeWorkspaceViewForBrowserWindow(bw)
4019 if wsView != nil {4019 if wsView != nil {
4020- if pv := wsView.GetPaneView(bw.pageModePaneID); pv != nil {4020+ if pv := wsView.GetPaneView(bw.vimModePaneID); pv != nil {
4021- pv.SetPageMode(false)4021+ pv.SetVimMode(false)
4022 logging.FromContext(ctx).Debug().4022 logging.FromContext(ctx).Debug().
4023 Str("window_id", bw.id).4023 Str("window_id", bw.id).
4024- Str("pane_id", string(bw.pageModePaneID)).4024+ Str("pane_id", string(bw.vimModePaneID)).
4025- Msg("page mode deactivated on pane")4025+ Msg("vim mode deactivated on pane")
4026 }4026 }
4027 }4027 }
4028- bw.pageModePaneID = ""4028+ bw.vimModePaneID = ""
4029 4029
4030 // Reset debounce timer so re-entry pulses are never mis-skipped.4030 // Reset debounce timer so re-entry pulses are never mis-skipped.
4031- a.pageModePulseMu.Lock()4031+ a.vimModePulseMu.Lock()
4032- a.pageModePulseLastTime = time.Time{}4032+ a.vimModePulseLastTime = time.Time{}
4033- a.pageModePulseMu.Unlock()4033+ a.vimModePulseMu.Unlock()
4034 }4034 }
4035 4035
4036 // updateModeIndicatorToaster reconciles one window's mode toaster with its4036 // updateModeIndicatorToaster reconciles one window's mode toaster with its
@@ -4057,8 +4057,10 @@ func (a *App) updateModeIndicatorToaster(ctx context.Context, bw *browserWindow,
4057 // getModeToastClass returns the CSS class for the given mode's toast styling.4057 // getModeToastClass returns the CSS class for the given mode's toast styling.
4058 func getModeToastClass(mode input.Mode) string {4058 func getModeToastClass(mode input.Mode) string {
4059 switch mode {4059 switch mode {
4060- case input.ModePane, input.ModePage:4060+ case input.ModePane:
4061 return "toast-pane-mode"4061 return "toast-pane-mode"
4062+ case input.ModeVim:
4063+ return "toast-vim-mode"
4062 case input.ModeTab:4064 case input.ModeTab:
4063 return "toast-tab-mode"4065 return "toast-tab-mode"
4064 case input.ModeSession:4066 case input.ModeSession:
@@ -4169,7 +4171,7 @@ func (a *App) createWorkspaceViewWithoutAttach(ctx context.Context, tab *entity.
4169 wsView.SetOnActivePaneChanged(func(paneID entity.PaneID) {4171 wsView.SetOnActivePaneChanged(func(paneID entity.PaneID) {
4170 a.contentCoord.SyncWebViewViewport(syncCtx, paneID, "workspace-pane-activated")4172 a.contentCoord.SyncWebViewViewport(syncCtx, paneID, "workspace-pane-activated")
4171 if bw := a.browserWindowForTab(tab.ID); bw != nil {4173 if bw := a.browserWindowForTab(tab.ID); bw != nil {
4172- a.transferPageModeOwnershipToPane(ctx, bw, paneID)4174+ a.transferVimModeOwnershipToPane(ctx, bw, paneID)
4173 }4175 }
4174 })4176 })
4175 }4177 }
R internal/ui/app_page_mode_test.go → internal/ui/app_vim_mode_test.go +309 -309 renamed, 58% similar
@@ -23,7 +23,7 @@ import (
23 // ---------------------------------------------------------------------------23 // ---------------------------------------------------------------------------
24 // Mock-based PaneView and WorkspaceView creation helpers24 // Mock-based PaneView and WorkspaceView creation helpers
25 //25 //
26-// These are used by pageModeTestFixture to build a minimal test environment26+// These are used by vimModeTestFixture to build a minimal test environment
27 // without any GTK infrastructure.27 // without any GTK infrastructure.
28 // ---------------------------------------------------------------------------28 // ---------------------------------------------------------------------------
29 29
@@ -99,7 +99,7 @@ func newLoadingSkeletonMocks(
99 }99 }
100 100
101 // createTestPaneView builds a mock-backed PaneView. Returns the PaneView and101 // createTestPaneView builds a mock-backed PaneView. Returns the PaneView and
102-// its overlay mock so subsequent page-mode indicator / pulse expectations can102+// its overlay mock so subsequent vim-mode indicator / pulse expectations can
103 // be set up by the caller.103 // be set up by the caller.
104 func createTestPaneView(104 func createTestPaneView(
105 t *testing.T,105 t *testing.T,
@@ -138,20 +138,20 @@ func createTestPaneView(
138 return pv, overlay138 return pv, overlay
139 }139 }
140 140
141-// enterPageMode marks the owning pane with the pane-local visual accent.141+// enterVimMode marks the owning pane with the pane-local visual accent.
142-func enterPageMode(142+func enterVimMode(
143 t *testing.T,143 t *testing.T,
144 app *App,144 app *App,
145 overlay *mocks.MockOverlayWidget,145 overlay *mocks.MockOverlayWidget,
146 bw *browserWindow,146 bw *browserWindow,
147 ) {147 ) {
148 t.Helper()148 t.Helper()
149- overlay.EXPECT().AddCssClass("page-mode-active").Once()149+ overlay.EXPECT().AddCssClass("vim-mode-active").Once()
150- app.handlePageModeOwnership(context.Background(), bw, input.ModePage, input.ModeNormal)150+ app.handleVimModeOwnership(context.Background(), bw, input.ModeVim, input.ModeNormal)
151 }151 }
152 152
153-func expectPageModeAccentHidden(overlay *mocks.MockOverlayWidget) {153+func expectVimModeAccentHidden(overlay *mocks.MockOverlayWidget) {
154- overlay.EXPECT().RemoveCssClass("page-mode-active").Once()154+ overlay.EXPECT().RemoveCssClass("vim-mode-active").Once()
155 }155 }
156 156
157 // setupWorkspaceViewMocks creates a mock-backed WorkspaceView.157 // setupWorkspaceViewMocks creates a mock-backed WorkspaceView.
@@ -181,22 +181,22 @@ func setupWorkspaceViewMocks(
181 return wv181 return wv
182 }182 }
183 183
184-// newKeyboardHandlerInPageMode creates a KeyboardHandler whose Mode() returns184+// newKeyboardHandlerInVimMode creates a KeyboardHandler whose Mode() returns
185-// ModePage — no GTK widgets required.185+// ModeVim — no GTK widgets required.
186-func newKeyboardHandlerInPageMode(t *testing.T) *input.KeyboardHandler {186+func newKeyboardHandlerInVimMode(t *testing.T) *input.KeyboardHandler {
187 t.Helper()187 t.Helper()
188 kh := input.NewKeyboardHandler(188 kh := input.NewKeyboardHandler(
189 context.Background(),189 context.Background(),
190 &entity.WorkspaceConfig{},190 &entity.WorkspaceConfig{},
191 &entity.SessionConfig{},191 &entity.SessionConfig{},
192 )192 )
193- kh.EnterPageMode()193+ kh.EnterVimMode()
194 return kh194 return kh
195 }195 }
196 196
197-func bindPageModeKeyboardHandler(t *testing.T, app *App, bw *browserWindow) *input.KeyboardHandler {197+func bindVimModeKeyboardHandler(t *testing.T, app *App, bw *browserWindow) *input.KeyboardHandler {
198 t.Helper()198 t.Helper()
199- kh := newKeyboardHandlerInPageMode(t)199+ kh := newKeyboardHandlerInVimMode(t)
200 kh.SetOnModeChange(func(from, to input.Mode) {200 kh.SetOnModeChange(func(from, to input.Mode) {
201 app.handleModeChange(context.Background(), bw, from, to)201 app.handleModeChange(context.Background(), bw, from, to)
202 })202 })
@@ -205,13 +205,13 @@ func bindPageModeKeyboardHandler(t *testing.T, app *App, bw *browserWindow) *inp
205 }205 }
206 206
207 // ---------------------------------------------------------------------------207 // ---------------------------------------------------------------------------
208-// pageModeTestFixture — complete, minimal App + workspace environment208+// vimModeTestFixture — complete, minimal App + workspace environment
209 //209 //
210 // Provides two browser windows, each with a two-pane workspace. The active210 // Provides two browser windows, each with a two-pane workspace. The active
211 // pane on both workspaces starts at "pane-a".211 // pane on both workspaces starts at "pane-a".
212 // ---------------------------------------------------------------------------212 // ---------------------------------------------------------------------------
213 213
214-type pageModeTestFixture struct {214+type vimModeTestFixture struct {
215 app *App215 app *App
216 bw1 *browserWindow216 bw1 *browserWindow
217 bw2 *browserWindow217 bw2 *browserWindow
@@ -227,7 +227,7 @@ type pageModeTestFixture struct {
227 overlay2B *mocks.MockOverlayWidget227 overlay2B *mocks.MockOverlayWidget
228 }228 }
229 229
230-func newPageModeTestFixture(t *testing.T) *pageModeTestFixture {230+func newVimModeTestFixture(t *testing.T) *vimModeTestFixture {
231 t.Helper()231 t.Helper()
232 factory := mocks.NewMockWidgetFactory(t)232 factory := mocks.NewMockWidgetFactory(t)
233 233
@@ -297,7 +297,7 @@ func newPageModeTestFixture(t *testing.T) *pageModeTestFixture {
297 lastFocusedWindowID: "win-1",297 lastFocusedWindowID: "win-1",
298 }298 }
299 299
300- return &pageModeTestFixture{300+ return &vimModeTestFixture{
301 app: app, bw1: bw1, bw2: bw2,301 app: app, bw1: bw1, bw2: bw2,
302 pv1A: pv1A, pv1B: pv1B, pv2A: pv2A, pv2B: pv2B,302 pv1A: pv1A, pv1B: pv1B, pv2A: pv2A, pv2B: pv2B,
303 factory: factory,303 factory: factory,
@@ -306,9 +306,9 @@ func newPageModeTestFixture(t *testing.T) *pageModeTestFixture {
306 }306 }
307 }307 }
308 308
309-func newSingleWindowPageModeFixture(t *testing.T) *pageModeTestFixture {309+func newSingleWindowVimModeFixture(t *testing.T) *vimModeTestFixture {
310 t.Helper()310 t.Helper()
311- f := newPageModeTestFixture(t)311+ f := newVimModeTestFixture(t)
312 delete(f.app.browserWindows, "win-2")312 delete(f.app.browserWindows, "win-2")
313 delete(f.app.workspaceViews, entity.TabID("tab-2"))313 delete(f.app.workspaceViews, entity.TabID("tab-2"))
314 f.bw2 = nil314 f.bw2 = nil
@@ -320,127 +320,127 @@ func newSingleWindowPageModeFixture(t *testing.T) *pageModeTestFixture {
320 }320 }
321 321
322 // ============================================================================322 // ============================================================================
323-// 1. Entering Page mode marks only the active pane323+// 1. Entering Vim mode marks only the active pane
324 // ============================================================================324 // ============================================================================
325 325
326-func TestPageMode_Enter_SetsOwnershipOnActivePane(t *testing.T) {326+func TestVimMode_Enter_SetsOwnershipOnActivePane(t *testing.T) {
327- f := newPageModeTestFixture(t)327+ f := newVimModeTestFixture(t)
328 328
329- enterPageMode(t, f.app, f.overlay1A, f.bw1)329+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
330 330
331- assert.Equal(t, entity.PaneID("pane-a"), f.bw1.pageModePaneID,331+ assert.Equal(t, entity.PaneID("pane-a"), f.bw1.vimModePaneID,
332- "entering page mode sets ownership on the active pane")332+ "entering vim mode sets ownership on the active pane")
333- assert.True(t, f.pv1A.IsPageMode(),333+ assert.True(t, f.pv1A.IsVimMode(),
334- "the active pane view enters page mode")334+ "the active pane view enters vim mode")
335- assert.False(t, f.pv1B.IsPageMode(),335+ assert.False(t, f.pv1B.IsVimMode(),
336- "the inactive pane is NOT in page mode")336+ "the inactive pane is NOT in vim mode")
337- assert.Empty(t, f.bw2.pageModePaneID,337+ assert.Empty(t, f.bw2.vimModePaneID,
338 "second window is untouched")338 "second window is untouched")
339- assert.False(t, f.pv2A.IsPageMode(),339+ assert.False(t, f.pv2A.IsVimMode(),
340- "second window active pane is NOT in page mode")340+ "second window active pane is NOT in vim mode")
341 }341 }
342 342
343-func TestPageMode_Enter_MarksCorrectWindow(t *testing.T) {343+func TestVimMode_Enter_MarksCorrectWindow(t *testing.T) {
344- f := newPageModeTestFixture(t)344+ f := newVimModeTestFixture(t)
345 345
346- enterPageMode(t, f.app, f.overlay2A, f.bw2)346+ enterVimMode(t, f.app, f.overlay2A, f.bw2)
347 347
348- assert.Equal(t, entity.PaneID("pane-a"), f.bw2.pageModePaneID)348+ assert.Equal(t, entity.PaneID("pane-a"), f.bw2.vimModePaneID)
349- assert.True(t, f.pv2A.IsPageMode())349+ assert.True(t, f.pv2A.IsVimMode())
350- assert.Empty(t, f.bw1.pageModePaneID)350+ assert.Empty(t, f.bw1.vimModePaneID)
351- assert.False(t, f.pv1A.IsPageMode())351+ assert.False(t, f.pv1A.IsVimMode())
352 }352 }
353 353
354 // ============================================================================354 // ============================================================================
355-// 2. Leaving Page mode clears ownership355+// 2. Leaving Vim mode clears ownership
356 // ============================================================================356 // ============================================================================
357 357
358-func TestPageMode_Leave_ClearsOwnershipFromOwningPane(t *testing.T) {358+func TestVimMode_Leave_ClearsOwnershipFromOwningPane(t *testing.T) {
359- f := newPageModeTestFixture(t)359+ f := newVimModeTestFixture(t)
360 360
361 // Enter first361 // Enter first
362- enterPageMode(t, f.app, f.overlay1A, f.bw1)362+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
363- require.Equal(t, entity.PaneID("pane-a"), f.bw1.pageModePaneID)363+ require.Equal(t, entity.PaneID("pane-a"), f.bw1.vimModePaneID)
364 364
365- // Leave — SetPageMode(false) hides indicator and removes the CSS class365+ // Leave — SetVimMode(false) hides indicator and removes the CSS class
366- expectPageModeAccentHidden(f.overlay1A)366+ expectVimModeAccentHidden(f.overlay1A)
367- f.app.handlePageModeOwnership(context.Background(), f.bw1, input.ModeNormal, input.ModePage)367+ f.app.handleVimModeOwnership(context.Background(), f.bw1, input.ModeNormal, input.ModeVim)
368 368
369- assert.Empty(t, f.bw1.pageModePaneID,369+ assert.Empty(t, f.bw1.vimModePaneID,
370- "leaving page mode clears ownership field")370+ "leaving vim mode clears ownership field")
371- assert.False(t, f.pv1A.IsPageMode(),371+ assert.False(t, f.pv1A.IsVimMode(),
372- "the previously owning pane exits page mode")372+ "the previously owning pane exits vim mode")
373 }373 }
374 374
375 // ============================================================================375 // ============================================================================
376 // 3. Transfer within the same window376 // 3. Transfer within the same window
377 // ============================================================================377 // ============================================================================
378 378
379-func TestPageMode_Transfer_MovesOwnershipToNewPane(t *testing.T) {379+func TestVimMode_Transfer_MovesOwnershipToNewPane(t *testing.T) {
380- f := newPageModeTestFixture(t)380+ f := newVimModeTestFixture(t)
381 381
382- // Enter page mode on bw1 pane-a — save label for hide expectation382+ // Enter vim mode on bw1 pane-a — save label for hide expectation
383- enterPageMode(t, f.app, f.overlay1A, f.bw1)383+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
384- require.Equal(t, entity.PaneID("pane-a"), f.bw1.pageModePaneID)384+ require.Equal(t, entity.PaneID("pane-a"), f.bw1.vimModePaneID)
385 385
386- // Give bw1 a keyboard handler in page mode so transfer performs the switch386+ // Give bw1 a keyboard handler in vim mode so transfer performs the switch
387- f.bw1.keyboardHandler = newKeyboardHandlerInPageMode(t)387+ f.bw1.keyboardHandler = newKeyboardHandlerInVimMode(t)
388 388
389- // transferPageModeOwnershipToPane calls SetPageMode(false) on the old389+ // transferVimModeOwnershipToPane calls SetVimMode(false) on the old
390- // pane before SetPageMode(true) on the new pane. Set up both.390+ // pane before SetVimMode(true) on the new pane. Set up both.
391- expectPageModeAccentHidden(f.overlay1A)391+ expectVimModeAccentHidden(f.overlay1A)
392 392
393 // Set up the pane-local accent for pane-b.393 // Set up the pane-local accent for pane-b.
394- f.overlay1B.EXPECT().AddCssClass("page-mode-active").Once()394+ f.overlay1B.EXPECT().AddCssClass("vim-mode-active").Once()
395 395
396 // Transfer ownership to pane-b396 // Transfer ownership to pane-b
397- f.app.transferPageModeOwnershipToPane(context.Background(), f.bw1, entity.PaneID("pane-b"))397+ f.app.transferVimModeOwnershipToPane(context.Background(), f.bw1, entity.PaneID("pane-b"))
398 398
399- assert.Equal(t, entity.PaneID("pane-b"), f.bw1.pageModePaneID,399+ assert.Equal(t, entity.PaneID("pane-b"), f.bw1.vimModePaneID,
400 "ownership transferred to new pane")400 "ownership transferred to new pane")
401- assert.False(t, f.pv1A.IsPageMode(),401+ assert.False(t, f.pv1A.IsVimMode(),
402 "previous owning pane deactivated")402 "previous owning pane deactivated")
403- assert.True(t, f.pv1B.IsPageMode(),403+ assert.True(t, f.pv1B.IsVimMode(),
404- "new pane is now in page mode")404+ "new pane is now in vim mode")
405 }405 }
406 406
407-func TestPageMode_Transfer_NoPageModeClearsStale(t *testing.T) {407+func TestVimMode_Transfer_NoVimModeClearsStale(t *testing.T) {
408- f := newPageModeTestFixture(t)408+ f := newVimModeTestFixture(t)
409 409
410- f.bw1.pageModePaneID = entity.PaneID("pane-a")410+ f.bw1.vimModePaneID = entity.PaneID("pane-a")
411 f.bw1.keyboardHandler = input.NewKeyboardHandler(411 f.bw1.keyboardHandler = input.NewKeyboardHandler(
412 context.Background(),412 context.Background(),
413 &entity.WorkspaceConfig{},413 &entity.WorkspaceConfig{},
414 &entity.SessionConfig{},414 &entity.SessionConfig{},
415 )415 )
416- // No EnterPageMode — handler stays in ModeNormal.416+ // No EnterVimMode — handler stays in ModeNormal.
417 417
418- f.app.transferPageModeOwnershipToPane(context.Background(), f.bw1, entity.PaneID("pane-b"))418+ f.app.transferVimModeOwnershipToPane(context.Background(), f.bw1, entity.PaneID("pane-b"))
419 419
420- assert.Empty(t, f.bw1.pageModePaneID,420+ assert.Empty(t, f.bw1.vimModePaneID,
421- "stale ownership cleared when window is not in page mode")421+ "stale ownership cleared when window is not in vim mode")
422- assert.False(t, f.pv1A.IsPageMode())422+ assert.False(t, f.pv1A.IsVimMode())
423- assert.False(t, f.pv1B.IsPageMode())423+ assert.False(t, f.pv1B.IsVimMode())
424 }424 }
425 425
426-func TestPageMode_Transfer_NilBwDoesNotCrash(t *testing.T) {426+func TestVimMode_Transfer_NilBwDoesNotCrash(t *testing.T) {
427 app := &App{}427 app := &App{}
428- app.transferPageModeOwnershipToPane(context.Background(), nil, entity.PaneID("pane-1"))428+ app.transferVimModeOwnershipToPane(context.Background(), nil, entity.PaneID("pane-1"))
429 }429 }
430 430
431-func TestPageMode_Transfer_SamePaneIsNoop(t *testing.T) {431+func TestVimMode_Transfer_SamePaneIsNoop(t *testing.T) {
432 app := &App{}432 app := &App{}
433 paneID := entity.PaneID("pane-a")433 paneID := entity.PaneID("pane-a")
434- bw := &browserWindow{pageModePaneID: paneID}434+ bw := &browserWindow{vimModePaneID: paneID}
435- app.transferPageModeOwnershipToPane(context.Background(), bw, paneID)435+ app.transferVimModeOwnershipToPane(context.Background(), bw, paneID)
436- assert.Equal(t, paneID, bw.pageModePaneID)436+ assert.Equal(t, paneID, bw.vimModePaneID)
437 }437 }
438 438
439-func TestPageMode_Transfer_EmptyOwnershipIsNoop(t *testing.T) {439+func TestVimMode_Transfer_EmptyOwnershipIsNoop(t *testing.T) {
440 app := &App{}440 app := &App{}
441 bw := &browserWindow{}441 bw := &browserWindow{}
442- app.transferPageModeOwnershipToPane(context.Background(), bw, entity.PaneID("pane-a"))442+ app.transferVimModeOwnershipToPane(context.Background(), bw, entity.PaneID("pane-a"))
443- assert.Empty(t, bw.pageModePaneID)443+ assert.Empty(t, bw.vimModePaneID)
444 }444 }
445 445
446 // ============================================================================446 // ============================================================================
@@ -448,52 +448,52 @@ func TestPageMode_Transfer_EmptyOwnershipIsNoop(t *testing.T) {
448 // ============================================================================448 // ============================================================================
449 449
450 func setUpNormalPulse(overlay *mocks.MockOverlayWidget) {450 func setUpNormalPulse(overlay *mocks.MockOverlayWidget) {
451- overlay.EXPECT().RemoveCssClass("page-mode-pulse").Once()451+ overlay.EXPECT().RemoveCssClass("vim-mode-pulse").Once()
452- overlay.EXPECT().RemoveCssClass("page-mode-pulse-fast").Once()452+ overlay.EXPECT().RemoveCssClass("vim-mode-pulse-fast").Once()
453- overlay.EXPECT().RemoveCssClass("page-mode-pulse-cycle-a").Once()453+ overlay.EXPECT().RemoveCssClass("vim-mode-pulse-cycle-a").Once()
454- overlay.EXPECT().RemoveCssClass("page-mode-pulse-cycle-b").Once()454+ overlay.EXPECT().RemoveCssClass("vim-mode-pulse-cycle-b").Once()
455- overlay.EXPECT().AddCssClass("page-mode-pulse").Once()455+ overlay.EXPECT().AddCssClass("vim-mode-pulse").Once()
456- overlay.EXPECT().AddCssClass("page-mode-pulse-cycle-a").Once()456+ overlay.EXPECT().AddCssClass("vim-mode-pulse-cycle-a").Once()
457 }457 }
458 458
459 func setUpFastPulse(overlay *mocks.MockOverlayWidget) {459 func setUpFastPulse(overlay *mocks.MockOverlayWidget) {
460- overlay.EXPECT().RemoveCssClass("page-mode-pulse").Once()460+ overlay.EXPECT().RemoveCssClass("vim-mode-pulse").Once()
461- overlay.EXPECT().RemoveCssClass("page-mode-pulse-fast").Once()461+ overlay.EXPECT().RemoveCssClass("vim-mode-pulse-fast").Once()
462- overlay.EXPECT().RemoveCssClass("page-mode-pulse-cycle-a").Once()462+ overlay.EXPECT().RemoveCssClass("vim-mode-pulse-cycle-a").Once()
463- overlay.EXPECT().RemoveCssClass("page-mode-pulse-cycle-b").Once()463+ overlay.EXPECT().RemoveCssClass("vim-mode-pulse-cycle-b").Once()
464- overlay.EXPECT().AddCssClass("page-mode-pulse-fast").Once()464+ overlay.EXPECT().AddCssClass("vim-mode-pulse-fast").Once()
465- overlay.EXPECT().AddCssClass("page-mode-pulse-cycle-a").Once()465+ overlay.EXPECT().AddCssClass("vim-mode-pulse-cycle-a").Once()
466 }466 }
467 467
468-func TestPageMode_Pulse_NormalTriggersOnOwningPane(t *testing.T) {468+func TestVimMode_Pulse_NormalTriggersOnOwningPane(t *testing.T) {
469- f := newPageModeTestFixture(t)469+ f := newVimModeTestFixture(t)
470 470
471- enterPageMode(t, f.app, f.overlay1A, f.bw1)471+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
472- require.Equal(t, entity.PaneID("pane-a"), f.bw1.pageModePaneID)472+ require.Equal(t, entity.PaneID("pane-a"), f.bw1.vimModePaneID)
473 473
474 // Pulse needs the indicator to exist — it was created lazily during474 // Pulse needs the indicator to exist — it was created lazily during
475- // enterPageMode, but we also need to set up pulse expectations.475+ // enterVimMode, but we also need to set up pulse expectations.
476- // (enterPageMode created it, so we just set up pulse mocks.)476+ // (enterVimMode created it, so we just set up pulse mocks.)
477 setUpNormalPulse(f.overlay1A)477 setUpNormalPulse(f.overlay1A)
478 478
479- f.app.triggerPageModePulse(context.Background(), false)479+ f.app.triggerVimModePulse(context.Background(), false)
480 }480 }
481 481
482-func TestPageMode_Pulse_FastTriggersOnOwningPane(t *testing.T) {482+func TestVimMode_Pulse_FastTriggersOnOwningPane(t *testing.T) {
483- f := newPageModeTestFixture(t)483+ f := newVimModeTestFixture(t)
484 484
485- enterPageMode(t, f.app, f.overlay1A, f.bw1)485+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
486- require.Equal(t, entity.PaneID("pane-a"), f.bw1.pageModePaneID)486+ require.Equal(t, entity.PaneID("pane-a"), f.bw1.vimModePaneID)
487 487
488 setUpFastPulse(f.overlay1A)488 setUpFastPulse(f.overlay1A)
489 489
490- f.app.triggerPageModePulse(context.Background(), true)490+ f.app.triggerVimModePulse(context.Background(), true)
491 }491 }
492 492
493-func TestPageMode_Pulse_NoOwnerIsNoop(t *testing.T) {493+func TestVimMode_Pulse_NoOwnerIsNoop(t *testing.T) {
494- f := newPageModeTestFixture(t)494+ f := newVimModeTestFixture(t)
495- f.app.triggerPageModePulse(context.Background(), false)495+ f.app.triggerVimModePulse(context.Background(), false)
496- f.app.triggerPageModePulse(context.Background(), true)496+ f.app.triggerVimModePulse(context.Background(), true)
497 // No pane has ownership — no pulse expectations needed.497 // No pane has ownership — no pulse expectations needed.
498 }498 }
499 499
@@ -501,141 +501,141 @@ func TestPageMode_Pulse_NoOwnerIsNoop(t *testing.T) {
501 // 5. Multi-window isolation501 // 5. Multi-window isolation
502 // ============================================================================502 // ============================================================================
503 503
504-func TestPageMode_MultiWindow_TwoWindowsEnterLeave(t *testing.T) {504+func TestVimMode_MultiWindow_TwoWindowsEnterLeave(t *testing.T) {
505- f := newPageModeTestFixture(t)505+ f := newVimModeTestFixture(t)
506 506
507- enterPageMode(t, f.app, f.overlay1A, f.bw1)507+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
508- enterPageMode(t, f.app, f.overlay2A, f.bw2)508+ enterVimMode(t, f.app, f.overlay2A, f.bw2)
509 509
510- assert.Equal(t, entity.PaneID("pane-a"), f.bw1.pageModePaneID)510+ assert.Equal(t, entity.PaneID("pane-a"), f.bw1.vimModePaneID)
511- assert.Equal(t, entity.PaneID("pane-a"), f.bw2.pageModePaneID)511+ assert.Equal(t, entity.PaneID("pane-a"), f.bw2.vimModePaneID)
512 512
513 // Leave bw1 only513 // Leave bw1 only
514- expectPageModeAccentHidden(f.overlay1A)514+ expectVimModeAccentHidden(f.overlay1A)
515- f.app.handlePageModeOwnership(context.Background(), f.bw1, input.ModeNormal, input.ModePage)515+ f.app.handleVimModeOwnership(context.Background(), f.bw1, input.ModeNormal, input.ModeVim)
516 516
517- assert.Empty(t, f.bw1.pageModePaneID)517+ assert.Empty(t, f.bw1.vimModePaneID)
518- assert.False(t, f.pv1A.IsPageMode())518+ assert.False(t, f.pv1A.IsVimMode())
519 519
520 // bw2 unchanged520 // bw2 unchanged
521- assert.Equal(t, entity.PaneID("pane-a"), f.bw2.pageModePaneID)521+ assert.Equal(t, entity.PaneID("pane-a"), f.bw2.vimModePaneID)
522- assert.True(t, f.pv2A.IsPageMode())522+ assert.True(t, f.pv2A.IsVimMode())
523 }523 }
524 524
525-func TestPageMode_MultiWindow_TransferIsolated(t *testing.T) {525+func TestVimMode_MultiWindow_TransferIsolated(t *testing.T) {
526- f := newPageModeTestFixture(t)526+ f := newVimModeTestFixture(t)
527 527
528- enterPageMode(t, f.app, f.overlay1A, f.bw1)528+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
529- enterPageMode(t, f.app, f.overlay2A, f.bw2)529+ enterVimMode(t, f.app, f.overlay2A, f.bw2)
530 530
531- f.bw1.keyboardHandler = newKeyboardHandlerInPageMode(t)531+ f.bw1.keyboardHandler = newKeyboardHandlerInVimMode(t)
532 532
533- // transferPageModeOwnershipToPane calls SetPageMode(false) on old pane first533+ // transferVimModeOwnershipToPane calls SetVimMode(false) on old pane first
534- expectPageModeAccentHidden(f.overlay1A)534+ expectVimModeAccentHidden(f.overlay1A)
535 535
536 // Transfer bw1 to pane-b.536 // Transfer bw1 to pane-b.
537- f.overlay1B.EXPECT().AddCssClass("page-mode-active").Once()537+ f.overlay1B.EXPECT().AddCssClass("vim-mode-active").Once()
538- f.app.transferPageModeOwnershipToPane(context.Background(), f.bw1, entity.PaneID("pane-b"))538+ f.app.transferVimModeOwnershipToPane(context.Background(), f.bw1, entity.PaneID("pane-b"))
539 539
540- assert.Equal(t, entity.PaneID("pane-b"), f.bw1.pageModePaneID)540+ assert.Equal(t, entity.PaneID("pane-b"), f.bw1.vimModePaneID)
541- assert.True(t, f.pv1B.IsPageMode())541+ assert.True(t, f.pv1B.IsVimMode())
542- assert.False(t, f.pv1A.IsPageMode())542+ assert.False(t, f.pv1A.IsVimMode())
543 543
544 // bw2 unchanged544 // bw2 unchanged
545- assert.Equal(t, entity.PaneID("pane-a"), f.bw2.pageModePaneID)545+ assert.Equal(t, entity.PaneID("pane-a"), f.bw2.vimModePaneID)
546- assert.True(t, f.pv2A.IsPageMode())546+ assert.True(t, f.pv2A.IsVimMode())
547 }547 }
548 548
549-func TestPageMode_MultiWindow_PulseIsolated(t *testing.T) {549+func TestVimMode_MultiWindow_PulseIsolated(t *testing.T) {
550- f := newPageModeTestFixture(t)550+ f := newVimModeTestFixture(t)
551 551
552- enterPageMode(t, f.app, f.overlay1A, f.bw1)552+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
553- enterPageMode(t, f.app, f.overlay2A, f.bw2)553+ enterVimMode(t, f.app, f.overlay2A, f.bw2)
554 554
555 // Pulse on bw1 (lastFocusedWindowID = "win-1")555 // Pulse on bw1 (lastFocusedWindowID = "win-1")
556 setUpNormalPulse(f.overlay1A)556 setUpNormalPulse(f.overlay1A)
557- f.app.triggerPageModePulse(context.Background(), false)557+ f.app.triggerVimModePulse(context.Background(), false)
558 558
559- // bw2 pane still in page mode559+ // bw2 pane still in vim mode
560- assert.True(t, f.pv2A.IsPageMode())560+ assert.True(t, f.pv2A.IsVimMode())
561 }561 }
562 562
563 // ============================================================================563 // ============================================================================
564 // Edge cases — nil / no-workspace / startup guards564 // Edge cases — nil / no-workspace / startup guards
565 // ============================================================================565 // ============================================================================
566 566
567-func TestPageMode_Enter_NoWorkspaceIsNoop(t *testing.T) {567+func TestVimMode_Enter_NoWorkspaceIsNoop(t *testing.T) {
568 app := &App{}568 app := &App{}
569 bw := &browserWindow{id: "win-1"}569 bw := &browserWindow{id: "win-1"}
570- app.handlePageModeOwnership(context.Background(), bw, input.ModePage, input.ModeNormal)570+ app.handleVimModeOwnership(context.Background(), bw, input.ModeVim, input.ModeNormal)
571- assert.Empty(t, bw.pageModePaneID)571+ assert.Empty(t, bw.vimModePaneID)
572 }572 }
573 573
574-func TestPageMode_Clear_NilBwDoesNotCrash(t *testing.T) {574+func TestVimMode_Clear_NilBwDoesNotCrash(t *testing.T) {
575- (&App{}).clearPageModeOwnership(context.Background(), nil)575+ (&App{}).clearVimModeOwnership(context.Background(), nil)
576 }576 }
577 577
578-func TestPageMode_Clear_NoPaneID(t *testing.T) {578+func TestVimMode_Clear_NoPaneID(t *testing.T) {
579 app := &App{}579 app := &App{}
580 bw := &browserWindow{id: "test-win"}580 bw := &browserWindow{id: "test-win"}
581- app.clearPageModeOwnership(context.Background(), bw)581+ app.clearVimModeOwnership(context.Background(), bw)
582- assert.Empty(t, bw.pageModePaneID)582+ assert.Empty(t, bw.vimModePaneID)
583 }583 }
584 584
585-func TestPageMode_Clear_StalePaneWithoutWorkspace(t *testing.T) {585+func TestVimMode_Clear_StalePaneWithoutWorkspace(t *testing.T) {
586 app := &App{}586 app := &App{}
587- bw := &browserWindow{id: "test-win", pageModePaneID: entity.PaneID("stale-pane")}587+ bw := &browserWindow{id: "test-win", vimModePaneID: entity.PaneID("stale-pane")}
588- app.clearPageModeOwnership(context.Background(), bw)588+ app.clearVimModeOwnership(context.Background(), bw)
589- assert.Empty(t, bw.pageModePaneID)589+ assert.Empty(t, bw.vimModePaneID)
590 }590 }
591 591
592-func TestPageMode_HandleModeChange_NilBwDoesNotCrash(t *testing.T) {592+func TestVimMode_HandleModeChange_NilBwDoesNotCrash(t *testing.T) {
593- (&App{}).handleModeChange(context.Background(), nil, input.ModeNormal, input.ModePage)593+ (&App{}).handleModeChange(context.Background(), nil, input.ModeNormal, input.ModeVim)
594 }594 }
595 595
596-func TestPageMode_EditableFocusOnActivePaneExitsPageMode(t *testing.T) {596+func TestVimMode_EditableFocusOnActivePaneExitsVimMode(t *testing.T) {
597- f := newSingleWindowPageModeFixture(t)597+ f := newSingleWindowVimModeFixture(t)
598- enterPageMode(t, f.app, f.overlay1A, f.bw1)598+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
599- kh := bindPageModeKeyboardHandler(t, f.app, f.bw1)599+ kh := bindVimModeKeyboardHandler(t, f.app, f.bw1)
600 600
601- expectPageModeAccentHidden(f.overlay1A)601+ expectVimModeAccentHidden(f.overlay1A)
602 f.app.handlePageEditableFocusChanged(context.Background(), entity.PaneID("pane-a"), true)602 f.app.handlePageEditableFocusChanged(context.Background(), entity.PaneID("pane-a"), true)
603 603
604 assert.Equal(t, input.ModeNormal, kh.Mode())604 assert.Equal(t, input.ModeNormal, kh.Mode())
605- assert.Empty(t, f.bw1.pageModePaneID)605+ assert.Empty(t, f.bw1.vimModePaneID)
606- assert.False(t, f.pv1A.IsPageMode())606+ assert.False(t, f.pv1A.IsVimMode())
607 }607 }
608 608
609-func TestPageMode_EditableFocusOnInactivePaneDoesNotExitPageMode(t *testing.T) {609+func TestVimMode_EditableFocusOnInactivePaneDoesNotExitVimMode(t *testing.T) {
610- f := newSingleWindowPageModeFixture(t)610+ f := newSingleWindowVimModeFixture(t)
611- enterPageMode(t, f.app, f.overlay1A, f.bw1)611+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
612- kh := bindPageModeKeyboardHandler(t, f.app, f.bw1)612+ kh := bindVimModeKeyboardHandler(t, f.app, f.bw1)
613 613
614 f.app.handlePageEditableFocusChanged(context.Background(), entity.PaneID("pane-b"), true)614 f.app.handlePageEditableFocusChanged(context.Background(), entity.PaneID("pane-b"), true)
615 615
616- assert.Equal(t, input.ModePage, kh.Mode())616+ assert.Equal(t, input.ModeVim, kh.Mode())
617- assert.Equal(t, entity.PaneID("pane-a"), f.bw1.pageModePaneID)617+ assert.Equal(t, entity.PaneID("pane-a"), f.bw1.vimModePaneID)
618- assert.True(t, f.pv1A.IsPageMode())618+ assert.True(t, f.pv1A.IsVimMode())
619 }619 }
620 620
621-func TestPageMode_PaneSwitchToEditablePaneExitsPageMode(t *testing.T) {621+func TestVimMode_PaneSwitchToEditablePaneExitsVimMode(t *testing.T) {
622- f := newSingleWindowPageModeFixture(t)622+ f := newSingleWindowVimModeFixture(t)
623- enterPageMode(t, f.app, f.overlay1A, f.bw1)623+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
624- kh := bindPageModeKeyboardHandler(t, f.app, f.bw1)624+ kh := bindVimModeKeyboardHandler(t, f.app, f.bw1)
625 f.app.pageEditableFocusByPane = map[entity.PaneID]bool{625 f.app.pageEditableFocusByPane = map[entity.PaneID]bool{
626 "pane-b": true,626 "pane-b": true,
627 }627 }
628 628
629- expectPageModeAccentHidden(f.overlay1A)629+ expectVimModeAccentHidden(f.overlay1A)
630- f.app.transferPageModeOwnershipToPane(context.Background(), f.bw1, entity.PaneID("pane-b"))630+ f.app.transferVimModeOwnershipToPane(context.Background(), f.bw1, entity.PaneID("pane-b"))
631 631
632 assert.Equal(t, input.ModeNormal, kh.Mode())632 assert.Equal(t, input.ModeNormal, kh.Mode())
633- assert.Empty(t, f.bw1.pageModePaneID)633+ assert.Empty(t, f.bw1.vimModePaneID)
634- assert.False(t, f.pv1A.IsPageMode())634+ assert.False(t, f.pv1A.IsVimMode())
635- assert.False(t, f.pv1B.IsPageMode())635+ assert.False(t, f.pv1B.IsVimMode())
636 }636 }
637 637
638-func TestPageMode_BackgroundWindowEditableFocusDoesNotExitFocusedWindowPageMode(t *testing.T) {638+func TestVimMode_BackgroundWindowEditableFocusDoesNotExitFocusedWindowVimMode(t *testing.T) {
639 factory := mocks.NewMockWidgetFactory(t)639 factory := mocks.NewMockWidgetFactory(t)
640 pv1, overlay1 := createTestPaneView(t, factory, "pane-a")640 pv1, overlay1 := createTestPaneView(t, factory, "pane-a")
641 pv2, _ := createTestPaneView(t, factory, "pane-x")641 pv2, _ := createTestPaneView(t, factory, "pane-x")
@@ -661,44 +661,44 @@ func TestPageMode_BackgroundWindowEditableFocusDoesNotExitFocusedWindowPageMode(
661 lastFocusedWindowID: "win-1",661 lastFocusedWindowID: "win-1",
662 }662 }
663 663
664- enterPageMode(t, app, overlay1, bw1)664+ enterVimMode(t, app, overlay1, bw1)
665- kh := bindPageModeKeyboardHandler(t, app, bw1)665+ kh := bindVimModeKeyboardHandler(t, app, bw1)
666 666
667 app.handlePageEditableFocusChanged(context.Background(), entity.PaneID("pane-x"), true)667 app.handlePageEditableFocusChanged(context.Background(), entity.PaneID("pane-x"), true)
668 668
669- assert.Equal(t, input.ModePage, kh.Mode())669+ assert.Equal(t, input.ModeVim, kh.Mode())
670- assert.Equal(t, entity.PaneID("pane-a"), bw1.pageModePaneID)670+ assert.Equal(t, entity.PaneID("pane-a"), bw1.vimModePaneID)
671- assert.True(t, pv1.IsPageMode())671+ assert.True(t, pv1.IsVimMode())
672 }672 }
673 673
674-func TestPageMode_OmniboxFocusExitsPageMode(t *testing.T) {674+func TestVimMode_OmniboxFocusExitsVimMode(t *testing.T) {
675- f := newSingleWindowPageModeFixture(t)675+ f := newSingleWindowVimModeFixture(t)
676- enterPageMode(t, f.app, f.overlay1A, f.bw1)676+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
677- kh := bindPageModeKeyboardHandler(t, f.app, f.bw1)677+ kh := bindVimModeKeyboardHandler(t, f.app, f.bw1)
678 678
679- expectPageModeAccentHidden(f.overlay1A)679+ expectVimModeAccentHidden(f.overlay1A)
680- f.app.handlePageModeFocusTrigger(context.Background(), f.bw1, usecase.PageModePolicyTriggerOmniboxFocus)680+ f.app.handleVimModeFocusTrigger(context.Background(), f.bw1, usecase.VimModePolicyTriggerOmniboxFocus)
681 681
682 assert.Equal(t, input.ModeNormal, kh.Mode())682 assert.Equal(t, input.ModeNormal, kh.Mode())
683- assert.Empty(t, f.bw1.pageModePaneID)683+ assert.Empty(t, f.bw1.vimModePaneID)
684 }684 }
685 685
686-func TestPageMode_FindBarFocusExitsPageMode(t *testing.T) {686+func TestVimMode_FindBarFocusExitsVimMode(t *testing.T) {
687- f := newSingleWindowPageModeFixture(t)687+ f := newSingleWindowVimModeFixture(t)
688- enterPageMode(t, f.app, f.overlay1A, f.bw1)688+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
689- kh := bindPageModeKeyboardHandler(t, f.app, f.bw1)689+ kh := bindVimModeKeyboardHandler(t, f.app, f.bw1)
690 690
691- expectPageModeAccentHidden(f.overlay1A)691+ expectVimModeAccentHidden(f.overlay1A)
692- f.app.handlePageModeFocusTrigger(context.Background(), f.bw1, usecase.PageModePolicyTriggerFindBarFocus)692+ f.app.handleVimModeFocusTrigger(context.Background(), f.bw1, usecase.VimModePolicyTriggerFindBarFocus)
693 693
694 assert.Equal(t, input.ModeNormal, kh.Mode())694 assert.Equal(t, input.ModeNormal, kh.Mode())
695- assert.Empty(t, f.bw1.pageModePaneID)695+ assert.Empty(t, f.bw1.vimModePaneID)
696 }696 }
697 697
698-func TestPageMode_TabSwitchExitsAndClearsOldAccent(t *testing.T) {698+func TestVimMode_TabSwitchExitsAndClearsOldAccent(t *testing.T) {
699- f := newSingleWindowPageModeFixture(t)699+ f := newSingleWindowVimModeFixture(t)
700- enterPageMode(t, f.app, f.overlay1A, f.bw1)700+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
701- kh := bindPageModeKeyboardHandler(t, f.app, f.bw1)701+ kh := bindVimModeKeyboardHandler(t, f.app, f.bw1)
702 702
703 tab2 := &entity.Tab{ID: "tab-2b", Workspace: &entity.Workspace{703 tab2 := &entity.Tab{ID: "tab-2b", Workspace: &entity.Workspace{
704 ID: "ws-2b",704 ID: "ws-2b",
@@ -709,57 +709,57 @@ func TestPageMode_TabSwitchExitsAndClearsOldAccent(t *testing.T) {
709 f.bw1.tabs.SetActive(tab2.ID)709 f.bw1.tabs.SetActive(tab2.ID)
710 f.app.workspaceViews[tab2.ID] = setupWorkspaceViewMocks(t, f.factory)710 f.app.workspaceViews[tab2.ID] = setupWorkspaceViewMocks(t, f.factory)
711 711
712- expectPageModeAccentHidden(f.overlay1A)712+ expectVimModeAccentHidden(f.overlay1A)
713- f.app.handlePageModeTabSwitch(context.Background(), f.bw1)713+ f.app.handleVimModeTabSwitch(context.Background(), f.bw1)
714 714
715 assert.Equal(t, input.ModeNormal, kh.Mode())715 assert.Equal(t, input.ModeNormal, kh.Mode())
716- assert.Empty(t, f.bw1.pageModePaneID)716+ assert.Empty(t, f.bw1.vimModePaneID)
717- assert.False(t, f.pv1A.IsPageMode())717+ assert.False(t, f.pv1A.IsVimMode())
718 }718 }
719 719
720-func TestPageMode_ActivationBypassWhenActivePageIsEditable(t *testing.T) {720+func TestVimMode_ActivationBypassWhenActivePageIsEditable(t *testing.T) {
721- f := newSingleWindowPageModeFixture(t)721+ f := newSingleWindowVimModeFixture(t)
722 f.app.pageEditableFocusByPane = map[entity.PaneID]bool{722 f.app.pageEditableFocusByPane = map[entity.PaneID]bool{
723 "pane-a": true,723 "pane-a": true,
724 }724 }
725 725
726- assert.True(t, f.app.shouldBypassPageModeActivation(f.bw1))726+ assert.True(t, f.app.shouldBypassVimModeActivation(f.bw1))
727 }727 }
728 728
729-func TestPageMode_ActivationBypassClearsWhenEditableFocusLeaves(t *testing.T) {729+func TestVimMode_ActivationBypassClearsWhenEditableFocusLeaves(t *testing.T) {
730- f := newSingleWindowPageModeFixture(t)730+ f := newSingleWindowVimModeFixture(t)
731 731
732 f.app.handlePageEditableFocusChanged(context.Background(), entity.PaneID("pane-a"), true)732 f.app.handlePageEditableFocusChanged(context.Background(), entity.PaneID("pane-a"), true)
733- assert.True(t, f.app.shouldBypassPageModeActivation(f.bw1))733+ assert.True(t, f.app.shouldBypassVimModeActivation(f.bw1))
734 734
735 f.app.handlePageEditableFocusChanged(context.Background(), entity.PaneID("pane-a"), false)735 f.app.handlePageEditableFocusChanged(context.Background(), entity.PaneID("pane-a"), false)
736- assert.False(t, f.app.shouldBypassPageModeActivation(f.bw1))736+ assert.False(t, f.app.shouldBypassVimModeActivation(f.bw1))
737 }737 }
738 738
739-func TestPageMode_ClearEditableFocusStateRemovesStoredBypass(t *testing.T) {739+func TestVimMode_ClearEditableFocusStateRemovesStoredBypass(t *testing.T) {
740- f := newSingleWindowPageModeFixture(t)740+ f := newSingleWindowVimModeFixture(t)
741 f.app.pageEditableFocusByPane = map[entity.PaneID]bool{"pane-a": true}741 f.app.pageEditableFocusByPane = map[entity.PaneID]bool{"pane-a": true}
742 742
743 f.app.clearPageEditableFocusState(entity.PaneID("pane-a"))743 f.app.clearPageEditableFocusState(entity.PaneID("pane-a"))
744 744
745- assert.False(t, f.app.shouldBypassPageModeActivation(f.bw1))745+ assert.False(t, f.app.shouldBypassVimModeActivation(f.bw1))
746 }746 }
747 747
748 // ============================================================================748 // ============================================================================
749 // CSS class constants749 // CSS class constants
750 // ============================================================================750 // ============================================================================
751 751
752-func TestPageMode_CSSClassConstants(t *testing.T) {752+func TestVimMode_CSSClassConstants(t *testing.T) {
753- assert.Equal(t, "page-mode-active", component.PageModeActiveClass)753+ assert.Equal(t, "vim-mode-active", component.VimModeActiveClass)
754- assert.Equal(t, "page-mode-pulse", component.PageModePulseClass)754+ assert.Equal(t, "vim-mode-pulse", component.VimModePulseClass)
755- assert.Equal(t, "page-mode-pulse-fast", component.PageModeFastPulseClass)755+ assert.Equal(t, "vim-mode-pulse-fast", component.VimModeFastPulseClass)
756 }756 }
757 757
758 // ============================================================================758 // ============================================================================
759 // Mode toaster759 // Mode toaster
760 // ============================================================================760 // ============================================================================
761 761
762-func newModeToasterForPageModeTest(t *testing.T, factory *mocks.MockWidgetFactory) (*component.Toaster, *mocks.MockBoxWidget, *mocks.MockLabelWidget) {762+func newModeToasterForVimModeTest(t *testing.T, factory *mocks.MockWidgetFactory) (*component.Toaster, *mocks.MockBoxWidget, *mocks.MockLabelWidget) {
763 t.Helper()763 t.Helper()
764 764
765 container := mocks.NewMockBoxWidget(t)765 container := mocks.NewMockBoxWidget(t)
@@ -782,21 +782,21 @@ func newModeToasterForPageModeTest(t *testing.T, factory *mocks.MockWidgetFactor
782 return component.NewToaster(factory), container, label782 return component.NewToaster(factory), container, label
783 }783 }
784 784
785-func expectPageModeToastShown(container *mocks.MockBoxWidget, label *mocks.MockLabelWidget) {785+func expectVimModeToastShown(container *mocks.MockBoxWidget, label *mocks.MockLabelWidget) {
786 container.EXPECT().RemoveCssClass("toast-info").Once()786 container.EXPECT().RemoveCssClass("toast-info").Once()
787- container.EXPECT().AddCssClass("toast-pane-mode").Once()787+ container.EXPECT().AddCssClass("toast-vim-mode").Once()
788 container.EXPECT().SetHalign(mock.Anything).Once()788 container.EXPECT().SetHalign(mock.Anything).Once()
789 container.EXPECT().SetValign(mock.Anything).Once()789 container.EXPECT().SetValign(mock.Anything).Once()
790- label.EXPECT().SetText("PAGE MODE").Once()790+ label.EXPECT().SetText("VIM MODE").Once()
791 container.EXPECT().SetVisible(true).Once()791 container.EXPECT().SetVisible(true).Once()
792 }792 }
793 793
794-func TestPageMode_ToasterRemainsVisibleUntilModeExit(t *testing.T) {794+func TestVimMode_ToasterRemainsVisibleUntilModeExit(t *testing.T) {
795 factory := mocks.NewMockWidgetFactory(t)795 factory := mocks.NewMockWidgetFactory(t)
796- toaster, container, label := newModeToasterForPageModeTest(t, factory)796+ toaster, container, label := newModeToasterForVimModeTest(t, factory)
797- expectPageModeToastShown(container, label)797+ expectVimModeToastShown(container, label)
798 // These optional calls are made only by the old brief auto-dismiss path.798 // These optional calls are made only by the old brief auto-dismiss path.
799- container.EXPECT().RemoveCssClass("toast-pane-mode").Maybe()799+ container.EXPECT().RemoveCssClass("toast-vim-mode").Maybe()
800 container.EXPECT().SetVisible(false).Maybe()800 container.EXPECT().SetVisible(false).Maybe()
801 801
802 app := &App{runtimeConfig: runtimeConfigStateFromSnapshotForTest(entity.RuntimeConfigSnapshot{802 app := &App{runtimeConfig: runtimeConfigStateFromSnapshotForTest(entity.RuntimeConfigSnapshot{
@@ -808,7 +808,7 @@ func TestPageMode_ToasterRemainsVisibleUntilModeExit(t *testing.T) {
808 app.browserWindows = map[string]*browserWindow{bw.id: bw}808 app.browserWindows = map[string]*browserWindow{bw.id: bw}
809 app.lastFocusedWindowID = bw.id809 app.lastFocusedWindowID = bw.id
810 810
811- app.updateModeIndicatorToaster(context.Background(), bw, input.ModePage)811+ app.updateModeIndicatorToaster(context.Background(), bw, input.ModeVim)
812 require.True(t, toaster.IsVisible())812 require.True(t, toaster.IsVisible())
813 813
814 deadline := time.Now().Add(time.Duration(component.ToastBriefDurationMs+100) * time.Millisecond)814 deadline := time.Now().Add(time.Duration(component.ToastBriefDurationMs+100) * time.Millisecond)
@@ -820,15 +820,15 @@ func TestPageMode_ToasterRemainsVisibleUntilModeExit(t *testing.T) {
820 }820 }
821 }821 }
822 822
823- assert.True(t, toaster.IsVisible(), "Page Mode toast must remain visible until mode exit")823+ assert.True(t, toaster.IsVisible(), "Vim Mode toast must remain visible until mode exit")
824 }824 }
825 825
826-func TestPageMode_ModeExitHidesToaster(t *testing.T) {826+func TestVimMode_ModeExitHidesToaster(t *testing.T) {
827 factory := mocks.NewMockWidgetFactory(t)827 factory := mocks.NewMockWidgetFactory(t)
828- toaster, container, label := newModeToasterForPageModeTest(t, factory)828+ toaster, container, label := newModeToasterForVimModeTest(t, factory)
829- expectPageModeToastShown(container, label)829+ expectVimModeToastShown(container, label)
830 container.EXPECT().RemoveCssClass("toast-custom").Once()830 container.EXPECT().RemoveCssClass("toast-custom").Once()
831- container.EXPECT().RemoveCssClass("toast-pane-mode").Once()831+ container.EXPECT().RemoveCssClass("toast-vim-mode").Once()
832 container.EXPECT().SetVisible(false).Once()832 container.EXPECT().SetVisible(false).Once()
833 833
834 app := &App{runtimeConfig: runtimeConfigStateFromSnapshotForTest(entity.RuntimeConfigSnapshot{834 app := &App{runtimeConfig: runtimeConfigStateFromSnapshotForTest(entity.RuntimeConfigSnapshot{
@@ -838,32 +838,32 @@ func TestPageMode_ModeExitHidesToaster(t *testing.T) {
838 })}838 })}
839 bw := &browserWindow{id: "win-1", modeToaster: toaster}839 bw := &browserWindow{id: "win-1", modeToaster: toaster}
840 840
841- app.updateModeIndicatorToaster(context.Background(), bw, input.ModePage)841+ app.updateModeIndicatorToaster(context.Background(), bw, input.ModeVim)
842 require.True(t, toaster.IsVisible())842 require.True(t, toaster.IsVisible())
843 app.updateModeIndicatorToaster(context.Background(), bw, input.ModeNormal)843 app.updateModeIndicatorToaster(context.Background(), bw, input.ModeNormal)
844 844
845- assert.False(t, toaster.IsVisible(), "leaving Page Mode must hide its persistent toast")845+ assert.False(t, toaster.IsVisible(), "leaving Vim Mode must hide its persistent toast")
846 }846 }
847 847
848-func TestPageMode_DisabledModeIndicatorPreferenceSuppressesToaster(t *testing.T) {848+func TestVimMode_DisabledModeIndicatorPreferenceSuppressesToaster(t *testing.T) {
849 factory := mocks.NewMockWidgetFactory(t)849 factory := mocks.NewMockWidgetFactory(t)
850- toaster, _, _ := newModeToasterForPageModeTest(t, factory)850+ toaster, _, _ := newModeToasterForVimModeTest(t, factory)
851 app := &App{runtimeConfig: runtimeConfigStateFromSnapshotForTest(entity.RuntimeConfigSnapshot{})}851 app := &App{runtimeConfig: runtimeConfigStateFromSnapshotForTest(entity.RuntimeConfigSnapshot{})}
852 bw := &browserWindow{id: "win-1", modeToaster: toaster}852 bw := &browserWindow{id: "win-1", modeToaster: toaster}
853 853
854- app.updateModeIndicatorToaster(context.Background(), bw, input.ModePage)854+ app.updateModeIndicatorToaster(context.Background(), bw, input.ModeVim)
855 855
856 assert.False(t, toaster.IsVisible())856 assert.False(t, toaster.IsVisible())
857 }857 }
858 858
859-func TestPageMode_MultiWindowToastersExitIndependently(t *testing.T) {859+func TestVimMode_MultiWindowToastersExitIndependently(t *testing.T) {
860 factory := mocks.NewMockWidgetFactory(t)860 factory := mocks.NewMockWidgetFactory(t)
861- toaster1, container1, label1 := newModeToasterForPageModeTest(t, factory)861+ toaster1, container1, label1 := newModeToasterForVimModeTest(t, factory)
862- toaster2, container2, label2 := newModeToasterForPageModeTest(t, factory)862+ toaster2, container2, label2 := newModeToasterForVimModeTest(t, factory)
863- expectPageModeToastShown(container1, label1)863+ expectVimModeToastShown(container1, label1)
864- expectPageModeToastShown(container2, label2)864+ expectVimModeToastShown(container2, label2)
865 container1.EXPECT().RemoveCssClass("toast-custom").Once()865 container1.EXPECT().RemoveCssClass("toast-custom").Once()
866- container1.EXPECT().RemoveCssClass("toast-pane-mode").Once()866+ container1.EXPECT().RemoveCssClass("toast-vim-mode").Once()
867 container1.EXPECT().SetVisible(false).Once()867 container1.EXPECT().SetVisible(false).Once()
868 868
869 app := &App{runtimeConfig: runtimeConfigStateFromSnapshotForTest(entity.RuntimeConfigSnapshot{869 app := &App{runtimeConfig: runtimeConfigStateFromSnapshotForTest(entity.RuntimeConfigSnapshot{
@@ -874,20 +874,20 @@ func TestPageMode_MultiWindowToastersExitIndependently(t *testing.T) {
874 bw1 := &browserWindow{id: "win-1", modeToaster: toaster1}874 bw1 := &browserWindow{id: "win-1", modeToaster: toaster1}
875 bw2 := &browserWindow{id: "win-2", modeToaster: toaster2}875 bw2 := &browserWindow{id: "win-2", modeToaster: toaster2}
876 876
877- app.updateModeIndicatorToaster(context.Background(), bw1, input.ModePage)877+ app.updateModeIndicatorToaster(context.Background(), bw1, input.ModeVim)
878- app.updateModeIndicatorToaster(context.Background(), bw2, input.ModePage)878+ app.updateModeIndicatorToaster(context.Background(), bw2, input.ModeVim)
879 app.updateModeIndicatorToaster(context.Background(), bw1, input.ModeNormal)879 app.updateModeIndicatorToaster(context.Background(), bw1, input.ModeNormal)
880 880
881 assert.False(t, toaster1.IsVisible())881 assert.False(t, toaster1.IsVisible())
882- assert.True(t, toaster2.IsVisible(), "exiting one window must not hide another window's Page Mode toast")882+ assert.True(t, toaster2.IsVisible(), "exiting one window must not hide another window's Vim Mode toast")
883 }883 }
884 884
885-func TestPageMode_RuntimePreferenceDisableHidesPersistentToaster(t *testing.T) {885+func TestVimMode_RuntimePreferenceDisableHidesPersistentToaster(t *testing.T) {
886 factory := mocks.NewMockWidgetFactory(t)886 factory := mocks.NewMockWidgetFactory(t)
887- toaster, container, label := newModeToasterForPageModeTest(t, factory)887+ toaster, container, label := newModeToasterForVimModeTest(t, factory)
888- expectPageModeToastShown(container, label)888+ expectVimModeToastShown(container, label)
889 container.EXPECT().RemoveCssClass("toast-custom").Once()889 container.EXPECT().RemoveCssClass("toast-custom").Once()
890- container.EXPECT().RemoveCssClass("toast-pane-mode").Once()890+ container.EXPECT().RemoveCssClass("toast-vim-mode").Once()
891 container.EXPECT().SetVisible(false).Once()891 container.EXPECT().SetVisible(false).Once()
892 892
893 enabled := entity.RuntimeConfigSnapshot{893 enabled := entity.RuntimeConfigSnapshot{
@@ -895,26 +895,26 @@ func TestPageMode_RuntimePreferenceDisableHidesPersistentToaster(t *testing.T) {
895 ModeIndicatorToasterEnabled: true,895 ModeIndicatorToasterEnabled: true,
896 }}},896 }}},
897 }897 }
898- keyboardHandler := newKeyboardHandlerInPageMode(t)898+ keyboardHandler := newKeyboardHandlerInVimMode(t)
899 bw := &browserWindow{id: "win-1", modeToaster: toaster, keyboardHandler: keyboardHandler}899 bw := &browserWindow{id: "win-1", modeToaster: toaster, keyboardHandler: keyboardHandler}
900 app := &App{900 app := &App{
901 runtimeConfig: runtimeConfigStateFromSnapshotForTest(enabled),901 runtimeConfig: runtimeConfigStateFromSnapshotForTest(enabled),
902 browserWindows: map[string]*browserWindow{bw.id: bw},902 browserWindows: map[string]*browserWindow{bw.id: bw},
903 }903 }
904 904
905- app.updateModeIndicatorToaster(context.Background(), bw, input.ModePage)905+ app.updateModeIndicatorToaster(context.Background(), bw, input.ModeVim)
906 require.True(t, toaster.IsVisible())906 require.True(t, toaster.IsVisible())
907 app.applyRuntimeConfigChange(context.Background(), entity.RuntimeConfigSnapshot{})907 app.applyRuntimeConfigChange(context.Background(), entity.RuntimeConfigSnapshot{})
908 908
909 assert.False(t, toaster.IsVisible(), "runtime preference disable must reconcile an existing persistent toast")909 assert.False(t, toaster.IsVisible(), "runtime preference disable must reconcile an existing persistent toast")
910 }910 }
911 911
912-func TestPageMode_RuntimePreferenceEnableShowsPersistentToaster(t *testing.T) {912+func TestVimMode_RuntimePreferenceEnableShowsPersistentToaster(t *testing.T) {
913 factory := mocks.NewMockWidgetFactory(t)913 factory := mocks.NewMockWidgetFactory(t)
914- toaster, container, label := newModeToasterForPageModeTest(t, factory)914+ toaster, container, label := newModeToasterForVimModeTest(t, factory)
915- expectPageModeToastShown(container, label)915+ expectVimModeToastShown(container, label)
916 916
917- keyboardHandler := newKeyboardHandlerInPageMode(t)917+ keyboardHandler := newKeyboardHandlerInVimMode(t)
918 bw := &browserWindow{id: "win-1", modeToaster: toaster, keyboardHandler: keyboardHandler}918 bw := &browserWindow{id: "win-1", modeToaster: toaster, keyboardHandler: keyboardHandler}
919 app := &App{919 app := &App{
920 runtimeConfig: runtimeConfigStateFromSnapshotForTest(entity.RuntimeConfigSnapshot{}),920 runtimeConfig: runtimeConfigStateFromSnapshotForTest(entity.RuntimeConfigSnapshot{}),
@@ -928,14 +928,14 @@ func TestPageMode_RuntimePreferenceEnableShowsPersistentToaster(t *testing.T) {
928 928
929 app.applyRuntimeConfigChange(context.Background(), enabled)929 app.applyRuntimeConfigChange(context.Background(), enabled)
930 930
931- assert.True(t, toaster.IsVisible(), "runtime preference enable must reconcile active Page Mode")931+ assert.True(t, toaster.IsVisible(), "runtime preference enable must reconcile active Vim Mode")
932 }932 }
933 933
934 // ============================================================================934 // ============================================================================
935-// BorderManager — Page mode must NOT use the global border overlay935+// BorderManager — Vim mode must NOT use the global border overlay
936 // ============================================================================936 // ============================================================================
937 937
938-func TestPageMode_GlobalBorderOverlayStaysOff(t *testing.T) {938+func TestVimMode_GlobalBorderOverlayStaysOff(t *testing.T) {
939 mockFactory := mocks.NewMockWidgetFactory(t)939 mockFactory := mocks.NewMockWidgetFactory(t)
940 mockBox := mocks.NewMockBoxWidget(t)940 mockBox := mocks.NewMockBoxWidget(t)
941 941
@@ -949,113 +949,113 @@ func TestPageMode_GlobalBorderOverlayStaysOff(t *testing.T) {
949 bm := focus.NewBorderManager(mockFactory)949 bm := focus.NewBorderManager(mockFactory)
950 950
951 mockBox.EXPECT().SetVisible(false).Once()951 mockBox.EXPECT().SetVisible(false).Once()
952- bm.OnModeChange(context.Background(), input.ModeNormal, input.ModePage)952+ bm.OnModeChange(context.Background(), input.ModeNormal, input.ModeVim)
953 953
954 mockBox.EXPECT().SetVisible(false).Once()954 mockBox.EXPECT().SetVisible(false).Once()
955- bm.OnModeChange(context.Background(), input.ModePage, input.ModeNormal)955+ bm.OnModeChange(context.Background(), input.ModeVim, input.ModeNormal)
956 }956 }
957 957
958 // ============================================================================958 // ============================================================================
959 // Pulse Debounce959 // Pulse Debounce
960 // ============================================================================960 // ============================================================================
961 961
962-func TestPageMode_Pulse_DebounceSkipsRapidConsecutiveCalls(t *testing.T) {962+func TestVimMode_Pulse_DebounceSkipsRapidConsecutiveCalls(t *testing.T) {
963- f := newSingleWindowPageModeFixture(t)963+ f := newSingleWindowVimModeFixture(t)
964- enterPageMode(t, f.app, f.overlay1A, f.bw1)964+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
965 965
966 // Set up expectations for exactly ONE normal pulse.966 // Set up expectations for exactly ONE normal pulse.
967 setUpNormalPulse(f.overlay1A)967 setUpNormalPulse(f.overlay1A)
968 968
969 // First call fires the pulse (debounce timer is zero).969 // First call fires the pulse (debounce timer is zero).
970- f.app.triggerPageModePulse(context.Background(), false)970+ f.app.triggerVimModePulse(context.Background(), false)
971 971
972 // Second call within the same frame should be debounced — no972 // Second call within the same frame should be debounced — no
973 // mock expectations set up for it, so the test fails if it fires.973 // mock expectations set up for it, so the test fails if it fires.
974- f.app.triggerPageModePulse(context.Background(), false)974+ f.app.triggerVimModePulse(context.Background(), false)
975 }975 }
976 976
977-func TestPageMode_Pulse_DebounceAllowsSpacedCalls(t *testing.T) {977+func TestVimMode_Pulse_DebounceAllowsSpacedCalls(t *testing.T) {
978 // Verify that the debounce timer starts at zero on a fresh App,978 // Verify that the debounce timer starts at zero on a fresh App,
979- // so the first pulse after entering page mode always fires.979+ // so the first pulse after entering vim mode always fires.
980 // The exit-and-re-enter path is covered by the980 // The exit-and-re-enter path is covered by the
981- // TestPageMode_Pulse_DebounceResetOnClearOwnership test below.981+ // TestVimMode_Pulse_DebounceResetOnClearOwnership test below.
982 982
983- f := newSingleWindowPageModeFixture(t)983+ f := newSingleWindowVimModeFixture(t)
984 984
985 // Fresh app: debounce timer is zero.985 // Fresh app: debounce timer is zero.
986- assert.True(t, f.app.pageModePulseLastTime.IsZero(),986+ assert.True(t, f.app.vimModePulseLastTime.IsZero(),
987 "fresh app should have zero pulse timer")987 "fresh app should have zero pulse timer")
988 988
989- enterPageMode(t, f.app, f.overlay1A, f.bw1)989+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
990 990
991 // Pulse #1 fires because timer is zero (time.Since(zero) is huge).991 // Pulse #1 fires because timer is zero (time.Since(zero) is huge).
992 setUpNormalPulse(f.overlay1A)992 setUpNormalPulse(f.overlay1A)
993- f.app.triggerPageModePulse(context.Background(), false)993+ f.app.triggerVimModePulse(context.Background(), false)
994 994
995 // Timer is now set, proving the first pulse registered.995 // Timer is now set, proving the first pulse registered.
996- assert.False(t, f.app.pageModePulseLastTime.IsZero(),996+ assert.False(t, f.app.vimModePulseLastTime.IsZero(),
997 "pulse timer should be non-zero after first pulse")997 "pulse timer should be non-zero after first pulse")
998 }998 }
999 999
1000-func TestPageMode_Pulse_DebounceResetOnClearOwnership(t *testing.T) {1000+func TestVimMode_Pulse_DebounceResetOnClearOwnership(t *testing.T) {
1001- // Verify that clearPageModeOwnership resets the debounce timer1001+ // Verify that clearVimModeOwnership resets the debounce timer
1002- // so a subsequent pulse in a new page mode session is not skipped.1002+ // so a subsequent pulse in a new vim mode session is not skipped.
1003 // We check the timer state directly rather than triggering a second1003 // We check the timer state directly rather than triggering a second
1004 // pulse (which would need pulse-cycle alternation handling).1004 // pulse (which would need pulse-cycle alternation handling).
1005 1005
1006- f := newSingleWindowPageModeFixture(t)1006+ f := newSingleWindowVimModeFixture(t)
1007- enterPageMode(t, f.app, f.overlay1A, f.bw1)1007+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
1008 1008
1009 // Pulse #1 fires and sets the timer.1009 // Pulse #1 fires and sets the timer.
1010 setUpNormalPulse(f.overlay1A)1010 setUpNormalPulse(f.overlay1A)
1011- f.app.triggerPageModePulse(context.Background(), false)1011+ f.app.triggerVimModePulse(context.Background(), false)
1012- assert.False(t, f.app.pageModePulseLastTime.IsZero(),1012+ assert.False(t, f.app.vimModePulseLastTime.IsZero(),
1013 "pulse timer should be non-zero after first pulse")1013 "pulse timer should be non-zero after first pulse")
1014 1014
1015 // Clear ownership — this resets the debounce timer.1015 // Clear ownership — this resets the debounce timer.
1016- expectPageModeAccentHidden(f.overlay1A)1016+ expectVimModeAccentHidden(f.overlay1A)
1017- f.app.clearPageModeOwnership(context.Background(), f.bw1)1017+ f.app.clearVimModeOwnership(context.Background(), f.bw1)
1018- assert.Empty(t, f.bw1.pageModePaneID)1018+ assert.Empty(t, f.bw1.vimModePaneID)
1019 1019
1020 // Debounce timer should now be back to zero.1020 // Debounce timer should now be back to zero.
1021- assert.True(t, f.app.pageModePulseLastTime.IsZero(),1021+ assert.True(t, f.app.vimModePulseLastTime.IsZero(),
1022- "pulse timer should be reset after clearPageModeOwnership")1022+ "pulse timer should be reset after clearVimModeOwnership")
1023 }1023 }
1024 1024
1025-func TestPageMode_Pulse_DebounceFirstPulseAlwaysFires(t *testing.T) {1025+func TestVimMode_Pulse_DebounceFirstPulseAlwaysFires(t *testing.T) {
1026- // Even if triggerPageModePulse is called rapidly on a fresh1026+ // Even if triggerVimModePulse is called rapidly on a fresh
1027- // App (e.g., first keystroke after page mode entry), the first1027+ // App (e.g., first keystroke after vim mode entry), the first
1028 // call must always fire. This is guaranteed by the zero value1028 // call must always fire. This is guaranteed by the zero value
1029 // of time.Time producing a time.Since result much larger than1029 // of time.Time producing a time.Since result much larger than
1030 // the debounce interval.1030 // the debounce interval.
1031- f := newSingleWindowPageModeFixture(t)1031+ f := newSingleWindowVimModeFixture(t)
1032- enterPageMode(t, f.app, f.overlay1A, f.bw1)1032+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
1033 1033
1034 // Rapid double pulse — only first should fire regardless of order.1034 // Rapid double pulse — only first should fire regardless of order.
1035 // Set up just one set of expectations.1035 // Set up just one set of expectations.
1036 setUpNormalPulse(f.overlay1A)1036 setUpNormalPulse(f.overlay1A)
1037- f.app.triggerPageModePulse(context.Background(), false)1037+ f.app.triggerVimModePulse(context.Background(), false)
1038- f.app.triggerPageModePulse(context.Background(), false)1038+ f.app.triggerVimModePulse(context.Background(), false)
1039 }1039 }
1040 1040
1041 // ============================================================================1041 // ============================================================================
1042-// UI Scale: Page mode CSS uses relative (em) units1042+// UI Scale: Vim mode CSS uses relative (em) units
1043 // ============================================================================1043 // ============================================================================
1044 1044
1045-func TestPageMode_CSSUsesEms(t *testing.T) {1045+func TestVimMode_CSSUsesEms(t *testing.T) {
1046 css := theme.GenerateCSS(theme.DefaultDarkPalette())1046 css := theme.GenerateCSS(theme.DefaultDarkPalette())
1047 1047
1048 assert.Contains(t, css, "box-shadow: inset 0 0 0 0.125em",1048 assert.Contains(t, css, "box-shadow: inset 0 0 0 0.125em",
1049- "page mode overlay box-shadow should use em for scaling")1049+ "vim mode overlay box-shadow should use em for scaling")
1050 assert.Contains(t, css, "0.2em",1050 assert.Contains(t, css, "0.2em",
1051- "page mode overlay pulse glow radius uses em")1051+ "vim mode overlay pulse glow radius uses em")
1052 assert.Contains(t, css, "0.26em",1052 assert.Contains(t, css, "0.26em",
1053- "page mode overlay fast-pulse glow radius uses em")1053+ "vim mode overlay fast-pulse glow radius uses em")
1054- assert.NotContains(t, css, "page-mode-indicator",1054+ assert.NotContains(t, css, "vim-mode-indicator",
1055 "removed PAGE label styling must not remain")1055 "removed PAGE label styling must not remain")
1056 }1056 }
1057 1057
1058-func TestPageMode_CSSPulseTimingUsesDefaultTransitionDuration(t *testing.T) {1058+func TestVimMode_CSSPulseTimingUsesDefaultTransitionDuration(t *testing.T) {
1059 // The pulse animation durations should be derived from the1059 // The pulse animation durations should be derived from the
1060 // default transition duration (normal = 3x, fast = 6x).1060 // default transition duration (normal = 3x, fast = 6x).
1061 // Default = 120ms → normal = 360ms, fast = 720ms.1061 // Default = 120ms → normal = 360ms, fast = 720ms.
@@ -1067,7 +1067,7 @@ func TestPageMode_CSSPulseTimingUsesDefaultTransitionDuration(t *testing.T) {
1067 "fast pulse duration should be 6x transition duration (720ms for 120ms base)")1067 "fast pulse duration should be 6x transition duration (720ms for 120ms base)")
1068 }1068 }
1069 1069
1070-func TestPageMode_CSSWithCustomTransitionDuration(t *testing.T) {1070+func TestVimMode_CSSWithCustomTransitionDuration(t *testing.T) {
1071 // Custom transition duration (e.g., 200ms) should produce1071 // Custom transition duration (e.g., 200ms) should produce
1072 // adjusted pulse timings: normal = 600ms, fast = 1200ms.1072 // adjusted pulse timings: normal = 600ms, fast = 1200ms.
1073 css := theme.GenerateCSSFullWithTiming(1073 css := theme.GenerateCSSFullWithTiming(
M internal/ui/browser_window.go +3 -3
@@ -53,8 +53,8 @@ type browserWindow struct {
53 sidebarVisible bool53 sidebarVisible bool
54 activeSidebarKind nativeSidebarKind54 activeSidebarKind nativeSidebarKind
55 55
56- // pageModePaneID tracks this window's pane-local Page Mode accent/pulse owner.56+ // vimModePaneID tracks this window's pane-local Vim Mode accent/pulse owner.
57- pageModePaneID entity.PaneID57+ vimModePaneID entity.PaneID
58 }58 }
59 59
60 func (bw *browserWindow) detachInputForDestroy() {60 func (bw *browserWindow) detachInputForDestroy() {
@@ -111,7 +111,7 @@ func (bw *browserWindow) clearShellState() {
111 bw.favoritesSidebar = nil111 bw.favoritesSidebar = nil
112 bw.historySidebarReloader = nil112 bw.historySidebarReloader = nil
113 bw.activeSidebarKind = nativeSidebarNone113 bw.activeSidebarKind = nativeSidebarNone
114- bw.pageModePaneID = ""114+ bw.vimModePaneID = ""
115 }115 }
116 116
117 func (bw *browserWindow) initChrome(ctx context.Context, a *App) {117 func (bw *browserWindow) initChrome(ctx context.Context, a *App) {
M internal/ui/component/pane_view.go +35 -35
@@ -15,26 +15,26 @@ const (
15 // CSS class applied to active pane's border overlay15 // CSS class applied to active pane's border overlay
16 activePaneClass = "pane-active"16 activePaneClass = "pane-active"
17 17
18- // pageModeActiveClass is added to the pane overlay when Page mode is active.18+ // vimModeActiveClass is added to the pane overlay when Vim mode is active.
19 // It produces a subtle local border accent using the pane mode color.19 // It produces a subtle local border accent using the pane mode color.
20- pageModeActiveClass = "page-mode-active"20+ vimModeActiveClass = "vim-mode-active"
21 21
22- // pageModePulseClass triggers a normal scroll pulse on the pane overlay.22+ // vimModePulseClass triggers a normal scroll pulse on the pane overlay.
23 // Repeated calls must remove-then-add to restart the CSS animation.23 // Repeated calls must remove-then-add to restart the CSS animation.
24- pageModePulseClass = "page-mode-pulse"24+ vimModePulseClass = "vim-mode-pulse"
25 25
26- // pageModeFastPulseClass triggers a fast/stronger scroll pulse.26+ // vimModeFastPulseClass triggers a fast/stronger scroll pulse.
27- pageModeFastPulseClass = "page-mode-pulse-fast"27+ vimModeFastPulseClass = "vim-mode-pulse-fast"
28 28
29 // Alternate equivalent animations so repeated scrolls reliably restart29 // Alternate equivalent animations so repeated scrolls reliably restart
30 // the GTK CSS animation.30 // the GTK CSS animation.
31- pageModePulseCycleClassA = "page-mode-pulse-cycle-a"31+ vimModePulseCycleClassA = "vim-mode-pulse-cycle-a"
32- pageModePulseCycleClassB = "page-mode-pulse-cycle-b"32+ vimModePulseCycleClassB = "vim-mode-pulse-cycle-b"
33 33
34 // Exported aliases for tests and external inspection.34 // Exported aliases for tests and external inspection.
35- PageModeActiveClass = pageModeActiveClass35+ VimModeActiveClass = vimModeActiveClass
36- PageModePulseClass = pageModePulseClass36+ VimModePulseClass = vimModePulseClass
37- PageModeFastPulseClass = pageModeFastPulseClass37+ VimModeFastPulseClass = vimModeFastPulseClass
38 )38 )
39 39
40 // PaneView is a container for a single WebView with active state indication.40 // PaneView is a container for a single WebView with active state indication.
@@ -52,8 +52,8 @@ type PaneView struct {
52 paneID entity.PaneID52 paneID entity.PaneID
53 isActive bool53 isActive bool
54 54
55- // Page Mode pane-local visual state.55+ // Vim Mode pane-local visual state.
56- pageMode bool56+ vimMode bool
57 pulseCycle bool57 pulseCycle bool
58 58
59 onFocusIn func(paneID entity.PaneID)59 onFocusIn func(paneID entity.PaneID)
@@ -117,7 +117,7 @@ func NewPaneView(ctx context.Context, factory layout.WidgetFactory, paneID entit
117 loading: loading,117 loading: loading,
118 paneID: paneID,118 paneID: paneID,
119 isActive: false,119 isActive: false,
120- pageMode: false,120+ vimMode: false,
121 }121 }
122 }122 }
123 123
@@ -546,39 +546,39 @@ func (pv *PaneView) HideLinkStatus() {
546 }546 }
547 }547 }
548 548
549-// SetPageMode activates or deactivates the pane-local Page Mode accent.549+// SetVimMode activates or deactivates the pane-local Vim Mode accent.
550-func (pv *PaneView) SetPageMode(active bool) {550+func (pv *PaneView) SetVimMode(active bool) {
551 pv.mu.Lock()551 pv.mu.Lock()
552 defer pv.mu.Unlock()552 defer pv.mu.Unlock()
553 553
554- if pv.pageMode == active {554+ if pv.vimMode == active {
555 return555 return
556 }556 }
557 557
558- pv.pageMode = active558+ pv.vimMode = active
559 559
560 if active {560 if active {
561- pv.overlay.AddCssClass(pageModeActiveClass)561+ pv.overlay.AddCssClass(vimModeActiveClass)
562 } else {562 } else {
563- pv.overlay.RemoveCssClass(pageModeActiveClass)563+ pv.overlay.RemoveCssClass(vimModeActiveClass)
564 }564 }
565 }565 }
566 566
567-// IsPageMode returns whether Page mode is currently active on this pane.567+// IsVimMode returns whether Vim mode is currently active on this pane.
568-func (pv *PaneView) IsPageMode() bool {568+func (pv *PaneView) IsVimMode() bool {
569 pv.mu.RLock()569 pv.mu.RLock()
570 defer pv.mu.RUnlock()570 defer pv.mu.RUnlock()
571 571
572- return pv.pageMode572+ return pv.vimMode
573 }573 }
574 574
575-// TriggerPageModePulse triggers a normal scroll pulse on the pane overlay.575+// TriggerVimModePulse triggers a normal scroll pulse on the pane overlay.
576-func (pv *PaneView) TriggerPageModePulse() {576+func (pv *PaneView) TriggerVimModePulse() {
577 pv.triggerOverlayPulse(false)577 pv.triggerOverlayPulse(false)
578 }578 }
579 579
580-// TriggerPageModePulseFast triggers a stronger, longer pane-overlay pulse.580+// TriggerVimModePulseFast triggers a stronger, longer pane-overlay pulse.
581-func (pv *PaneView) TriggerPageModePulseFast() {581+func (pv *PaneView) TriggerVimModePulseFast() {
582 pv.triggerOverlayPulse(true)582 pv.triggerOverlayPulse(true)
583 }583 }
584 584
@@ -589,18 +589,18 @@ func (pv *PaneView) triggerOverlayPulse(fast bool) {
589 pv.mu.Lock()589 pv.mu.Lock()
590 defer pv.mu.Unlock()590 defer pv.mu.Unlock()
591 591
592- pv.overlay.RemoveCssClass(pageModePulseClass)592+ pv.overlay.RemoveCssClass(vimModePulseClass)
593- pv.overlay.RemoveCssClass(pageModeFastPulseClass)593+ pv.overlay.RemoveCssClass(vimModeFastPulseClass)
594- pv.overlay.RemoveCssClass(pageModePulseCycleClassA)594+ pv.overlay.RemoveCssClass(vimModePulseCycleClassA)
595- pv.overlay.RemoveCssClass(pageModePulseCycleClassB)595+ pv.overlay.RemoveCssClass(vimModePulseCycleClassB)
596 if fast {596 if fast {
597- pv.overlay.AddCssClass(pageModeFastPulseClass)597+ pv.overlay.AddCssClass(vimModeFastPulseClass)
598 } else {598 } else {
599- pv.overlay.AddCssClass(pageModePulseClass)599+ pv.overlay.AddCssClass(vimModePulseClass)
600 }600 }
601- cycle := pageModePulseCycleClassA601+ cycle := vimModePulseCycleClassA
602 if pv.pulseCycle {602 if pv.pulseCycle {
603- cycle = pageModePulseCycleClassB603+ cycle = vimModePulseCycleClassB
604 }604 }
605 pv.pulseCycle = !pv.pulseCycle605 pv.pulseCycle = !pv.pulseCycle
606 pv.overlay.AddCssClass(cycle)606 pv.overlay.AddCssClass(cycle)
M internal/ui/component/pane_view_test.go +43 -43
@@ -682,7 +682,7 @@ func setupPaneViewMocksNoWebView(
682 mockOverlay.EXPECT().SetMeasureOverlay(mockBorderBox, false).Once()682 mockOverlay.EXPECT().SetMeasureOverlay(mockBorderBox, false).Once()
683 }683 }
684 684
685-func TestSetPageMode_TrueAddsPaneAccentWithoutCreatingLabel(t *testing.T) {685+func TestSetVimMode_TrueAddsPaneAccentWithoutCreatingLabel(t *testing.T) {
686 mockFactory := mocks.NewMockWidgetFactory(t)686 mockFactory := mocks.NewMockWidgetFactory(t)
687 mockOverlay := mocks.NewMockOverlayWidget(t)687 mockOverlay := mocks.NewMockOverlayWidget(t)
688 mockBorderBox := mocks.NewMockBoxWidget(t)688 mockBorderBox := mocks.NewMockBoxWidget(t)
@@ -690,13 +690,13 @@ func TestSetPageMode_TrueAddsPaneAccentWithoutCreatingLabel(t *testing.T) {
690 setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)690 setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
691 pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)691 pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
692 692
693- mockOverlay.EXPECT().AddCssClass("page-mode-active").Once()693+ mockOverlay.EXPECT().AddCssClass("vim-mode-active").Once()
694- pv.SetPageMode(true)694+ pv.SetVimMode(true)
695 695
696- require.True(t, pv.IsPageMode())696+ require.True(t, pv.IsVimMode())
697 }697 }
698 698
699-func TestSetPageMode_FalseRemovesPaneAccent(t *testing.T) {699+func TestSetVimMode_FalseRemovesPaneAccent(t *testing.T) {
700 mockFactory := mocks.NewMockWidgetFactory(t)700 mockFactory := mocks.NewMockWidgetFactory(t)
701 mockOverlay := mocks.NewMockOverlayWidget(t)701 mockOverlay := mocks.NewMockOverlayWidget(t)
702 mockBorderBox := mocks.NewMockBoxWidget(t)702 mockBorderBox := mocks.NewMockBoxWidget(t)
@@ -704,15 +704,15 @@ func TestSetPageMode_FalseRemovesPaneAccent(t *testing.T) {
704 setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)704 setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
705 pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)705 pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
706 706
707- mockOverlay.EXPECT().AddCssClass("page-mode-active").Once()707+ mockOverlay.EXPECT().AddCssClass("vim-mode-active").Once()
708- pv.SetPageMode(true)708+ pv.SetVimMode(true)
709- mockOverlay.EXPECT().RemoveCssClass("page-mode-active").Once()709+ mockOverlay.EXPECT().RemoveCssClass("vim-mode-active").Once()
710- pv.SetPageMode(false)710+ pv.SetVimMode(false)
711 711
712- require.False(t, pv.IsPageMode())712+ require.False(t, pv.IsVimMode())
713 }713 }
714 714
715-func TestSetPageMode_NoChangeWhenSameState(t *testing.T) {715+func TestSetVimMode_NoChangeWhenSameState(t *testing.T) {
716 mockFactory := mocks.NewMockWidgetFactory(t)716 mockFactory := mocks.NewMockWidgetFactory(t)
717 mockOverlay := mocks.NewMockOverlayWidget(t)717 mockOverlay := mocks.NewMockOverlayWidget(t)
718 mockBorderBox := mocks.NewMockBoxWidget(t)718 mockBorderBox := mocks.NewMockBoxWidget(t)
@@ -720,12 +720,12 @@ func TestSetPageMode_NoChangeWhenSameState(t *testing.T) {
720 setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)720 setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
721 pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)721 pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
722 722
723- pv.SetPageMode(false)723+ pv.SetVimMode(false)
724 724
725- require.False(t, pv.IsPageMode())725+ require.False(t, pv.IsVimMode())
726 }726 }
727 727
728-func TestSetPageMode_TrueThenTrueIsIdempotent(t *testing.T) {728+func TestSetVimMode_TrueThenTrueIsIdempotent(t *testing.T) {
729 mockFactory := mocks.NewMockWidgetFactory(t)729 mockFactory := mocks.NewMockWidgetFactory(t)
730 mockOverlay := mocks.NewMockOverlayWidget(t)730 mockOverlay := mocks.NewMockOverlayWidget(t)
731 mockBorderBox := mocks.NewMockBoxWidget(t)731 mockBorderBox := mocks.NewMockBoxWidget(t)
@@ -733,27 +733,27 @@ func TestSetPageMode_TrueThenTrueIsIdempotent(t *testing.T) {
733 setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)733 setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
734 pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)734 pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
735 735
736- mockOverlay.EXPECT().AddCssClass("page-mode-active").Once()736+ mockOverlay.EXPECT().AddCssClass("vim-mode-active").Once()
737- pv.SetPageMode(true)737+ pv.SetVimMode(true)
738- pv.SetPageMode(true)738+ pv.SetVimMode(true)
739 739
740- require.True(t, pv.IsPageMode())740+ require.True(t, pv.IsVimMode())
741 }741 }
742 742
743-func expectPageModeOverlayPulse(overlay *mocks.MockOverlayWidget, fast bool, cycle string) {743+func expectVimModeOverlayPulse(overlay *mocks.MockOverlayWidget, fast bool, cycle string) {
744- overlay.EXPECT().RemoveCssClass("page-mode-pulse").Once()744+ overlay.EXPECT().RemoveCssClass("vim-mode-pulse").Once()
745- overlay.EXPECT().RemoveCssClass("page-mode-pulse-fast").Once()745+ overlay.EXPECT().RemoveCssClass("vim-mode-pulse-fast").Once()
746- overlay.EXPECT().RemoveCssClass("page-mode-pulse-cycle-a").Once()746+ overlay.EXPECT().RemoveCssClass("vim-mode-pulse-cycle-a").Once()
747- overlay.EXPECT().RemoveCssClass("page-mode-pulse-cycle-b").Once()747+ overlay.EXPECT().RemoveCssClass("vim-mode-pulse-cycle-b").Once()
748 if fast {748 if fast {
749- overlay.EXPECT().AddCssClass("page-mode-pulse-fast").Once()749+ overlay.EXPECT().AddCssClass("vim-mode-pulse-fast").Once()
750 } else {750 } else {
751- overlay.EXPECT().AddCssClass("page-mode-pulse").Once()751+ overlay.EXPECT().AddCssClass("vim-mode-pulse").Once()
752 }752 }
753 overlay.EXPECT().AddCssClass(cycle).Once()753 overlay.EXPECT().AddCssClass(cycle).Once()
754 }754 }
755 755
756-func TestTriggerPageModePulse_PulsesOnlyOverlay(t *testing.T) {756+func TestTriggerVimModePulse_PulsesOnlyOverlay(t *testing.T) {
757 mockFactory := mocks.NewMockWidgetFactory(t)757 mockFactory := mocks.NewMockWidgetFactory(t)
758 mockOverlay := mocks.NewMockOverlayWidget(t)758 mockOverlay := mocks.NewMockOverlayWidget(t)
759 mockBorderBox := mocks.NewMockBoxWidget(t)759 mockBorderBox := mocks.NewMockBoxWidget(t)
@@ -761,11 +761,11 @@ func TestTriggerPageModePulse_PulsesOnlyOverlay(t *testing.T) {
761 setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)761 setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
762 pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)762 pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
763 763
764- expectPageModeOverlayPulse(mockOverlay, false, "page-mode-pulse-cycle-a")764+ expectVimModeOverlayPulse(mockOverlay, false, "vim-mode-pulse-cycle-a")
765- pv.TriggerPageModePulse()765+ pv.TriggerVimModePulse()
766 }766 }
767 767
768-func TestTriggerPageModePulseFast_PulsesOnlyOverlay(t *testing.T) {768+func TestTriggerVimModePulseFast_PulsesOnlyOverlay(t *testing.T) {
769 mockFactory := mocks.NewMockWidgetFactory(t)769 mockFactory := mocks.NewMockWidgetFactory(t)
770 mockOverlay := mocks.NewMockOverlayWidget(t)770 mockOverlay := mocks.NewMockOverlayWidget(t)
771 mockBorderBox := mocks.NewMockBoxWidget(t)771 mockBorderBox := mocks.NewMockBoxWidget(t)
@@ -773,11 +773,11 @@ func TestTriggerPageModePulseFast_PulsesOnlyOverlay(t *testing.T) {
773 setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)773 setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
774 pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)774 pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
775 775
776- expectPageModeOverlayPulse(mockOverlay, true, "page-mode-pulse-cycle-a")776+ expectVimModeOverlayPulse(mockOverlay, true, "vim-mode-pulse-cycle-a")
777- pv.TriggerPageModePulseFast()777+ pv.TriggerVimModePulseFast()
778 }778 }
779 779
780-func TestTriggerPageModePulse_RepeatedCallsReTriggerOverlayAnimation(t *testing.T) {780+func TestTriggerVimModePulse_RepeatedCallsReTriggerOverlayAnimation(t *testing.T) {
781 mockFactory := mocks.NewMockWidgetFactory(t)781 mockFactory := mocks.NewMockWidgetFactory(t)
782 mockOverlay := mocks.NewMockOverlayWidget(t)782 mockOverlay := mocks.NewMockOverlayWidget(t)
783 mockBorderBox := mocks.NewMockBoxWidget(t)783 mockBorderBox := mocks.NewMockBoxWidget(t)
@@ -785,15 +785,15 @@ func TestTriggerPageModePulse_RepeatedCallsReTriggerOverlayAnimation(t *testing.
785 setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)785 setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
786 pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)786 pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
787 787
788- expectPageModeOverlayPulse(mockOverlay, false, "page-mode-pulse-cycle-a")788+ expectVimModeOverlayPulse(mockOverlay, false, "vim-mode-pulse-cycle-a")
789- pv.TriggerPageModePulse()789+ pv.TriggerVimModePulse()
790- expectPageModeOverlayPulse(mockOverlay, true, "page-mode-pulse-cycle-b")790+ expectVimModeOverlayPulse(mockOverlay, true, "vim-mode-pulse-cycle-b")
791- pv.TriggerPageModePulseFast()791+ pv.TriggerVimModePulseFast()
792- expectPageModeOverlayPulse(mockOverlay, false, "page-mode-pulse-cycle-a")792+ expectVimModeOverlayPulse(mockOverlay, false, "vim-mode-pulse-cycle-a")
793- pv.TriggerPageModePulse()793+ pv.TriggerVimModePulse()
794 }794 }
795 795
796-func TestPageMode_CleanupHasNoIndicatorOverlay(t *testing.T) {796+func TestVimMode_CleanupHasNoIndicatorOverlay(t *testing.T) {
797 mockFactory := mocks.NewMockWidgetFactory(t)797 mockFactory := mocks.NewMockWidgetFactory(t)
798 mockOverlay := mocks.NewMockOverlayWidget(t)798 mockOverlay := mocks.NewMockOverlayWidget(t)
799 mockBorderBox := mocks.NewMockBoxWidget(t)799 mockBorderBox := mocks.NewMockBoxWidget(t)
@@ -801,15 +801,15 @@ func TestPageMode_CleanupHasNoIndicatorOverlay(t *testing.T) {
801 setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)801 setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
802 pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)802 pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
803 803
804- mockOverlay.EXPECT().AddCssClass("page-mode-active").Once()804+ mockOverlay.EXPECT().AddCssClass("vim-mode-active").Once()
805- pv.SetPageMode(true)805+ pv.SetVimMode(true)
806 mockOverlay.EXPECT().RemoveOverlay(mock.Anything).Once()806 mockOverlay.EXPECT().RemoveOverlay(mock.Anything).Once()
807 mockOverlay.EXPECT().SetChild(nil).Once()807 mockOverlay.EXPECT().SetChild(nil).Once()
808 808
809 pv.Cleanup()809 pv.Cleanup()
810 }810 }
811 811
812-func TestPageMode_NewPaneViewInactiveByDefault(t *testing.T) {812+func TestVimMode_NewPaneViewInactiveByDefault(t *testing.T) {
813 mockFactory := mocks.NewMockWidgetFactory(t)813 mockFactory := mocks.NewMockWidgetFactory(t)
814 mockOverlay := mocks.NewMockOverlayWidget(t)814 mockOverlay := mocks.NewMockOverlayWidget(t)
815 mockBorderBox := mocks.NewMockBoxWidget(t)815 mockBorderBox := mocks.NewMockBoxWidget(t)
@@ -818,5 +818,5 @@ func TestPageMode_NewPaneViewInactiveByDefault(t *testing.T) {
818 818
819 pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)819 pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
820 820
821- require.False(t, pv.IsPageMode())821+ require.False(t, pv.IsVimMode())
822 }822 }
M internal/ui/coordinator/navigation.go +1 -1
@@ -304,7 +304,7 @@ func (c *NavigationCoordinator) ScrollWebView(ctx context.Context, wv port.WebVi
304 return c.pageScrollUC.Scroll(ctx, wv, cmd)304 return c.pageScrollUC.Scroll(ctx, wv, cmd)
305 }305 }
306 306
307-// ScrollWebViewContinuous applies one autonomous held-key page-scroll step.307+// ScrollWebViewContinuous applies one autonomous held-key vim-scroll step.
308 func (c *NavigationCoordinator) ScrollWebViewContinuous(ctx context.Context, wv port.WebView, cmd usecase.PageScrollCommand) error {308 func (c *NavigationCoordinator) ScrollWebViewContinuous(ctx context.Context, wv port.WebView, cmd usecase.PageScrollCommand) error {
309 if err := requireWebView(wv); err != nil {309 if err := requireWebView(wv); err != nil {
310 return err310 return err
M internal/ui/dispatcher/keyboard.go +37 -37
@@ -52,7 +52,7 @@ type KeyboardDispatcher struct {
52 onMovePaneToTab func(ctx context.Context, paneID entity.PaneID) error52 onMovePaneToTab func(ctx context.Context, paneID entity.PaneID) error
53 onMovePaneToNext func(ctx context.Context, paneID entity.PaneID) error53 onMovePaneToNext func(ctx context.Context, paneID entity.PaneID) error
54 onEjectPaneToWindow func(ctx context.Context, paneID entity.PaneID) error54 onEjectPaneToWindow func(ctx context.Context, paneID entity.PaneID) error
55- onPageModePulse func(ctx context.Context, fast bool)55+ onVimModePulse func(ctx context.Context, fast bool)
56 onToggleHistorySidebar func(ctx context.Context) error56 onToggleHistorySidebar func(ctx context.Context) error
57 onToggleFavoritesSidebar func(ctx context.Context) error57 onToggleFavoritesSidebar func(ctx context.Context) error
58 onToggleCurrentFavorite func(ctx context.Context) error58 onToggleCurrentFavorite func(ctx context.Context) error
@@ -157,19 +157,19 @@ func (d *KeyboardDispatcher) SetOnOpenFloatingTarget(fn func(ctx context.Context
157 d.onOpenFloating = fn157 d.onOpenFloating = fn
158 }158 }
159 159
160-// SetOnPageModePulse registers a callback for pane-local Page mode pulse160+// SetOnVimModePulse registers a callback for pane-local Vim mode pulse
161 // visual feedback triggered after a page scroll action.161 // visual feedback triggered after a page scroll action.
162 // The cb receives a bool indicating whether the pulse should be "fast"162 // The cb receives a bool indicating whether the pulse should be "fast"
163 // (stronger/longer for up-fast/down-fast).163 // (stronger/longer for up-fast/down-fast).
164 // If nil, pulse callbacks are no-ops.164 // If nil, pulse callbacks are no-ops.
165-func (d *KeyboardDispatcher) SetOnPageModePulse(cb func(ctx context.Context, fast bool)) {165+func (d *KeyboardDispatcher) SetOnVimModePulse(cb func(ctx context.Context, fast bool)) {
166- d.onPageModePulse = cb166+ d.onVimModePulse = cb
167 }167 }
168 168
169 // triggerPulse calls the registered pulse callback if set, or no-ops otherwise.169 // triggerPulse calls the registered pulse callback if set, or no-ops otherwise.
170 func (d *KeyboardDispatcher) triggerPulse(ctx context.Context, fast bool) {170 func (d *KeyboardDispatcher) triggerPulse(ctx context.Context, fast bool) {
171- if d.onPageModePulse != nil {171+ if d.onVimModePulse != nil {
172- d.onPageModePulse(ctx, fast)172+ d.onVimModePulse(ctx, fast)
173 }173 }
174 }174 }
175 175
@@ -266,13 +266,13 @@ func (d *KeyboardDispatcher) initActionHandlers() {
266 input.ActionZoomIn: func(ctx context.Context) error { return d.handleZoom(ctx, "in") },266 input.ActionZoomIn: func(ctx context.Context) error { return d.handleZoom(ctx, "in") },
267 input.ActionZoomOut: func(ctx context.Context) error { return d.handleZoom(ctx, "out") },267 input.ActionZoomOut: func(ctx context.Context) error { return d.handleZoom(ctx, "out") },
268 input.ActionZoomReset: func(ctx context.Context) error { return d.handleZoom(ctx, "reset") },268 input.ActionZoomReset: func(ctx context.Context) error { return d.handleZoom(ctx, "reset") },
269- // Page mode scroll actions — single mapping drives tap pulse + command.269+ // Vim mode scroll actions — single mapping drives tap pulse + command.
270- input.ActionPageScrollLeft: d.pageScrollTapHandler(input.ActionPageScrollLeft),270+ input.ActionVimScrollLeft: d.vimScrollTapHandler(input.ActionVimScrollLeft),
271- input.ActionPageScrollRight: d.pageScrollTapHandler(input.ActionPageScrollRight),271+ input.ActionVimScrollRight: d.vimScrollTapHandler(input.ActionVimScrollRight),
272- input.ActionPageScrollUp: d.pageScrollTapHandler(input.ActionPageScrollUp),272+ input.ActionVimScrollUp: d.vimScrollTapHandler(input.ActionVimScrollUp),
273- input.ActionPageScrollDown: d.pageScrollTapHandler(input.ActionPageScrollDown),273+ input.ActionVimScrollDown: d.vimScrollTapHandler(input.ActionVimScrollDown),
274- input.ActionPageScrollUpFast: d.pageScrollTapHandler(input.ActionPageScrollUpFast),274+ input.ActionVimScrollUpFast: d.vimScrollTapHandler(input.ActionVimScrollUpFast),
275- input.ActionPageScrollDownFast: d.pageScrollTapHandler(input.ActionPageScrollDownFast),275+ input.ActionVimScrollDownFast: d.vimScrollTapHandler(input.ActionVimScrollDownFast),
276 // UI276 // UI
277 input.ActionOpenOmnibox: d.navCoord.OpenOmnibox,277 input.ActionOpenOmnibox: d.navCoord.OpenOmnibox,
278 input.ActionOpenFind: d.handleFindOpen,278 input.ActionOpenFind: d.handleFindOpen,
@@ -322,50 +322,50 @@ func (d *KeyboardDispatcher) initActionHandlers() {
322 }322 }
323 }323 }
324 324
325-// DispatchPageScrollLifecycle routes autonomous held-key page scroll events.325+// DispatchVimScrollLifecycle routes autonomous held-key page scroll events.
326 // Stop deliberately avoids the normal action map so it emits neither another326 // Stop deliberately avoids the normal action map so it emits neither another
327 // scroll tap nor visual pulse.327 // scroll tap nor visual pulse.
328-func (d *KeyboardDispatcher) DispatchPageScrollLifecycle(ctx context.Context, action input.Action, phase input.PageScrollPhase) error {328+func (d *KeyboardDispatcher) DispatchVimScrollLifecycle(ctx context.Context, action input.Action, phase input.VimScrollPhase) error {
329- spec, ok := pageScrollSpec(action)329+ spec, ok := vimScrollSpec(action)
330 if !ok {330 if !ok {
331 return nil331 return nil
332 }332 }
333 return d.withActiveWebView(ctx, "page scroll lifecycle", func(wv port.WebView) error {333 return d.withActiveWebView(ctx, "page scroll lifecycle", func(wv port.WebView) error {
334- if phase == input.PageScrollStop {334+ if phase == input.VimScrollStop {
335 return d.navCoord.StopPageScroll(ctx, wv)335 return d.navCoord.StopPageScroll(ctx, wv)
336 }336 }
337 return d.navCoord.ScrollWebViewContinuous(ctx, wv, spec.cmd)337 return d.navCoord.ScrollWebViewContinuous(ctx, wv, spec.cmd)
338 })338 })
339 }339 }
340 340
341-type pageScrollActionSpec struct {341+type vimScrollActionSpec struct {
342 cmd usecase.PageScrollCommand342 cmd usecase.PageScrollCommand
343 fast bool343 fast bool
344 }344 }
345 345
346-// pageScrollSpec is the single source of truth for Page Mode action→command346+// vimScrollSpec is the single source of truth for Vim Mode action→command
347 // and fast/slow pulse used by tap handlers and held lifecycle routing.347 // and fast/slow pulse used by tap handlers and held lifecycle routing.
348-func pageScrollSpec(action input.Action) (pageScrollActionSpec, bool) {348+func vimScrollSpec(action input.Action) (vimScrollActionSpec, bool) {
349 switch action {349 switch action {
350- case input.ActionPageScrollLeft:350+ case input.ActionVimScrollLeft:
351- return pageScrollActionSpec{cmd: usecase.PageScrollLeft, fast: false}, true351+ return vimScrollActionSpec{cmd: usecase.PageScrollLeft, fast: false}, true
352- case input.ActionPageScrollRight:352+ case input.ActionVimScrollRight:
353- return pageScrollActionSpec{cmd: usecase.PageScrollRight, fast: false}, true353+ return vimScrollActionSpec{cmd: usecase.PageScrollRight, fast: false}, true
354- case input.ActionPageScrollUp:354+ case input.ActionVimScrollUp:
355- return pageScrollActionSpec{cmd: usecase.PageScrollUp, fast: false}, true355+ return vimScrollActionSpec{cmd: usecase.PageScrollUp, fast: false}, true
356- case input.ActionPageScrollDown:356+ case input.ActionVimScrollDown:
357- return pageScrollActionSpec{cmd: usecase.PageScrollDown, fast: false}, true357+ return vimScrollActionSpec{cmd: usecase.PageScrollDown, fast: false}, true
358- case input.ActionPageScrollUpFast:358+ case input.ActionVimScrollUpFast:
359- return pageScrollActionSpec{cmd: usecase.PageScrollUpFast, fast: true}, true359+ return vimScrollActionSpec{cmd: usecase.PageScrollUpFast, fast: true}, true
360- case input.ActionPageScrollDownFast:360+ case input.ActionVimScrollDownFast:
361- return pageScrollActionSpec{cmd: usecase.PageScrollDownFast, fast: true}, true361+ return vimScrollActionSpec{cmd: usecase.PageScrollDownFast, fast: true}, true
362 default:362 default:
363- return pageScrollActionSpec{}, false363+ return vimScrollActionSpec{}, false
364 }364 }
365 }365 }
366 366
367-func (d *KeyboardDispatcher) pageScrollTapHandler(action input.Action) func(context.Context) error {367+func (d *KeyboardDispatcher) vimScrollTapHandler(action input.Action) func(context.Context) error {
368- spec, ok := pageScrollSpec(action)368+ spec, ok := vimScrollSpec(action)
369 if !ok {369 if !ok {
370 return func(context.Context) error { return nil }370 return func(context.Context) error { return nil }
371 }371 }
@@ -381,8 +381,8 @@ func (d *KeyboardDispatcher) pageScrollTapHandler(action input.Action) func(cont
381 }381 }
382 }382 }
383 383
384-func pageScrollCommand(action input.Action) (usecase.PageScrollCommand, bool) {384+func vimScrollCommand(action input.Action) (usecase.PageScrollCommand, bool) {
385- spec, ok := pageScrollSpec(action)385+ spec, ok := vimScrollSpec(action)
386 if !ok {386 if !ok {
387 return 0, false387 return 0, false
388 }388 }
M internal/ui/dispatcher/keyboard_test.go +28 -28
@@ -220,7 +220,7 @@ func (wv *lifecycleScrollableWebView) CancelPageScroll(context.Context) {
220 wv.cancelCalls++220 wv.cancelCalls++
221 }221 }
222 222
223-func TestKeyboardDispatcher_PageModeActionsRouteToCorrectScrollCommand(t *testing.T) {223+func TestKeyboardDispatcher_VimModeActionsRouteToCorrectScrollCommand(t *testing.T) {
224 ctx := context.Background()224 ctx := context.Background()
225 225
226 tests := []struct {226 tests := []struct {
@@ -229,12 +229,12 @@ func TestKeyboardDispatcher_PageModeActionsRouteToCorrectScrollCommand(t *testin
229 expectedDx int229 expectedDx int
230 expectedDy int230 expectedDy int
231 }{231 }{
232- {input.ActionPageScrollLeft, usecase.PageScrollLeft, -80, 0},232+ {input.ActionVimScrollLeft, usecase.PageScrollLeft, -80, 0},
233- {input.ActionPageScrollRight, usecase.PageScrollRight, 80, 0},233+ {input.ActionVimScrollRight, usecase.PageScrollRight, 80, 0},
234- {input.ActionPageScrollUp, usecase.PageScrollUp, 0, -80},234+ {input.ActionVimScrollUp, usecase.PageScrollUp, 0, -80},
235- {input.ActionPageScrollDown, usecase.PageScrollDown, 0, 80},235+ {input.ActionVimScrollDown, usecase.PageScrollDown, 0, 80},
236- {input.ActionPageScrollUpFast, usecase.PageScrollUpFast, 0, -320},236+ {input.ActionVimScrollUpFast, usecase.PageScrollUpFast, 0, -320},
237- {input.ActionPageScrollDownFast, usecase.PageScrollDownFast, 0, 320},237+ {input.ActionVimScrollDownFast, usecase.PageScrollDownFast, 0, 320},
238 }238 }
239 239
240 for _, tc := range tests {240 for _, tc := range tests {
@@ -268,7 +268,7 @@ func TestKeyboardDispatcher_PageModeActionsRouteToCorrectScrollCommand(t *testin
268 }268 }
269 }269 }
270 270
271-func TestKeyboardDispatcher_PageScrollLifecycleRoutesContinuousAndStop(t *testing.T) {271+func TestKeyboardDispatcher_VimScrollLifecycleRoutesContinuousAndStop(t *testing.T) {
272 ctx := context.Background()272 ctx := context.Background()
273 base := mocks.NewMockWebView(t)273 base := mocks.NewMockWebView(t)
274 scroller := mocks.NewMockPageScrollable(t)274 scroller := mocks.NewMockPageScrollable(t)
@@ -282,12 +282,12 @@ func TestKeyboardDispatcher_PageScrollLifecycleRoutesContinuousAndStop(t *testin
282 scroller.EXPECT().ScrollPage(ctx, port.PageScrollRequest{282 scroller.EXPECT().ScrollPage(ctx, port.PageScrollRequest{
283 Command: port.PageScrollCommandDown, FallbackDY: 80, Continuous: true,283 Command: port.PageScrollCommandDown, FallbackDY: 80, Continuous: true,
284 }).Return(nil).Once()284 }).Return(nil).Once()
285- require.NoError(t, d.DispatchPageScrollLifecycle(ctx, input.ActionPageScrollDown, input.PageScrollContinuous))285+ require.NoError(t, d.DispatchVimScrollLifecycle(ctx, input.ActionVimScrollDown, input.VimScrollContinuous))
286- require.NoError(t, d.DispatchPageScrollLifecycle(ctx, input.ActionPageScrollDown, input.PageScrollStop))286+ require.NoError(t, d.DispatchVimScrollLifecycle(ctx, input.ActionVimScrollDown, input.VimScrollStop))
287 assert.Equal(t, 1, wv.cancelCalls)287 assert.Equal(t, 1, wv.cancelCalls)
288 }288 }
289 289
290-func TestKeyboardDispatcher_PageModeNoopWhenNoActiveWebView(t *testing.T) {290+func TestKeyboardDispatcher_VimModeNoopWhenNoActiveWebView(t *testing.T) {
291 ctx := context.Background()291 ctx := context.Background()
292 292
293 navCoord := &coordinator.NavigationCoordinator{}293 navCoord := &coordinator.NavigationCoordinator{}
@@ -304,11 +304,11 @@ func TestKeyboardDispatcher_PageModeNoopWhenNoActiveWebView(t *testing.T) {
304 )304 )
305 305
306 // No ActiveWebView set — dispatcher should no-op cleanly306 // No ActiveWebView set — dispatcher should no-op cleanly
307- err := d.Dispatch(ctx, input.ActionPageScrollDown)307+ err := d.Dispatch(ctx, input.ActionVimScrollDown)
308 require.NoError(t, err)308 require.NoError(t, err)
309 }309 }
310 310
311-func TestKeyboardDispatcher_PageModeNoopWhenActiveWebViewReturnsNil(t *testing.T) {311+func TestKeyboardDispatcher_VimModeNoopWhenActiveWebViewReturnsNil(t *testing.T) {
312 ctx := context.Background()312 ctx := context.Background()
313 313
314 navCoord := &coordinator.NavigationCoordinator{}314 navCoord := &coordinator.NavigationCoordinator{}
@@ -326,37 +326,37 @@ func TestKeyboardDispatcher_PageModeNoopWhenActiveWebViewReturnsNil(t *testing.T
326 func(context.Context) entity.PaneID { return "" },326 func(context.Context) entity.PaneID { return "" },
327 )327 )
328 328
329- err := d.Dispatch(ctx, input.ActionPageScrollDown)329+ err := d.Dispatch(ctx, input.ActionVimScrollDown)
330 require.NoError(t, err)330 require.NoError(t, err)
331 }331 }
332 332
333-func TestPageScrollSpec_IsSingleSourceForCommandAndPulse(t *testing.T) {333+func TestVimScrollSpec_IsSingleSourceForCommandAndPulse(t *testing.T) {
334 tests := []struct {334 tests := []struct {
335 action input.Action335 action input.Action
336 cmd usecase.PageScrollCommand336 cmd usecase.PageScrollCommand
337 fast bool337 fast bool
338 }{338 }{
339- {input.ActionPageScrollLeft, usecase.PageScrollLeft, false},339+ {input.ActionVimScrollLeft, usecase.PageScrollLeft, false},
340- {input.ActionPageScrollRight, usecase.PageScrollRight, false},340+ {input.ActionVimScrollRight, usecase.PageScrollRight, false},
341- {input.ActionPageScrollUp, usecase.PageScrollUp, false},341+ {input.ActionVimScrollUp, usecase.PageScrollUp, false},
342- {input.ActionPageScrollDown, usecase.PageScrollDown, false},342+ {input.ActionVimScrollDown, usecase.PageScrollDown, false},
343- {input.ActionPageScrollUpFast, usecase.PageScrollUpFast, true},343+ {input.ActionVimScrollUpFast, usecase.PageScrollUpFast, true},
344- {input.ActionPageScrollDownFast, usecase.PageScrollDownFast, true},344+ {input.ActionVimScrollDownFast, usecase.PageScrollDownFast, true},
345 }345 }
346 for _, tc := range tests {346 for _, tc := range tests {
347- spec, ok := pageScrollSpec(tc.action)347+ spec, ok := vimScrollSpec(tc.action)
348 require.True(t, ok, string(tc.action))348 require.True(t, ok, string(tc.action))
349 assert.Equal(t, tc.cmd, spec.cmd)349 assert.Equal(t, tc.cmd, spec.cmd)
350 assert.Equal(t, tc.fast, spec.fast)350 assert.Equal(t, tc.fast, spec.fast)
351- cmd, ok := pageScrollCommand(tc.action)351+ cmd, ok := vimScrollCommand(tc.action)
352 require.True(t, ok)352 require.True(t, ok)
353 assert.Equal(t, tc.cmd, cmd)353 assert.Equal(t, tc.cmd, cmd)
354 }354 }
355- _, ok := pageScrollSpec(input.ActionQuit)355+ _, ok := vimScrollSpec(input.ActionQuit)
356 assert.False(t, ok)356 assert.False(t, ok)
357 }357 }
358 358
359-func TestKeyboardDispatcher_PageScrollTapUsesSharedSpecPulse(t *testing.T) {359+func TestKeyboardDispatcher_VimScrollTapUsesSharedSpecPulse(t *testing.T) {
360 ctx := context.Background()360 ctx := context.Background()
361 base := mocks.NewMockWebView(t)361 base := mocks.NewMockWebView(t)
362 scroller := mocks.NewMockPageScrollable(t)362 scroller := mocks.NewMockPageScrollable(t)
@@ -368,7 +368,7 @@ func TestKeyboardDispatcher_PageScrollTapUsesSharedSpecPulse(t *testing.T) {
368 }, func(context.Context) entity.PaneID { return "" })368 }, func(context.Context) entity.PaneID { return "" })
369 369
370 var pulses []bool370 var pulses []bool
371- d.SetOnPageModePulse(func(_ context.Context, fast bool) {371+ d.SetOnVimModePulse(func(_ context.Context, fast bool) {
372 pulses = append(pulses, fast)372 pulses = append(pulses, fast)
373 })373 })
374 374
@@ -380,7 +380,7 @@ func TestKeyboardDispatcher_PageScrollTapUsesSharedSpecPulse(t *testing.T) {
380 Command: port.PageScrollCommandDownFast, FallbackDY: 320,380 Command: port.PageScrollCommandDownFast, FallbackDY: 320,
381 }).Return(nil).Once()381 }).Return(nil).Once()
382 382
383- require.NoError(t, d.Dispatch(ctx, input.ActionPageScrollDown))383+ require.NoError(t, d.Dispatch(ctx, input.ActionVimScrollDown))
384- require.NoError(t, d.Dispatch(ctx, input.ActionPageScrollDownFast))384+ require.NoError(t, d.Dispatch(ctx, input.ActionVimScrollDownFast))
385 assert.Equal(t, []bool{false, true}, pulses)385 assert.Equal(t, []bool{false, true}, pulses)
386 }386 }
M internal/ui/focus/borders.go +6 -6
@@ -61,8 +61,8 @@ func (bm *BorderManager) OnModeChange(ctx context.Context, from, to input.Mode)
61 bm.borderOverlay.RemoveCssClass(tabModeClass)61 bm.borderOverlay.RemoveCssClass(tabModeClass)
62 case input.ModeSession:62 case input.ModeSession:
63 bm.borderOverlay.RemoveCssClass(sessionModeClass)63 bm.borderOverlay.RemoveCssClass(sessionModeClass)
64- case input.ModePage:64+ case input.ModeVim:
65- // Page mode never activates the global border overlay.65+ // Vim mode never activates the global border overlay.
66 // No class to remove.66 // No class to remove.
67 }67 }
68 68
@@ -77,10 +77,10 @@ func (bm *BorderManager) OnModeChange(ctx context.Context, from, to input.Mode)
77 case input.ModeSession:77 case input.ModeSession:
78 bm.borderOverlay.AddCssClass(sessionModeClass)78 bm.borderOverlay.AddCssClass(sessionModeClass)
79 bm.borderOverlay.SetVisible(true)79 bm.borderOverlay.SetVisible(true)
80- case input.ModePage:80+ case input.ModeVim:
81- // Page mode must not activate the workspace-wide border overlay.81+ // Vim mode must not activate the workspace-wide border overlay.
82- // The pane-local page-mode-active CSS class and indicator are82+ // The pane-local vim-mode-active CSS class and indicator are
83- // managed by PaneView.SetPageMode / app handleModeChange.83+ // managed by PaneView.SetVimMode / app handleModeChange.
84 bm.borderOverlay.SetVisible(false)84 bm.borderOverlay.SetVisible(false)
85 case input.ModeNormal:85 case input.ModeNormal:
86 bm.borderOverlay.SetVisible(false)86 bm.borderOverlay.SetVisible(false)
M internal/ui/input/global_shortcuts.go +4 -4
@@ -114,7 +114,7 @@ func NewGlobalShortcutHandler(
114 114
115 // Register app-reserved shortcuts (Ctrl+L, Ctrl+F, other mode activations, etc.)115 // Register app-reserved shortcuts (Ctrl+L, Ctrl+F, other mode activations, etc.)
116 // so they work even when WebView has focus.116 // so they work even when WebView has focus.
117- // Page mode activation is intentionally excluded here: it must flow through117+ // Vim mode activation is intentionally excluded here: it must flow through
118 // the normal KeyboardHandler path so editable page contexts can pass Ctrl+Y118 // the normal KeyboardHandler path so editable page contexts can pass Ctrl+Y
119 // through instead of having a global shortcut controller consume it first.119 // through instead of having a global shortcut controller consume it first.
120 shortcuts := NewShortcutSet(ctx, workspace, session)120 shortcuts := NewShortcutSet(ctx, workspace, session)
@@ -496,11 +496,11 @@ func appendGlobalShortcutEventFields(evt *zerolog.Event, binding KeyBinding, act
496 }496 }
497 497
498 func shouldRegisterGTKGlobalShortcut(action Action) bool {498 func shouldRegisterGTKGlobalShortcut(action Action) bool {
499- return action != ActionEnterPageMode499+ return action != ActionEnterVimMode
500 }500 }
501 501
502 func shouldIgnoreGlobalShortcutInMode(mode Mode, action Action) bool {502 func shouldIgnoreGlobalShortcutInMode(mode Mode, action Action) bool {
503- return mode == ModePage && action != ActionEnterPageMode503+ return mode == ModeVim && action != ActionEnterVimMode
504 }504 }
505 505
506 func globalShortcutActionMap() map[string]Action {506 func globalShortcutActionMap() map[string]Action {
@@ -840,7 +840,7 @@ func formatEventType(eventType gdk.EventType) string {
840 840
841 func isModeAction(action Action) bool {841 func isModeAction(action Action) bool {
842 switch action {842 switch action {
843- case ActionEnterTabMode, ActionEnterPaneMode, ActionEnterSessionMode, ActionEnterResizeMode, ActionEnterPageMode, ActionExitMode:843+ case ActionEnterTabMode, ActionEnterPaneMode, ActionEnterSessionMode, ActionEnterResizeMode, ActionEnterVimMode, ActionExitMode:
844 return true844 return true
845 default:845 default:
846 return false846 return false
M internal/ui/input/global_shortcuts_test.go +9 -9
@@ -10,24 +10,24 @@ import (
10 "github.com/bnema/puregotk/v4/gtk"10 "github.com/bnema/puregotk/v4/gtk"
11 )11 )
12 12
13-func TestShouldRegisterGTKGlobalShortcut_ExcludesPageModeActivation(t *testing.T) {13+func TestShouldRegisterGTKGlobalShortcut_ExcludesVimModeActivation(t *testing.T) {
14- if shouldRegisterGTKGlobalShortcut(ActionEnterPageMode) {14+ if shouldRegisterGTKGlobalShortcut(ActionEnterVimMode) {
15- t.Fatal("ActionEnterPageMode should stay off the GTK global shortcut controller")15+ t.Fatal("ActionEnterVimMode should stay off the GTK global shortcut controller")
16 }16 }
17 if !shouldRegisterGTKGlobalShortcut(ActionOpenOmnibox) {17 if !shouldRegisterGTKGlobalShortcut(ActionOpenOmnibox) {
18- t.Fatal("non-page-mode global shortcuts should remain registerable")18+ t.Fatal("non-vim-mode global shortcuts should remain registerable")
19 }19 }
20 }20 }
21 21
22-func TestShouldIgnoreGlobalShortcutInMode_PageModeBlocksNonToggleActions(t *testing.T) {22+func TestShouldIgnoreGlobalShortcutInMode_VimModeBlocksNonToggleActions(t *testing.T) {
23- if !shouldIgnoreGlobalShortcutInMode(ModePage, ActionOpenOmnibox) {23+ if !shouldIgnoreGlobalShortcutInMode(ModeVim, ActionOpenOmnibox) {
24- t.Fatal("Page mode should ignore app-global shortcuts like omnibox")24+ t.Fatal("Vim mode should ignore app-global shortcuts like omnibox")
25 }25 }
26 if shouldIgnoreGlobalShortcutInMode(ModeNormal, ActionOpenOmnibox) {26 if shouldIgnoreGlobalShortcutInMode(ModeNormal, ActionOpenOmnibox) {
27 t.Fatal("normal mode should not ignore app-global shortcuts")27 t.Fatal("normal mode should not ignore app-global shortcuts")
28 }28 }
29- if shouldIgnoreGlobalShortcutInMode(ModePage, ActionEnterPageMode) {29+ if shouldIgnoreGlobalShortcutInMode(ModeVim, ActionEnterVimMode) {
30- t.Fatal("page mode toggle should not be blocked by the mode guard")30+ t.Fatal("vim mode toggle should not be blocked by the mode guard")
31 }31 }
32 }32 }
33 33
M internal/ui/input/keyboard.go +116 -116
@@ -36,22 +36,22 @@ import (
36 // Return an error if the action fails.36 // Return an error if the action fails.
37 type ActionHandler func(ctx context.Context, action Action) error37 type ActionHandler func(ctx context.Context, action Action) error
38 38
39-// PageScrollPhase identifies held-scroll lifecycle events. It is internal input39+// VimScrollPhase identifies held-scroll lifecycle events. It is internal input
40 // state, not a configurable action or shortcut.40 // state, not a configurable action or shortcut.
41-type PageScrollPhase uint841+type VimScrollPhase uint8
42 42
43 const (43 const (
44- PageScrollContinuous PageScrollPhase = iota44+ VimScrollContinuous VimScrollPhase = iota
45- PageScrollStop45+ VimScrollStop
46 )46 )
47 47
48-// PageScrollLifecycleHandler receives autonomous held-scroll ticks and the48+// VimScrollLifecycleHandler receives autonomous held-scroll ticks and the
49 // matching stop event without changing the normal configurable action API.49 // matching stop event without changing the normal configurable action API.
50-type PageScrollLifecycleHandler func(ctx context.Context, action Action, phase PageScrollPhase) error50+type VimScrollLifecycleHandler func(ctx context.Context, action Action, phase VimScrollPhase) error
51 51
52 const (52 const (
53- pageScrollHoldDelay = 250 * time.Millisecond53+ vimScrollHoldDelay = 250 * time.Millisecond
54- pageScrollCadence = 16 * time.Millisecond54+ vimScrollCadence = 16 * time.Millisecond
55 )55 )
56 56
57 // AccentHandler handles long-press accent detection.57 // AccentHandler handles long-press accent detection.
@@ -77,8 +77,8 @@ type KeyboardHandler struct {
77 session *entity.SessionConfig77 session *entity.SessionConfig
78 78
79 // Action handler callbacks.79 // Action handler callbacks.
80- onAction ActionHandler80+ onAction ActionHandler
81- onPageScrollLifecycle PageScrollLifecycleHandler81+ onVimScrollLifecycle VimScrollLifecycleHandler
82 // Optional routing callback that determines how a key should be handled.82 // Optional routing callback that determines how a key should be handled.
83 // Returns RouteHandleShortcuts (default), RoutePassToWidget (let focused83 // Returns RouteHandleShortcuts (default), RoutePassToWidget (let focused
84 // widget handle it), or RouteAccentDetection (long-press accent for GTK entries).84 // widget handle it), or RouteAccentDetection (long-press accent for GTK entries).
@@ -87,9 +87,9 @@ type KeyboardHandler struct {
87 accentHandler AccentHandler87 accentHandler AccentHandler
88 // Optional escape hook for app-level overlays88 // Optional escape hook for app-level overlays
89 onEscape func(ctx context.Context) bool89 onEscape func(ctx context.Context) bool
90- // Optional hook that blocks Page mode activation and lets the original key90+ // Optional hook that blocks Vim mode activation and lets the original key
91 // event pass through to the focused widget/page instead.91 // event pass through to the focused widget/page instead.
92- pageModeActivationPassthrough func() bool92+ vimModeActivationPassthrough func() bool
93 93
94 // GTK controller (nil until attached)94 // GTK controller (nil until attached)
95 controller *gtk.EventControllerKey95 controller *gtk.EventControllerKey
@@ -105,15 +105,15 @@ type KeyboardHandler struct {
105 ctx context.Context105 ctx context.Context
106 mu sync.RWMutex106 mu sync.RWMutex
107 107
108- pageScrollRepeatAction Action108+ vimScrollRepeatAction Action
109- pageScrollRepeatKeyval uint109+ vimScrollRepeatKeyval uint
110- pageScrollRepeatTimer uint110+ vimScrollRepeatTimer uint
111- pageScrollRepeatGeneration uint64111+ vimScrollRepeatGeneration uint64
112- pageScrollRepeatPressedAt time.Time112+ vimScrollRepeatPressedAt time.Time
113- pageScrollRepeatCb glib.SourceFunc113+ vimScrollRepeatCb glib.SourceFunc
114- pageScrollRepeatAdd func(intervalMS uint, cb *glib.SourceFunc) uint114+ vimScrollRepeatAdd func(intervalMS uint, cb *glib.SourceFunc) uint
115- pageScrollRepeatRemove func(id uint) bool115+ vimScrollRepeatRemove func(id uint) bool
116- pageScrollNow func() time.Time116+ vimScrollNow func() time.Time
117 }117 }
118 118
119 // NewKeyboardHandler creates a new keyboard handler.119 // NewKeyboardHandler creates a new keyboard handler.
@@ -129,11 +129,11 @@ func NewKeyboardHandler(ctx context.Context, workspace *entity.WorkspaceConfig,
129 session: session,129 session: session,
130 activePressedActions: make(map[Action]uint),130 activePressedActions: make(map[Action]uint),
131 ctx: ctx,131 ctx: ctx,
132- pageScrollRepeatAdd: func(intervalMS uint, cb *glib.SourceFunc) uint {132+ vimScrollRepeatAdd: func(intervalMS uint, cb *glib.SourceFunc) uint {
133 return glib.TimeoutAdd(intervalMS, cb, 0)133 return glib.TimeoutAdd(intervalMS, cb, 0)
134 },134 },
135- pageScrollRepeatRemove: glib.SourceRemove,135+ vimScrollRepeatRemove: glib.SourceRemove,
136- pageScrollNow: time.Now,136+ vimScrollNow: time.Now,
137 }137 }
138 h.SetOnModeChange(nil)138 h.SetOnModeChange(nil)
139 139
@@ -147,26 +147,26 @@ func (h *KeyboardHandler) SetOnAction(fn ActionHandler) {
147 h.onAction = fn147 h.onAction = fn
148 }148 }
149 149
150-// SetOnPageScrollLifecycle sets the callback for autonomous held-scroll ticks150+// SetOnVimScrollLifecycle sets the callback for autonomous held-scroll ticks
151 // and stop notification. Tap actions continue through SetOnAction.151 // and stop notification. Tap actions continue through SetOnAction.
152-func (h *KeyboardHandler) SetOnPageScrollLifecycle(fn PageScrollLifecycleHandler) {152+func (h *KeyboardHandler) SetOnVimScrollLifecycle(fn VimScrollLifecycleHandler) {
153 h.mu.Lock()153 h.mu.Lock()
154 defer h.mu.Unlock()154 defer h.mu.Unlock()
155- h.onPageScrollLifecycle = fn155+ h.onVimScrollLifecycle = fn
156 }156 }
157 157
158-// SetPageModeActivationPassthrough sets the callback that can block Page mode158+// SetVimModeActivationPassthrough sets the callback that can block Vim mode
159 // activation and let the original key event propagate instead.159 // activation and let the original key event propagate instead.
160-func (h *KeyboardHandler) SetPageModeActivationPassthrough(fn func() bool) {160+func (h *KeyboardHandler) SetVimModeActivationPassthrough(fn func() bool) {
161 h.mu.Lock()161 h.mu.Lock()
162 defer h.mu.Unlock()162 defer h.mu.Unlock()
163- h.pageModeActivationPassthrough = fn163+ h.vimModeActivationPassthrough = fn
164 }164 }
165 165
166 // ReloadShortcuts rebuilds the shortcut set from new config values.166 // ReloadShortcuts rebuilds the shortcut set from new config values.
167 // This enables hot-reloading of keybindings without restarting.167 // This enables hot-reloading of keybindings without restarting.
168 func (h *KeyboardHandler) ReloadShortcuts(ctx context.Context, workspace *entity.WorkspaceConfig, session *entity.SessionConfig) {168 func (h *KeyboardHandler) ReloadShortcuts(ctx context.Context, workspace *entity.WorkspaceConfig, session *entity.SessionConfig) {
169- h.stopPageScrollRepeat()169+ h.stopVimScrollRepeat()
170 h.mu.Lock()170 h.mu.Lock()
171 defer h.mu.Unlock()171 defer h.mu.Unlock()
172 172
@@ -189,8 +189,8 @@ func (h *KeyboardHandler) SetOnModeChange(fn func(from, to Mode)) {
189 } else if from == ModeNormal {189 } else if from == ModeNormal {
190 h.setControllerPhase(gtk.PhaseCaptureValue)190 h.setControllerPhase(gtk.PhaseCaptureValue)
191 }191 }
192- if to != ModePage {192+ if to != ModeVim {
193- h.stopPageScrollRepeat()193+ h.stopVimScrollRepeat()
194 }194 }
195 // Forward to app-level callback195 // Forward to app-level callback
196 if fn != nil {196 if fn != nil {
@@ -315,7 +315,7 @@ func (h *KeyboardHandler) DetachForDestroy() {
315 }315 }
316 316
317 func (h *KeyboardHandler) detach(removeController bool) {317 func (h *KeyboardHandler) detach(removeController bool) {
318- h.stopPageScrollRepeat()318+ h.stopVimScrollRepeat()
319 h.mu.Lock()319 h.mu.Lock()
320 if h.controller != nil && h.keyPressedHandlerID != 0 {320 if h.controller != nil && h.keyPressedHandlerID != 0 {
321 h.controller.DisconnectSignal(h.keyPressedHandlerID)321 h.controller.DisconnectSignal(h.keyPressedHandlerID)
@@ -367,8 +367,8 @@ func (h *KeyboardHandler) handleKeyPress(keyval, keycode uint, state gdk.Modifie
367 if mode != ModeNormal && modifiers == 0 {367 if mode != ModeNormal && modifiers == 0 {
368 switch keyval {368 switch keyval {
369 case uint(gdk.KEY_Escape), uint(gdk.KEY_Return), uint(gdk.KEY_KP_Enter):369 case uint(gdk.KEY_Escape), uint(gdk.KEY_Return), uint(gdk.KEY_KP_Enter):
370- if mode == ModePage {370+ if mode == ModeVim {
371- h.stopPageScrollRepeat()371+ h.stopVimScrollRepeat()
372 }372 }
373 h.modal.ExitMode(h.ctx)373 h.modal.ExitMode(h.ctx)
374 return true374 return true
@@ -424,17 +424,17 @@ func (h *KeyboardHandler) handleShortcutLookupResult(
424 modifiers Modifier,424 modifiers Modifier,
425 ) bool {425 ) bool {
426 if !found {426 if !found {
427- if shouldPassthroughNativePageModeNavigation(mode, keyval, modifiers) {427+ if shouldPassthroughNativeVimModeNavigation(mode, keyval, modifiers) {
428- log.Trace().Uint("keyval", keyval).Msg("routing native page navigation key to focused widget in page mode")428+ log.Trace().Uint("keyval", keyval).Msg("routing native page navigation key to focused widget in vim mode")
429 return false429 return false
430 }430 }
431 return mode != ModeNormal // Consume unrecognized keys in modal mode431 return mode != ModeNormal // Consume unrecognized keys in modal mode
432 }432 }
433- if h.shouldPassthroughPageModeActivation(action, mode) {433+ if h.shouldPassthroughVimModeActivation(action, mode) {
434 return false434 return false
435 }435 }
436- if mode == ModePage && isPageScrollAction(action) {436+ if mode == ModeVim && isVimScrollAction(action) {
437- return h.handlePageScrollAction(log, action, mode, keyval)437+ return h.handleVimScrollAction(log, action, mode, keyval)
438 }438 }
439 if h.suppressHeldAction(action, keyval) {439 if h.suppressHeldAction(action, keyval) {
440 log.Trace().440 log.Trace().
@@ -445,105 +445,105 @@ func (h *KeyboardHandler) handleShortcutLookupResult(
445 return h.dispatchAction(action, mode)445 return h.dispatchAction(action, mode)
446 }446 }
447 447
448-func (h *KeyboardHandler) handlePageScrollAction(log *zerolog.Logger, action Action, mode Mode, keyval uint) bool {448+func (h *KeyboardHandler) handleVimScrollAction(log *zerolog.Logger, action Action, mode Mode, keyval uint) bool {
449 h.mu.RLock()449 h.mu.RLock()
450- sameHeldKey := h.pageScrollRepeatAction == action &&450+ sameHeldKey := h.vimScrollRepeatAction == action &&
451- h.pageScrollRepeatKeyval == keyval && h.pageScrollRepeatTimer != 0451+ h.vimScrollRepeatKeyval == keyval && h.vimScrollRepeatTimer != 0
452 h.mu.RUnlock()452 h.mu.RUnlock()
453 if sameHeldKey {453 if sameHeldKey {
454- log.Trace().Str("action", string(action)).Msg("page scroll OS repeat suppressed")454+ log.Trace().Str("action", string(action)).Msg("vim scroll OS repeat suppressed")
455 return true455 return true
456 }456 }
457 457
458- h.stopPageScrollRepeat()458+ h.stopVimScrollRepeat()
459 if !h.dispatchAction(action, mode) {459 if !h.dispatchAction(action, mode) {
460 return false460 return false
461 }461 }
462- h.startPageScrollRepeat(action, keyval)462+ h.startVimScrollRepeat(action, keyval)
463 return true463 return true
464 }464 }
465 465
466-func (h *KeyboardHandler) startPageScrollRepeat(action Action, keyval uint) {466+func (h *KeyboardHandler) startVimScrollRepeat(action Action, keyval uint) {
467 h.mu.Lock()467 h.mu.Lock()
468- if h.pageScrollRepeatTimer != 0 || h.pageScrollRepeatAdd == nil {468+ if h.vimScrollRepeatTimer != 0 || h.vimScrollRepeatAdd == nil {
469 h.mu.Unlock()469 h.mu.Unlock()
470 return470 return
471 }471 }
472- h.pageScrollRepeatGeneration++472+ h.vimScrollRepeatGeneration++
473- generation := h.pageScrollRepeatGeneration473+ generation := h.vimScrollRepeatGeneration
474- h.pageScrollRepeatAction = action474+ h.vimScrollRepeatAction = action
475- h.pageScrollRepeatKeyval = keyval475+ h.vimScrollRepeatKeyval = keyval
476- h.pageScrollRepeatPressedAt = h.pageScrollNow()476+ h.vimScrollRepeatPressedAt = h.vimScrollNow()
477 cb := glib.SourceFunc(func(_ uintptr) bool {477 cb := glib.SourceFunc(func(_ uintptr) bool {
478- return h.pageScrollRepeatTick(action, keyval, generation)478+ return h.vimScrollRepeatTick(action, keyval, generation)
479 })479 })
480- h.pageScrollRepeatCb = cb480+ h.vimScrollRepeatCb = cb
481- add := h.pageScrollRepeatAdd481+ add := h.vimScrollRepeatAdd
482- id := add(uint(pageScrollCadence/time.Millisecond), &h.pageScrollRepeatCb)482+ id := add(uint(vimScrollCadence/time.Millisecond), &h.vimScrollRepeatCb)
483 if id == 0 {483 if id == 0 {
484- h.clearPageScrollRepeatLocked()484+ h.clearVimScrollRepeatLocked()
485 h.mu.Unlock()485 h.mu.Unlock()
486 return486 return
487 }487 }
488- h.pageScrollRepeatTimer = id488+ h.vimScrollRepeatTimer = id
489 h.mu.Unlock()489 h.mu.Unlock()
490 }490 }
491 491
492-func (h *KeyboardHandler) pageScrollRepeatTick(action Action, keyval uint, generation uint64) bool {492+func (h *KeyboardHandler) vimScrollRepeatTick(action Action, keyval uint, generation uint64) bool {
493 h.mu.RLock()493 h.mu.RLock()
494- active := h.pageScrollRepeatAction == action && h.pageScrollRepeatKeyval == keyval &&494+ active := h.vimScrollRepeatAction == action && h.vimScrollRepeatKeyval == keyval &&
495- h.pageScrollRepeatTimer != 0 && h.pageScrollRepeatGeneration == generation495+ h.vimScrollRepeatTimer != 0 && h.vimScrollRepeatGeneration == generation
496- pressedAt := h.pageScrollRepeatPressedAt496+ pressedAt := h.vimScrollRepeatPressedAt
497- now := h.pageScrollNow497+ now := h.vimScrollNow
498 h.mu.RUnlock()498 h.mu.RUnlock()
499- if !active || h.modal.Mode() != ModePage {499+ if !active || h.modal.Mode() != ModeVim {
500 return false500 return false
501 }501 }
502- if now().Sub(pressedAt) < pageScrollHoldDelay {502+ if now().Sub(pressedAt) < vimScrollHoldDelay {
503 return true503 return true
504 }504 }
505- // Continuous dispatch bypasses dispatchAction, so refresh Page Mode timeout here.505+ // Continuous dispatch bypasses dispatchAction, so refresh Vim Mode timeout here.
506 h.modal.ResetTimeout(h.ctx)506 h.modal.ResetTimeout(h.ctx)
507- h.dispatchPageScrollLifecycle(action, PageScrollContinuous)507+ h.dispatchVimScrollLifecycle(action, VimScrollContinuous)
508 return true508 return true
509 }509 }
510 510
511-func (h *KeyboardHandler) stopPageScrollRepeat() {511+func (h *KeyboardHandler) stopVimScrollRepeat() {
512 h.mu.Lock()512 h.mu.Lock()
513- action := h.pageScrollRepeatAction513+ action := h.vimScrollRepeatAction
514- timerID := h.pageScrollRepeatTimer514+ timerID := h.vimScrollRepeatTimer
515- remove := h.pageScrollRepeatRemove515+ remove := h.vimScrollRepeatRemove
516- h.clearPageScrollRepeatLocked()516+ h.clearVimScrollRepeatLocked()
517 h.mu.Unlock()517 h.mu.Unlock()
518 if timerID != 0 && remove != nil {518 if timerID != 0 && remove != nil {
519 remove(timerID)519 remove(timerID)
520 }520 }
521 if action != "" {521 if action != "" {
522- h.dispatchPageScrollLifecycle(action, PageScrollStop)522+ h.dispatchVimScrollLifecycle(action, VimScrollStop)
523 }523 }
524 }524 }
525 525
526-func (h *KeyboardHandler) clearPageScrollRepeatLocked() {526+func (h *KeyboardHandler) clearVimScrollRepeatLocked() {
527- h.pageScrollRepeatGeneration++527+ h.vimScrollRepeatGeneration++
528- h.pageScrollRepeatAction = ""528+ h.vimScrollRepeatAction = ""
529- h.pageScrollRepeatKeyval = 0529+ h.vimScrollRepeatKeyval = 0
530- h.pageScrollRepeatTimer = 0530+ h.vimScrollRepeatTimer = 0
531- h.pageScrollRepeatPressedAt = time.Time{}531+ h.vimScrollRepeatPressedAt = time.Time{}
532- h.pageScrollRepeatCb = nil532+ h.vimScrollRepeatCb = nil
533 }533 }
534 534
535-func (h *KeyboardHandler) dispatchPageScrollLifecycle(action Action, phase PageScrollPhase) {535+func (h *KeyboardHandler) dispatchVimScrollLifecycle(action Action, phase VimScrollPhase) {
536 h.mu.RLock()536 h.mu.RLock()
537- handler := h.onPageScrollLifecycle537+ handler := h.onVimScrollLifecycle
538 h.mu.RUnlock()538 h.mu.RUnlock()
539 if handler == nil {539 if handler == nil {
540- if phase == PageScrollContinuous {540+ if phase == VimScrollContinuous {
541- h.dispatchAction(action, ModePage)541+ h.dispatchAction(action, ModeVim)
542 }542 }
543 return543 return
544 }544 }
545 if err := handler(h.ctx, action, phase); err != nil {545 if err := handler(h.ctx, action, phase); err != nil {
546- logging.FromContext(h.ctx).Error().Err(err).Str("action", string(action)).Msg("page scroll lifecycle handler error")546+ logging.FromContext(h.ctx).Error().Err(err).Str("action", string(action)).Msg("vim scroll lifecycle handler error")
547 }547 }
548 }548 }
549 549
@@ -575,8 +575,8 @@ func normalizeKeyval(keyval uint) uint {
575 return keyval575 return keyval
576 }576 }
577 577
578-func shouldPassthroughNativePageModeNavigation(mode Mode, keyval uint, modifiers Modifier) bool {578+func shouldPassthroughNativeVimModeNavigation(mode Mode, keyval uint, modifiers Modifier) bool {
579- if mode != ModePage || modifiers != ModNone {579+ if mode != ModeVim || modifiers != ModNone {
580 return false580 return false
581 }581 }
582 switch keyval {582 switch keyval {
@@ -607,7 +607,7 @@ func (h *KeyboardHandler) dispatchAction(action Action, mode Mode) bool {
607 if mode == ModeResize && isResizeAction(action) {607 if mode == ModeResize && isResizeAction(action) {
608 h.modal.ResetTimeout(h.ctx)608 h.modal.ResetTimeout(h.ctx)
609 }609 }
610- if mode == ModePage && isPageScrollAction(action) {610+ if mode == ModeVim && isVimScrollAction(action) {
611 h.modal.ResetTimeout(h.ctx)611 h.modal.ResetTimeout(h.ctx)
612 }612 }
613 613
@@ -644,7 +644,7 @@ func isRepeatedKeyboardActionSuppressed(action Action) bool {
644 ActionEnterPaneMode,644 ActionEnterPaneMode,
645 ActionEnterSessionMode,645 ActionEnterSessionMode,
646 ActionEnterResizeMode,646 ActionEnterResizeMode,
647- ActionEnterPageMode,647+ ActionEnterVimMode,
648 ActionNewTab,648 ActionNewTab,
649 ActionRenameTab,649 ActionRenameTab,
650 ActionSplitRight,650 ActionSplitRight,
@@ -721,14 +721,14 @@ func isResizeAction(action Action) bool {
721 }721 }
722 }722 }
723 723
724-func isPageScrollAction(action Action) bool {724+func isVimScrollAction(action Action) bool {
725 switch action {725 switch action {
726- case ActionPageScrollLeft,726+ case ActionVimScrollLeft,
727- ActionPageScrollDown,727+ ActionVimScrollDown,
728- ActionPageScrollUp,728+ ActionVimScrollUp,
729- ActionPageScrollRight,729+ ActionVimScrollRight,
730- ActionPageScrollDownFast,730+ ActionVimScrollDownFast,
731- ActionPageScrollUpFast:731+ ActionVimScrollUpFast:
732 return true732 return true
733 default:733 default:
734 return false734 return false
@@ -774,21 +774,21 @@ func (h *KeyboardHandler) handleModeAction(action Action) bool {
774 }774 }
775 h.modal.EnterResizeMode(h.ctx, time.Duration(ms)*time.Millisecond)775 h.modal.EnterResizeMode(h.ctx, time.Duration(ms)*time.Millisecond)
776 return true776 return true
777- case ActionEnterPageMode:777+ case ActionEnterVimMode:
778- if h.modal.Mode() == ModePage {778+ if h.modal.Mode() == ModeVim {
779- h.stopPageScrollRepeat()779+ h.stopVimScrollRepeat()
780 h.modal.ExitMode(h.ctx)780 h.modal.ExitMode(h.ctx)
781 return true781 return true
782 }782 }
783 var pgms int783 var pgms int
784 if workspace != nil {784 if workspace != nil {
785- pgms = workspace.PageMode.TimeoutMilliseconds785+ pgms = workspace.VimMode.TimeoutMilliseconds
786 }786 }
787- h.modal.EnterPageMode(h.ctx, time.Duration(pgms)*time.Millisecond)787+ h.modal.EnterVimMode(h.ctx, time.Duration(pgms)*time.Millisecond)
788 return true788 return true
789 case ActionExitMode:789 case ActionExitMode:
790- if h.modal.Mode() == ModePage {790+ if h.modal.Mode() == ModeVim {
791- h.stopPageScrollRepeat()791+ h.stopVimScrollRepeat()
792 }792 }
793 h.modal.ExitMode(h.ctx)793 h.modal.ExitMode(h.ctx)
794 return true794 return true
@@ -836,17 +836,17 @@ func (h *KeyboardHandler) EnterSessionMode() {
836 h.modal.EnterSessionMode(h.ctx, time.Duration(ms)*time.Millisecond)836 h.modal.EnterSessionMode(h.ctx, time.Duration(ms)*time.Millisecond)
837 }837 }
838 838
839-// EnterPageMode programmatically enters page scrolling mode.839+// EnterVimMode programmatically enters Vim-style scrolling mode.
840 // Useful for testing or programmatic mode changes.840 // Useful for testing or programmatic mode changes.
841-func (h *KeyboardHandler) EnterPageMode() {841+func (h *KeyboardHandler) EnterVimMode() {
842 h.mu.RLock()842 h.mu.RLock()
843 workspace := h.workspace843 workspace := h.workspace
844 h.mu.RUnlock()844 h.mu.RUnlock()
845 var ms int845 var ms int
846 if workspace != nil {846 if workspace != nil {
847- ms = workspace.PageMode.TimeoutMilliseconds847+ ms = workspace.VimMode.TimeoutMilliseconds
848 }848 }
849- h.modal.EnterPageMode(h.ctx, time.Duration(ms)*time.Millisecond)849+ h.modal.EnterVimMode(h.ctx, time.Duration(ms)*time.Millisecond)
850 }850 }
851 851
852 // ExitMode programmatically exits modal mode.852 // ExitMode programmatically exits modal mode.
@@ -855,12 +855,12 @@ func (h *KeyboardHandler) ExitMode() {
855 h.modal.ExitMode(h.ctx)855 h.modal.ExitMode(h.ctx)
856 }856 }
857 857
858-func (h *KeyboardHandler) shouldPassthroughPageModeActivation(action Action, mode Mode) bool {858+func (h *KeyboardHandler) shouldPassthroughVimModeActivation(action Action, mode Mode) bool {
859- if action != ActionEnterPageMode || mode != ModeNormal {859+ if action != ActionEnterVimMode || mode != ModeNormal {
860 return false860 return false
861 }861 }
862 h.mu.RLock()862 h.mu.RLock()
863- passthrough := h.pageModeActivationPassthrough863+ passthrough := h.vimModeActivationPassthrough
864 h.mu.RUnlock()864 h.mu.RUnlock()
865 return passthrough != nil && passthrough()865 return passthrough != nil && passthrough()
866 }866 }
@@ -870,7 +870,7 @@ func (h *KeyboardHandler) shouldPassthroughPageModeActivation(action Action, mod
870 // to the registered action handler. It returns whether the action was consumed.870 // to the registered action handler. It returns whether the action was consumed.
871 func (h *KeyboardHandler) DispatchAction(action Action) bool {871 func (h *KeyboardHandler) DispatchAction(action Action) bool {
872 mode := h.modal.Mode()872 mode := h.modal.Mode()
873- if h.shouldPassthroughPageModeActivation(action, mode) {873+ if h.shouldPassthroughVimModeActivation(action, mode) {
874 return false874 return false
875 }875 }
876 h.dispatchAction(action, mode)876 h.dispatchAction(action, mode)
@@ -889,10 +889,10 @@ func (h *KeyboardHandler) handleKeyRelease(keyval uint) {
889 }889 }
890 }890 }
891 }891 }
892- stopPageScroll := h.pageScrollRepeatKeyval == keyval892+ stopVimScroll := h.vimScrollRepeatKeyval == keyval
893 h.mu.Unlock()893 h.mu.Unlock()
894- if stopPageScroll {894+ if stopVimScroll {
895- h.stopPageScrollRepeat()895+ h.stopVimScrollRepeat()
896 }896 }
897 897
898 if accentHandler == nil {898 if accentHandler == nil {
M internal/ui/input/keyboard_test.go +159 -159
@@ -369,10 +369,10 @@ func TestIsRepeatedKeyboardActionSuppressed_AllowsResizeStepRepeats(t *testing.T
369 }369 }
370 }370 }
371 371
372-func TestHandleKeyPress_EnterPageMode(t *testing.T) {372+func TestHandleKeyPress_EnterVimMode(t *testing.T) {
373 ctx := context.Background()373 ctx := context.Background()
374 workspace := newTestWorkspace()374 workspace := newTestWorkspace()
375- workspace.PageMode = entity.PageModeConfig{375+ workspace.VimMode = entity.VimModeConfig{
376 ActivationShortcut: "ctrl+y",376 ActivationShortcut: "ctrl+y",
377 }377 }
378 378
@@ -382,25 +382,25 @@ func TestHandleKeyPress_EnterPageMode(t *testing.T) {
382 return nil382 return nil
383 })383 })
384 384
385- // Ctrl+Y should enter page mode385+ // Ctrl+Y should enter vim mode
386 result := h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)386 result := h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)
387 if !result {387 if !result {
388 t.Fatal("Ctrl+Y should be consumed")388 t.Fatal("Ctrl+Y should be consumed")
389 }389 }
390- if h.Mode() != ModePage {390+ if h.Mode() != ModeVim {
391- t.Fatalf("mode = %v, want ModePage", h.Mode())391+ t.Fatalf("mode = %v, want ModeVim", h.Mode())
392 }392 }
393 }393 }
394 394
395-func TestHandleKeyPress_PageModeStaysActiveAfterScroll(t *testing.T) {395+func TestHandleKeyPress_VimModeStaysActiveAfterScroll(t *testing.T) {
396 ctx := context.Background()396 ctx := context.Background()
397 workspace := newTestWorkspace()397 workspace := newTestWorkspace()
398- workspace.PageMode = entity.PageModeConfig{398+ workspace.VimMode = entity.VimModeConfig{
399 ActivationShortcut: "ctrl+y",399 ActivationShortcut: "ctrl+y",
400 Actions: map[string]entity.ActionBinding{400 Actions: map[string]entity.ActionBinding{
401- "page-scroll-down": {Keys: []string{"j"}},401+ "vim-scroll-down": {Keys: []string{"j"}},
402- "page-scroll-up": {Keys: []string{"k"}},402+ "vim-scroll-up": {Keys: []string{"k"}},
403- "cancel": {Keys: []string{"escape"}},403+ "cancel": {Keys: []string{"escape"}},
404 },404 },
405 }405 }
406 406
@@ -411,50 +411,50 @@ func TestHandleKeyPress_PageModeStaysActiveAfterScroll(t *testing.T) {
411 return nil411 return nil
412 })412 })
413 413
414- // Enter page mode414+ // Enter vim mode
415 result := h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)415 result := h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)
416- if !result || h.Mode() != ModePage {416+ if !result || h.Mode() != ModeVim {
417- t.Fatal("failed to enter page mode")417+ t.Fatal("failed to enter vim mode")
418 }418 }
419 419
420 var repeatTick glib.SourceFunc420 var repeatTick glib.SourceFunc
421 repeatNow := time.Unix(2_000, 0)421 repeatNow := time.Unix(2_000, 0)
422- h.pageScrollNow = func() time.Time { return repeatNow }422+ h.vimScrollNow = func() time.Time { return repeatNow }
423- h.pageScrollRepeatAdd = func(_ uint, cb *glib.SourceFunc) uint {423+ h.vimScrollRepeatAdd = func(_ uint, cb *glib.SourceFunc) uint {
424 repeatTick = *cb424 repeatTick = *cb
425 return 1425 return 1
426 }426 }
427- h.pageScrollRepeatRemove = func(uint) bool { return true }427+ h.vimScrollRepeatRemove = func(uint) bool { return true }
428 428
429- // Scroll actions stay in page mode; the first press arms the smooth429+ // Scroll actions stay in vim mode; the first press arms the smooth
430 // repeater and later physical auto-repeat events are ignored.430 // repeater and later physical auto-repeat events are ignored.
431 for i := 0; i < 2; i++ {431 for i := 0; i < 2; i++ {
432 scrolled := h.handleKeyPress(uint('j'), 0, 0)432 scrolled := h.handleKeyPress(uint('j'), 0, 0)
433 if !scrolled {433 if !scrolled {
434 t.Fatalf("scroll down iteration %d: key not consumed", i)434 t.Fatalf("scroll down iteration %d: key not consumed", i)
435 }435 }
436- if h.Mode() != ModePage {436+ if h.Mode() != ModeVim {
437- t.Fatalf("scroll down iteration %d: mode = %v, want ModePage", i, h.Mode())437+ t.Fatalf("scroll down iteration %d: mode = %v, want ModeVim", i, h.Mode())
438 }438 }
439 }439 }
440 if repeatTick == nil {440 if repeatTick == nil {
441- t.Fatal("expected held page scroll to register smooth repeater")441+ t.Fatal("expected held vim scroll to register smooth repeater")
442 }442 }
443- repeatNow = repeatNow.Add(pageScrollHoldDelay)443+ repeatNow = repeatNow.Add(vimScrollHoldDelay)
444 for i := 0; i < 3; i++ {444 for i := 0; i < 3; i++ {
445 if !repeatTick(0) {445 if !repeatTick(0) {
446 t.Fatalf("repeat tick %d stopped unexpectedly", i)446 t.Fatalf("repeat tick %d stopped unexpectedly", i)
447 }447 }
448- if h.Mode() != ModePage {448+ if h.Mode() != ModeVim {
449- t.Fatalf("repeat tick %d: mode = %v, want ModePage", i, h.Mode())449+ t.Fatalf("repeat tick %d: mode = %v, want ModeVim", i, h.Mode())
450 }450 }
451 }451 }
452 h.handleKeyRelease(uint('j'))452 h.handleKeyRelease(uint('j'))
453 453
454- // Scroll up also stays in page mode454+ // Scroll up also stays in vim mode
455 result = h.handleKeyPress(uint('k'), 0, 0)455 result = h.handleKeyPress(uint('k'), 0, 0)
456- if !result || h.Mode() != ModePage {456+ if !result || h.Mode() != ModeVim {
457- t.Fatal("scroll up should stay in page mode")457+ t.Fatal("scroll up should stay in vim mode")
458 }458 }
459 459
460 // Ensure scroll actions were dispatched460 // Ensure scroll actions were dispatched
@@ -463,15 +463,15 @@ func TestHandleKeyPress_PageModeStaysActiveAfterScroll(t *testing.T) {
463 }463 }
464 }464 }
465 465
466-func TestHandleKeyPress_PageModeTimeoutReset(t *testing.T) {466+func TestHandleKeyPress_VimModeTimeoutReset(t *testing.T) {
467 ctx := context.Background()467 ctx := context.Background()
468 workspace := newTestWorkspace()468 workspace := newTestWorkspace()
469- workspace.PageMode = entity.PageModeConfig{469+ workspace.VimMode = entity.VimModeConfig{
470 ActivationShortcut: "ctrl+y",470 ActivationShortcut: "ctrl+y",
471 TimeoutMilliseconds: 100,471 TimeoutMilliseconds: 100,
472 Actions: map[string]entity.ActionBinding{472 Actions: map[string]entity.ActionBinding{
473- "page-scroll-down": {Keys: []string{"j"}},473+ "vim-scroll-down": {Keys: []string{"j"}},
474- "cancel": {Keys: []string{"escape"}},474+ "cancel": {Keys: []string{"escape"}},
475 },475 },
476 }476 }
477 477
@@ -485,34 +485,34 @@ func TestHandleKeyPress_PageModeTimeoutReset(t *testing.T) {
485 return nil485 return nil
486 })486 })
487 487
488- // Enter page mode with timeout.488+ // Enter vim mode with timeout.
489 h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)489 h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)
490- if h.Mode() != ModePage {490+ if h.Mode() != ModeVim {
491- t.Fatal("failed to enter page mode")491+ t.Fatal("failed to enter vim mode")
492 }492 }
493 if timerStarts != 1 {493 if timerStarts != 1 {
494- t.Fatalf("timer starts after entering page mode = %d, want 1", timerStarts)494+ t.Fatalf("timer starts after entering vim mode = %d, want 1", timerStarts)
495 }495 }
496 496
497 // Scroll action should reset the timeout by starting a replacement timer.497 // Scroll action should reset the timeout by starting a replacement timer.
498 h.handleKeyPress(uint('j'), 0, 0)498 h.handleKeyPress(uint('j'), 0, 0)
499 499
500- if h.Mode() != ModePage {500+ if h.Mode() != ModeVim {
501- t.Fatalf("mode after scroll = %v, want ModePage", h.Mode())501+ t.Fatalf("mode after scroll = %v, want ModeVim", h.Mode())
502 }502 }
503 if timerStarts != 2 {503 if timerStarts != 2 {
504 t.Fatalf("timer starts after scroll reset = %d, want 2", timerStarts)504 t.Fatalf("timer starts after scroll reset = %d, want 2", timerStarts)
505 }505 }
506 }506 }
507 507
508-func TestPageScrollRepeatTick_RefreshesConfiguredPageModeTimeout(t *testing.T) {508+func TestVimScrollRepeatTick_RefreshesConfiguredVimModeTimeout(t *testing.T) {
509 ctx := context.Background()509 ctx := context.Background()
510 workspace := newTestWorkspace()510 workspace := newTestWorkspace()
511- workspace.PageMode = entity.PageModeConfig{511+ workspace.VimMode = entity.VimModeConfig{
512 ActivationShortcut: "ctrl+y",512 ActivationShortcut: "ctrl+y",
513 TimeoutMilliseconds: 100,513 TimeoutMilliseconds: 100,
514 Actions: map[string]entity.ActionBinding{514 Actions: map[string]entity.ActionBinding{
515- "page-scroll-down": {Keys: []string{"j"}},515+ "vim-scroll-down": {Keys: []string{"j"}},
516 },516 },
517 }517 }
518 518
@@ -523,8 +523,8 @@ func TestPageScrollRepeatTick_RefreshesConfiguredPageModeTimeout(t *testing.T) {
523 return &fakeModalTimer{}523 return &fakeModalTimer{}
524 }524 }
525 var continuous int525 var continuous int
526- h.SetOnPageScrollLifecycle(func(_ context.Context, _ Action, phase PageScrollPhase) error {526+ h.SetOnVimScrollLifecycle(func(_ context.Context, _ Action, phase VimScrollPhase) error {
527- if phase == PageScrollContinuous {527+ if phase == VimScrollContinuous {
528 continuous++528 continuous++
529 }529 }
530 return nil530 return nil
@@ -532,16 +532,16 @@ func TestPageScrollRepeatTick_RefreshesConfiguredPageModeTimeout(t *testing.T) {
532 532
533 var repeatTick glib.SourceFunc533 var repeatTick glib.SourceFunc
534 repeatNow := time.Unix(3_000, 0)534 repeatNow := time.Unix(3_000, 0)
535- h.pageScrollNow = func() time.Time { return repeatNow }535+ h.vimScrollNow = func() time.Time { return repeatNow }
536- h.pageScrollRepeatAdd = func(_ uint, cb *glib.SourceFunc) uint {536+ h.vimScrollRepeatAdd = func(_ uint, cb *glib.SourceFunc) uint {
537 repeatTick = *cb537 repeatTick = *cb
538 return 1538 return 1
539 }539 }
540- h.pageScrollRepeatRemove = func(uint) bool { return true }540+ h.vimScrollRepeatRemove = func(uint) bool { return true }
541 541
542 h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)542 h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)
543- if h.Mode() != ModePage {543+ if h.Mode() != ModeVim {
544- t.Fatal("failed to enter page mode")544+ t.Fatal("failed to enter vim mode")
545 }545 }
546 enterStarts := timerStarts546 enterStarts := timerStarts
547 h.handleKeyPress(uint('j'), 0, 0)547 h.handleKeyPress(uint('j'), 0, 0)
@@ -553,7 +553,7 @@ func TestPageScrollRepeatTick_RefreshesConfiguredPageModeTimeout(t *testing.T) {
553 t.Fatalf("tap should refresh timeout: enter=%d afterTap=%d", enterStarts, afterTap)553 t.Fatalf("tap should refresh timeout: enter=%d afterTap=%d", enterStarts, afterTap)
554 }554 }
555 555
556- repeatNow = repeatNow.Add(pageScrollHoldDelay)556+ repeatNow = repeatNow.Add(vimScrollHoldDelay)
557 if !repeatTick(0) {557 if !repeatTick(0) {
558 t.Fatal("continuous tick stopped unexpectedly")558 t.Fatal("continuous tick stopped unexpectedly")
559 }559 }
@@ -561,21 +561,21 @@ func TestPageScrollRepeatTick_RefreshesConfiguredPageModeTimeout(t *testing.T) {
561 t.Fatalf("continuous dispatches=%d, want 1", continuous)561 t.Fatalf("continuous dispatches=%d, want 1", continuous)
562 }562 }
563 if timerStarts <= afterTap {563 if timerStarts <= afterTap {
564- t.Fatalf("continuous tick must refresh page-mode timeout: afterTap=%d afterTick=%d", afterTap, timerStarts)564+ t.Fatalf("continuous tick must refresh vim-mode timeout: afterTap=%d afterTick=%d", afterTap, timerStarts)
565 }565 }
566- if h.Mode() != ModePage {566+ if h.Mode() != ModeVim {
567- t.Fatalf("mode after continuous tick = %v, want ModePage", h.Mode())567+ t.Fatalf("mode after continuous tick = %v, want ModeVim", h.Mode())
568 }568 }
569 }569 }
570 570
571-func TestHandleKeyPress_PageModeEscapeExits(t *testing.T) {571+func TestHandleKeyPress_VimModeEscapeExits(t *testing.T) {
572 ctx := context.Background()572 ctx := context.Background()
573 workspace := newTestWorkspace()573 workspace := newTestWorkspace()
574- workspace.PageMode = entity.PageModeConfig{574+ workspace.VimMode = entity.VimModeConfig{
575 ActivationShortcut: "ctrl+y",575 ActivationShortcut: "ctrl+y",
576 Actions: map[string]entity.ActionBinding{576 Actions: map[string]entity.ActionBinding{
577- "page-scroll-down": {Keys: []string{"j"}},577+ "vim-scroll-down": {Keys: []string{"j"}},
578- "cancel": {Keys: []string{"escape"}},578+ "cancel": {Keys: []string{"escape"}},
579 },579 },
580 }580 }
581 581
@@ -584,13 +584,13 @@ func TestHandleKeyPress_PageModeEscapeExits(t *testing.T) {
584 return nil584 return nil
585 })585 })
586 586
587- // Enter page mode587+ // Enter vim mode
588 h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)588 h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)
589- if h.Mode() != ModePage {589+ if h.Mode() != ModeVim {
590- t.Fatal("failed to enter page mode")590+ t.Fatal("failed to enter vim mode")
591 }591 }
592 592
593- // Escape should exit page mode593+ // Escape should exit vim mode
594 result := h.handleKeyPress(uint(gdk.KEY_Escape), 0, 0)594 result := h.handleKeyPress(uint(gdk.KEY_Escape), 0, 0)
595 if !result {595 if !result {
596 t.Fatal("Escape should be consumed")596 t.Fatal("Escape should be consumed")
@@ -600,10 +600,10 @@ func TestHandleKeyPress_PageModeEscapeExits(t *testing.T) {
600 }600 }
601 }601 }
602 602
603-func TestHandleKeyPress_PageModeEnterExits(t *testing.T) {603+func TestHandleKeyPress_VimModeEnterExits(t *testing.T) {
604 ctx := context.Background()604 ctx := context.Background()
605 workspace := newTestWorkspace()605 workspace := newTestWorkspace()
606- workspace.PageMode = entity.PageModeConfig{606+ workspace.VimMode = entity.VimModeConfig{
607 ActivationShortcut: "ctrl+y",607 ActivationShortcut: "ctrl+y",
608 Actions: map[string]entity.ActionBinding{608 Actions: map[string]entity.ActionBinding{
609 "confirm": {Keys: []string{"enter"}},609 "confirm": {Keys: []string{"enter"}},
@@ -615,13 +615,13 @@ func TestHandleKeyPress_PageModeEnterExits(t *testing.T) {
615 return nil615 return nil
616 })616 })
617 617
618- // Enter page mode618+ // Enter vim mode
619 h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)619 h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)
620- if h.Mode() != ModePage {620+ if h.Mode() != ModeVim {
621- t.Fatal("failed to enter page mode")621+ t.Fatal("failed to enter vim mode")
622 }622 }
623 623
624- // Enter should exit page mode624+ // Enter should exit vim mode
625 result := h.handleKeyPress(uint(gdk.KEY_Return), 0, 0)625 result := h.handleKeyPress(uint(gdk.KEY_Return), 0, 0)
626 if !result {626 if !result {
627 t.Fatal("Enter should be consumed")627 t.Fatal("Enter should be consumed")
@@ -631,10 +631,10 @@ func TestHandleKeyPress_PageModeEnterExits(t *testing.T) {
631 }631 }
632 }632 }
633 633
634-func TestHandleKeyPress_PageModeActivationTogglesOff(t *testing.T) {634+func TestHandleKeyPress_VimModeActivationTogglesOff(t *testing.T) {
635 ctx := context.Background()635 ctx := context.Background()
636 workspace := newTestWorkspace()636 workspace := newTestWorkspace()
637- workspace.PageMode = entity.PageModeConfig{637+ workspace.VimMode = entity.VimModeConfig{
638 ActivationShortcut: "ctrl+y",638 ActivationShortcut: "ctrl+y",
639 }639 }
640 640
@@ -643,8 +643,8 @@ func TestHandleKeyPress_PageModeActivationTogglesOff(t *testing.T) {
643 if !h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue) {643 if !h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue) {
644 t.Fatal("first Ctrl+Y should be consumed")644 t.Fatal("first Ctrl+Y should be consumed")
645 }645 }
646- if h.Mode() != ModePage {646+ if h.Mode() != ModeVim {
647- t.Fatalf("mode after first Ctrl+Y = %v, want ModePage", h.Mode())647+ t.Fatalf("mode after first Ctrl+Y = %v, want ModeVim", h.Mode())
648 }648 }
649 649
650 h.handleKeyRelease(uint('y'))650 h.handleKeyRelease(uint('y'))
@@ -656,13 +656,13 @@ func TestHandleKeyPress_PageModeActivationTogglesOff(t *testing.T) {
656 }656 }
657 }657 }
658 658
659-func TestHandleKeyPress_PageModeEscapeExitsWithoutCancelBinding(t *testing.T) {659+func TestHandleKeyPress_VimModeEscapeExitsWithoutCancelBinding(t *testing.T) {
660 ctx := context.Background()660 ctx := context.Background()
661 workspace := newTestWorkspace()661 workspace := newTestWorkspace()
662- workspace.PageMode = entity.PageModeConfig{662+ workspace.VimMode = entity.VimModeConfig{
663 ActivationShortcut: "ctrl+y",663 ActivationShortcut: "ctrl+y",
664 Actions: map[string]entity.ActionBinding{664 Actions: map[string]entity.ActionBinding{
665- "page-scroll-down": {Keys: []string{"j"}},665+ "vim-scroll-down": {Keys: []string{"j"}},
666 },666 },
667 }667 }
668 668
@@ -670,8 +670,8 @@ func TestHandleKeyPress_PageModeEscapeExitsWithoutCancelBinding(t *testing.T) {
670 if !h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue) {670 if !h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue) {
671 t.Fatal("Ctrl+Y should be consumed")671 t.Fatal("Ctrl+Y should be consumed")
672 }672 }
673- if h.Mode() != ModePage {673+ if h.Mode() != ModeVim {
674- t.Fatal("failed to enter page mode")674+ t.Fatal("failed to enter vim mode")
675 }675 }
676 676
677 if !h.handleKeyPress(uint(gdk.KEY_Escape), 0, 0) {677 if !h.handleKeyPress(uint(gdk.KEY_Escape), 0, 0) {
@@ -682,10 +682,10 @@ func TestHandleKeyPress_PageModeEscapeExitsWithoutCancelBinding(t *testing.T) {
682 }682 }
683 }683 }
684 684
685-func TestHandleKeyPress_PageModeActivationNotInPassThrough(t *testing.T) {685+func TestHandleKeyPress_VimModeActivationNotInPassThrough(t *testing.T) {
686 ctx := context.Background()686 ctx := context.Background()
687 workspace := newTestWorkspace()687 workspace := newTestWorkspace()
688- workspace.PageMode = entity.PageModeConfig{688+ workspace.VimMode = entity.VimModeConfig{
689 ActivationShortcut: "ctrl+y",689 ActivationShortcut: "ctrl+y",
690 }690 }
691 691
@@ -696,55 +696,55 @@ func TestHandleKeyPress_PageModeActivationNotInPassThrough(t *testing.T) {
696 return RoutePassToWidget696 return RoutePassToWidget
697 })697 })
698 698
699- // Ctrl+Y should NOT activate page mode because route says pass to widget699+ // Ctrl+Y should NOT activate vim mode because route says pass to widget
700 result := h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)700 result := h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)
701 if result {701 if result {
702 t.Fatal("Ctrl+Y should NOT be consumed when routed to widget")702 t.Fatal("Ctrl+Y should NOT be consumed when routed to widget")
703 }703 }
704 if h.Mode() != ModeNormal {704 if h.Mode() != ModeNormal {
705- t.Fatalf("mode = %v, want ModeNormal (should not enter page mode)", h.Mode())705+ t.Fatalf("mode = %v, want ModeNormal (should not enter vim mode)", h.Mode())
706 }706 }
707 }707 }
708 708
709-func TestHandleKeyPress_PageModeActivationPassesThroughWhenBlocked(t *testing.T) {709+func TestHandleKeyPress_VimModeActivationPassesThroughWhenBlocked(t *testing.T) {
710 ctx := context.Background()710 ctx := context.Background()
711 workspace := newTestWorkspace()711 workspace := newTestWorkspace()
712- workspace.PageMode = entity.PageModeConfig{712+ workspace.VimMode = entity.VimModeConfig{
713 ActivationShortcut: "ctrl+y",713 ActivationShortcut: "ctrl+y",
714 }714 }
715 715
716 h := NewKeyboardHandler(ctx, workspace, newTestSession())716 h := NewKeyboardHandler(ctx, workspace, newTestSession())
717- h.SetPageModeActivationPassthrough(func() bool { return true })717+ h.SetVimModeActivationPassthrough(func() bool { return true })
718 718
719 result := h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)719 result := h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)
720 if result {720 if result {
721- t.Fatal("Ctrl+Y should pass through when page mode activation is blocked")721+ t.Fatal("Ctrl+Y should pass through when vim mode activation is blocked")
722 }722 }
723 if h.Mode() != ModeNormal {723 if h.Mode() != ModeNormal {
724 t.Fatalf("mode = %v, want ModeNormal", h.Mode())724 t.Fatalf("mode = %v, want ModeNormal", h.Mode())
725 }725 }
726 }726 }
727 727
728-func TestDispatchAction_PageModeActivationPassesThroughWhenBlocked(t *testing.T) {728+func TestDispatchAction_VimModeActivationPassesThroughWhenBlocked(t *testing.T) {
729 ctx := context.Background()729 ctx := context.Background()
730 workspace := newTestWorkspace()730 workspace := newTestWorkspace()
731- workspace.PageMode = entity.PageModeConfig{731+ workspace.VimMode = entity.VimModeConfig{
732 ActivationShortcut: "ctrl+y",732 ActivationShortcut: "ctrl+y",
733 }733 }
734 734
735 h := NewKeyboardHandler(ctx, workspace, newTestSession())735 h := NewKeyboardHandler(ctx, workspace, newTestSession())
736- h.SetPageModeActivationPassthrough(func() bool { return true })736+ h.SetVimModeActivationPassthrough(func() bool { return true })
737 737
738- consumed := h.DispatchAction(ActionEnterPageMode)738+ consumed := h.DispatchAction(ActionEnterVimMode)
739 if consumed {739 if consumed {
740- t.Fatal("DispatchAction should not consume blocked page mode activation")740+ t.Fatal("DispatchAction should not consume blocked vim mode activation")
741 }741 }
742 if h.Mode() != ModeNormal {742 if h.Mode() != ModeNormal {
743 t.Fatalf("mode = %v, want ModeNormal", h.Mode())743 t.Fatalf("mode = %v, want ModeNormal", h.Mode())
744 }744 }
745 }745 }
746 746
747-func TestHandleKeyPress_PageModeFastScrollLookup(t *testing.T) {747+func TestHandleKeyPress_VimModeFastScrollLookup(t *testing.T) {
748 tests := []struct {748 tests := []struct {
749 name string749 name string
750 configAction string750 configAction string
@@ -753,15 +753,15 @@ func TestHandleKeyPress_PageModeFastScrollLookup(t *testing.T) {
753 }{753 }{
754 {754 {
755 name: "scroll down fast",755 name: "scroll down fast",
756- configAction: "page-scroll-down-fast",756+ configAction: "vim-scroll-down-fast",
757 key: uint('d'),757 key: uint('d'),
758- expected: ActionPageScrollDownFast,758+ expected: ActionVimScrollDownFast,
759 },759 },
760 {760 {
761 name: "scroll up fast",761 name: "scroll up fast",
762- configAction: "page-scroll-up-fast",762+ configAction: "vim-scroll-up-fast",
763 key: uint('u'),763 key: uint('u'),
764- expected: ActionPageScrollUpFast,764+ expected: ActionVimScrollUpFast,
765 },765 },
766 }766 }
767 767
@@ -769,7 +769,7 @@ func TestHandleKeyPress_PageModeFastScrollLookup(t *testing.T) {
769 t.Run(tt.name, func(t *testing.T) {769 t.Run(tt.name, func(t *testing.T) {
770 ctx := context.Background()770 ctx := context.Background()
771 workspace := newTestWorkspace()771 workspace := newTestWorkspace()
772- workspace.PageMode = entity.PageModeConfig{772+ workspace.VimMode = entity.VimModeConfig{
773 ActivationShortcut: "ctrl+y",773 ActivationShortcut: "ctrl+y",
774 Actions: map[string]entity.ActionBinding{774 Actions: map[string]entity.ActionBinding{
775 tt.configAction: {Keys: []string{"ctrl+" + string(rune(tt.key))}},775 tt.configAction: {Keys: []string{"ctrl+" + string(rune(tt.key))}},
@@ -785,35 +785,35 @@ func TestHandleKeyPress_PageModeFastScrollLookup(t *testing.T) {
785 return nil785 return nil
786 })786 })
787 787
788- // Enter page mode.788+ // Enter vim mode.
789 h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)789 h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)
790- if h.Mode() != ModePage {790+ if h.Mode() != ModeVim {
791- t.Fatal("failed to enter page mode")791+ t.Fatal("failed to enter vim mode")
792 }792 }
793 793
794- // Fast-scroll shortcut in page mode should dispatch the expected action.794+ // Fast-scroll shortcut in vim mode should dispatch the expected action.
795 result := h.handleKeyPress(tt.key, 0, gdk.ControlMaskValue)795 result := h.handleKeyPress(tt.key, 0, gdk.ControlMaskValue)
796 if !result {796 if !result {
797- t.Fatal("fast scroll shortcut in page mode should be consumed")797+ t.Fatal("fast scroll shortcut in vim mode should be consumed")
798 }798 }
799 if actionCalls != 1 || lastAction != tt.expected {799 if actionCalls != 1 || lastAction != tt.expected {
800 t.Fatalf("expected %s, got action=%s calls=%d", tt.expected, lastAction, actionCalls)800 t.Fatalf("expected %s, got action=%s calls=%d", tt.expected, lastAction, actionCalls)
801 }801 }
802- if h.Mode() != ModePage {802+ if h.Mode() != ModeVim {
803- t.Fatal("should stay in page mode after scroll action")803+ t.Fatal("should stay in vim mode after scroll action")
804 }804 }
805 })805 })
806 }806 }
807 }807 }
808 808
809-func TestHandleKeyPress_PageModeNoAutoExitForScrollActions(t *testing.T) {809+func TestHandleKeyPress_VimModeNoAutoExitForScrollActions(t *testing.T) {
810 ctx := context.Background()810 ctx := context.Background()
811 workspace := newTestWorkspace()811 workspace := newTestWorkspace()
812- workspace.PageMode = entity.PageModeConfig{812+ workspace.VimMode = entity.VimModeConfig{
813 ActivationShortcut: "ctrl+y",813 ActivationShortcut: "ctrl+y",
814 Actions: map[string]entity.ActionBinding{814 Actions: map[string]entity.ActionBinding{
815- "page-scroll-left": {Keys: []string{"h"}},815+ "vim-scroll-left": {Keys: []string{"h"}},
816- "page-scroll-right": {Keys: []string{"l"}},816+ "vim-scroll-right": {Keys: []string{"l"}},
817 },817 },
818 }818 }
819 819
@@ -824,22 +824,22 @@ func TestHandleKeyPress_PageModeNoAutoExitForScrollActions(t *testing.T) {
824 return nil824 return nil
825 })825 })
826 826
827- // Enter page mode827+ // Enter vim mode
828 h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)828 h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)
829- if h.Mode() != ModePage {829+ if h.Mode() != ModeVim {
830- t.Fatal("failed to enter page mode")830+ t.Fatal("failed to enter vim mode")
831 }831 }
832 832
833 // Scroll left should not auto-exit833 // Scroll left should not auto-exit
834 h.handleKeyPress(uint('h'), 0, 0)834 h.handleKeyPress(uint('h'), 0, 0)
835- if h.Mode() != ModePage {835+ if h.Mode() != ModeVim {
836- t.Fatal("scroll left should not exit page mode")836+ t.Fatal("scroll left should not exit vim mode")
837 }837 }
838 838
839 // Scroll right should not auto-exit839 // Scroll right should not auto-exit
840 h.handleKeyPress(uint('l'), 0, 0)840 h.handleKeyPress(uint('l'), 0, 0)
841- if h.Mode() != ModePage {841+ if h.Mode() != ModeVim {
842- t.Fatal("scroll right should not exit page mode")842+ t.Fatal("scroll right should not exit vim mode")
843 }843 }
844 844
845 if actionCalls != 2 {845 if actionCalls != 2 {
@@ -847,13 +847,13 @@ func TestHandleKeyPress_PageModeNoAutoExitForScrollActions(t *testing.T) {
847 }847 }
848 }848 }
849 849
850-func TestHandleKeyPress_PageModeArrowKeysPassThroughNatively(t *testing.T) {850+func TestHandleKeyPress_VimModeArrowKeysPassThroughNatively(t *testing.T) {
851 ctx := context.Background()851 ctx := context.Background()
852 workspace := newTestWorkspace()852 workspace := newTestWorkspace()
853- workspace.PageMode = entity.PageModeConfig{853+ workspace.VimMode = entity.VimModeConfig{
854 ActivationShortcut: "ctrl+y",854 ActivationShortcut: "ctrl+y",
855 Actions: map[string]entity.ActionBinding{855 Actions: map[string]entity.ActionBinding{
856- "page-scroll-down": {Keys: []string{"j"}},856+ "vim-scroll-down": {Keys: []string{"j"}},
857 },857 },
858 }858 }
859 859
@@ -865,32 +865,32 @@ func TestHandleKeyPress_PageModeArrowKeysPassThroughNatively(t *testing.T) {
865 })865 })
866 866
867 h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)867 h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)
868- if h.Mode() != ModePage {868+ if h.Mode() != ModeVim {
869- t.Fatal("failed to enter page mode")869+ t.Fatal("failed to enter vim mode")
870 }870 }
871 871
872 for _, keyval := range []uint{uint(gdk.KEY_Left), uint(gdk.KEY_Right), uint(gdk.KEY_Up), uint(gdk.KEY_Down)} {872 for _, keyval := range []uint{uint(gdk.KEY_Left), uint(gdk.KEY_Right), uint(gdk.KEY_Up), uint(gdk.KEY_Down)} {
873 consumed := h.handleKeyPress(keyval, 0, 0)873 consumed := h.handleKeyPress(keyval, 0, 0)
874 if consumed {874 if consumed {
875- t.Fatalf("arrow key %d should pass through to native page handling in page mode", keyval)875+ t.Fatalf("arrow key %d should pass through to native page handling in vim mode", keyval)
876 }876 }
877- if h.Mode() != ModePage {877+ if h.Mode() != ModeVim {
878- t.Fatalf("arrow key %d should keep page mode active", keyval)878+ t.Fatalf("arrow key %d should keep vim mode active", keyval)
879 }879 }
880 }880 }
881 881
882 if actionCalls != 0 {882 if actionCalls != 0 {
883- t.Fatalf("arrow key passthrough should not dispatch page-mode action, got %d calls", actionCalls)883+ t.Fatalf("arrow key passthrough should not dispatch vim-mode action, got %d calls", actionCalls)
884 }884 }
885 }885 }
886 886
887-func TestHandleKeyPress_PageModeBlocksGlobalShortcutFallback(t *testing.T) {887+func TestHandleKeyPress_VimModeBlocksGlobalShortcutFallback(t *testing.T) {
888 ctx := context.Background()888 ctx := context.Background()
889 workspace := newTestWorkspace()889 workspace := newTestWorkspace()
890- workspace.PageMode = entity.PageModeConfig{890+ workspace.VimMode = entity.VimModeConfig{
891 ActivationShortcut: "ctrl+y",891 ActivationShortcut: "ctrl+y",
892 Actions: map[string]entity.ActionBinding{892 Actions: map[string]entity.ActionBinding{
893- "page-scroll-down": {Keys: []string{"j"}},893+ "vim-scroll-down": {Keys: []string{"j"}},
894 },894 },
895 }895 }
896 896
@@ -906,28 +906,28 @@ func TestHandleKeyPress_PageModeBlocksGlobalShortcutFallback(t *testing.T) {
906 if !h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue) {906 if !h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue) {
907 t.Fatal("Ctrl+Y should be consumed")907 t.Fatal("Ctrl+Y should be consumed")
908 }908 }
909- if h.Mode() != ModePage {909+ if h.Mode() != ModeVim {
910- t.Fatal("failed to enter page mode")910+ t.Fatal("failed to enter vim mode")
911 }911 }
912 912
913 if !h.handleKeyPress(uint('l'), 0, gdk.ControlMaskValue) {913 if !h.handleKeyPress(uint('l'), 0, gdk.ControlMaskValue) {
914- t.Fatal("Ctrl+L should be consumed inside page mode")914+ t.Fatal("Ctrl+L should be consumed inside vim mode")
915 }915 }
916 if actionCalls != 0 {916 if actionCalls != 0 {
917- t.Fatalf("Ctrl+L should not dispatch a global action in page mode, got %d calls (%s)", actionCalls, lastAction)917+ t.Fatalf("Ctrl+L should not dispatch a global action in vim mode, got %d calls (%s)", actionCalls, lastAction)
918 }918 }
919- if h.Mode() != ModePage {919+ if h.Mode() != ModeVim {
920- t.Fatalf("mode after blocked Ctrl+L = %v, want ModePage", h.Mode())920+ t.Fatalf("mode after blocked Ctrl+L = %v, want ModeVim", h.Mode())
921 }921 }
922 }922 }
923 923
924-func TestPageScrollRepeat_FirstKeyDownDispatchesTapAndArmsHold(t *testing.T) {924+func TestVimScrollRepeat_FirstKeyDownDispatchesTapAndArmsHold(t *testing.T) {
925- h, now, tick := newPageScrollRepeatTestHandler(t)925+ h, now, tick := newVimScrollRepeatTestHandler(t)
926 actionCalls := 0926 actionCalls := 0
927 h.SetOnAction(func(_ context.Context, action Action) error {927 h.SetOnAction(func(_ context.Context, action Action) error {
928 actionCalls++928 actionCalls++
929- if action != ActionPageScrollDown {929+ if action != ActionVimScrollDown {
930- t.Fatalf("action = %s, want %s", action, ActionPageScrollDown)930+ t.Fatalf("action = %s, want %s", action, ActionVimScrollDown)
931 }931 }
932 return nil932 return nil
933 })933 })
@@ -942,7 +942,7 @@ func TestPageScrollRepeat_FirstKeyDownDispatchesTapAndArmsHold(t *testing.T) {
942 t.Fatal("first keydown must arm the hold source without an OS repeat")942 t.Fatal("first keydown must arm the hold source without an OS repeat")
943 }943 }
944 944
945- *now = now.Add(pageScrollHoldDelay - time.Millisecond)945+ *now = now.Add(vimScrollHoldDelay - time.Millisecond)
946 if !(*tick)(0) {946 if !(*tick)(0) {
947 t.Fatal("hold source should remain armed before the delay")947 t.Fatal("hold source should remain armed before the delay")
948 }948 }
@@ -951,20 +951,20 @@ func TestPageScrollRepeat_FirstKeyDownDispatchesTapAndArmsHold(t *testing.T) {
951 }951 }
952 }952 }
953 953
954-func TestPageScrollRepeat_HoldStartsWithoutOSRepeat(t *testing.T) {954+func TestVimScrollRepeat_HoldStartsWithoutOSRepeat(t *testing.T) {
955- h, now, tick := newPageScrollRepeatTestHandler(t)955+ h, now, tick := newVimScrollRepeatTestHandler(t)
956 continuous := 0956 continuous := 0
957 h.SetOnAction(func(context.Context, Action) error { return nil })957 h.SetOnAction(func(context.Context, Action) error { return nil })
958- h.SetOnPageScrollLifecycle(func(_ context.Context, action Action, phase PageScrollPhase) error {958+ h.SetOnVimScrollLifecycle(func(_ context.Context, action Action, phase VimScrollPhase) error {
959- if action != ActionPageScrollDown || phase != PageScrollContinuous {959+ if action != ActionVimScrollDown || phase != VimScrollContinuous {
960- t.Fatalf("lifecycle = (%s, %v), want (%s, continuous)", action, phase, ActionPageScrollDown)960+ t.Fatalf("lifecycle = (%s, %v), want (%s, continuous)", action, phase, ActionVimScrollDown)
961 }961 }
962 continuous++962 continuous++
963 return nil963 return nil
964 })964 })
965 965
966 h.handleKeyPress(uint('j'), 0, 0)966 h.handleKeyPress(uint('j'), 0, 0)
967- *now = now.Add(pageScrollHoldDelay)967+ *now = now.Add(vimScrollHoldDelay)
968 if !(*tick)(0) {968 if !(*tick)(0) {
969 t.Fatal("hold source stopped at the delay")969 t.Fatal("hold source stopped at the delay")
970 }970 }
@@ -973,19 +973,19 @@ func TestPageScrollRepeat_HoldStartsWithoutOSRepeat(t *testing.T) {
973 }973 }
974 }974 }
975 975
976-func TestPageScrollRepeat_FixedCadenceIgnoresDisplayAndOSRepeatRates(t *testing.T) {976+func TestVimScrollRepeat_FixedCadenceIgnoresDisplayAndOSRepeatRates(t *testing.T) {
977 const simulatedHold = time.Second977 const simulatedHold = time.Second
978 simulate := func(t *testing.T, externalHz int) int {978 simulate := func(t *testing.T, externalHz int) int {
979 t.Helper()979 t.Helper()
980- h, now, tick := newPageScrollRepeatTestHandler(t)980+ h, now, tick := newVimScrollRepeatTestHandler(t)
981 tapDistance := 0981 tapDistance := 0
982 continuousDistance := 0982 continuousDistance := 0
983 h.SetOnAction(func(context.Context, Action) error {983 h.SetOnAction(func(context.Context, Action) error {
984 tapDistance += 80984 tapDistance += 80
985 return nil985 return nil
986 })986 })
987- h.SetOnPageScrollLifecycle(func(_ context.Context, _ Action, phase PageScrollPhase) error {987+ h.SetOnVimScrollLifecycle(func(_ context.Context, _ Action, phase VimScrollPhase) error {
988- if phase == PageScrollContinuous {988+ if phase == VimScrollContinuous {
989 continuousDistance += 80989 continuousDistance += 80
990 }990 }
991 return nil991 return nil
@@ -994,7 +994,7 @@ func TestPageScrollRepeat_FixedCadenceIgnoresDisplayAndOSRepeatRates(t *testing.
994 994
995 start := *now995 start := *now
996 nextExternal := time.Second / time.Duration(externalHz)996 nextExternal := time.Second / time.Duration(externalHz)
997- for elapsed := pageScrollCadence; elapsed <= simulatedHold; elapsed += pageScrollCadence {997+ for elapsed := vimScrollCadence; elapsed <= simulatedHold; elapsed += vimScrollCadence {
998 *now = start.Add(elapsed)998 *now = start.Add(elapsed)
999 for elapsed >= nextExternal {999 for elapsed >= nextExternal {
1000 h.handleKeyPress(uint('j'), 0, 0) // ignored OS auto-repeat1000 h.handleKeyPress(uint('j'), 0, 0) // ignored OS auto-repeat
@@ -1017,7 +1017,7 @@ func TestPageScrollRepeat_FixedCadenceIgnoresDisplayAndOSRepeatRates(t *testing.
1017 }1017 }
1018 }1018 }
1019 1019
1020-func TestPageScrollRepeat_ReleaseAndEscapeStopImmediately(t *testing.T) {1020+func TestVimScrollRepeat_ReleaseAndEscapeStopImmediately(t *testing.T) {
1021 for _, tc := range []struct {1021 for _, tc := range []struct {
1022 name string1022 name string
1023 stop func(*KeyboardHandler)1023 stop func(*KeyboardHandler)
@@ -1026,19 +1026,19 @@ func TestPageScrollRepeat_ReleaseAndEscapeStopImmediately(t *testing.T) {
1026 {name: "escape", stop: func(h *KeyboardHandler) { h.handleKeyPress(uint(gdk.KEY_Escape), 0, 0) }},1026 {name: "escape", stop: func(h *KeyboardHandler) { h.handleKeyPress(uint(gdk.KEY_Escape), 0, 0) }},
1027 } {1027 } {
1028 t.Run(tc.name, func(t *testing.T) {1028 t.Run(tc.name, func(t *testing.T) {
1029- h, now, tick := newPageScrollRepeatTestHandler(t)1029+ h, now, tick := newVimScrollRepeatTestHandler(t)
1030 removed := 01030 removed := 0
1031 stops := 01031 stops := 0
1032- h.pageScrollRepeatRemove = func(uint) bool { removed++; return true }1032+ h.vimScrollRepeatRemove = func(uint) bool { removed++; return true }
1033 h.SetOnAction(func(context.Context, Action) error { return nil })1033 h.SetOnAction(func(context.Context, Action) error { return nil })
1034- h.SetOnPageScrollLifecycle(func(_ context.Context, _ Action, phase PageScrollPhase) error {1034+ h.SetOnVimScrollLifecycle(func(_ context.Context, _ Action, phase VimScrollPhase) error {
1035- if phase == PageScrollStop {1035+ if phase == VimScrollStop {
1036 stops++1036 stops++
1037 }1037 }
1038 return nil1038 return nil
1039 })1039 })
1040 h.handleKeyPress(uint('j'), 0, 0)1040 h.handleKeyPress(uint('j'), 0, 0)
1041- *now = now.Add(pageScrollHoldDelay)1041+ *now = now.Add(vimScrollHoldDelay)
1042 (*tick)(0)1042 (*tick)(0)
1043 1043
1044 tc.stop(h)1044 tc.stop(h)
@@ -1052,24 +1052,24 @@ func TestPageScrollRepeat_ReleaseAndEscapeStopImmediately(t *testing.T) {
1052 }1052 }
1053 }1053 }
1054 1054
1055-func newPageScrollRepeatTestHandler(t *testing.T) (*KeyboardHandler, *time.Time, *glib.SourceFunc) {1055+func newVimScrollRepeatTestHandler(t *testing.T) (*KeyboardHandler, *time.Time, *glib.SourceFunc) {
1056 t.Helper()1056 t.Helper()
1057 workspace := newTestWorkspace()1057 workspace := newTestWorkspace()
1058- workspace.PageMode = entity.PageModeConfig{1058+ workspace.VimMode = entity.VimModeConfig{
1059- Actions: map[string]entity.ActionBinding{"page-scroll-down": {Keys: []string{"j"}}},1059+ Actions: map[string]entity.ActionBinding{"vim-scroll-down": {Keys: []string{"j"}}},
1060 }1060 }
1061 h := NewKeyboardHandler(context.Background(), workspace, newTestSession())1061 h := NewKeyboardHandler(context.Background(), workspace, newTestSession())
1062- h.EnterPageMode()1062+ h.EnterVimMode()
1063 now := time.Unix(1_000, 0)1063 now := time.Unix(1_000, 0)
1064 var tick glib.SourceFunc1064 var tick glib.SourceFunc
1065- h.pageScrollNow = func() time.Time { return now }1065+ h.vimScrollNow = func() time.Time { return now }
1066- h.pageScrollRepeatAdd = func(intervalMS uint, cb *glib.SourceFunc) uint {1066+ h.vimScrollRepeatAdd = func(intervalMS uint, cb *glib.SourceFunc) uint {
1067- if intervalMS != uint(pageScrollCadence/time.Millisecond) {1067+ if intervalMS != uint(vimScrollCadence/time.Millisecond) {
1068- t.Fatalf("cadence = %dms, want %dms", intervalMS, pageScrollCadence/time.Millisecond)1068+ t.Fatalf("cadence = %dms, want %dms", intervalMS, vimScrollCadence/time.Millisecond)
1069 }1069 }
1070 tick = *cb1070 tick = *cb
1071 return 991071 return 99
1072 }1072 }
1073- h.pageScrollRepeatRemove = func(uint) bool { return true }1073+ h.vimScrollRepeatRemove = func(uint) bool { return true }
1074 return h, &now, &tick1074 return h, &now, &tick
1075 }1075 }
M internal/ui/input/modal.go +9 -9
@@ -26,8 +26,8 @@ const (
26 ModeSession26 ModeSession
27 // ModeResize is the modal pane resizing mode.27 // ModeResize is the modal pane resizing mode.
28 ModeResize28 ModeResize
29- // ModePage is the modal page scrolling mode.29+ // ModeVim is the modal Vim-style scrolling mode.
30- ModePage30+ ModeVim
31 )31 )
32 32
33 // String returns a human-readable mode name.33 // String returns a human-readable mode name.
@@ -43,8 +43,8 @@ func (m Mode) String() string {
43 return "session"43 return "session"
44 case ModeResize:44 case ModeResize:
45 return "resize"45 return "resize"
46- case ModePage:46+ case ModeVim:
47- return "page"47+ return "vim"
48 default:48 default:
49 return "unknown"49 return "unknown"
50 }50 }
@@ -61,8 +61,8 @@ func (m Mode) DisplayName() string {
61 return "SESSION MODE"61 return "SESSION MODE"
62 case ModeResize:62 case ModeResize:
63 return "RESIZE MODE"63 return "RESIZE MODE"
64- case ModePage:64+ case ModeVim:
65- return "PAGE MODE"65+ return "VIM MODE"
66 default:66 default:
67 return ""67 return ""
68 }68 }
@@ -101,9 +101,9 @@ func NewModalState(ctx context.Context) *ModalState {
101 }101 }
102 }102 }
103 103
104-// EnterPageMode switches to page scrolling mode with an optional timeout.104+// EnterVimMode switches to Vim-style scrolling mode with an optional timeout.
105-func (m *ModalState) EnterPageMode(ctx context.Context, timeout time.Duration) {105+func (m *ModalState) EnterVimMode(ctx context.Context, timeout time.Duration) {
106- m.enterMode(ctx, ModePage, timeout)106+ m.enterMode(ctx, ModeVim, timeout)
107 }107 }
108 108
109 // SetMainThreadScheduler sets the function used to dispatch timer callbacks109 // SetMainThreadScheduler sets the function used to dispatch timer callbacks
M internal/ui/input/shortcuts.go +44 -44
@@ -203,14 +203,14 @@ const (
203 // Application203 // Application
204 ActionQuit Action = "quit"204 ActionQuit Action = "quit"
205 205
206- // Page mode actions206+ // Vim mode actions
207- ActionEnterPageMode Action = "enter_page_mode"207+ ActionEnterVimMode Action = "enter_vim_mode"
208- ActionPageScrollLeft Action = "page_scroll_left"208+ ActionVimScrollLeft Action = "vim_scroll_left"
209- ActionPageScrollDown Action = "page_scroll_down"209+ ActionVimScrollDown Action = "vim_scroll_down"
210- ActionPageScrollUp Action = "page_scroll_up"210+ ActionVimScrollUp Action = "vim_scroll_up"
211- ActionPageScrollRight Action = "page_scroll_right"211+ ActionVimScrollRight Action = "vim_scroll_right"
212- ActionPageScrollDownFast Action = "page_scroll_down_fast"212+ ActionVimScrollDownFast Action = "vim_scroll_down_fast"
213- ActionPageScrollUpFast Action = "page_scroll_up_fast"213+ ActionVimScrollUpFast Action = "vim_scroll_up_fast"
214 )214 )
215 215
216 const floatingProfileActionPrefix = "open_floating_profile:"216 const floatingProfileActionPrefix = "open_floating_profile:"
@@ -231,8 +231,8 @@ type ShortcutSet struct {
231 SessionMode ShortcutTable231 SessionMode ShortcutTable
232 // ResizeMode shortcuts are only active in resize mode.232 // ResizeMode shortcuts are only active in resize mode.
233 ResizeMode ShortcutTable233 ResizeMode ShortcutTable
234- // PageMode shortcuts are only active in page mode.234+ // VimMode shortcuts are only active in vim mode.
235- PageMode ShortcutTable235+ VimMode ShortcutTable
236 }236 }
237 237
238 // NewShortcutSet creates a ShortcutSet from workspace and session configuration.238 // NewShortcutSet creates a ShortcutSet from workspace and session configuration.
@@ -246,7 +246,7 @@ func NewShortcutSet(ctx context.Context, workspace *entity.WorkspaceConfig, sess
246 PaneMode: make(ShortcutTable),246 PaneMode: make(ShortcutTable),
247 SessionMode: make(ShortcutTable),247 SessionMode: make(ShortcutTable),
248 ResizeMode: make(ShortcutTable),248 ResizeMode: make(ShortcutTable),
249- PageMode: make(ShortcutTable),249+ VimMode: make(ShortcutTable),
250 }250 }
251 251
252 set.buildGlobalShortcutsFromParts(ctx, workspace, session)252 set.buildGlobalShortcutsFromParts(ctx, workspace, session)
@@ -254,7 +254,7 @@ func NewShortcutSet(ctx context.Context, workspace *entity.WorkspaceConfig, sess
254 set.buildTabModeShortcuts(ctx, workspace)254 set.buildTabModeShortcuts(ctx, workspace)
255 set.buildPaneModeShortcuts(ctx, workspace)255 set.buildPaneModeShortcuts(ctx, workspace)
256 set.buildResizeModeShortcuts(ctx, workspace)256 set.buildResizeModeShortcuts(ctx, workspace)
257- set.buildPageModeShortcuts(ctx, workspace)257+ set.buildVimModeShortcuts(ctx, workspace)
258 }258 }
259 if session != nil {259 if session != nil {
260 set.buildSessionModeShortcuts(ctx, session)260 set.buildSessionModeShortcuts(ctx, session)
@@ -266,7 +266,7 @@ func NewShortcutSet(ctx context.Context, workspace *entity.WorkspaceConfig, sess
266 Int("pane", len(set.PaneMode)).266 Int("pane", len(set.PaneMode)).
267 Int("resize", len(set.ResizeMode)).267 Int("resize", len(set.ResizeMode)).
268 Int("session", len(set.SessionMode)).268 Int("session", len(set.SessionMode)).
269- Int("page", len(set.PageMode)).269+ Int("vim", len(set.VimMode)).
270 Msg("shortcuts registered")270 Msg("shortcuts registered")
271 271
272 return set272 return set
@@ -302,19 +302,19 @@ func (s *ShortcutSet) buildResizeModeShortcuts(ctx context.Context, cfg *entity.
302 s.buildModeShortcuts(ctx, cfg.ResizeMode.GetKeyBindings(), s.ResizeMode, "resize")302 s.buildModeShortcuts(ctx, cfg.ResizeMode.GetKeyBindings(), s.ResizeMode, "resize")
303 }303 }
304 304
305-// buildPageModeShortcuts populates page mode shortcuts from config.305+// buildVimModeShortcuts populates vim mode shortcuts from config.
306-func (s *ShortcutSet) buildPageModeShortcuts(ctx context.Context, cfg *entity.WorkspaceConfig) {306+func (s *ShortcutSet) buildVimModeShortcuts(ctx context.Context, cfg *entity.WorkspaceConfig) {
307 log := logging.FromContext(ctx)307 log := logging.FromContext(ctx)
308- s.buildModeShortcuts(ctx, cfg.PageMode.GetKeyBindings(), s.PageMode, "page")308+ s.buildModeShortcuts(ctx, cfg.VimMode.GetKeyBindings(), s.VimMode, "vim")
309- if binding, ok := ParseKeyString(cfg.PageMode.ActivationShortcut); ok {309+ if binding, ok := ParseKeyString(cfg.VimMode.ActivationShortcut); ok {
310- s.PageMode[binding] = ActionEnterPageMode310+ s.VimMode[binding] = ActionEnterVimMode
311 log.Trace().311 log.Trace().
312- Str("shortcut", cfg.PageMode.ActivationShortcut).312+ Str("shortcut", cfg.VimMode.ActivationShortcut).
313 Uint("keyval", binding.Keyval).313 Uint("keyval", binding.Keyval).
314 Uint("mod", uint(binding.Modifiers)).314 Uint("mod", uint(binding.Modifiers)).
315- Msg("page mode toggle registered in page mode table")315+ Msg("vim mode toggle registered in vim mode table")
316 } else {316 } else {
317- log.Warn().Str("shortcut", cfg.PageMode.ActivationShortcut).Msg("failed to parse page mode activation shortcut for page mode table")317+ log.Warn().Str("shortcut", cfg.VimMode.ActivationShortcut).Msg("failed to parse vim mode activation shortcut for vim mode table")
318 }318 }
319 }319 }
320 320
@@ -338,15 +338,15 @@ func (s *ShortcutSet) registerActivationShortcutsFromParts(
338 }338 }
339 return339 return
340 }340 }
341- if binding, ok := ParseKeyString(workspace.PageMode.ActivationShortcut); ok {341+ if binding, ok := ParseKeyString(workspace.VimMode.ActivationShortcut); ok {
342- s.Global[binding] = ActionEnterPageMode342+ s.Global[binding] = ActionEnterVimMode
343 log.Trace().343 log.Trace().
344- Str("shortcut", workspace.PageMode.ActivationShortcut).344+ Str("shortcut", workspace.VimMode.ActivationShortcut).
345 Uint("keyval", binding.Keyval).345 Uint("keyval", binding.Keyval).
346 Uint("mod", uint(binding.Modifiers)).346 Uint("mod", uint(binding.Modifiers)).
347- Msg("page mode activation registered")347+ Msg("vim mode activation registered")
348 } else {348 } else {
349- log.Warn().Str("shortcut", workspace.PageMode.ActivationShortcut).Msg("failed to parse page mode activation shortcut")349+ log.Warn().Str("shortcut", workspace.VimMode.ActivationShortcut).Msg("failed to parse vim mode activation shortcut")
350 }350 }
351 if binding, ok := ParseKeyString(workspace.TabMode.ActivationShortcut); ok {351 if binding, ok := ParseKeyString(workspace.TabMode.ActivationShortcut); ok {
352 s.Global[binding] = ActionEnterTabMode352 s.Global[binding] = ActionEnterTabMode
@@ -683,19 +683,19 @@ var configActionToAction = map[string]Action{
683 // Session actions683 // Session actions
684 "session-manager": ActionOpenSessionManager,684 "session-manager": ActionOpenSessionManager,
685 685
686- // Page mode scroll actions686+ // Vim mode scroll actions
687- "page_scroll_left": ActionPageScrollLeft,687+ "vim_scroll_left": ActionVimScrollLeft,
688- "page-scroll-left": ActionPageScrollLeft,688+ "vim-scroll-left": ActionVimScrollLeft,
689- "page_scroll_down": ActionPageScrollDown,689+ "vim_scroll_down": ActionVimScrollDown,
690- "page-scroll-down": ActionPageScrollDown,690+ "vim-scroll-down": ActionVimScrollDown,
691- "page_scroll_up": ActionPageScrollUp,691+ "vim_scroll_up": ActionVimScrollUp,
692- "page-scroll-up": ActionPageScrollUp,692+ "vim-scroll-up": ActionVimScrollUp,
693- "page_scroll_right": ActionPageScrollRight,693+ "vim_scroll_right": ActionVimScrollRight,
694- "page-scroll-right": ActionPageScrollRight,694+ "vim-scroll-right": ActionVimScrollRight,
695- "page_scroll_down_fast": ActionPageScrollDownFast,695+ "vim_scroll_down_fast": ActionVimScrollDownFast,
696- "page-scroll-down-fast": ActionPageScrollDownFast,696+ "vim-scroll-down-fast": ActionVimScrollDownFast,
697- "page_scroll_up_fast": ActionPageScrollUpFast,697+ "vim_scroll_up_fast": ActionVimScrollUpFast,
698- "page-scroll-up-fast": ActionPageScrollUpFast,698+ "vim-scroll-up-fast": ActionVimScrollUpFast,
699 }699 }
700 700
701 // FloatingProfileTarget carries the session identity and URL for a floating profile action.701 // FloatingProfileTarget carries the session identity and URL for a floating profile action.
@@ -846,8 +846,8 @@ func stringToKeyval(s string) (uint, bool) {
846 846
847 // Lookup finds an action for the given key binding in the appropriate table.847 // Lookup finds an action for the given key binding in the appropriate table.
848 // It first checks the mode-specific table, then falls back to global unless848 // It first checks the mode-specific table, then falls back to global unless
849-// the current mode is Page Mode. Page Mode intentionally stays self-contained849+// the current mode is Vim Mode. Vim Mode intentionally stays self-contained
850-// so app-global shortcuts do not interrupt held page scrolling.850+// so app-global shortcuts do not interrupt held Vim-style scrolling.
851 func (s *ShortcutSet) Lookup(binding KeyBinding, mode Mode) (Action, bool) {851 func (s *ShortcutSet) Lookup(binding KeyBinding, mode Mode) (Action, bool) {
852 // Normalize the binding modifiers852 // Normalize the binding modifiers
853 binding.Modifiers &= modifierMask853 binding.Modifiers &= modifierMask
@@ -863,15 +863,15 @@ func (s *ShortcutSet) Lookup(binding KeyBinding, mode Mode) (Action, bool) {
863 modeTable = s.ResizeMode863 modeTable = s.ResizeMode
864 case ModeSession:864 case ModeSession:
865 modeTable = s.SessionMode865 modeTable = s.SessionMode
866- case ModePage:866+ case ModeVim:
867- modeTable = s.PageMode867+ modeTable = s.VimMode
868 }868 }
869 869
870 if modeTable != nil {870 if modeTable != nil {
871 if action, ok := modeTable[binding]; ok {871 if action, ok := modeTable[binding]; ok {
872 return action, true872 return action, true
873 }873 }
874- if mode == ModePage {874+ if mode == ModeVim {
875 return "", false875 return "", false
876 }876 }
877 }877 }
M internal/ui/input/shortcuts_test.go +35 -35
@@ -465,11 +465,11 @@ func TestGlobalShortcutActionMap_ConsumeOrExpelHyphenAliases(t *testing.T) {
465 }465 }
466 }466 }
467 467
468-func TestNewShortcutSet_PageModeActivationShortcut(t *testing.T) {468+func TestNewShortcutSet_VimModeActivationShortcut(t *testing.T) {
469 workspace := &entity.WorkspaceConfig{469 workspace := &entity.WorkspaceConfig{
470 TabMode: entity.TabModeConfig{ActivationShortcut: "ctrl+t"},470 TabMode: entity.TabModeConfig{ActivationShortcut: "ctrl+t"},
471 PaneMode: entity.PaneModeConfig{ActivationShortcut: "ctrl+p"},471 PaneMode: entity.PaneModeConfig{ActivationShortcut: "ctrl+p"},
472- PageMode: entity.PageModeConfig{ActivationShortcut: "ctrl+y"},472+ VimMode: entity.VimModeConfig{ActivationShortcut: "ctrl+y"},
473 }473 }
474 474
475 set := NewShortcutSet(context.Background(), workspace, nil)475 set := NewShortcutSet(context.Background(), workspace, nil)
@@ -481,26 +481,26 @@ func TestNewShortcutSet_PageModeActivationShortcut(t *testing.T) {
481 481
482 action, found := set.Global[binding]482 action, found := set.Global[binding]
483 if !found {483 if !found {
484- t.Fatal("page mode activation shortcut not found in Global")484+ t.Fatal("vim mode activation shortcut not found in Global")
485 }485 }
486- if action != ActionEnterPageMode {486+ if action != ActionEnterVimMode {
487- t.Fatalf("Global shortcut action = %s, want %s", action, ActionEnterPageMode)487+ t.Fatalf("Global shortcut action = %s, want %s", action, ActionEnterVimMode)
488 }488 }
489 }489 }
490 490
491-func TestShortcutSet_Lookup_PageMode(t *testing.T) {491+func TestShortcutSet_Lookup_VimMode(t *testing.T) {
492 set := &ShortcutSet{492 set := &ShortcutSet{
493 Global: ShortcutTable{493 Global: ShortcutTable{
494 {uint(gdk.KEY_q), ModCtrl}: ActionQuit,494 {uint(gdk.KEY_q), ModCtrl}: ActionQuit,
495 },495 },
496- PageMode: ShortcutTable{496+ VimMode: ShortcutTable{
497- {uint('h'), ModNone}: ActionPageScrollLeft,497+ {uint('h'), ModNone}: ActionVimScrollLeft,
498- {uint('j'), ModNone}: ActionPageScrollDown,498+ {uint('j'), ModNone}: ActionVimScrollDown,
499- {uint('k'), ModNone}: ActionPageScrollUp,499+ {uint('k'), ModNone}: ActionVimScrollUp,
500- {uint('l'), ModNone}: ActionPageScrollRight,500+ {uint('l'), ModNone}: ActionVimScrollRight,
501 {uint(gdk.KEY_Escape), ModNone}: ActionExitMode,501 {uint(gdk.KEY_Escape), ModNone}: ActionExitMode,
502 {uint(gdk.KEY_Return), ModNone}: ActionExitMode,502 {uint(gdk.KEY_Return), ModNone}: ActionExitMode,
503- {uint('y'), ModCtrl}: ActionEnterPageMode,503+ {uint('y'), ModCtrl}: ActionEnterVimMode,
504 },504 },
505 }505 }
506 506
@@ -513,47 +513,47 @@ func TestShortcutSet_Lookup_PageMode(t *testing.T) {
513 {513 {
514 name: "scroll left",514 name: "scroll left",
515 binding: KeyBinding{uint('h'), ModNone},515 binding: KeyBinding{uint('h'), ModNone},
516- want: ActionPageScrollLeft,516+ want: ActionVimScrollLeft,
517 wantOk: true,517 wantOk: true,
518 },518 },
519 {519 {
520 name: "scroll down",520 name: "scroll down",
521 binding: KeyBinding{uint('j'), ModNone},521 binding: KeyBinding{uint('j'), ModNone},
522- want: ActionPageScrollDown,522+ want: ActionVimScrollDown,
523 wantOk: true,523 wantOk: true,
524 },524 },
525 {525 {
526 name: "scroll up",526 name: "scroll up",
527 binding: KeyBinding{uint('k'), ModNone},527 binding: KeyBinding{uint('k'), ModNone},
528- want: ActionPageScrollUp,528+ want: ActionVimScrollUp,
529 wantOk: true,529 wantOk: true,
530 },530 },
531 {531 {
532 name: "scroll right",532 name: "scroll right",
533 binding: KeyBinding{uint('l'), ModNone},533 binding: KeyBinding{uint('l'), ModNone},
534- want: ActionPageScrollRight,534+ want: ActionVimScrollRight,
535 wantOk: true,535 wantOk: true,
536 },536 },
537 {537 {
538- name: "escape exits page mode",538+ name: "escape exits vim mode",
539 binding: KeyBinding{uint(gdk.KEY_Escape), ModNone},539 binding: KeyBinding{uint(gdk.KEY_Escape), ModNone},
540 want: ActionExitMode,540 want: ActionExitMode,
541 wantOk: true,541 wantOk: true,
542 },542 },
543 {543 {
544- name: "enter exits page mode",544+ name: "enter exits vim mode",
545 binding: KeyBinding{uint(gdk.KEY_Return), ModNone},545 binding: KeyBinding{uint(gdk.KEY_Return), ModNone},
546 want: ActionExitMode,546 want: ActionExitMode,
547 wantOk: true,547 wantOk: true,
548 },548 },
549 {549 {
550- name: "activation shortcut still toggles page mode",550+ name: "activation shortcut still toggles vim mode",
551 binding: KeyBinding{uint('y'), ModCtrl},551 binding: KeyBinding{uint('y'), ModCtrl},
552- want: ActionEnterPageMode,552+ want: ActionEnterVimMode,
553 wantOk: true,553 wantOk: true,
554 },554 },
555 {555 {
556- name: "global shortcut does not fire inside page mode",556+ name: "global shortcut does not fire inside vim mode",
557 binding: KeyBinding{uint(gdk.KEY_q), ModCtrl},557 binding: KeyBinding{uint(gdk.KEY_q), ModCtrl},
558 want: "",558 want: "",
559 wantOk: false,559 wantOk: false,
@@ -562,7 +562,7 @@ func TestShortcutSet_Lookup_PageMode(t *testing.T) {
562 562
563 for _, tt := range tests {563 for _, tt := range tests {
564 t.Run(tt.name, func(t *testing.T) {564 t.Run(tt.name, func(t *testing.T) {
565- got, ok := set.Lookup(tt.binding, ModePage)565+ got, ok := set.Lookup(tt.binding, ModeVim)
566 if ok != tt.wantOk {566 if ok != tt.wantOk {
567 t.Errorf("Lookup() ok = %v, want %v", ok, tt.wantOk)567 t.Errorf("Lookup() ok = %v, want %v", ok, tt.wantOk)
568 return568 return
@@ -574,24 +574,24 @@ func TestShortcutSet_Lookup_PageMode(t *testing.T) {
574 }574 }
575 }575 }
576 576
577-func TestMapConfigAction_PageScrollActions(t *testing.T) {577+func TestMapConfigAction_VimScrollActions(t *testing.T) {
578 tests := []struct {578 tests := []struct {
579 name string579 name string
580 config string580 config string
581 expected Action581 expected Action
582 }{582 }{
583- {name: "page_scroll_left", config: "page_scroll_left", expected: ActionPageScrollLeft},583+ {name: "vim_scroll_left", config: "vim_scroll_left", expected: ActionVimScrollLeft},
584- {name: "page-scroll-left", config: "page-scroll-left", expected: ActionPageScrollLeft},584+ {name: "vim-scroll-left", config: "vim-scroll-left", expected: ActionVimScrollLeft},
585- {name: "page_scroll_down", config: "page_scroll_down", expected: ActionPageScrollDown},585+ {name: "vim_scroll_down", config: "vim_scroll_down", expected: ActionVimScrollDown},
586- {name: "page-scroll-down", config: "page-scroll-down", expected: ActionPageScrollDown},586+ {name: "vim-scroll-down", config: "vim-scroll-down", expected: ActionVimScrollDown},
587- {name: "page_scroll_up", config: "page_scroll_up", expected: ActionPageScrollUp},587+ {name: "vim_scroll_up", config: "vim_scroll_up", expected: ActionVimScrollUp},
588- {name: "page-scroll-up", config: "page-scroll-up", expected: ActionPageScrollUp},588+ {name: "vim-scroll-up", config: "vim-scroll-up", expected: ActionVimScrollUp},
589- {name: "page_scroll_right", config: "page_scroll_right", expected: ActionPageScrollRight},589+ {name: "vim_scroll_right", config: "vim_scroll_right", expected: ActionVimScrollRight},
590- {name: "page-scroll-right", config: "page-scroll-right", expected: ActionPageScrollRight},590+ {name: "vim-scroll-right", config: "vim-scroll-right", expected: ActionVimScrollRight},
591- {name: "page_scroll_down_fast", config: "page_scroll_down_fast", expected: ActionPageScrollDownFast},591+ {name: "vim_scroll_down_fast", config: "vim_scroll_down_fast", expected: ActionVimScrollDownFast},
592- {name: "page-scroll-down-fast", config: "page-scroll-down-fast", expected: ActionPageScrollDownFast},592+ {name: "vim-scroll-down-fast", config: "vim-scroll-down-fast", expected: ActionVimScrollDownFast},
593- {name: "page_scroll_up_fast", config: "page_scroll_up_fast", expected: ActionPageScrollUpFast},593+ {name: "vim_scroll_up_fast", config: "vim_scroll_up_fast", expected: ActionVimScrollUpFast},
594- {name: "page-scroll-up-fast", config: "page-scroll-up-fast", expected: ActionPageScrollUpFast},594+ {name: "vim-scroll-up-fast", config: "vim-scroll-up-fast", expected: ActionVimScrollUpFast},
595 }595 }
596 596
597 for _, tt := range tests {597 for _, tt := range tests {
M internal/ui/theme/css.go +25 -20
@@ -111,8 +111,8 @@ func GenerateCSSFullWithTiming(p Palette, _ float64, fonts FontConfig, modeColor
111 sb.WriteString(generateStackedPaneCSS(p))111 sb.WriteString(generateStackedPaneCSS(p))
112 sb.WriteString("\n")112 sb.WriteString("\n")
113 113
114- // Page mode styling114+ // Vim mode styling
115- sb.WriteString(generatePageModeCSS(transitionDurationMs))115+ sb.WriteString(generateVimModeCSS(transitionDurationMs))
116 sb.WriteString("\n")116 sb.WriteString("\n")
117 117
118 // Progress bar styling118 // Progress bar styling
@@ -593,65 +593,65 @@ entry.find-bar-entry:focus-visible {
593 593
594 // generatePaneCSS creates pane border styles.594 // generatePaneCSS creates pane border styles.
595 // Uses em units for scalable UI.595 // Uses em units for scalable UI.
596-// generatePageModeCSS creates the pane-local Page Mode accent and pulse styling.596+// generateVimModeCSS creates the pane-local Vim Mode accent and pulse styling.
597 // It reuses --pane-mode-color (from workspace.styling.pane_mode_color).597 // It reuses --pane-mode-color (from workspace.styling.pane_mode_color).
598-func generatePageModeCSS(transitionDurationMs int) string {598+func generateVimModeCSS(transitionDurationMs int) string {
599 if transitionDurationMs < 0 {599 if transitionDurationMs < 0 {
600 transitionDurationMs = defaultTransitionDurationMs600 transitionDurationMs = defaultTransitionDurationMs
601 }601 }
602 normalPulseMs := transitionDurationMs * 3602 normalPulseMs := transitionDurationMs * 3
603 fastPulseMs := transitionDurationMs * 6603 fastPulseMs := transitionDurationMs * 6
604- return fmt.Sprintf(`/* ===== Page Mode Styling ===== */604+ return fmt.Sprintf(`/* ===== Vim Mode Styling ===== */
605 605
606-/* Page mode active — subtle local border accent on the pane overlay.606+/* Vim mode active — subtle local border accent on the pane overlay.
607 Uses the existing pane mode color token via CSS variable.607 Uses the existing pane mode color token via CSS variable.
608 This is independent of the workspace-level pane-mode border overlay. */608 This is independent of the workspace-level pane-mode border overlay. */
609-.page-mode-active {609+.vim-mode-active {
610 box-shadow: inset 0 0 0 0.125em alpha(var(--pane-mode-color), 0.35);610 box-shadow: inset 0 0 0 0.125em alpha(var(--pane-mode-color), 0.35);
611 border-radius: 0;611 border-radius: 0;
612 }612 }
613 613
614 /* Pane overlay pulse keyframes — start/end at the active accent state so the614 /* Pane overlay pulse keyframes — start/end at the active accent state so the
615- pane keeps its Page mode border while briefly flaring brighter. */615+ pane keeps its Vim mode border while briefly flaring brighter. */
616-@keyframes page-mode-overlay-pulse-anim-a {616+@keyframes vim-mode-overlay-pulse-anim-a {
617 0%% { box-shadow: inset 0 0 0 0.125em alpha(var(--pane-mode-color), 0.35); }617 0%% { box-shadow: inset 0 0 0 0.125em alpha(var(--pane-mode-color), 0.35); }
618 35%% { box-shadow: inset 0 0 0 0.2em alpha(var(--pane-mode-color), 0.78); }618 35%% { box-shadow: inset 0 0 0 0.2em alpha(var(--pane-mode-color), 0.78); }
619 100%% { box-shadow: inset 0 0 0 0.125em alpha(var(--pane-mode-color), 0.35); }619 100%% { box-shadow: inset 0 0 0 0.125em alpha(var(--pane-mode-color), 0.35); }
620 }620 }
621 621
622-@keyframes page-mode-overlay-pulse-anim-b {622+@keyframes vim-mode-overlay-pulse-anim-b {
623 0%% { box-shadow: inset 0 0 0 0.125em alpha(var(--pane-mode-color), 0.35); }623 0%% { box-shadow: inset 0 0 0 0.125em alpha(var(--pane-mode-color), 0.35); }
624 35%% { box-shadow: inset 0 0 0 0.2em alpha(var(--pane-mode-color), 0.78); }624 35%% { box-shadow: inset 0 0 0 0.2em alpha(var(--pane-mode-color), 0.78); }
625 100%% { box-shadow: inset 0 0 0 0.125em alpha(var(--pane-mode-color), 0.35); }625 100%% { box-shadow: inset 0 0 0 0.125em alpha(var(--pane-mode-color), 0.35); }
626 }626 }
627 627
628-@keyframes page-mode-overlay-pulse-fast-anim-a {628+@keyframes vim-mode-overlay-pulse-fast-anim-a {
629 0%% { box-shadow: inset 0 0 0 0.125em alpha(var(--pane-mode-color), 0.35); }629 0%% { box-shadow: inset 0 0 0 0.125em alpha(var(--pane-mode-color), 0.35); }
630 25%% { box-shadow: inset 0 0 0 0.26em alpha(var(--pane-mode-color), 0.95); }630 25%% { box-shadow: inset 0 0 0 0.26em alpha(var(--pane-mode-color), 0.95); }
631 100%% { box-shadow: inset 0 0 0 0.125em alpha(var(--pane-mode-color), 0.35); }631 100%% { box-shadow: inset 0 0 0 0.125em alpha(var(--pane-mode-color), 0.35); }
632 }632 }
633 633
634-@keyframes page-mode-overlay-pulse-fast-anim-b {634+@keyframes vim-mode-overlay-pulse-fast-anim-b {
635 0%% { box-shadow: inset 0 0 0 0.125em alpha(var(--pane-mode-color), 0.35); }635 0%% { box-shadow: inset 0 0 0 0.125em alpha(var(--pane-mode-color), 0.35); }
636 25%% { box-shadow: inset 0 0 0 0.26em alpha(var(--pane-mode-color), 0.95); }636 25%% { box-shadow: inset 0 0 0 0.26em alpha(var(--pane-mode-color), 0.95); }
637 100%% { box-shadow: inset 0 0 0 0.125em alpha(var(--pane-mode-color), 0.35); }637 100%% { box-shadow: inset 0 0 0 0.125em alpha(var(--pane-mode-color), 0.35); }
638 }638 }
639 639
640 /* Pane overlay scroll pulse — derived from transition_duration. */640 /* Pane overlay scroll pulse — derived from transition_duration. */
641-.page-mode-pulse.page-mode-pulse-cycle-a {641+.vim-mode-pulse.vim-mode-pulse-cycle-a {
642- animation: page-mode-overlay-pulse-anim-a %dms ease-in-out;642+ animation: vim-mode-overlay-pulse-anim-a %dms ease-in-out;
643 }643 }
644 644
645-.page-mode-pulse.page-mode-pulse-cycle-b {645+.vim-mode-pulse.vim-mode-pulse-cycle-b {
646- animation: page-mode-overlay-pulse-anim-b %dms ease-in-out;646+ animation: vim-mode-overlay-pulse-anim-b %dms ease-in-out;
647 }647 }
648 648
649-.page-mode-pulse-fast.page-mode-pulse-cycle-a {649+.vim-mode-pulse-fast.vim-mode-pulse-cycle-a {
650- animation: page-mode-overlay-pulse-fast-anim-a %dms ease-in-out;650+ animation: vim-mode-overlay-pulse-fast-anim-a %dms ease-in-out;
651 }651 }
652 652
653-.page-mode-pulse-fast.page-mode-pulse-cycle-b {653+.vim-mode-pulse-fast.vim-mode-pulse-cycle-b {
654- animation: page-mode-overlay-pulse-fast-anim-b %dms ease-in-out;654+ animation: vim-mode-overlay-pulse-fast-anim-b %dms ease-in-out;
655 }655 }
656 `, normalPulseMs, normalPulseMs, fastPulseMs, fastPulseMs)656 `, normalPulseMs, normalPulseMs, fastPulseMs, fastPulseMs)
657 }657 }
@@ -1214,6 +1214,11 @@ func generateToasterCSS(p Palette) string {
1214 color: #ffffff;1214 color: #ffffff;
1215 }1215 }
1216 1216
1217+.toast-vim-mode {
1218+ background-color: var(--pane-mode-color);
1219+ color: #ffffff;
1220+}
1221+
1217 .toast-tab-mode {1222 .toast-tab-mode {
1218 background-color: var(--tab-mode-color);1223 background-color: var(--tab-mode-color);
1219 color: #ffffff;1224 color: #ffffff;
M internal/ui/theme/css_test.go +5 -5
@@ -34,14 +34,14 @@ func TestGenerateCSS_StandaloneOmniboxWindowUsesTransparentBackground(t *testing
34 }34 }
35 }35 }
36 36
37-func TestGenerateCSSFullWithTiming_PageModePulseUsesConfiguredTransitionDuration(t *testing.T) {37+func TestGenerateCSSFullWithTiming_VimModePulseUsesConfiguredTransitionDuration(t *testing.T) {
38- // Page Mode overlay pulse timings scale from the base transition duration:38+ // Vim Mode overlay pulse timings scale from the base transition duration:
39 // normal pulse = 3× (150ms -> 450ms), fast pulse = 6× (150ms -> 900ms).39 // normal pulse = 3× (150ms -> 450ms), fast pulse = 6× (150ms -> 900ms).
40 css := GenerateCSSFullWithTiming(DefaultDarkPalette(), 1.0, DefaultFontConfig(), DefaultModeColors(), 150)40 css := GenerateCSSFullWithTiming(DefaultDarkPalette(), 1.0, DefaultFontConfig(), DefaultModeColors(), 150)
41 41
42- assert.Contains(t, css, "animation: page-mode-overlay-pulse-anim-a 450ms ease-in-out;")42+ assert.Contains(t, css, "animation: vim-mode-overlay-pulse-anim-a 450ms ease-in-out;")
43- assert.Contains(t, css, "animation: page-mode-overlay-pulse-fast-anim-a 900ms ease-in-out;")43+ assert.Contains(t, css, "animation: vim-mode-overlay-pulse-fast-anim-a 900ms ease-in-out;")
44- assert.NotContains(t, css, "page-mode-indicator")44+ assert.NotContains(t, css, "vim-mode-indicator")
45 }45 }
46 46
47 func TestGenerateCSS_OmniboxHeaderIsOpaque(t *testing.T) {47 func TestGenerateCSS_OmniboxHeaderIsOpaque(t *testing.T) {
M internal/ui/theme/manager.go +1 -1
@@ -65,7 +65,7 @@ func (m *Manager) applyResolvedTheme(resolved entity.ResolvedTheme) {
65 }65 }
66 66
67 // SetTransitionDuration sets the CSS transition duration used for theme-driven67 // SetTransitionDuration sets the CSS transition duration used for theme-driven
68-// animations such as Page mode pulse timing.68+// animations such as Vim mode pulse timing.
69 func (m *Manager) SetTransitionDuration(ms int) {69 func (m *Manager) SetTransitionDuration(ms int) {
70 if ms < 0 {70 if ms < 0 {
71 ms = defaultTransitionDurationMs71 ms = defaultTransitionDurationMs

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