detached 23fc389

feat(input): rename page mode to vim mode

commit
23fc389a687e10c19932212fdbb6ddba6120b72b
author
brice · · 2026-08-13 08:32 UTC
committer
brice · · 2026-08-14 02:31 UTC
parent
48cc1e2
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
52 files · +1731 -1565
M README.md +1 -1
@@ -21,7 +21,7 @@ Dumber uses three layout levels:
2121 - **Workspaces** hold a layout of panes.
2222 - **Panes** display web pages and can be split, stacked, moved, resized, or closed.
2323
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.
2525
2626 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.
2727
M docs/config/index.md +21 -21
@@ -398,56 +398,56 @@ cancel = ["escape"]
398398
399399 > **Note:** Actions are inverted to key→action map in memory for O(1) lookup performance during navigation.
400400
401-### Page Mode
401+### Vim Mode
402402
403403 | Key | Type | Default | Description |
404404 |-----|------|---------|-------------|
405-| `workspace.page_mode.activation_shortcut` | string | `"ctrl+y"` | Page mode activation key |
406-| `workspace.page_mode.timeout_ms` | int | `0` | Page mode timeout in milliseconds (`0` disables auto-timeout) |
407-| `workspace.page_mode.actions` | map | See below | Action→binding mappings |
405+| `workspace.vim_mode.activation_shortcut` | string | `"ctrl+y"` | Vim mode activation key |
406+| `workspace.vim_mode.timeout_ms` | int | `0` | Vim mode timeout in milliseconds (`0` disables auto-timeout) |
407+| `workspace.vim_mode.actions` | map | See below | Action→binding mappings |
408408
409-**Default page mode actions:**
409+**Default vim mode actions:**
410410
411411 ```toml
412-[workspace.page_mode.actions.page-scroll-left]
412+[workspace.vim_mode.actions.vim-scroll-left]
413413 keys = ["h"]
414414 desc = "Scroll page left"
415415
416-[workspace.page_mode.actions.page-scroll-down]
416+[workspace.vim_mode.actions.vim-scroll-down]
417417 keys = ["j"]
418418 desc = "Scroll page down"
419419
420-[workspace.page_mode.actions.page-scroll-up]
420+[workspace.vim_mode.actions.vim-scroll-up]
421421 keys = ["k"]
422422 desc = "Scroll page up"
423423
424-[workspace.page_mode.actions.page-scroll-right]
424+[workspace.vim_mode.actions.vim-scroll-right]
425425 keys = ["l"]
426426 desc = "Scroll page right"
427427
428-[workspace.page_mode.actions.page-scroll-down-fast]
428+[workspace.vim_mode.actions.vim-scroll-down-fast]
429429 keys = ["shift+j"]
430430 desc = "Scroll page down fast"
431431
432-[workspace.page_mode.actions.page-scroll-up-fast]
432+[workspace.vim_mode.actions.vim-scroll-up-fast]
433433 keys = ["shift+k"]
434434 desc = "Scroll page up fast"
435435
436-[workspace.page_mode.actions.confirm]
436+[workspace.vim_mode.actions.confirm]
437437 keys = ["enter"]
438438 desc = "Confirm action"
439439
440-[workspace.page_mode.actions.cancel]
440+[workspace.vim_mode.actions.cancel]
441441 keys = ["escape"]
442442 desc = "Cancel/exit mode"
443443 ```
444444
445445 Notes:
446-- Page 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.
448-- Arrow keys still use the browser engine's native page-navigation path while Page Mode is active.
449-- Other app-level shortcuts stay suspended until Page Mode exits, except for the Page 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.
446+- Vim Mode scrolls the active webpage inside the active pane only.
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 Vim Mode is active.
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 Vim Mode visuals.
451451 - 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.
452452
453453 ### Resize Mode
@@ -581,11 +581,11 @@ desc = "Open floating pane on GitHub"
581581 | `workspace.styling.border_width` | int | `1` | Active pane border width (px) - overlay |
582582 | `workspace.styling.border_color` | string | `"@theme_selected_bg_color"` | Active pane border color |
583583 | `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 |
585585 | `workspace.styling.tab_mode_color` | string | `"#FFA500"` | Tab mode color (orange) - used for border and toaster |
586586 | `workspace.styling.session_mode_color` | string | `"#9B59B6"` | Session mode color (purple) - used for border and toaster |
587587 | `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 |
589589 | `workspace.styling.transition_duration` | int | `120` | Border and pulse transition duration (ms) |
590590
591591 **Example:**
@@ -595,7 +595,7 @@ desc = "Open floating pane on GitHub"
595595 border_width = 1
596596 border_color = "@theme_selected_bg_color"
597597 mode_border_width = 4
598-pane_mode_color = "#4A90E2" # Blue for pane mode and Page mode local accent
598+pane_mode_color = "#4A90E2" # Blue for pane mode and Vim mode local accent
599599 tab_mode_color = "#FFA500" # Orange for tab mode
600600 session_mode_color = "#9B59B6" # Purple for session mode
601601 resize_mode_color = "#00D4AA" # Teal for resize mode
M docs/getting-started.md +7 -7
@@ -16,12 +16,12 @@ Dumber uses modal keybindings inspired by Zellij:
1616 |------|------------|---------|
1717 | Pane Mode | `Ctrl+P` | Split, close, focus panes |
1818 | 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 |
2020 | Resize Mode | `Ctrl+N` | Resize pane splits |
2121 | Session Mode | `Ctrl+O` | Session management |
2222
2323 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.
2525
2626 ## Pane Mode Quick Reference
2727
@@ -30,17 +30,17 @@ Page Mode stays local to the active pane and automatically leaves the mode when
3030 3. `Shift+arrows` to focus adjacent panes
3131 4. `X` to close current pane
3232
33-## Page Mode Quick Reference
33+## Vim Mode Quick Reference
3434
35-1. Press `Ctrl+Y` to enter Page Mode
35+1. Press `Ctrl+Y` to enter Vim Mode
3636 2. Use `h`, `j`, `k`, `l` to scroll left, down, up, or right
3737 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 active
39-5. Other app-level shortcuts stay suspended until you leave Page Mode
38+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 Vim Mode
4040 6. Press `Escape` or `Enter` to leave the mode
4141 7. If a page input or editor is already focused, `Ctrl+Y` is passed through instead of stealing the keypress
4242
43-> **Engine behavior**: CEF and WebKit execute Page Mode scroll steps with the
43+> **Engine behavior**: CEF and WebKit execute Vim Mode scroll steps with the
4444 > shared `BuildScrollByJS` resolver (viewport-center start, nested-scroller
4545 > handoff, document fallback). The application repeater owns held-key cadence;
4646 > each engine runs one immediate step per tick. Cross-origin frames are
M docs/reference/configuration.md +3 -3
@@ -100,9 +100,9 @@
100100 | `workspace.tab_mode.activation_shortcut` | string | `ctrl+t` | |
101101 | `workspace.tab_mode.timeout_ms` | int | `3000` | |
102102 | `workspace.tab_mode.actions.<action>` | []string | see defaults | tab mode key mappings |
103-| `workspace.page_mode.activation_shortcut` | string | `ctrl+y` | |
104-| `workspace.page_mode.timeout_ms` | int | `0` | >= 0 |
105-| `workspace.page_mode.actions.<action>` | []string | see defaults | page mode key mappings |
103+| `workspace.vim_mode.activation_shortcut` | string | `ctrl+y` | |
104+| `workspace.vim_mode.timeout_ms` | int | `0` | >= 0 |
105+| `workspace.vim_mode.actions.<action>` | []string | see defaults | vim mode key mappings |
106106 | `workspace.resize_mode.activation_shortcut` | string | `ctrl+n` | |
107107 | `workspace.resize_mode.timeout_ms` | int | `3000` | |
108108 | `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
88 |------|-----|---------|
99 | Pane Mode | `Ctrl+P` | Split, close, focus panes |
1010 | 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 |
1212 | Resize Mode | `Ctrl+N` | Resize pane splits |
1313 | Session Mode | `Ctrl+O` | Session management |
1414
1515 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.
1717
1818 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.
1919
@@ -53,11 +53,11 @@ Keybinding tables use uppercase letters as visual labels for unshifted letter ke
5353 | Confirm | `Enter` |
5454 | Cancel | `Escape` |
5555
56-## Page Mode (`Ctrl+Y`)
56+## Vim Mode (`Ctrl+Y`)
5757
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.
5959
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 `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.
6161
6262 | Action | Keys |
6363 |--------|------|
@@ -137,26 +137,26 @@ All keybindings can be customized in `~/.config/dumber/config.toml`:
137137 split-right = ["arrowright", "r"]
138138 close-pane = ["x", "q"]
139139
140-[workspace.page_mode]
140+[workspace.vim_mode]
141141 activation_shortcut = "ctrl+y"
142142 timeout_ms = 0
143143
144-[workspace.page_mode.actions.page-scroll-left]
144+[workspace.vim_mode.actions.vim-scroll-left]
145145 keys = ["h"]
146146
147-[workspace.page_mode.actions.page-scroll-down]
147+[workspace.vim_mode.actions.vim-scroll-down]
148148 keys = ["j"]
149149
150-[workspace.page_mode.actions.page-scroll-up]
150+[workspace.vim_mode.actions.vim-scroll-up]
151151 keys = ["k"]
152152
153-[workspace.page_mode.actions.page-scroll-right]
153+[workspace.vim_mode.actions.vim-scroll-right]
154154 keys = ["l"]
155155
156-[workspace.page_mode.actions.page-scroll-down-fast]
156+[workspace.vim_mode.actions.vim-scroll-down-fast]
157157 keys = ["shift+j"]
158158
159-[workspace.page_mode.actions.page-scroll-up-fast]
159+[workspace.vim_mode.actions.vim-scroll-up-fast]
160160 keys = ["shift+k"]
161161
162162 [workspace.shortcuts.actions.close-pane]
M internal/application/port/webview.go +4 -4
@@ -395,7 +395,7 @@ type TextInputTargetProvider interface {
395395 TextInputTarget() TextInputTarget
396396 }
397397
398-// PageScrollCommand identifies a semantic page-scroll request at the
398+// PageScrollCommand identifies a semantic vim-scroll request at the
399399 // application boundary without depending on usecase package enums.
400400 type PageScrollCommand int
401401
@@ -408,7 +408,7 @@ const (
408408 PageScrollCommandDownFast
409409 )
410410
411-// PageScrollRequest carries a semantic page-scroll command identity and its
411+// PageScrollRequest carries a semantic vim-scroll command identity and its
412412 // fallback pixel deltas. Engines own the execution strategy and may use the
413413 // fallback delta when no native mechanism exists for the command.
414414 type PageScrollRequest struct {
@@ -422,7 +422,7 @@ type PageScrollRequest struct {
422422
423423 // PageScrollable is an optional capability for WebViews that support semantic
424424 // page scrolling. Page scrolling is used by keyboard-driven navigation modes
425-// such as Page mode.
425+// such as Vim mode.
426426 //
427427 // Engines own the execution strategy. When a native mechanism exists for the
428428 // requested command, the engine may ignore the fallback delta. Otherwise it
@@ -432,7 +432,7 @@ type PageScrollable interface {
432432 }
433433
434434 // PageScrollCanceler is an optional capability for adapters that queue held
435-// page-scroll work. Implementations discard unconsumed continuous deltas while
435+// vim-scroll work. Implementations discard unconsumed continuous deltas while
436436 // preserving any guaranteed tap already accepted.
437437 type PageScrollCanceler interface {
438438 CancelPageScroll(ctx context.Context)
M internal/application/usecase/keybindings.go +1 -0
@@ -18,6 +18,7 @@ var validModes = map[string]bool{
1818 "global": true,
1919 "pane": true,
2020 "tab": true,
21+ "vim": true,
2122 "resize": true,
2223 "session": true,
2324 }
M internal/application/usecase/keybindings_test.go +11 -0
@@ -72,6 +72,17 @@ func TestGetKeybindingsUseCase_Execute(t *testing.T) {
7272 }
7373
7474 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+
7586 t.Run("successfully sets keybinding", func(t *testing.T) {
7687 // Arrange
7788 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
140140 func cloneWorkspaceConfig(in entity.WorkspaceConfig) entity.WorkspaceConfig {
141141 in.PaneMode.Actions = cloneActionBindings(in.PaneMode.Actions)
142142 in.TabMode.Actions = cloneActionBindings(in.TabMode.Actions)
143- in.PageMode.Actions = cloneActionBindings(in.PageMode.Actions)
143+ in.VimMode.Actions = cloneActionBindings(in.VimMode.Actions)
144144 in.ResizeMode.Actions = cloneActionBindings(in.ResizeMode.Actions)
145145 in.Shortcuts.Actions = cloneActionBindings(in.Shortcuts.Actions)
146146 in.FloatingPane.Profiles = cloneFloatingPaneProfiles(in.FloatingPane.Profiles)
M internal/bootstrap/runtime_config_test.go +8 -8
@@ -258,8 +258,8 @@ func runtimeConfigWithNestedMutableFields() *config.Config {
258258 cfg.Workspace.TabMode.Actions = map[string]entity.ActionBinding{
259259 "next-tab": {Keys: []string{"l"}, Desc: "Next tab"},
260260 }
261- cfg.Workspace.PageMode.Actions = map[string]entity.ActionBinding{
262- "page-scroll-down": {Keys: []string{"j"}, Desc: "Scroll down"},
261+ cfg.Workspace.VimMode.Actions = map[string]entity.ActionBinding{
262+ "vim-scroll-down": {Keys: []string{"j"}, Desc: "Scroll down"},
263263 }
264264 cfg.Workspace.ResizeMode.Actions = map[string]entity.ActionBinding{
265265 "grow-right": {Keys: []string{"right"}, Desc: "Grow right"},
@@ -283,8 +283,8 @@ func mutateNestedRuntimeConfigSnapshot(snapshot entity.RuntimeConfigSnapshot) {
283283 mutateActionBinding(snapshot.UI.Workspace.TabMode.Actions, "next-tab", "mutated-tab")
284284 snapshot.UI.Workspace.TabMode.Actions["added-tab"] = entity.ActionBinding{Keys: []string{"added"}}
285285
286- mutateActionBinding(snapshot.UI.Workspace.PageMode.Actions, "page-scroll-down", "mutated-page")
287- snapshot.UI.Workspace.PageMode.Actions["added-page"] = entity.ActionBinding{Keys: []string{"added"}}
286+ mutateActionBinding(snapshot.UI.Workspace.VimMode.Actions, "vim-scroll-down", "mutated-page")
287+ snapshot.UI.Workspace.VimMode.Actions["added-page"] = entity.ActionBinding{Keys: []string{"added"}}
288288
289289 mutateActionBinding(snapshot.UI.Workspace.ResizeMode.Actions, "grow-right", "mutated-resize")
290290 snapshot.UI.Workspace.ResizeMode.Actions["added-resize"] = entity.ActionBinding{Keys: []string{"added"}}
@@ -316,8 +316,8 @@ func assertNestedRuntimeConfigSnapshotUnchanged(t *testing.T, snapshot entity.Ru
316316 assertActionBinding(t, snapshot.UI.Workspace.TabMode.Actions, "next-tab", "l")
317317 assertMapEntryAbsent(t, snapshot.UI.Workspace.TabMode.Actions, "added-tab")
318318
319- assertActionBinding(t, snapshot.UI.Workspace.PageMode.Actions, "page-scroll-down", "j")
320- assertMapEntryAbsent(t, snapshot.UI.Workspace.PageMode.Actions, "added-page")
319+ assertActionBinding(t, snapshot.UI.Workspace.VimMode.Actions, "vim-scroll-down", "j")
320+ assertMapEntryAbsent(t, snapshot.UI.Workspace.VimMode.Actions, "added-page")
321321
322322 assertActionBinding(t, snapshot.UI.Workspace.ResizeMode.Actions, "grow-right", "right")
323323 assertMapEntryAbsent(t, snapshot.UI.Workspace.ResizeMode.Actions, "added-resize")
@@ -366,8 +366,8 @@ func assertNestedSourceConfigUnchanged(t *testing.T, cfg *config.Config) {
366366 assertActionBinding(t, cfg.Workspace.TabMode.Actions, "next-tab", "l")
367367 assertMapEntryAbsent(t, cfg.Workspace.TabMode.Actions, "added-tab")
368368
369- assertActionBinding(t, cfg.Workspace.PageMode.Actions, "page-scroll-down", "j")
370- assertMapEntryAbsent(t, cfg.Workspace.PageMode.Actions, "added-page")
369+ assertActionBinding(t, cfg.Workspace.VimMode.Actions, "vim-scroll-down", "j")
370+ assertMapEntryAbsent(t, cfg.Workspace.VimMode.Actions, "added-page")
371371
372372 assertActionBinding(t, cfg.Workspace.ResizeMode.Actions, "grow-right", "right")
373373 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 {
7878 return keyBindingsFromActions(t.Actions)
7979 }
8080
81-// PageModeConfig holds page scroll mode shortcut configuration.
82-type PageModeConfig struct {
81+// VimModeConfig holds vim scroll mode shortcut configuration.
82+type VimModeConfig struct {
8383 ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" toml:"activation_shortcut" json:"activation_shortcut"` //nolint:lll // struct tags must stay on one line
8484 TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" toml:"timeout_ms" json:"timeout_ms"`
8585 Actions map[string]ActionBinding `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"`
8686 }
8787
8888 // 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 {
9090 return keyBindingsFromActions(p.Actions)
9191 }
9292
@@ -240,7 +240,7 @@ type WorkspaceConfig struct {
240240 NewPaneURL string `mapstructure:"new_pane_url" yaml:"new_pane_url" toml:"new_pane_url" json:"new_pane_url"`
241241 PaneMode PaneModeConfig `mapstructure:"pane_mode" yaml:"pane_mode" toml:"pane_mode" json:"pane_mode"`
242242 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"`
244244 ResizeMode ResizeModeConfig `mapstructure:"resize_mode" yaml:"resize_mode" toml:"resize_mode" json:"resize_mode"`
245245 Shortcuts GlobalShortcutsConfig `mapstructure:"shortcuts" yaml:"shortcuts" toml:"shortcuts" json:"shortcuts"`
246246 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 {
23962396 }
23972397
23982398 // 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 the
2399+// Vim Mode scrolling. It targets the center of the current OSR view so the
24002400 // scroll goes through Chromium's compositor/input pipeline instead of a series
24012401 // of synthetic key taps.
24022402 func (wv *WebView) pageScrollWheelEvent() purecef.MouseEvent {
@@ -2441,7 +2441,7 @@ func (wv *WebView) ScrollPage(ctx context.Context, request port.PageScrollReques
24412441 return nil
24422442 }
24432443
2444- wv.RunJavaScript(ctx, webutil.BuildScrollByJS(request.FallbackDX, request.FallbackDY))
2444+ wv.RunJavaScript(ctx, webutil.BuildPageScrollByJS(request.FallbackDX, request.FallbackDY))
24452445 return nil
24462446 }
24472447
@@ -2510,7 +2510,7 @@ func (wv *WebView) flushPageScroll() {
25102510 }
25112511 q.mu.Unlock()
25122512 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))
25142514 }
25152515 q.commitMu.Unlock()
25162516
M internal/infrastructure/cef/webview_scroll_test.go +3 -3
@@ -42,7 +42,7 @@ func TestPageScrollWheelDeltas_UseFallbackDeltasWithCEFSign(t *testing.T) {
4242 }
4343
4444 // 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.
4646 type scrollRecorderHost struct {
4747 purecef.BrowserHost // embedded nil — only SendMouseWheelEvent is called in these tests
4848
@@ -174,7 +174,7 @@ func TestScrollPage_ReadyBrowser_HorizontalDirectionsUseDOMSigns(t *testing.T) {
174174 func TestScrollPage_ReadyBrowser_NestedBoundaryHandsOffToDocument(t *testing.T) {
175175 node, err := exec.LookPath("node")
176176 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")
178178 }
179179
180180 host := &scrollRecorderHost{}
@@ -211,7 +211,7 @@ process.stdout.write(inner.scrollTop+','+root.scrollTop);
211211 `, pageScrollScript)
212212 output, err := exec.Command(node, "-e", harness).CombinedOutput()
213213 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)
215215 }
216216 if got := strings.TrimSpace(string(output)); got != "200,80" {
217217 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 {
304304 "cancel": {Keys: []string{"escape"}, Desc: "Cancel/exit mode"},
305305 },
306306 },
307- PageMode: PageModeConfig{
307+ VimMode: VimModeConfig{
308308 ActivationShortcut: defaultPageActivationShortcut,
309309 TimeoutMilliseconds: defaultPageTimeoutMilliseconds,
310310 Actions: map[string]ActionBinding{
311- "page-scroll-left": {Keys: []string{"h"}, Desc: "Scroll page left"},
312- "page-scroll-down": {Keys: []string{"j"}, Desc: "Scroll page down"},
313- "page-scroll-up": {Keys: []string{"k"}, Desc: "Scroll page up"},
314- "page-scroll-right": {Keys: []string{"l"}, Desc: "Scroll page right"},
315- "page-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"},
317- "confirm": {Keys: []string{"enter"}, Desc: "Confirm action"},
318- "cancel": {Keys: []string{"escape"}, Desc: "Cancel/exit mode"},
311+ "vim-scroll-left": {Keys: []string{"h"}, Desc: "Scroll page left"},
312+ "vim-scroll-down": {Keys: []string{"j"}, Desc: "Scroll page down"},
313+ "vim-scroll-up": {Keys: []string{"k"}, Desc: "Scroll page up"},
314+ "vim-scroll-right": {Keys: []string{"l"}, Desc: "Scroll page right"},
315+ "vim-scroll-down-fast": {Keys: []string{"shift+j"}, Desc: "Scroll page down fast"},
316+ "vim-scroll-up-fast": {Keys: []string{"shift+k"}, Desc: "Scroll page up fast"},
317+ "confirm": {Keys: []string{"enter"}, Desc: "Confirm action"},
318+ "cancel": {Keys: []string{"escape"}, Desc: "Cancel/exit mode"},
319319 },
320320 },
321321 ResizeMode: ResizeModeConfig{
M internal/infrastructure/config/keybindings_gateway.go +15 -15
@@ -32,7 +32,7 @@ func (g *KeybindingsGateway) GetKeybindings(ctx context.Context) (port.Keybindin
3232 g.buildGlobalGroup(cfg, defaults),
3333 g.buildPaneModeGroup(cfg, defaults),
3434 g.buildTabModeGroup(cfg, defaults),
35- g.buildPageModeGroup(cfg, defaults),
35+ g.buildVimModeGroup(cfg, defaults),
3636 g.buildResizeModeGroup(cfg, defaults),
3737 g.buildSessionModeGroup(cfg, defaults),
3838 }
@@ -49,7 +49,7 @@ func (g *KeybindingsGateway) GetDefaultKeybindings(ctx context.Context) (port.Ke
4949 g.buildGlobalGroup(defaults, defaults),
5050 g.buildPaneModeGroup(defaults, defaults),
5151 g.buildTabModeGroup(defaults, defaults),
52- g.buildPageModeGroup(defaults, defaults),
52+ g.buildVimModeGroup(defaults, defaults),
5353 g.buildResizeModeGroup(defaults, defaults),
5454 g.buildSessionModeGroup(defaults, defaults),
5555 }
@@ -110,7 +110,7 @@ func (g *KeybindingsGateway) ResetAllKeybindings(ctx context.Context) error {
110110
111111 cfg.Workspace.PaneMode.Actions = defaults.Workspace.PaneMode.Actions
112112 cfg.Workspace.TabMode.Actions = defaults.Workspace.TabMode.Actions
113- cfg.Workspace.PageMode.Actions = defaults.Workspace.PageMode.Actions
113+ cfg.Workspace.VimMode.Actions = defaults.Workspace.VimMode.Actions
114114 cfg.Workspace.ResizeMode.Actions = defaults.Workspace.ResizeMode.Actions
115115 cfg.Workspace.Shortcuts = defaults.Workspace.Shortcuts
116116 cfg.Session.SessionMode.Actions = defaults.Session.SessionMode.Actions
@@ -147,13 +147,13 @@ func (g *KeybindingsGateway) buildTabModeGroup(cfg, defaults *Config) port.Keybi
147147 }
148148 }
149149
150-// buildPageModeGroup builds the page mode group.
151-func (g *KeybindingsGateway) buildPageModeGroup(cfg, defaults *Config) port.KeybindingGroup {
150+// buildVimModeGroup builds the vim mode group.
151+func (g *KeybindingsGateway) buildVimModeGroup(cfg, defaults *Config) port.KeybindingGroup {
152152 return port.KeybindingGroup{
153- Mode: "page",
154- DisplayName: "Page Mode",
155- Bindings: g.buildModeBindings(cfg.Workspace.PageMode.Actions, defaults.Workspace.PageMode.Actions),
156- Activation: cfg.Workspace.PageMode.ActivationShortcut,
153+ Mode: "vim",
154+ DisplayName: "Vim Mode",
155+ Bindings: g.buildModeBindings(cfg.Workspace.VimMode.Actions, defaults.Workspace.VimMode.Actions),
156+ Activation: cfg.Workspace.VimMode.ActivationShortcut,
157157 }
158158 }
159159
@@ -221,10 +221,10 @@ func (*KeybindingsGateway) updateKeybinding(cfg *Config, req port.SetKeybindingR
221221 existing.Keys = req.Keys
222222 cfg.Workspace.TabMode.Actions[req.Action] = existing
223223 }
224- case "page":
225- if existing, ok := cfg.Workspace.PageMode.Actions[req.Action]; ok {
224+ case "vim":
225+ if existing, ok := cfg.Workspace.VimMode.Actions[req.Action]; ok {
226226 existing.Keys = req.Keys
227- cfg.Workspace.PageMode.Actions[req.Action] = existing
227+ cfg.Workspace.VimMode.Actions[req.Action] = existing
228228 }
229229 case "resize":
230230 if existing, ok := cfg.Workspace.ResizeMode.Actions[req.Action]; ok {
@@ -254,8 +254,8 @@ func (*KeybindingsGateway) getDefaultKeys(defaults *Config, mode, action string)
254254 if binding, ok := defaults.Workspace.TabMode.Actions[action]; ok {
255255 return binding.Keys
256256 }
257- case "page":
258- if binding, ok := defaults.Workspace.PageMode.Actions[action]; ok {
257+ case "vim":
258+ if binding, ok := defaults.Workspace.VimMode.Actions[action]; ok {
259259 return binding.Keys
260260 }
261261 case "resize":
@@ -291,7 +291,7 @@ func (*KeybindingsGateway) checkConflicts(cfg *Config, targetMode, targetAction
291291 addBindings(modeGlobal, cfg.Workspace.Shortcuts.Actions)
292292 addBindings("pane", cfg.Workspace.PaneMode.Actions)
293293 addBindings("tab", cfg.Workspace.TabMode.Actions)
294- addBindings("page", cfg.Workspace.PageMode.Actions)
294+ addBindings("vim", cfg.Workspace.VimMode.Actions)
295295 addBindings("resize", cfg.Workspace.ResizeMode.Actions)
296296 addBindings("session", cfg.Session.SessionMode.Actions)
297297
M internal/infrastructure/config/keybindings_gateway_test.go +47 -47
@@ -37,83 +37,83 @@ func inMemoryManager() *Manager {
3737 }
3838 }
3939
40-func TestKeybindingsGateway_PageModeIncludedInGetKeybindings(t *testing.T) {
40+func TestKeybindingsGateway_VimModeIncludedInGetKeybindings(t *testing.T) {
4141 gw := NewKeybindingsGateway(inMemoryManager())
4242 cfg, err := gw.GetKeybindings(context.Background())
4343 require.NoError(t, err)
4444
4545 var pageGroup *port.KeybindingGroup
4646 for i, g := range cfg.Groups {
47- if g.Mode == "page" {
47+ if g.Mode == "vim" {
4848 pageGroup = &cfg.Groups[i]
4949 break
5050 }
5151 }
52- require.NotNil(t, pageGroup, "page mode group must be present in GetKeybindings output")
53- assert.Equal(t, "Page Mode", pageGroup.DisplayName)
52+ require.NotNil(t, pageGroup, "vim mode group must be present in GetKeybindings output")
53+ assert.Equal(t, "Vim Mode", pageGroup.DisplayName)
5454 assert.Equal(t, "ctrl+y", pageGroup.Activation)
5555
5656 // Verify all expected actions are present.
5757 expectedActions := map[string]struct{}{
58- "page-scroll-left": {},
59- "page-scroll-down": {},
60- "page-scroll-up": {},
61- "page-scroll-right": {},
62- "page-scroll-down-fast": {},
63- "page-scroll-up-fast": {},
64- "confirm": {},
65- "cancel": {},
58+ "vim-scroll-left": {},
59+ "vim-scroll-down": {},
60+ "vim-scroll-up": {},
61+ "vim-scroll-right": {},
62+ "vim-scroll-down-fast": {},
63+ "vim-scroll-up-fast": {},
64+ "confirm": {},
65+ "cancel": {},
6666 }
6767
6868 for _, entry := range pageGroup.Bindings {
6969 delete(expectedActions, entry.Action)
7070 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")
7272 }
73- assert.Empty(t, expectedActions, "page mode group missing actions: %v", expectedActions)
73+ assert.Empty(t, expectedActions, "vim mode group missing actions: %v", expectedActions)
7474
7575 // Must be placed after global, pane, tab groups (consistent ordering).
7676 groups := cfg.Groups
7777 foundIdx := -1
7878 for i, g := range groups {
79- if g.Mode == "page" {
79+ if g.Mode == "vim" {
8080 foundIdx = i
8181 break
8282 }
8383 }
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")
8585 }
8686
87-func TestKeybindingsGateway_PageModeIncludedInGetDefaultKeybindings(t *testing.T) {
87+func TestKeybindingsGateway_VimModeIncludedInGetDefaultKeybindings(t *testing.T) {
8888 gw := NewKeybindingsGateway(inMemoryManager())
8989 cfg, err := gw.GetDefaultKeybindings(context.Background())
9090 require.NoError(t, err)
9191
9292 var pageGroup *port.KeybindingGroup
9393 for i, g := range cfg.Groups {
94- if g.Mode == "page" {
94+ if g.Mode == "vim" {
9595 pageGroup = &cfg.Groups[i]
9696 break
9797 }
9898 }
99- require.NotNil(t, pageGroup, "page mode group must be present in GetDefaultKeybindings output")
100- assert.Equal(t, "Page Mode", pageGroup.DisplayName)
99+ require.NotNil(t, pageGroup, "vim mode group must be present in GetDefaultKeybindings output")
100+ assert.Equal(t, "Vim Mode", pageGroup.DisplayName)
101101 assert.Equal(t, "ctrl+y", pageGroup.Activation)
102102
103103 // Verify default keys match actual keys.
104104 for _, entry := range pageGroup.Bindings {
105105 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)
107107 }
108108 }
109109
110-func TestKeybindingsGateway_PageModeSetKeybindingUpdatesFile(t *testing.T) {
110+func TestKeybindingsGateway_VimModeSetKeybindingUpdatesFile(t *testing.T) {
111111 mgr := newTestKeybindingsManager(t)
112112 gw := NewKeybindingsGateway(mgr)
113113
114114 err := gw.SetKeybinding(context.Background(), port.SetKeybindingRequest{
115- Mode: "page",
116- Action: "page-scroll-left",
115+ Mode: "vim",
116+ Action: "vim-scroll-left",
117117 Keys: []string{"a"},
118118 })
119119 require.NoError(t, err)
@@ -127,21 +127,21 @@ func TestKeybindingsGateway_PageModeSetKeybindingUpdatesFile(t *testing.T) {
127127 assert.NotContains(t, content, `keys = ['h']`)
128128 }
129129
130-func TestKeybindingsGateway_PageModeResetKeybinding(t *testing.T) {
130+func TestKeybindingsGateway_VimModeResetKeybinding(t *testing.T) {
131131 mgr := newTestKeybindingsManager(t)
132132 gw := NewKeybindingsGateway(mgr)
133133
134134 // Set a keybinding.
135135 require.NoError(t, gw.SetKeybinding(context.Background(), port.SetKeybindingRequest{
136- Mode: "page",
137- Action: "page-scroll-left",
136+ Mode: "vim",
137+ Action: "vim-scroll-left",
138138 Keys: []string{"a"},
139139 }))
140140
141141 // Reset it.
142142 err := gw.ResetKeybinding(context.Background(), port.ResetKeybindingRequest{
143- Mode: "page",
144- Action: "page-scroll-left",
143+ Mode: "vim",
144+ Action: "vim-scroll-left",
145145 })
146146 require.NoError(t, err)
147147
@@ -154,21 +154,21 @@ func TestKeybindingsGateway_PageModeResetKeybinding(t *testing.T) {
154154 assert.NotContains(t, content, `keys = ['a']`)
155155 }
156156
157-func TestKeybindingsGateway_PageModeIsPartOfResetAll(t *testing.T) {
157+func TestKeybindingsGateway_VimModeIsPartOfResetAll(t *testing.T) {
158158 mgr := newTestKeybindingsManager(t)
159159 gw := NewKeybindingsGateway(mgr)
160160
161- // Change a page mode binding.
161+ // Change a vim mode binding.
162162 require.NoError(t, gw.SetKeybinding(context.Background(), port.SetKeybindingRequest{
163- Mode: "page",
164- Action: "page-scroll-left",
163+ Mode: "vim",
164+ Action: "vim-scroll-left",
165165 Keys: []string{"a"},
166166 }))
167167
168168 // Change another action.
169169 require.NoError(t, gw.SetKeybinding(context.Background(), port.SetKeybindingRequest{
170- Mode: "page",
171- Action: "page-scroll-down",
170+ Mode: "vim",
171+ Action: "vim-scroll-down",
172172 Keys: []string{"b"},
173173 }))
174174
@@ -181,36 +181,36 @@ func TestKeybindingsGateway_PageModeIsPartOfResetAll(t *testing.T) {
181181 data, err := os.ReadFile(configFile)
182182 require.NoError(t, err)
183183 content := string(data)
184- assert.Contains(t, content, `keys = ['h']`, "page-scroll-left should be restored to default")
185- assert.Contains(t, content, `keys = ['j']`, "page-scroll-down 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']`, "vim-scroll-down should be restored to default")
186186 assert.NotContains(t, content, "keys = ['a']")
187187 assert.NotContains(t, content, "keys = ['b']")
188188 }
189189
190-func TestKeybindingsGateway_PageModeConflictsWithinSelf(t *testing.T) {
190+func TestKeybindingsGateway_VimModeConflictsWithinSelf(t *testing.T) {
191191 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"})
193193 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'")
195195
196196 // The conflict should reference the same mode.
197- assert.Equal(t, "page-scroll-left", conflicts[0].ConflictingAction)
198- assert.Equal(t, "page", conflicts[0].ConflictingMode)
197+ assert.Equal(t, "vim-scroll-left", conflicts[0].ConflictingAction)
198+ assert.Equal(t, "vim", conflicts[0].ConflictingMode)
199199 }
200200
201-func TestKeybindingsGateway_PageModeConflictsWithOtherModes(t *testing.T) {
201+func TestKeybindingsGateway_VimModeConflictsWithOtherModes(t *testing.T) {
202202 gw := NewKeybindingsGateway(inMemoryManager())
203- // "enter" is used by page mode's confirm - but also by other modes like pane_mode.
204- conflicts, err := gw.CheckConflicts(context.Background(), "page", "custom", []string{"enter"})
203+ // "enter" is used by vim mode's confirm - but also by other modes like pane_mode.
204+ conflicts, err := gw.CheckConflicts(context.Background(), "vim", "custom", []string{"enter"})
205205 require.NoError(t, err)
206206
207207 // There should be conflicts since "enter" is used in other modes too.
208208 assert.NotEmpty(t, conflicts, "should detect cross-mode conflict for 'enter'")
209209 }
210210
211-func TestKeybindingsGateway_PageModeNoConflictForUnusedKey(t *testing.T) {
211+func TestKeybindingsGateway_VimModeNoConflictForUnusedKey(t *testing.T) {
212212 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"})
214214 require.NoError(t, err)
215215 assert.Empty(t, conflicts, "no conflict expected for unused key")
216216 }
M internal/infrastructure/config/loader.go +3 -3
@@ -706,9 +706,9 @@ func (m *Manager) setWorkspaceDefaults(defaults *Config) {
706706 m.viper.SetDefault("workspace.tab_mode.activation_shortcut", defaults.Workspace.TabMode.ActivationShortcut)
707707 m.viper.SetDefault("workspace.tab_mode.timeout_ms", defaults.Workspace.TabMode.TimeoutMilliseconds)
708708 m.viper.SetDefault("workspace.tab_mode.actions", defaults.Workspace.TabMode.Actions)
709- m.viper.SetDefault("workspace.page_mode.activation_shortcut", defaults.Workspace.PageMode.ActivationShortcut)
710- m.viper.SetDefault("workspace.page_mode.timeout_ms", defaults.Workspace.PageMode.TimeoutMilliseconds)
711- m.viper.SetDefault("workspace.page_mode.actions", defaults.Workspace.PageMode.Actions)
709+ m.viper.SetDefault("workspace.vim_mode.activation_shortcut", defaults.Workspace.VimMode.ActivationShortcut)
710+ m.viper.SetDefault("workspace.vim_mode.timeout_ms", defaults.Workspace.VimMode.TimeoutMilliseconds)
711+ m.viper.SetDefault("workspace.vim_mode.actions", defaults.Workspace.VimMode.Actions)
712712 m.viper.SetDefault("workspace.resize_mode.activation_shortcut", defaults.Workspace.ResizeMode.ActivationShortcut)
713713 m.viper.SetDefault("workspace.resize_mode.timeout_ms", defaults.Workspace.ResizeMode.TimeoutMilliseconds)
714714 m.viper.SetDefault("workspace.resize_mode.step_percent", defaults.Workspace.ResizeMode.StepPercent)
M internal/infrastructure/config/migrate.go +73 -1
@@ -123,6 +123,7 @@ func (m *Migrator) DetectChanges() ([]port.KeyChange, error) {
123123 m.detectLegacyBrowsingContextRenames(rawUserKeysWithValues),
124124 m.detectLegacyFavoritesSidebarRename(userKeysWithValues)...,
125125 )
126+ legacyRenames = append(legacyRenames, m.detectLegacyVimModeRenames(rawUserKeysWithValues)...)
126127 var changes []port.KeyChange
127128 changes = append(changes, legacyRenames...)
128129
@@ -302,7 +303,7 @@ func (m *Migrator) defaultActionMaps() []defaultActionMap {
302303 {key: "workspace.shortcuts.actions", actions: m.defaultConfig.Workspace.Shortcuts.Actions},
303304 {key: "workspace.pane_mode.actions", actions: m.defaultConfig.Workspace.PaneMode.Actions},
304305 {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},
306307 {key: "workspace.resize_mode.actions", actions: m.defaultConfig.Workspace.ResizeMode.Actions},
307308 {key: "session.session_mode.actions", actions: m.defaultConfig.Session.SessionMode.Actions},
308309 }
@@ -420,6 +421,7 @@ func (m *Migrator) Migrate() ([]string, error) {
420421 transformer.TransformLegacyActions(rawConfig)
421422 transformer.TransformLegacyEngineConfig(rawConfig)
422423 transformer.TransformLegacyPopupsToBrowsingContexts(rawConfig)
424+ m.migrateLegacyVimMode(rawConfig)
423425
424426 // Build sets of keys to remove and renames to apply
425427 keysToRemove := make(map[string]bool)
@@ -788,6 +790,76 @@ func (m *Migrator) detectLegacyBrowsingContextRenames(rawUserKeys map[string]any
788790 return changes
789791 }
790792
793+func (m *Migrator) detectLegacyVimModeRenames(rawUserKeys map[string]any) []port.KeyChange {
794+ const oldPrefix = "workspace.page_mode"
795+ const newPrefix = "workspace.vim_mode"
796+
797+ changes := make([]port.KeyChange, 0, 3)
798+ for oldKey, oldValue := range rawUserKeys {
799+ if !strings.HasPrefix(oldKey, oldPrefix+".") {
800+ continue
801+ }
802+ newKey := newPrefix + strings.TrimPrefix(oldKey, oldPrefix)
803+ if _, hasNew := rawUserKeys[newKey]; hasNew {
804+ continue
805+ }
806+ changes = append(changes, port.KeyChange{
807+ Type: port.KeyChangeRenamed,
808+ OldKey: oldKey,
809+ NewKey: newKey,
810+ OldValue: m.formatValue(oldValue),
811+ NewValue: m.formatValue(m.defaultValueForKey(newKey)),
812+ })
813+ }
814+ return changes
815+}
816+
817+func (*Migrator) migrateLegacyVimMode(rawConfig map[string]any) {
818+ workspace, ok := rawConfig["workspace"].(map[string]any)
819+ if !ok {
820+ return
821+ }
822+ legacyMode, ok := workspace["page_mode"].(map[string]any)
823+ if !ok {
824+ return
825+ }
826+ vimMode, ok := workspace["vim_mode"].(map[string]any)
827+ if !ok {
828+ vimMode = make(map[string]any)
829+ workspace["vim_mode"] = vimMode
830+ }
831+ for key, value := range legacyMode {
832+ if key == "actions" {
833+ continue
834+ }
835+ if _, exists := vimMode[key]; !exists {
836+ vimMode[key] = value
837+ }
838+ }
839+ legacyActions, _ := legacyMode["actions"].(map[string]any)
840+ vimActions, _ := vimMode["actions"].(map[string]any)
841+ if vimActions == nil {
842+ vimActions = make(map[string]any)
843+ vimMode["actions"] = vimActions
844+ }
845+ for oldAction, value := range legacyActions {
846+ newAction := legacyVimActionName(oldAction)
847+ if _, exists := vimActions[newAction]; !exists {
848+ vimActions[newAction] = value
849+ }
850+ }
851+ delete(workspace, "page_mode")
852+}
853+
854+func legacyVimActionName(action string) string {
855+ for _, prefix := range []string{"page-scroll-", "page_scroll_"} {
856+ if strings.HasPrefix(action, prefix) {
857+ return "vim-scroll-" + strings.TrimPrefix(action, prefix)
858+ }
859+ }
860+ return action
861+}
862+
791863 // flattenMapWithValues recursively flattens a nested map to dot-notation keys with values.
792864 func (m *Migrator) flattenMapWithValues(data map[string]any, prefix string, result map[string]any) {
793865 for k, v := range data {
M internal/infrastructure/config/migrate_test.go +75 -0
@@ -1175,6 +1175,81 @@ func TestMigrator_Migrate_AppliesLegacyBrowsingContextsRename(t *testing.T) {
11751175 assert.NotContains(t, string(migrated), "[workspace.popups]")
11761176 }
11771177
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+`), 0o644))
1202+
1203+ migrator := NewMigrator()
1204+ applied, err := migrator.Migrate()
1205+ require.NoError(t, err)
1206+ assert.Contains(t, applied, "workspace.page_mode.activation_shortcut -> workspace.vim_mode.activation_shortcut")
1207+ assert.Contains(t, applied, "workspace.page_mode.timeout_ms -> workspace.vim_mode.timeout_ms")
1208+ assert.Contains(t, applied, "workspace.page_mode.actions -> workspace.vim_mode.actions")
1209+
1210+ migrated, err := os.ReadFile(configFile)
1211+ require.NoError(t, err)
1212+ assert.Contains(t, string(migrated), "[workspace.vim_mode]")
1213+ assert.Contains(t, string(migrated), "activation_shortcut = 'ctrl+alt+y'")
1214+ assert.Contains(t, string(migrated), "[workspace.vim_mode.actions.vim-scroll-down]")
1215+ assert.Contains(t, string(migrated), "[workspace.vim_mode.actions.vim-scroll-up]")
1216+ assert.Contains(t, string(migrated), "keys = ['u']")
1217+ assert.NotContains(t, string(migrated), "page_mode")
1218+ assert.NotContains(t, string(migrated), "page-scroll-down")
1219+}
1220+
1221+func TestMigrator_Migrate_MergesLegacyPageModeActionsIntoExistingVimMode(t *testing.T) {
1222+ home := t.TempDir()
1223+ t.Setenv("HOME", home)
1224+ t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
1225+ t.Setenv("XDG_DATA_HOME", filepath.Join(home, ".local", "share"))
1226+ t.Setenv("XDG_STATE_HOME", filepath.Join(home, ".local", "state"))
1227+ t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache"))
1228+
1229+ configFile, err := GetConfigFile()
1230+ require.NoError(t, err)
1231+ require.NoError(t, os.MkdirAll(filepath.Dir(configFile), 0o755))
1232+ require.NoError(t, os.WriteFile(configFile, []byte(`
1233+[workspace.page_mode.actions.page-scroll-down]
1234+keys = ["n"]
1235+
1236+[workspace.page_mode.actions.page-scroll-up]
1237+keys = ["u"]
1238+
1239+[workspace.vim_mode.actions.vim-scroll-down]
1240+keys = ["j"]
1241+`), 0o644))
1242+
1243+ _, err = NewMigrator().Migrate()
1244+ require.NoError(t, err)
1245+
1246+ migrated, err := os.ReadFile(configFile)
1247+ require.NoError(t, err)
1248+ assert.Contains(t, string(migrated), "[workspace.vim_mode.actions.vim-scroll-down]\n keys = ['j']")
1249+ assert.Contains(t, string(migrated), "[workspace.vim_mode.actions.vim-scroll-up]\n keys = ['u']")
1250+ assert.NotContains(t, string(migrated), "page_mode")
1251+}
1252+
11781253 func TestMigrator_DetectChanges_MixedBrowsingContextsDoesNotDoubleReportLegacyRename(t *testing.T) {
11791254 home := t.TempDir()
11801255 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
2424 // ResizeModeConfig defines modal behavior for resizing panes (Zellij-style).
2525 type ResizeModeConfig = entity.ResizeModeConfig
2626
27-// PageModeConfig defines modal behavior for page scrolling.
28-type PageModeConfig = entity.PageModeConfig
27+// VimModeConfig defines modal behavior for Vim-style scrolling.
28+type VimModeConfig = entity.VimModeConfig
2929
3030 // SessionModeConfig defines modal behavior for session management.
3131 type SessionModeConfig = entity.SessionModeConfig
M internal/infrastructure/config/schema_provider.go +9 -9
@@ -472,27 +472,27 @@ func (*SchemaProvider) getWorkspaceKeys(defaults *Config) []entity.ConfigKeyInfo
472472 Description: "Key bindings for tab mode actions",
473473 Section: SectionWorkspace,
474474 },
475- // Page mode
475+ // Vim mode
476476 {
477- Key: "workspace.page_mode.activation_shortcut",
477+ Key: "workspace.vim_mode.activation_shortcut",
478478 Type: "string",
479- Default: defaults.Workspace.PageMode.ActivationShortcut,
480- Description: "Shortcut to enter page scroll mode",
479+ Default: defaults.Workspace.VimMode.ActivationShortcut,
480+ Description: "Shortcut to enter vim scroll mode",
481481 Section: SectionWorkspace,
482482 },
483483 {
484- Key: "workspace.page_mode.timeout_ms",
484+ Key: "workspace.vim_mode.timeout_ms",
485485 Type: "int",
486- Default: fmt.Sprintf("%d", defaults.Workspace.PageMode.TimeoutMilliseconds),
487- Description: "Page mode timeout in milliseconds (0 = no timeout)",
486+ Default: fmt.Sprintf("%d", defaults.Workspace.VimMode.TimeoutMilliseconds),
487+ Description: "Vim mode timeout in milliseconds (0 = no timeout)",
488488 Range: ">=0",
489489 Section: SectionWorkspace,
490490 },
491491 {
492- Key: "workspace.page_mode.actions.<action>",
492+ Key: "workspace.vim_mode.actions.<action>",
493493 Type: "[]string",
494494 Default: "(see defaults)",
495- Description: "Key bindings for page mode actions",
495+ Description: "Key bindings for vim mode actions",
496496 Section: SectionWorkspace,
497497 },
498498 // Resize mode
M internal/infrastructure/config/schema_provider_test.go +1 -1
@@ -146,7 +146,7 @@ func TestSchemaProviderCoversViperDefaults(t *testing.T) {
146146 "search_shortcuts": "search_shortcuts.<key>",
147147 "workspace.pane_mode.actions": "workspace.pane_mode.actions.<action>",
148148 "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>",
150150 "workspace.resize_mode.actions": "workspace.resize_mode.actions.<action>",
151151 "workspace.floating_pane.profiles": "workspace.floating_pane.profiles.<name>",
152152 "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 {
2929 validationErrors = append(validationErrors, validatePaneMode(config)...)
3030 validationErrors = append(validationErrors, validateTabBar(config)...)
3131 validationErrors = append(validationErrors, validateTabMode(config)...)
32- validationErrors = append(validationErrors, validatePageMode(config)...)
32+ validationErrors = append(validationErrors, validateVimMode(config)...)
3333 validationErrors = append(validationErrors, validateFloatingPane(config)...)
3434 validationErrors = append(validationErrors, validateLogging(config)...)
3535 validationErrors = append(validationErrors, validateWorkspaceNewPaneURL(config)...)
@@ -242,8 +242,8 @@ func validateTabMode(config *Config) []string {
242242 return validateModalModeActions("tab_mode", config.Workspace.TabMode.TimeoutMilliseconds, config.Workspace.TabMode.Actions)
243243 }
244244
245-func validatePageMode(config *Config) []string {
246- return validateModalModeActions("page_mode", config.Workspace.PageMode.TimeoutMilliseconds, config.Workspace.PageMode.Actions)
245+func validateVimMode(config *Config) []string {
246+ return validateModalModeActions("vim_mode", config.Workspace.VimMode.TimeoutMilliseconds, config.Workspace.VimMode.Actions)
247247 }
248248
249249 // 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, "[]string", 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) {
289289 }
290290
291291 // 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.
293293 // 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 or
294+// updating editable-focus state on the sender WebView so Vim mode can block or
295295 // exit locally. The per-WebView token is a reuse/staleness guard, not a broad
296296 // authorization boundary for page privileges.
297297 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)
4040 var _ port.OAuthCallbackCapable = (*WebView)(nil)
4141 var _ port.PageScrollable = (*WebView)(nil)
4242
43-var buildPageScrollFallbackJS = webutil.BuildScrollByJS
43+var buildPageScrollFallbackJS = webutil.BuildPageScrollByJS
4444 var runPageScrollFallbackJS = func(ctx context.Context, wv *WebView, script string) {
4545 wv.RunJavaScript(ctx, script)
4646 }
M internal/infrastructure/webutil/pagescroll.go +3 -3
@@ -2,16 +2,16 @@ package webutil
22
33 import "fmt"
44
5-// BuildScrollByJS returns one immediate JavaScript scroll step for the given
5+// BuildPageScrollByJS returns one immediate JavaScript scroll step for the given
66 // CSS-pixel delta. It resolves the initial target under the viewport center,
77 // walks toward scrollable ancestors that can move in the requested direction,
88 // and hands off to the document when a nested scroller reaches its boundary.
99 // Cross-origin frame contents remain best-effort because elementFromPoint can
1010 // only return the accessible frame element from the top-level document.
1111 //
12-// The app-level Page Mode repeater owns held-key cadence; this helper performs
12+// The app-level Vim Mode repeater owns held-key cadence; this helper performs
1313 // no requestAnimationFrame loop of its own.
14-func BuildScrollByJS(dx, dy int) string {
14+func BuildPageScrollByJS(dx, dy int) string {
1515 return fmt.Sprintf(`(function(){
1616 var dx=%d,dy=%d,doc=document;
1717 function hasScrollableOverflow(value){
M internal/infrastructure/webutil/pagescroll_test.go +17 -17
@@ -5,8 +5,8 @@ import (
55 "testing"
66 )
77
8-func TestBuildScrollByJS_TargetsViewportCenterInsteadOfActiveElement(t *testing.T) {
9- js := BuildScrollByJS(0, 80)
8+func TestBuildPageScrollByJS_TargetsViewportCenterInsteadOfActiveElement(t *testing.T) {
9+ js := BuildPageScrollByJS(0, 80)
1010
1111 if !strings.Contains(js, "doc.elementFromPoint(window.innerWidth/2,window.innerHeight/2)") {
1212 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.
1616 }
1717 }
1818
19-func TestBuildScrollByJS_ContainsRequiredOperations(t *testing.T) {
19+func TestBuildPageScrollByJS_ContainsRequiredOperations(t *testing.T) {
2020 tests := []struct {
2121 name string
2222 dx int
@@ -50,7 +50,7 @@ func TestBuildScrollByJS_ContainsRequiredOperations(t *testing.T) {
5050
5151 for _, tt := range tests {
5252 t.Run(tt.name, func(t *testing.T) {
53- js := BuildScrollByJS(tt.dx, tt.dy)
53+ js := BuildPageScrollByJS(tt.dx, tt.dy)
5454 for _, check := range required {
5555 for _, snippet := range check.snippets {
5656 if !strings.Contains(js, snippet) {
@@ -66,8 +66,8 @@ func TestBuildScrollByJS_ContainsRequiredOperations(t *testing.T) {
6666 }
6767 }
6868
69-func TestBuildScrollByJS_SpecificDelta(t *testing.T) {
70- js := BuildScrollByJS(0, 80)
69+func TestBuildPageScrollByJS_SpecificDelta(t *testing.T) {
70+ js := BuildPageScrollByJS(0, 80)
7171 if !strings.Contains(js, "var dx=0,dy=80") {
7272 t.Errorf("expected dy=80 in JS variables, got: %s", js)
7373 }
@@ -75,38 +75,38 @@ func TestBuildScrollByJS_SpecificDelta(t *testing.T) {
7575 t.Errorf("expected immediate fallback scroll step without requestAnimationFrame, got: %s", js)
7676 }
7777
78- js = BuildScrollByJS(-80, 0)
78+ js = BuildPageScrollByJS(-80, 0)
7979 if !strings.Contains(js, "var dx=-80,dy=0") {
8080 t.Errorf("expected dx=-80 in JS variables, got: %s", js)
8181 }
8282 }
8383
84-func TestBuildScrollByJS_HorizontalAxisCheck(t *testing.T) {
85- js := BuildScrollByJS(-80, 0)
84+func TestBuildPageScrollByJS_HorizontalAxisCheck(t *testing.T) {
85+ js := BuildPageScrollByJS(-80, 0)
8686 if !strings.Contains(js, "clientWidth") || !strings.Contains(js, "scrollWidth") {
8787 t.Error("horizontal scroll must check scrollWidth/clientWidth")
8888 }
8989 }
9090
91-func TestBuildScrollByJS_VerticalAxisCheck(t *testing.T) {
92- js := BuildScrollByJS(0, 80)
91+func TestBuildPageScrollByJS_VerticalAxisCheck(t *testing.T) {
92+ js := BuildPageScrollByJS(0, 80)
9393 if !strings.Contains(js, "clientHeight") || !strings.Contains(js, "scrollHeight") {
9494 t.Error("vertical scroll must check scrollHeight/clientHeight")
9595 }
9696 }
9797
98-func TestBuildScrollByJS_NegativeDeltaMagnitude(t *testing.T) {
99- js := BuildScrollByJS(0, -320)
98+func TestBuildPageScrollByJS_NegativeDeltaMagnitude(t *testing.T) {
99+ js := BuildPageScrollByJS(0, -320)
100100 if !strings.Contains(js, "var dx=0,dy=-320") {
101101 t.Errorf("expected dy=-320 in JS variables, got: %s", js)
102102 }
103103 }
104104
105-func TestBuildScrollByJS_SyntacticallyValid(t *testing.T) {
105+func TestBuildPageScrollByJS_SyntacticallyValid(t *testing.T) {
106106 tests := []struct{ dx, dy int }{{0, 80}, {0, -320}, {-80, 0}, {80, 0}, {0, 0}}
107107
108108 for _, tt := range tests {
109- js := BuildScrollByJS(tt.dx, tt.dy)
109+ js := BuildPageScrollByJS(tt.dx, tt.dy)
110110
111111 depth := 0
112112 for _, ch := range js {
@@ -136,8 +136,8 @@ func TestBuildScrollByJS_SyntacticallyValid(t *testing.T) {
136136 }
137137 }
138138
139-func TestBuildScrollByJS_ZeroDeltaSafe(t *testing.T) {
140- js := BuildScrollByJS(0, 0)
139+func TestBuildPageScrollByJS_ZeroDeltaSafe(t *testing.T) {
140+ js := BuildPageScrollByJS(0, 0)
141141 if !strings.Contains(js, "var dx=0,dy=0") {
142142 t.Errorf("expected zero deltas in JS variables, got: %s", js)
143143 }
M internal/ui/app.go +118 -116
@@ -53,11 +53,11 @@ const (
5353 floatingSessionIDDefault = "default"
5454 floatingPaneVisibleClass = "floating-pane-visible"
5555
56- // pageModePulseInterval is the minimum interval between page mode pulses.
57- // Held Page Mode scrolling now runs on its own smooth repeater, so the pulse
56+ // vimModePulseInterval is the minimum interval between vim mode pulses.
57+ // Held Vim Mode scrolling now runs on its own smooth repeater, so the pulse
5858 // is intentionally throttled well below scroll cadence to avoid GTK CSS churn
5959 // becoming part of the perceived scroll jank.
60- pageModePulseInterval = 120 * time.Millisecond
60+ vimModePulseInterval = 120 * time.Millisecond
6161 )
6262
6363 func gtkApplicationFlags() gio.ApplicationFlags {
@@ -123,7 +123,7 @@ type App struct {
123123 // Focus tracking stays app-global.
124124 focusMgr *focus.Manager
125125
126- pageModePolicyUC *usecase.PageModePolicyUseCase
126+ vimModePolicyUC *usecase.VimModePolicyUseCase
127127 pageEditableFocusByPane map[entity.PaneID]bool
128128
129129 resizeModeBorderTarget layout.Widget
@@ -159,11 +159,11 @@ type App struct {
159159 // Accent picker for dead keys support
160160 accentFocusProvider port.FocusedInputProvider
161161
162- // Page mode pulse debounce - tracks last pulse time to skip rapid
163- // repeats during held-key scroll actions. Reset on page mode exit
162+ // Vim mode pulse debounce - tracks last pulse time to skip rapid
163+ // repeats during held-key scroll actions. Reset on vim mode exit
164164 // via handleModeChange.
165- pageModePulseLastTime time.Time
166- pageModePulseMu sync.Mutex
165+ vimModePulseLastTime time.Time
166+ vimModePulseMu sync.Mutex
167167
168168 // Deferred initialization - runs after first load_started to avoid blocking initial navigation
169169 deferredInitOnce sync.Once
@@ -216,7 +216,7 @@ func New(deps *Dependencies) (*App, error) {
216216 windowForTab: make(map[entity.TabID]*browserWindow),
217217 floatingSessions: make(map[floatingSessionKey]*floatingWorkspaceSession),
218218 browserWindows: make(map[string]*browserWindow),
219- pageModePolicyUC: usecase.NewPageModePolicyUseCase(),
219+ vimModePolicyUC: usecase.NewVimModePolicyUseCase(),
220220 pageEditableFocusByPane: make(map[entity.PaneID]bool),
221221 dispatchOnMainThread: func(label string, fn func()) syncdispatch.SyncDispatchResult {
222222 if fn != nil {
@@ -835,7 +835,7 @@ func (a *App) wireSessionManagerShortcut() {
835835 }
836836 bw.sessionManager.Toggle(ctx)
837837 if bw.sessionManager.IsVisible() {
838- a.handlePageModeFocusTrigger(ctx, bw, usecase.PageModePolicyTriggerOverlayFocus)
838+ a.handleVimModeFocusTrigger(ctx, bw, usecase.VimModePolicyTriggerOverlayFocus)
839839 }
840840 return nil
841841 })
@@ -1088,9 +1088,9 @@ func (a *App) initBrowserWindowInput(ctx context.Context, bw *browserWindow) {
10881088 }
10891089 return a.dispatchBrowserWindowAction(actionCtx, bw, action)
10901090 })
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 {
10921092 a.activateBrowserWindow(bw)
1093- return a.kbDispatcher.DispatchPageScrollLifecycle(scrollCtx, action, phase)
1093+ return a.kbDispatcher.DispatchVimScrollLifecycle(scrollCtx, action, phase)
10941094 })
10951095 bw.keyboardHandler.SetOnEscape(func(escapeCtx context.Context) bool {
10961096 a.activateBrowserWindow(bw)
@@ -1126,8 +1126,8 @@ func (a *App) initBrowserWindowInput(ctx context.Context, bw *browserWindow) {
11261126
11271127 return input.RouteHandleShortcuts
11281128 })
1129- bw.keyboardHandler.SetPageModeActivationPassthrough(func() bool {
1130- return a.shouldBypassPageModeActivation(bw)
1129+ bw.keyboardHandler.SetVimModeActivationPassthrough(func() bool {
1130+ return a.shouldBypassVimModeActivation(bw)
11311131 })
11321132 bw.keyboardHandler.SetAccentHandler(a)
11331133 bw.keyboardHandler.AttachTo(bw.mainWindow.Window())
@@ -1369,7 +1369,7 @@ func (a *App) initOmniboxConfig(ctx context.Context) {
13691369 a.accentFocusProvider.SetFocusedInput(a.deps.NewGTKEntryTarget(entry))
13701370 }
13711371 }
1372- a.handlePageModeFocusTrigger(ctx, a.lastFocusedBrowserWindow(), usecase.PageModePolicyTriggerOmniboxFocus)
1372+ a.handleVimModeFocusTrigger(ctx, a.lastFocusedBrowserWindow(), usecase.VimModePolicyTriggerOmniboxFocus)
13731373 },
13741374 OnFocusOut: func() {
13751375 // When omnibox loses focus, set WebView as the focused input
@@ -1408,7 +1408,7 @@ func (a *App) initFindBarConfig(ctx context.Context) {
14081408 a.accentFocusProvider.SetFocusedInput(a.deps.NewGTKEntryTarget(entry))
14091409 }
14101410 }
1411- a.handlePageModeFocusTrigger(ctx, a.lastFocusedBrowserWindow(), usecase.PageModePolicyTriggerFindBarFocus)
1411+ a.handleVimModeFocusTrigger(ctx, a.lastFocusedBrowserWindow(), usecase.VimModePolicyTriggerFindBarFocus)
14121412 },
14131413 OnFocusOut: func() {
14141414 // When find bar loses focus, set WebView as the focused input
@@ -1433,7 +1433,7 @@ func (a *App) ToggleSessionManager(ctx context.Context) {
14331433 }
14341434 bw.sessionManager.Toggle(ctx)
14351435 if bw.sessionManager.IsVisible() {
1436- a.handlePageModeFocusTrigger(ctx, bw, usecase.PageModePolicyTriggerOverlayFocus)
1436+ a.handleVimModeFocusTrigger(ctx, bw, usecase.VimModePolicyTriggerOverlayFocus)
14371437 }
14381438 }
14391439
@@ -1533,7 +1533,7 @@ func (a *App) HandleMovePaneToTab(ctx context.Context) error {
15331533
15341534 a.attachTabPickerToActivePane()
15351535 bw.tabPicker.Show(ctx, items)
1536- a.handlePageModeFocusTrigger(ctx, bw, usecase.PageModePolicyTriggerOverlayFocus)
1536+ a.handleVimModeFocusTrigger(ctx, bw, usecase.VimModePolicyTriggerOverlayFocus)
15371537 return nil
15381538 }
15391539
@@ -3108,7 +3108,7 @@ func (a *App) initTabCoordinator(ctx context.Context) {
31083108 // tracks ActiveTabID and PreviousActiveTabID on bw.tabs. No manual bw state needed.
31093109 if bw := a.browserWindowForTabTarget(target); bw != nil {
31103110 a.activateBrowserWindow(bw)
3111- a.handlePageModeTabSwitch(ctx, bw)
3111+ a.handleVimModeTabSwitch(ctx, bw)
31123112 }
31133113 a.switchWorkspaceView(ctx, tab.ID)
31143114 })
@@ -3393,8 +3393,8 @@ func (a *App) withFocusedTabTarget(ctx context.Context, action string, ensure bo
33933393
33943394 func (a *App) wireKeyboardActions() {
33953395 a.kbDispatcher.SetOnQuit(a.Quit)
3396- a.kbDispatcher.SetOnPageModePulse(func(ctx context.Context, fast bool) {
3397- a.triggerPageModePulse(ctx, fast)
3396+ a.kbDispatcher.SetOnVimModePulse(func(ctx context.Context, fast bool) {
3397+ a.triggerVimModePulse(ctx, fast)
33983398 })
33993399 a.kbDispatcher.SetOnFindOpen(func(ctx context.Context) error {
34003400 a.ToggleFindBar(ctx)
@@ -3727,14 +3727,14 @@ func (a *App) updateWindowTitleFromActivePane(tabID entity.TabID) {
37273727 a.updateWindowTitle(title, a.ownerOrLastFocusedBrowserWindow(tabID, ""))
37283728 }
37293729
3730-func (a *App) pageModePolicy() *usecase.PageModePolicyUseCase {
3730+func (a *App) vimModePolicy() *usecase.VimModePolicyUseCase {
37313731 if a == nil {
3732- return usecase.NewPageModePolicyUseCase()
3732+ return usecase.NewVimModePolicyUseCase()
37333733 }
3734- if a.pageModePolicyUC == nil {
3735- a.pageModePolicyUC = usecase.NewPageModePolicyUseCase()
3734+ if a.vimModePolicyUC == nil {
3735+ a.vimModePolicyUC = usecase.NewVimModePolicyUseCase()
37363736 }
3737- return a.pageModePolicyUC
3737+ return a.vimModePolicyUC
37383738 }
37393739
37403740 func (a *App) ensurePageEditableFocusMap() {
@@ -3766,51 +3766,51 @@ func (a *App) clearPageEditableFocusState(paneID entity.PaneID) {
37663766 a.setPageEditableFocused(paneID, false)
37673767 }
37683768
3769-func (a *App) pageModeActiveForBrowserWindow(bw *browserWindow) bool {
3770- return bw != nil && bw.keyboardHandler != nil && bw.keyboardHandler.Mode() == input.ModePage
3769+func (a *App) vimModeActiveForBrowserWindow(bw *browserWindow) bool {
3770+ return bw != nil && bw.keyboardHandler != nil && bw.keyboardHandler.Mode() == input.ModeVim
37713771 }
37723772
3773-func (a *App) applyPageModePolicyTransition(_ context.Context, bw *browserWindow, transition usecase.PageModePolicyTransition) {
3774- if transition != usecase.PageModePolicyTransitionExit || bw == nil || bw.keyboardHandler == nil {
3773+func (a *App) applyVimModePolicyTransition(_ context.Context, bw *browserWindow, transition usecase.VimModePolicyTransition) {
3774+ if transition != usecase.VimModePolicyTransitionExit || bw == nil || bw.keyboardHandler == nil {
37753775 return
37763776 }
3777- if bw.keyboardHandler.Mode() != input.ModePage {
3777+ if bw.keyboardHandler.Mode() != input.ModeVim {
37783778 return
37793779 }
37803780 bw.keyboardHandler.ExitMode()
37813781 }
37823782
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) {
37843784 if bw == nil {
37853785 return
37863786 }
3787- transition := a.pageModePolicy().Evaluate(usecase.PageModePolicyInput{
3788- Trigger: trigger,
3789- PageModeActive: a.pageModeActiveForBrowserWindow(bw),
3787+ transition := a.vimModePolicy().Evaluate(usecase.VimModePolicyInput{
3788+ Trigger: trigger,
3789+ VimModeActive: a.vimModeActiveForBrowserWindow(bw),
37903790 })
3791- a.applyPageModePolicyTransition(ctx, bw, transition)
3791+ a.applyVimModePolicyTransition(ctx, bw, transition)
37923792 }
37933793
3794-func (a *App) handlePageModeTabSwitch(_ context.Context, bw *browserWindow) {
3795- if bw == nil || bw.tabs == nil || !a.pageModeActiveForBrowserWindow(bw) {
3794+func (a *App) handleVimModeTabSwitch(_ context.Context, bw *browserWindow) {
3795+ if bw == nil || bw.tabs == nil || !a.vimModeActiveForBrowserWindow(bw) {
37963796 return
37973797 }
3798- transition := a.pageModePolicy().Evaluate(usecase.PageModePolicyInput{
3799- Trigger: usecase.PageModePolicyTriggerContextChanged,
3800- PageModeActive: true,
3798+ transition := a.vimModePolicy().Evaluate(usecase.VimModePolicyInput{
3799+ Trigger: usecase.VimModePolicyTriggerContextChanged,
3800+ VimModeActive: true,
38013801 PreserveOnContextChange: false,
38023802 })
3803- if transition != usecase.PageModePolicyTransitionExit {
3803+ if transition != usecase.VimModePolicyTransitionExit {
38043804 return
38053805 }
38063806 if prevTabID := bw.tabs.PreviousActiveTabID; prevTabID != "" {
38073807 if prevView := a.workspaceViews[prevTabID]; prevView != nil {
3808- if pv := prevView.GetPaneView(bw.pageModePaneID); pv != nil {
3809- pv.SetPageMode(false)
3808+ if pv := prevView.GetPaneView(bw.vimModePaneID); pv != nil {
3809+ pv.SetVimMode(false)
38103810 }
38113811 }
38123812 }
3813- bw.pageModePaneID = ""
3813+ bw.vimModePaneID = ""
38143814 bw.keyboardHandler.ExitMode()
38153815 }
38163816
@@ -3826,25 +3826,25 @@ func (a *App) handlePageEditableFocusChanged(ctx context.Context, paneID entity.
38263826 }
38273827 ws := a.activeWorkspaceForBrowserWindow(bw)
38283828 activeContext := ws != nil && ws.ActivePaneID == paneID && a.lastFocusedBrowserWindow() == bw
3829- transition := a.pageModePolicy().Evaluate(usecase.PageModePolicyInput{
3830- Trigger: usecase.PageModePolicyTriggerPageEditableFocusChanged,
3831- PageModeActive: a.pageModeActiveForBrowserWindow(bw),
3829+ transition := a.vimModePolicy().Evaluate(usecase.VimModePolicyInput{
3830+ Trigger: usecase.VimModePolicyTriggerPageEditableFocusChanged,
3831+ VimModeActive: a.vimModeActiveForBrowserWindow(bw),
38323832 PageEditableFocused: editable,
38333833 EventInActiveContext: activeContext,
38343834 })
3835- a.applyPageModePolicyTransition(ctx, bw, transition)
3835+ a.applyVimModePolicyTransition(ctx, bw, transition)
38363836 }
38373837
3838-func (a *App) shouldBypassPageModeActivation(bw *browserWindow) bool {
3838+func (a *App) shouldBypassVimModeActivation(bw *browserWindow) bool {
38393839 ws := a.activeWorkspaceForBrowserWindow(bw)
38403840 if ws == nil {
38413841 return false
38423842 }
3843- transition := a.pageModePolicy().Evaluate(usecase.PageModePolicyInput{
3844- Trigger: usecase.PageModePolicyTriggerActivationAttempt,
3843+ transition := a.vimModePolicy().Evaluate(usecase.VimModePolicyInput{
3844+ Trigger: usecase.VimModePolicyTriggerActivationAttempt,
38453845 PageEditableFocused: a.pageEditableFocused(ws.ActivePaneID),
38463846 })
3847- return transition == usecase.PageModePolicyTransitionBlockActivation
3847+ return transition == usecase.VimModePolicyTransitionBlockActivation
38483848 }
38493849
38503850 // 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
38603860 a.applyResizeModeBorder(ctx, a.activeWorkspace())
38613861 }
38623862
3863- // Handle pane-local Page Mode visual ownership.
3864- // Entering Page Mode accents the active pane; leaving removes the accent.
3865- a.handlePageModeOwnership(ctx, bw, to, from)
3863+ // Handle pane-local Vim Mode visual ownership.
3864+ // Entering Vim Mode accents the active pane; leaving removes the accent.
3865+ a.handleVimModeOwnership(ctx, bw, to, from)
38663866
38673867 // Update global border overlay visibility based on mode.
38683868 // 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.
38703870 if bw != nil && bw.borderMgr != nil {
38713871 bw.borderMgr.OnModeChange(ctx, from, to)
38723872 }
@@ -3875,23 +3875,23 @@ func (a *App) handleModeChange(ctx context.Context, bw *browserWindow, from, to
38753875 a.updateModeIndicatorToaster(ctx, bw, to)
38763876 }
38773877
3878-// transferPageModeOwnershipToPane transfers the pane-local Page Mode accent
3878+// transferVimModeOwnershipToPane transfers the pane-local Vim Mode accent
38793879 // 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.
3881-func (a *App) transferPageModeOwnershipToPane(ctx context.Context, bw *browserWindow, newPaneID entity.PaneID) {
3880+// The transfer only happens while that window is in Vim Mode.
3881+func (a *App) transferVimModeOwnershipToPane(ctx context.Context, bw *browserWindow, newPaneID entity.PaneID) {
38823882 if bw == nil {
38833883 return
38843884 }
38853885
3886- // Only transfer if page mode is currently active and the owner is changing.
3887- if bw.pageModePaneID == "" || bw.pageModePaneID == newPaneID {
3886+ // Only transfer if vim mode is currently active and the owner is changing.
3887+ if bw.vimModePaneID == "" || bw.vimModePaneID == newPaneID {
38883888 return
38893889 }
38903890
3891- // Check if this window is actually in page mode.
3892- if bw.keyboardHandler == nil || bw.keyboardHandler.Mode() != input.ModePage {
3893- // Page mode not active on this window; just clear stale ownership.
3894- bw.pageModePaneID = ""
3891+ // Check if this window is actually in vim mode.
3892+ if bw.keyboardHandler == nil || bw.keyboardHandler.Mode() != input.ModeVim {
3893+ // Vim mode not active on this window; just clear stale ownership.
3894+ bw.vimModePaneID = ""
38953895 return
38963896 }
38973897
@@ -3900,39 +3900,39 @@ func (a *App) transferPageModeOwnershipToPane(ctx context.Context, bw *browserWi
39003900 return
39013901 }
39023902
3903- transition := a.pageModePolicy().Evaluate(usecase.PageModePolicyInput{
3904- Trigger: usecase.PageModePolicyTriggerContextChanged,
3905- PageModeActive: true,
3903+ transition := a.vimModePolicy().Evaluate(usecase.VimModePolicyInput{
3904+ Trigger: usecase.VimModePolicyTriggerContextChanged,
3905+ VimModeActive: true,
39063906 PreserveOnContextChange: !a.pageEditableFocused(newPaneID),
39073907 })
3908- if transition == usecase.PageModePolicyTransitionExit {
3909- a.applyPageModePolicyTransition(ctx, bw, transition)
3908+ if transition == usecase.VimModePolicyTransitionExit {
3909+ a.applyVimModePolicyTransition(ctx, bw, transition)
39103910 return
39113911 }
39123912
3913- oldPaneID := bw.pageModePaneID
3913+ oldPaneID := bw.vimModePaneID
39143914
39153915 // Deactivate the old owning pane.
39163916 if oldPV := wsView.GetPaneView(oldPaneID); oldPV != nil {
3917- oldPV.SetPageMode(false)
3917+ oldPV.SetVimMode(false)
39183918 }
39193919
39203920 // Activate the new pane.
39213921 if newPV := wsView.GetPaneView(newPaneID); newPV != nil {
3922- newPV.SetPageMode(true)
3922+ newPV.SetVimMode(true)
39233923 }
3924- bw.pageModePaneID = newPaneID
3924+ bw.vimModePaneID = newPaneID
39253925
39263926 logging.FromContext(ctx).Debug().
39273927 Str("window_id", bw.id).
39283928 Str("old_pane_id", string(oldPaneID)).
39293929 Str("new_pane_id", string(newPaneID)).
3930- Msg("page mode ownership transferred")
3930+ Msg("vim mode ownership transferred")
39313931 }
39323932
3933-// handlePageModeOwnership manages the pane-local Page Mode accent and pulse
3933+// handleVimModeOwnership manages the pane-local Vim Mode accent and pulse
39343934 // 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) {
39363936 if bw == nil {
39373937 return
39383938 }
@@ -3941,96 +3941,96 @@ func (a *App) handlePageModeOwnership(ctx context.Context, bw *browserWindow, to
39413941 wsView := a.activeWorkspaceViewForBrowserWindow(bw)
39423942 if ws == nil || wsView == nil {
39433943 // If there's no workspace yet (startup), just track the intent.
3944- if to != input.ModePage && from == input.ModePage {
3945- bw.pageModePaneID = ""
3944+ if to != input.ModeVim && from == input.ModeVim {
3945+ bw.vimModePaneID = ""
39463946 }
39473947 return
39483948 }
39493949
3950- if to == input.ModePage && from != input.ModePage {
3951- // Entering page mode: activate on the current active pane of this window.
3950+ if to == input.ModeVim && from != input.ModeVim {
3951+ // Entering vim mode: activate on the current active pane of this window.
39523952 paneID := ws.ActivePaneID
39533953 if pv := wsView.GetPaneView(paneID); pv != nil {
3954- pv.SetPageMode(true)
3955- bw.pageModePaneID = paneID
3954+ pv.SetVimMode(true)
3955+ bw.vimModePaneID = paneID
39563956 logging.FromContext(ctx).Debug().
39573957 Str("window_id", bw.id).
39583958 Str("pane_id", string(paneID)).
3959- Msg("page mode activated on pane")
3959+ Msg("vim mode activated on pane")
39603960 }
3961- } else if from == input.ModePage && to != input.ModePage {
3962- // Leaving Page Mode: deactivate the pane that owns the visual accent.
3963- a.clearPageModeOwnership(ctx, bw)
3961+ } else if from == input.ModeVim && to != input.ModeVim {
3962+ // Leaving Vim Mode: deactivate the pane that owns the visual accent.
3963+ a.clearVimModeOwnership(ctx, bw)
39643964 }
39653965 }
39663966
3967-// triggerPageModePulse triggers a pane-local page mode pulse on the
3967+// triggerVimModePulse triggers a pane-local vim mode pulse on the
39683968 // last-focused browser window's owning pane. This is called by the
39693969 // keyboard dispatcher after a page scroll action. The bw used is the
39703970 // window that was active when the action was dispatched (via
39713971 // 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.
39733973 //
3974-// Pulses are debounced at pageModePulseInterval to prevent excessive GTK CSS
3974+// Pulses are debounced at vimModePulseInterval to prevent excessive GTK CSS
39753975 // class churn during held-key repeats. Smooth scroll cadence is owned by the
3976-// Page Mode repeater and backend scroll path; pulse feedback is deliberately
3976+// Vim Mode repeater and backend scroll path; pulse feedback is deliberately
39773977 // slower so indicator/overlay animation work does not become part of the
39783978 // scrolling critical path.
3979-func (a *App) triggerPageModePulse(_ context.Context, fast bool) {
3979+func (a *App) triggerVimModePulse(_ context.Context, fast bool) {
39803980 // Debounce: skip if called again too soon during a continuous hold.
39813981 // We still serialize the timestamp update so repeated dispatcher calls do
39823982 // not restart CSS animations at scroll cadence.
3983- a.pageModePulseMu.Lock()
3984- since := time.Since(a.pageModePulseLastTime)
3985- if since < pageModePulseInterval {
3986- a.pageModePulseMu.Unlock()
3983+ a.vimModePulseMu.Lock()
3984+ since := time.Since(a.vimModePulseLastTime)
3985+ if since < vimModePulseInterval {
3986+ a.vimModePulseMu.Unlock()
39873987 return
39883988 }
3989- a.pageModePulseLastTime = time.Now()
3990- a.pageModePulseMu.Unlock()
3989+ a.vimModePulseLastTime = time.Now()
3990+ a.vimModePulseMu.Unlock()
39913991
39923992 bw := a.lastFocusedBrowserWindow()
3993- if bw == nil || bw.pageModePaneID == "" {
3993+ if bw == nil || bw.vimModePaneID == "" {
39943994 return
39953995 }
39963996 wsView := a.activeWorkspaceViewForBrowserWindow(bw)
39973997 if wsView == nil {
39983998 return
39993999 }
4000- if pv := wsView.GetPaneView(bw.pageModePaneID); pv != nil {
4000+ if pv := wsView.GetPaneView(bw.vimModePaneID); pv != nil {
40014001 if fast {
4002- pv.TriggerPageModePulseFast()
4002+ pv.TriggerVimModePulseFast()
40034003 } else {
4004- pv.TriggerPageModePulse()
4004+ pv.TriggerVimModePulse()
40054005 }
40064006 }
40074007 }
40084008
4009-// clearPageModeOwnership deactivates page mode on the owning pane for a
4009+// clearVimModeOwnership deactivates vim mode on the owning pane for a
40104010 // specific browser window and resets its tracked pane ID.
40114011 // Also resets the pulse debounce timer so the first pulse on re-entry
40124012 // is never skipped.
4013-func (a *App) clearPageModeOwnership(ctx context.Context, bw *browserWindow) {
4014- if bw == nil || bw.pageModePaneID == "" {
4013+func (a *App) clearVimModeOwnership(ctx context.Context, bw *browserWindow) {
4014+ if bw == nil || bw.vimModePaneID == "" {
40154015 return
40164016 }
40174017
40184018 wsView := a.activeWorkspaceViewForBrowserWindow(bw)
40194019 if wsView != nil {
4020- if pv := wsView.GetPaneView(bw.pageModePaneID); pv != nil {
4021- pv.SetPageMode(false)
4020+ if pv := wsView.GetPaneView(bw.vimModePaneID); pv != nil {
4021+ pv.SetVimMode(false)
40224022 logging.FromContext(ctx).Debug().
40234023 Str("window_id", bw.id).
4024- Str("pane_id", string(bw.pageModePaneID)).
4025- Msg("page mode deactivated on pane")
4024+ Str("pane_id", string(bw.vimModePaneID)).
4025+ Msg("vim mode deactivated on pane")
40264026 }
40274027 }
4028- bw.pageModePaneID = ""
4028+ bw.vimModePaneID = ""
40294029
40304030 // Reset debounce timer so re-entry pulses are never mis-skipped.
4031- a.pageModePulseMu.Lock()
4032- a.pageModePulseLastTime = time.Time{}
4033- a.pageModePulseMu.Unlock()
4031+ a.vimModePulseMu.Lock()
4032+ a.vimModePulseLastTime = time.Time{}
4033+ a.vimModePulseMu.Unlock()
40344034 }
40354035
40364036 // updateModeIndicatorToaster reconciles one window's mode toaster with its
@@ -4057,8 +4057,10 @@ func (a *App) updateModeIndicatorToaster(ctx context.Context, bw *browserWindow,
40574057 // getModeToastClass returns the CSS class for the given mode's toast styling.
40584058 func getModeToastClass(mode input.Mode) string {
40594059 switch mode {
4060- case input.ModePane, input.ModePage:
4060+ case input.ModePane:
40614061 return "toast-pane-mode"
4062+ case input.ModeVim:
4063+ return "toast-vim-mode"
40624064 case input.ModeTab:
40634065 return "toast-tab-mode"
40644066 case input.ModeSession:
@@ -4169,7 +4171,7 @@ func (a *App) createWorkspaceViewWithoutAttach(ctx context.Context, tab *entity.
41694171 wsView.SetOnActivePaneChanged(func(paneID entity.PaneID) {
41704172 a.contentCoord.SyncWebViewViewport(syncCtx, paneID, "workspace-pane-activated")
41714173 if bw := a.browserWindowForTab(tab.ID); bw != nil {
4172- a.transferPageModeOwnershipToPane(ctx, bw, paneID)
4174+ a.transferVimModeOwnershipToPane(ctx, bw, paneID)
41734175 }
41744176 })
41754177 }
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 (
2323 // ---------------------------------------------------------------------------
2424 // Mock-based PaneView and WorkspaceView creation helpers
2525 //
26-// These are used by pageModeTestFixture to build a minimal test environment
26+// These are used by vimModeTestFixture to build a minimal test environment
2727 // without any GTK infrastructure.
2828 // ---------------------------------------------------------------------------
2929
@@ -99,7 +99,7 @@ func newLoadingSkeletonMocks(
9999 }
100100
101101 // createTestPaneView builds a mock-backed PaneView. Returns the PaneView and
102-// its overlay mock so subsequent page-mode indicator / pulse expectations can
102+// its overlay mock so subsequent vim-mode indicator / pulse expectations can
103103 // be set up by the caller.
104104 func createTestPaneView(
105105 t *testing.T,
@@ -138,20 +138,20 @@ func createTestPaneView(
138138 return pv, overlay
139139 }
140140
141-// enterPageMode marks the owning pane with the pane-local visual accent.
142-func enterPageMode(
141+// enterVimMode marks the owning pane with the pane-local visual accent.
142+func enterVimMode(
143143 t *testing.T,
144144 app *App,
145145 overlay *mocks.MockOverlayWidget,
146146 bw *browserWindow,
147147 ) {
148148 t.Helper()
149- overlay.EXPECT().AddCssClass("page-mode-active").Once()
150- app.handlePageModeOwnership(context.Background(), bw, input.ModePage, input.ModeNormal)
149+ overlay.EXPECT().AddCssClass("vim-mode-active").Once()
150+ app.handleVimModeOwnership(context.Background(), bw, input.ModeVim, input.ModeNormal)
151151 }
152152
153-func expectPageModeAccentHidden(overlay *mocks.MockOverlayWidget) {
154- overlay.EXPECT().RemoveCssClass("page-mode-active").Once()
153+func expectVimModeAccentHidden(overlay *mocks.MockOverlayWidget) {
154+ overlay.EXPECT().RemoveCssClass("vim-mode-active").Once()
155155 }
156156
157157 // setupWorkspaceViewMocks creates a mock-backed WorkspaceView.
@@ -181,22 +181,22 @@ func setupWorkspaceViewMocks(
181181 return wv
182182 }
183183
184-// newKeyboardHandlerInPageMode creates a KeyboardHandler whose Mode() returns
185-// ModePage — no GTK widgets required.
186-func newKeyboardHandlerInPageMode(t *testing.T) *input.KeyboardHandler {
184+// newKeyboardHandlerInVimMode creates a KeyboardHandler whose Mode() returns
185+// ModeVim — no GTK widgets required.
186+func newKeyboardHandlerInVimMode(t *testing.T) *input.KeyboardHandler {
187187 t.Helper()
188188 kh := input.NewKeyboardHandler(
189189 context.Background(),
190190 &entity.WorkspaceConfig{},
191191 &entity.SessionConfig{},
192192 )
193- kh.EnterPageMode()
193+ kh.EnterVimMode()
194194 return kh
195195 }
196196
197-func bindPageModeKeyboardHandler(t *testing.T, app *App, bw *browserWindow) *input.KeyboardHandler {
197+func bindVimModeKeyboardHandler(t *testing.T, app *App, bw *browserWindow) *input.KeyboardHandler {
198198 t.Helper()
199- kh := newKeyboardHandlerInPageMode(t)
199+ kh := newKeyboardHandlerInVimMode(t)
200200 kh.SetOnModeChange(func(from, to input.Mode) {
201201 app.handleModeChange(context.Background(), bw, from, to)
202202 })
@@ -205,13 +205,13 @@ func bindPageModeKeyboardHandler(t *testing.T, app *App, bw *browserWindow) *inp
205205 }
206206
207207 // ---------------------------------------------------------------------------
208-// pageModeTestFixture — complete, minimal App + workspace environment
208+// vimModeTestFixture — complete, minimal App + workspace environment
209209 //
210210 // Provides two browser windows, each with a two-pane workspace. The active
211211 // pane on both workspaces starts at "pane-a".
212212 // ---------------------------------------------------------------------------
213213
214-type pageModeTestFixture struct {
214+type vimModeTestFixture struct {
215215 app *App
216216 bw1 *browserWindow
217217 bw2 *browserWindow
@@ -227,7 +227,7 @@ type pageModeTestFixture struct {
227227 overlay2B *mocks.MockOverlayWidget
228228 }
229229
230-func newPageModeTestFixture(t *testing.T) *pageModeTestFixture {
230+func newVimModeTestFixture(t *testing.T) *vimModeTestFixture {
231231 t.Helper()
232232 factory := mocks.NewMockWidgetFactory(t)
233233
@@ -297,7 +297,7 @@ func newPageModeTestFixture(t *testing.T) *pageModeTestFixture {
297297 lastFocusedWindowID: "win-1",
298298 }
299299
300- return &pageModeTestFixture{
300+ return &vimModeTestFixture{
301301 app: app, bw1: bw1, bw2: bw2,
302302 pv1A: pv1A, pv1B: pv1B, pv2A: pv2A, pv2B: pv2B,
303303 factory: factory,
@@ -306,9 +306,9 @@ func newPageModeTestFixture(t *testing.T) *pageModeTestFixture {
306306 }
307307 }
308308
309-func newSingleWindowPageModeFixture(t *testing.T) *pageModeTestFixture {
309+func newSingleWindowVimModeFixture(t *testing.T) *vimModeTestFixture {
310310 t.Helper()
311- f := newPageModeTestFixture(t)
311+ f := newVimModeTestFixture(t)
312312 delete(f.app.browserWindows, "win-2")
313313 delete(f.app.workspaceViews, entity.TabID("tab-2"))
314314 f.bw2 = nil
@@ -320,127 +320,127 @@ func newSingleWindowPageModeFixture(t *testing.T) *pageModeTestFixture {
320320 }
321321
322322 // ============================================================================
323-// 1. Entering Page mode marks only the active pane
323+// 1. Entering Vim mode marks only the active pane
324324 // ============================================================================
325325
326-func TestPageMode_Enter_SetsOwnershipOnActivePane(t *testing.T) {
327- f := newPageModeTestFixture(t)
326+func TestVimMode_Enter_SetsOwnershipOnActivePane(t *testing.T) {
327+ f := newVimModeTestFixture(t)
328328
329- enterPageMode(t, f.app, f.overlay1A, f.bw1)
329+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
330330
331- assert.Equal(t, entity.PaneID("pane-a"), f.bw1.pageModePaneID,
332- "entering page mode sets ownership on the active pane")
333- assert.True(t, f.pv1A.IsPageMode(),
334- "the active pane view enters page mode")
335- assert.False(t, f.pv1B.IsPageMode(),
336- "the inactive pane is NOT in page mode")
337- assert.Empty(t, f.bw2.pageModePaneID,
331+ assert.Equal(t, entity.PaneID("pane-a"), f.bw1.vimModePaneID,
332+ "entering vim mode sets ownership on the active pane")
333+ assert.True(t, f.pv1A.IsVimMode(),
334+ "the active pane view enters vim mode")
335+ assert.False(t, f.pv1B.IsVimMode(),
336+ "the inactive pane is NOT in vim mode")
337+ assert.Empty(t, f.bw2.vimModePaneID,
338338 "second window is untouched")
339- assert.False(t, f.pv2A.IsPageMode(),
340- "second window active pane is NOT in page mode")
339+ assert.False(t, f.pv2A.IsVimMode(),
340+ "second window active pane is NOT in vim mode")
341341 }
342342
343-func TestPageMode_Enter_MarksCorrectWindow(t *testing.T) {
344- f := newPageModeTestFixture(t)
343+func TestVimMode_Enter_MarksCorrectWindow(t *testing.T) {
344+ f := newVimModeTestFixture(t)
345345
346- enterPageMode(t, f.app, f.overlay2A, f.bw2)
346+ enterVimMode(t, f.app, f.overlay2A, f.bw2)
347347
348- assert.Equal(t, entity.PaneID("pane-a"), f.bw2.pageModePaneID)
349- assert.True(t, f.pv2A.IsPageMode())
350- assert.Empty(t, f.bw1.pageModePaneID)
351- assert.False(t, f.pv1A.IsPageMode())
348+ assert.Equal(t, entity.PaneID("pane-a"), f.bw2.vimModePaneID)
349+ assert.True(t, f.pv2A.IsVimMode())
350+ assert.Empty(t, f.bw1.vimModePaneID)
351+ assert.False(t, f.pv1A.IsVimMode())
352352 }
353353
354354 // ============================================================================
355-// 2. Leaving Page mode clears ownership
355+// 2. Leaving Vim mode clears ownership
356356 // ============================================================================
357357
358-func TestPageMode_Leave_ClearsOwnershipFromOwningPane(t *testing.T) {
359- f := newPageModeTestFixture(t)
358+func TestVimMode_Leave_ClearsOwnershipFromOwningPane(t *testing.T) {
359+ f := newVimModeTestFixture(t)
360360
361361 // Enter first
362- enterPageMode(t, f.app, f.overlay1A, f.bw1)
363- require.Equal(t, entity.PaneID("pane-a"), f.bw1.pageModePaneID)
362+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
363+ require.Equal(t, entity.PaneID("pane-a"), f.bw1.vimModePaneID)
364364
365- // Leave — SetPageMode(false) hides indicator and removes the CSS class
366- expectPageModeAccentHidden(f.overlay1A)
367- f.app.handlePageModeOwnership(context.Background(), f.bw1, input.ModeNormal, input.ModePage)
365+ // Leave — SetVimMode(false) hides indicator and removes the CSS class
366+ expectVimModeAccentHidden(f.overlay1A)
367+ f.app.handleVimModeOwnership(context.Background(), f.bw1, input.ModeNormal, input.ModeVim)
368368
369- assert.Empty(t, f.bw1.pageModePaneID,
370- "leaving page mode clears ownership field")
371- assert.False(t, f.pv1A.IsPageMode(),
372- "the previously owning pane exits page mode")
369+ assert.Empty(t, f.bw1.vimModePaneID,
370+ "leaving vim mode clears ownership field")
371+ assert.False(t, f.pv1A.IsVimMode(),
372+ "the previously owning pane exits vim mode")
373373 }
374374
375375 // ============================================================================
376376 // 3. Transfer within the same window
377377 // ============================================================================
378378
379-func TestPageMode_Transfer_MovesOwnershipToNewPane(t *testing.T) {
380- f := newPageModeTestFixture(t)
379+func TestVimMode_Transfer_MovesOwnershipToNewPane(t *testing.T) {
380+ f := newVimModeTestFixture(t)
381381
382- // Enter page mode on bw1 pane-a — save label for hide expectation
383- enterPageMode(t, f.app, f.overlay1A, f.bw1)
384- require.Equal(t, entity.PaneID("pane-a"), f.bw1.pageModePaneID)
382+ // Enter vim mode on bw1 pane-a — save label for hide expectation
383+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
384+ require.Equal(t, entity.PaneID("pane-a"), f.bw1.vimModePaneID)
385385
386- // Give bw1 a keyboard handler in page mode so transfer performs the switch
387- f.bw1.keyboardHandler = newKeyboardHandlerInPageMode(t)
386+ // Give bw1 a keyboard handler in vim mode so transfer performs the switch
387+ f.bw1.keyboardHandler = newKeyboardHandlerInVimMode(t)
388388
389- // transferPageModeOwnershipToPane calls SetPageMode(false) on the old
390- // pane before SetPageMode(true) on the new pane. Set up both.
391- expectPageModeAccentHidden(f.overlay1A)
389+ // transferVimModeOwnershipToPane calls SetVimMode(false) on the old
390+ // pane before SetVimMode(true) on the new pane. Set up both.
391+ expectVimModeAccentHidden(f.overlay1A)
392392
393393 // 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()
395395
396396 // 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"))
398398
399- assert.Equal(t, entity.PaneID("pane-b"), f.bw1.pageModePaneID,
399+ assert.Equal(t, entity.PaneID("pane-b"), f.bw1.vimModePaneID,
400400 "ownership transferred to new pane")
401- assert.False(t, f.pv1A.IsPageMode(),
401+ assert.False(t, f.pv1A.IsVimMode(),
402402 "previous owning pane deactivated")
403- assert.True(t, f.pv1B.IsPageMode(),
404- "new pane is now in page mode")
403+ assert.True(t, f.pv1B.IsVimMode(),
404+ "new pane is now in vim mode")
405405 }
406406
407-func TestPageMode_Transfer_NoPageModeClearsStale(t *testing.T) {
408- f := newPageModeTestFixture(t)
407+func TestVimMode_Transfer_NoVimModeClearsStale(t *testing.T) {
408+ f := newVimModeTestFixture(t)
409409
410- f.bw1.pageModePaneID = entity.PaneID("pane-a")
410+ f.bw1.vimModePaneID = entity.PaneID("pane-a")
411411 f.bw1.keyboardHandler = input.NewKeyboardHandler(
412412 context.Background(),
413413 &entity.WorkspaceConfig{},
414414 &entity.SessionConfig{},
415415 )
416- // No EnterPageMode — handler stays in ModeNormal.
416+ // No EnterVimMode — handler stays in ModeNormal.
417417
418- f.app.transferPageModeOwnershipToPane(context.Background(), f.bw1, entity.PaneID("pane-b"))
418+ f.app.transferVimModeOwnershipToPane(context.Background(), f.bw1, entity.PaneID("pane-b"))
419419
420- assert.Empty(t, f.bw1.pageModePaneID,
421- "stale ownership cleared when window is not in page mode")
422- assert.False(t, f.pv1A.IsPageMode())
423- assert.False(t, f.pv1B.IsPageMode())
420+ assert.Empty(t, f.bw1.vimModePaneID,
421+ "stale ownership cleared when window is not in vim mode")
422+ assert.False(t, f.pv1A.IsVimMode())
423+ assert.False(t, f.pv1B.IsVimMode())
424424 }
425425
426-func TestPageMode_Transfer_NilBwDoesNotCrash(t *testing.T) {
426+func TestVimMode_Transfer_NilBwDoesNotCrash(t *testing.T) {
427427 app := &App{}
428- app.transferPageModeOwnershipToPane(context.Background(), nil, entity.PaneID("pane-1"))
428+ app.transferVimModeOwnershipToPane(context.Background(), nil, entity.PaneID("pane-1"))
429429 }
430430
431-func TestPageMode_Transfer_SamePaneIsNoop(t *testing.T) {
431+func TestVimMode_Transfer_SamePaneIsNoop(t *testing.T) {
432432 app := &App{}
433433 paneID := entity.PaneID("pane-a")
434- bw := &browserWindow{pageModePaneID: paneID}
435- app.transferPageModeOwnershipToPane(context.Background(), bw, paneID)
436- assert.Equal(t, paneID, bw.pageModePaneID)
434+ bw := &browserWindow{vimModePaneID: paneID}
435+ app.transferVimModeOwnershipToPane(context.Background(), bw, paneID)
436+ assert.Equal(t, paneID, bw.vimModePaneID)
437437 }
438438
439-func TestPageMode_Transfer_EmptyOwnershipIsNoop(t *testing.T) {
439+func TestVimMode_Transfer_EmptyOwnershipIsNoop(t *testing.T) {
440440 app := &App{}
441441 bw := &browserWindow{}
442- app.transferPageModeOwnershipToPane(context.Background(), bw, entity.PaneID("pane-a"))
443- assert.Empty(t, bw.pageModePaneID)
442+ app.transferVimModeOwnershipToPane(context.Background(), bw, entity.PaneID("pane-a"))
443+ assert.Empty(t, bw.vimModePaneID)
444444 }
445445
446446 // ============================================================================
@@ -448,52 +448,52 @@ func TestPageMode_Transfer_EmptyOwnershipIsNoop(t *testing.T) {
448448 // ============================================================================
449449
450450 func setUpNormalPulse(overlay *mocks.MockOverlayWidget) {
451- overlay.EXPECT().RemoveCssClass("page-mode-pulse").Once()
452- overlay.EXPECT().RemoveCssClass("page-mode-pulse-fast").Once()
453- overlay.EXPECT().RemoveCssClass("page-mode-pulse-cycle-a").Once()
454- overlay.EXPECT().RemoveCssClass("page-mode-pulse-cycle-b").Once()
455- overlay.EXPECT().AddCssClass("page-mode-pulse").Once()
456- overlay.EXPECT().AddCssClass("page-mode-pulse-cycle-a").Once()
451+ overlay.EXPECT().RemoveCssClass("vim-mode-pulse").Once()
452+ overlay.EXPECT().RemoveCssClass("vim-mode-pulse-fast").Once()
453+ overlay.EXPECT().RemoveCssClass("vim-mode-pulse-cycle-a").Once()
454+ overlay.EXPECT().RemoveCssClass("vim-mode-pulse-cycle-b").Once()
455+ overlay.EXPECT().AddCssClass("vim-mode-pulse").Once()
456+ overlay.EXPECT().AddCssClass("vim-mode-pulse-cycle-a").Once()
457457 }
458458
459459 func setUpFastPulse(overlay *mocks.MockOverlayWidget) {
460- overlay.EXPECT().RemoveCssClass("page-mode-pulse").Once()
461- overlay.EXPECT().RemoveCssClass("page-mode-pulse-fast").Once()
462- overlay.EXPECT().RemoveCssClass("page-mode-pulse-cycle-a").Once()
463- overlay.EXPECT().RemoveCssClass("page-mode-pulse-cycle-b").Once()
464- overlay.EXPECT().AddCssClass("page-mode-pulse-fast").Once()
465- overlay.EXPECT().AddCssClass("page-mode-pulse-cycle-a").Once()
460+ overlay.EXPECT().RemoveCssClass("vim-mode-pulse").Once()
461+ overlay.EXPECT().RemoveCssClass("vim-mode-pulse-fast").Once()
462+ overlay.EXPECT().RemoveCssClass("vim-mode-pulse-cycle-a").Once()
463+ overlay.EXPECT().RemoveCssClass("vim-mode-pulse-cycle-b").Once()
464+ overlay.EXPECT().AddCssClass("vim-mode-pulse-fast").Once()
465+ overlay.EXPECT().AddCssClass("vim-mode-pulse-cycle-a").Once()
466466 }
467467
468-func TestPageMode_Pulse_NormalTriggersOnOwningPane(t *testing.T) {
469- f := newPageModeTestFixture(t)
468+func TestVimMode_Pulse_NormalTriggersOnOwningPane(t *testing.T) {
469+ f := newVimModeTestFixture(t)
470470
471- enterPageMode(t, f.app, f.overlay1A, f.bw1)
472- require.Equal(t, entity.PaneID("pane-a"), f.bw1.pageModePaneID)
471+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
472+ require.Equal(t, entity.PaneID("pane-a"), f.bw1.vimModePaneID)
473473
474474 // Pulse needs the indicator to exist — it was created lazily during
475- // enterPageMode, but we also need to set up pulse expectations.
476- // (enterPageMode created it, so we just set up pulse mocks.)
475+ // enterVimMode, but we also need to set up pulse expectations.
476+ // (enterVimMode created it, so we just set up pulse mocks.)
477477 setUpNormalPulse(f.overlay1A)
478478
479- f.app.triggerPageModePulse(context.Background(), false)
479+ f.app.triggerVimModePulse(context.Background(), false)
480480 }
481481
482-func TestPageMode_Pulse_FastTriggersOnOwningPane(t *testing.T) {
483- f := newPageModeTestFixture(t)
482+func TestVimMode_Pulse_FastTriggersOnOwningPane(t *testing.T) {
483+ f := newVimModeTestFixture(t)
484484
485- enterPageMode(t, f.app, f.overlay1A, f.bw1)
486- require.Equal(t, entity.PaneID("pane-a"), f.bw1.pageModePaneID)
485+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
486+ require.Equal(t, entity.PaneID("pane-a"), f.bw1.vimModePaneID)
487487
488488 setUpFastPulse(f.overlay1A)
489489
490- f.app.triggerPageModePulse(context.Background(), true)
490+ f.app.triggerVimModePulse(context.Background(), true)
491491 }
492492
493-func TestPageMode_Pulse_NoOwnerIsNoop(t *testing.T) {
494- f := newPageModeTestFixture(t)
495- f.app.triggerPageModePulse(context.Background(), false)
496- f.app.triggerPageModePulse(context.Background(), true)
493+func TestVimMode_Pulse_NoOwnerIsNoop(t *testing.T) {
494+ f := newVimModeTestFixture(t)
495+ f.app.triggerVimModePulse(context.Background(), false)
496+ f.app.triggerVimModePulse(context.Background(), true)
497497 // No pane has ownership — no pulse expectations needed.
498498 }
499499
@@ -501,141 +501,141 @@ func TestPageMode_Pulse_NoOwnerIsNoop(t *testing.T) {
501501 // 5. Multi-window isolation
502502 // ============================================================================
503503
504-func TestPageMode_MultiWindow_TwoWindowsEnterLeave(t *testing.T) {
505- f := newPageModeTestFixture(t)
504+func TestVimMode_MultiWindow_TwoWindowsEnterLeave(t *testing.T) {
505+ f := newVimModeTestFixture(t)
506506
507- enterPageMode(t, f.app, f.overlay1A, f.bw1)
508- enterPageMode(t, f.app, f.overlay2A, f.bw2)
507+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
508+ enterVimMode(t, f.app, f.overlay2A, f.bw2)
509509
510- assert.Equal(t, entity.PaneID("pane-a"), f.bw1.pageModePaneID)
511- assert.Equal(t, entity.PaneID("pane-a"), f.bw2.pageModePaneID)
510+ assert.Equal(t, entity.PaneID("pane-a"), f.bw1.vimModePaneID)
511+ assert.Equal(t, entity.PaneID("pane-a"), f.bw2.vimModePaneID)
512512
513513 // Leave bw1 only
514- expectPageModeAccentHidden(f.overlay1A)
515- f.app.handlePageModeOwnership(context.Background(), f.bw1, input.ModeNormal, input.ModePage)
514+ expectVimModeAccentHidden(f.overlay1A)
515+ f.app.handleVimModeOwnership(context.Background(), f.bw1, input.ModeNormal, input.ModeVim)
516516
517- assert.Empty(t, f.bw1.pageModePaneID)
518- assert.False(t, f.pv1A.IsPageMode())
517+ assert.Empty(t, f.bw1.vimModePaneID)
518+ assert.False(t, f.pv1A.IsVimMode())
519519
520520 // bw2 unchanged
521- assert.Equal(t, entity.PaneID("pane-a"), f.bw2.pageModePaneID)
522- assert.True(t, f.pv2A.IsPageMode())
521+ assert.Equal(t, entity.PaneID("pane-a"), f.bw2.vimModePaneID)
522+ assert.True(t, f.pv2A.IsVimMode())
523523 }
524524
525-func TestPageMode_MultiWindow_TransferIsolated(t *testing.T) {
526- f := newPageModeTestFixture(t)
525+func TestVimMode_MultiWindow_TransferIsolated(t *testing.T) {
526+ f := newVimModeTestFixture(t)
527527
528- enterPageMode(t, f.app, f.overlay1A, f.bw1)
529- enterPageMode(t, f.app, f.overlay2A, f.bw2)
528+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
529+ enterVimMode(t, f.app, f.overlay2A, f.bw2)
530530
531- f.bw1.keyboardHandler = newKeyboardHandlerInPageMode(t)
531+ f.bw1.keyboardHandler = newKeyboardHandlerInVimMode(t)
532532
533- // transferPageModeOwnershipToPane calls SetPageMode(false) on old pane first
534- expectPageModeAccentHidden(f.overlay1A)
533+ // transferVimModeOwnershipToPane calls SetVimMode(false) on old pane first
534+ expectVimModeAccentHidden(f.overlay1A)
535535
536536 // Transfer bw1 to pane-b.
537- f.overlay1B.EXPECT().AddCssClass("page-mode-active").Once()
538- f.app.transferPageModeOwnershipToPane(context.Background(), f.bw1, entity.PaneID("pane-b"))
537+ f.overlay1B.EXPECT().AddCssClass("vim-mode-active").Once()
538+ f.app.transferVimModeOwnershipToPane(context.Background(), f.bw1, entity.PaneID("pane-b"))
539539
540- assert.Equal(t, entity.PaneID("pane-b"), f.bw1.pageModePaneID)
541- assert.True(t, f.pv1B.IsPageMode())
542- assert.False(t, f.pv1A.IsPageMode())
540+ assert.Equal(t, entity.PaneID("pane-b"), f.bw1.vimModePaneID)
541+ assert.True(t, f.pv1B.IsVimMode())
542+ assert.False(t, f.pv1A.IsVimMode())
543543
544544 // bw2 unchanged
545- assert.Equal(t, entity.PaneID("pane-a"), f.bw2.pageModePaneID)
546- assert.True(t, f.pv2A.IsPageMode())
545+ assert.Equal(t, entity.PaneID("pane-a"), f.bw2.vimModePaneID)
546+ assert.True(t, f.pv2A.IsVimMode())
547547 }
548548
549-func TestPageMode_MultiWindow_PulseIsolated(t *testing.T) {
550- f := newPageModeTestFixture(t)
549+func TestVimMode_MultiWindow_PulseIsolated(t *testing.T) {
550+ f := newVimModeTestFixture(t)
551551
552- enterPageMode(t, f.app, f.overlay1A, f.bw1)
553- enterPageMode(t, f.app, f.overlay2A, f.bw2)
552+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
553+ enterVimMode(t, f.app, f.overlay2A, f.bw2)
554554
555555 // Pulse on bw1 (lastFocusedWindowID = "win-1")
556556 setUpNormalPulse(f.overlay1A)
557- f.app.triggerPageModePulse(context.Background(), false)
557+ f.app.triggerVimModePulse(context.Background(), false)
558558
559- // bw2 pane still in page mode
560- assert.True(t, f.pv2A.IsPageMode())
559+ // bw2 pane still in vim mode
560+ assert.True(t, f.pv2A.IsVimMode())
561561 }
562562
563563 // ============================================================================
564564 // Edge cases — nil / no-workspace / startup guards
565565 // ============================================================================
566566
567-func TestPageMode_Enter_NoWorkspaceIsNoop(t *testing.T) {
567+func TestVimMode_Enter_NoWorkspaceIsNoop(t *testing.T) {
568568 app := &App{}
569569 bw := &browserWindow{id: "win-1"}
570- app.handlePageModeOwnership(context.Background(), bw, input.ModePage, input.ModeNormal)
571- assert.Empty(t, bw.pageModePaneID)
570+ app.handleVimModeOwnership(context.Background(), bw, input.ModeVim, input.ModeNormal)
571+ assert.Empty(t, bw.vimModePaneID)
572572 }
573573
574-func TestPageMode_Clear_NilBwDoesNotCrash(t *testing.T) {
575- (&App{}).clearPageModeOwnership(context.Background(), nil)
574+func TestVimMode_Clear_NilBwDoesNotCrash(t *testing.T) {
575+ (&App{}).clearVimModeOwnership(context.Background(), nil)
576576 }
577577
578-func TestPageMode_Clear_NoPaneID(t *testing.T) {
578+func TestVimMode_Clear_NoPaneID(t *testing.T) {
579579 app := &App{}
580580 bw := &browserWindow{id: "test-win"}
581- app.clearPageModeOwnership(context.Background(), bw)
582- assert.Empty(t, bw.pageModePaneID)
581+ app.clearVimModeOwnership(context.Background(), bw)
582+ assert.Empty(t, bw.vimModePaneID)
583583 }
584584
585-func TestPageMode_Clear_StalePaneWithoutWorkspace(t *testing.T) {
585+func TestVimMode_Clear_StalePaneWithoutWorkspace(t *testing.T) {
586586 app := &App{}
587- bw := &browserWindow{id: "test-win", pageModePaneID: entity.PaneID("stale-pane")}
588- app.clearPageModeOwnership(context.Background(), bw)
589- assert.Empty(t, bw.pageModePaneID)
587+ bw := &browserWindow{id: "test-win", vimModePaneID: entity.PaneID("stale-pane")}
588+ app.clearVimModeOwnership(context.Background(), bw)
589+ assert.Empty(t, bw.vimModePaneID)
590590 }
591591
592-func TestPageMode_HandleModeChange_NilBwDoesNotCrash(t *testing.T) {
593- (&App{}).handleModeChange(context.Background(), nil, input.ModeNormal, input.ModePage)
592+func TestVimMode_HandleModeChange_NilBwDoesNotCrash(t *testing.T) {
593+ (&App{}).handleModeChange(context.Background(), nil, input.ModeNormal, input.ModeVim)
594594 }
595595
596-func TestPageMode_EditableFocusOnActivePaneExitsPageMode(t *testing.T) {
597- f := newSingleWindowPageModeFixture(t)
598- enterPageMode(t, f.app, f.overlay1A, f.bw1)
599- kh := bindPageModeKeyboardHandler(t, f.app, f.bw1)
596+func TestVimMode_EditableFocusOnActivePaneExitsVimMode(t *testing.T) {
597+ f := newSingleWindowVimModeFixture(t)
598+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
599+ kh := bindVimModeKeyboardHandler(t, f.app, f.bw1)
600600
601- expectPageModeAccentHidden(f.overlay1A)
601+ expectVimModeAccentHidden(f.overlay1A)
602602 f.app.handlePageEditableFocusChanged(context.Background(), entity.PaneID("pane-a"), true)
603603
604604 assert.Equal(t, input.ModeNormal, kh.Mode())
605- assert.Empty(t, f.bw1.pageModePaneID)
606- assert.False(t, f.pv1A.IsPageMode())
605+ assert.Empty(t, f.bw1.vimModePaneID)
606+ assert.False(t, f.pv1A.IsVimMode())
607607 }
608608
609-func TestPageMode_EditableFocusOnInactivePaneDoesNotExitPageMode(t *testing.T) {
610- f := newSingleWindowPageModeFixture(t)
611- enterPageMode(t, f.app, f.overlay1A, f.bw1)
612- kh := bindPageModeKeyboardHandler(t, f.app, f.bw1)
609+func TestVimMode_EditableFocusOnInactivePaneDoesNotExitVimMode(t *testing.T) {
610+ f := newSingleWindowVimModeFixture(t)
611+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
612+ kh := bindVimModeKeyboardHandler(t, f.app, f.bw1)
613613
614614 f.app.handlePageEditableFocusChanged(context.Background(), entity.PaneID("pane-b"), true)
615615
616- assert.Equal(t, input.ModePage, kh.Mode())
617- assert.Equal(t, entity.PaneID("pane-a"), f.bw1.pageModePaneID)
618- assert.True(t, f.pv1A.IsPageMode())
616+ assert.Equal(t, input.ModeVim, kh.Mode())
617+ assert.Equal(t, entity.PaneID("pane-a"), f.bw1.vimModePaneID)
618+ assert.True(t, f.pv1A.IsVimMode())
619619 }
620620
621-func TestPageMode_PaneSwitchToEditablePaneExitsPageMode(t *testing.T) {
622- f := newSingleWindowPageModeFixture(t)
623- enterPageMode(t, f.app, f.overlay1A, f.bw1)
624- kh := bindPageModeKeyboardHandler(t, f.app, f.bw1)
621+func TestVimMode_PaneSwitchToEditablePaneExitsVimMode(t *testing.T) {
622+ f := newSingleWindowVimModeFixture(t)
623+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
624+ kh := bindVimModeKeyboardHandler(t, f.app, f.bw1)
625625 f.app.pageEditableFocusByPane = map[entity.PaneID]bool{
626626 "pane-b": true,
627627 }
628628
629- expectPageModeAccentHidden(f.overlay1A)
630- f.app.transferPageModeOwnershipToPane(context.Background(), f.bw1, entity.PaneID("pane-b"))
629+ expectVimModeAccentHidden(f.overlay1A)
630+ f.app.transferVimModeOwnershipToPane(context.Background(), f.bw1, entity.PaneID("pane-b"))
631631
632632 assert.Equal(t, input.ModeNormal, kh.Mode())
633- assert.Empty(t, f.bw1.pageModePaneID)
634- assert.False(t, f.pv1A.IsPageMode())
635- assert.False(t, f.pv1B.IsPageMode())
633+ assert.Empty(t, f.bw1.vimModePaneID)
634+ assert.False(t, f.pv1A.IsVimMode())
635+ assert.False(t, f.pv1B.IsVimMode())
636636 }
637637
638-func TestPageMode_BackgroundWindowEditableFocusDoesNotExitFocusedWindowPageMode(t *testing.T) {
638+func TestVimMode_BackgroundWindowEditableFocusDoesNotExitFocusedWindowVimMode(t *testing.T) {
639639 factory := mocks.NewMockWidgetFactory(t)
640640 pv1, overlay1 := createTestPaneView(t, factory, "pane-a")
641641 pv2, _ := createTestPaneView(t, factory, "pane-x")
@@ -661,44 +661,44 @@ func TestPageMode_BackgroundWindowEditableFocusDoesNotExitFocusedWindowPageMode(
661661 lastFocusedWindowID: "win-1",
662662 }
663663
664- enterPageMode(t, app, overlay1, bw1)
665- kh := bindPageModeKeyboardHandler(t, app, bw1)
664+ enterVimMode(t, app, overlay1, bw1)
665+ kh := bindVimModeKeyboardHandler(t, app, bw1)
666666
667667 app.handlePageEditableFocusChanged(context.Background(), entity.PaneID("pane-x"), true)
668668
669- assert.Equal(t, input.ModePage, kh.Mode())
670- assert.Equal(t, entity.PaneID("pane-a"), bw1.pageModePaneID)
671- assert.True(t, pv1.IsPageMode())
669+ assert.Equal(t, input.ModeVim, kh.Mode())
670+ assert.Equal(t, entity.PaneID("pane-a"), bw1.vimModePaneID)
671+ assert.True(t, pv1.IsVimMode())
672672 }
673673
674-func TestPageMode_OmniboxFocusExitsPageMode(t *testing.T) {
675- f := newSingleWindowPageModeFixture(t)
676- enterPageMode(t, f.app, f.overlay1A, f.bw1)
677- kh := bindPageModeKeyboardHandler(t, f.app, f.bw1)
674+func TestVimMode_OmniboxFocusExitsVimMode(t *testing.T) {
675+ f := newSingleWindowVimModeFixture(t)
676+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
677+ kh := bindVimModeKeyboardHandler(t, f.app, f.bw1)
678678
679- expectPageModeAccentHidden(f.overlay1A)
680- f.app.handlePageModeFocusTrigger(context.Background(), f.bw1, usecase.PageModePolicyTriggerOmniboxFocus)
679+ expectVimModeAccentHidden(f.overlay1A)
680+ f.app.handleVimModeFocusTrigger(context.Background(), f.bw1, usecase.VimModePolicyTriggerOmniboxFocus)
681681
682682 assert.Equal(t, input.ModeNormal, kh.Mode())
683- assert.Empty(t, f.bw1.pageModePaneID)
683+ assert.Empty(t, f.bw1.vimModePaneID)
684684 }
685685
686-func TestPageMode_FindBarFocusExitsPageMode(t *testing.T) {
687- f := newSingleWindowPageModeFixture(t)
688- enterPageMode(t, f.app, f.overlay1A, f.bw1)
689- kh := bindPageModeKeyboardHandler(t, f.app, f.bw1)
686+func TestVimMode_FindBarFocusExitsVimMode(t *testing.T) {
687+ f := newSingleWindowVimModeFixture(t)
688+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
689+ kh := bindVimModeKeyboardHandler(t, f.app, f.bw1)
690690
691- expectPageModeAccentHidden(f.overlay1A)
692- f.app.handlePageModeFocusTrigger(context.Background(), f.bw1, usecase.PageModePolicyTriggerFindBarFocus)
691+ expectVimModeAccentHidden(f.overlay1A)
692+ f.app.handleVimModeFocusTrigger(context.Background(), f.bw1, usecase.VimModePolicyTriggerFindBarFocus)
693693
694694 assert.Equal(t, input.ModeNormal, kh.Mode())
695- assert.Empty(t, f.bw1.pageModePaneID)
695+ assert.Empty(t, f.bw1.vimModePaneID)
696696 }
697697
698-func TestPageMode_TabSwitchExitsAndClearsOldAccent(t *testing.T) {
699- f := newSingleWindowPageModeFixture(t)
700- enterPageMode(t, f.app, f.overlay1A, f.bw1)
701- kh := bindPageModeKeyboardHandler(t, f.app, f.bw1)
698+func TestVimMode_TabSwitchExitsAndClearsOldAccent(t *testing.T) {
699+ f := newSingleWindowVimModeFixture(t)
700+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
701+ kh := bindVimModeKeyboardHandler(t, f.app, f.bw1)
702702
703703 tab2 := &entity.Tab{ID: "tab-2b", Workspace: &entity.Workspace{
704704 ID: "ws-2b",
@@ -709,57 +709,57 @@ func TestPageMode_TabSwitchExitsAndClearsOldAccent(t *testing.T) {
709709 f.bw1.tabs.SetActive(tab2.ID)
710710 f.app.workspaceViews[tab2.ID] = setupWorkspaceViewMocks(t, f.factory)
711711
712- expectPageModeAccentHidden(f.overlay1A)
713- f.app.handlePageModeTabSwitch(context.Background(), f.bw1)
712+ expectVimModeAccentHidden(f.overlay1A)
713+ f.app.handleVimModeTabSwitch(context.Background(), f.bw1)
714714
715715 assert.Equal(t, input.ModeNormal, kh.Mode())
716- assert.Empty(t, f.bw1.pageModePaneID)
717- assert.False(t, f.pv1A.IsPageMode())
716+ assert.Empty(t, f.bw1.vimModePaneID)
717+ assert.False(t, f.pv1A.IsVimMode())
718718 }
719719
720-func TestPageMode_ActivationBypassWhenActivePageIsEditable(t *testing.T) {
721- f := newSingleWindowPageModeFixture(t)
720+func TestVimMode_ActivationBypassWhenActivePageIsEditable(t *testing.T) {
721+ f := newSingleWindowVimModeFixture(t)
722722 f.app.pageEditableFocusByPane = map[entity.PaneID]bool{
723723 "pane-a": true,
724724 }
725725
726- assert.True(t, f.app.shouldBypassPageModeActivation(f.bw1))
726+ assert.True(t, f.app.shouldBypassVimModeActivation(f.bw1))
727727 }
728728
729-func TestPageMode_ActivationBypassClearsWhenEditableFocusLeaves(t *testing.T) {
730- f := newSingleWindowPageModeFixture(t)
729+func TestVimMode_ActivationBypassClearsWhenEditableFocusLeaves(t *testing.T) {
730+ f := newSingleWindowVimModeFixture(t)
731731
732732 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))
734734
735735 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))
737737 }
738738
739-func TestPageMode_ClearEditableFocusStateRemovesStoredBypass(t *testing.T) {
740- f := newSingleWindowPageModeFixture(t)
739+func TestVimMode_ClearEditableFocusStateRemovesStoredBypass(t *testing.T) {
740+ f := newSingleWindowVimModeFixture(t)
741741 f.app.pageEditableFocusByPane = map[entity.PaneID]bool{"pane-a": true}
742742
743743 f.app.clearPageEditableFocusState(entity.PaneID("pane-a"))
744744
745- assert.False(t, f.app.shouldBypassPageModeActivation(f.bw1))
745+ assert.False(t, f.app.shouldBypassVimModeActivation(f.bw1))
746746 }
747747
748748 // ============================================================================
749749 // CSS class constants
750750 // ============================================================================
751751
752-func TestPageMode_CSSClassConstants(t *testing.T) {
753- assert.Equal(t, "page-mode-active", component.PageModeActiveClass)
754- assert.Equal(t, "page-mode-pulse", component.PageModePulseClass)
755- assert.Equal(t, "page-mode-pulse-fast", component.PageModeFastPulseClass)
752+func TestVimMode_CSSClassConstants(t *testing.T) {
753+ assert.Equal(t, "vim-mode-active", component.VimModeActiveClass)
754+ assert.Equal(t, "vim-mode-pulse", component.VimModePulseClass)
755+ assert.Equal(t, "vim-mode-pulse-fast", component.VimModeFastPulseClass)
756756 }
757757
758758 // ============================================================================
759759 // Mode toaster
760760 // ============================================================================
761761
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) {
763763 t.Helper()
764764
765765 container := mocks.NewMockBoxWidget(t)
@@ -782,21 +782,21 @@ func newModeToasterForPageModeTest(t *testing.T, factory *mocks.MockWidgetFactor
782782 return component.NewToaster(factory), container, label
783783 }
784784
785-func expectPageModeToastShown(container *mocks.MockBoxWidget, label *mocks.MockLabelWidget) {
785+func expectVimModeToastShown(container *mocks.MockBoxWidget, label *mocks.MockLabelWidget) {
786786 container.EXPECT().RemoveCssClass("toast-info").Once()
787- container.EXPECT().AddCssClass("toast-pane-mode").Once()
787+ container.EXPECT().AddCssClass("toast-vim-mode").Once()
788788 container.EXPECT().SetHalign(mock.Anything).Once()
789789 container.EXPECT().SetValign(mock.Anything).Once()
790- label.EXPECT().SetText("PAGE MODE").Once()
790+ label.EXPECT().SetText("VIM MODE").Once()
791791 container.EXPECT().SetVisible(true).Once()
792792 }
793793
794-func TestPageMode_ToasterRemainsVisibleUntilModeExit(t *testing.T) {
794+func TestVimMode_ToasterRemainsVisibleUntilModeExit(t *testing.T) {
795795 factory := mocks.NewMockWidgetFactory(t)
796- toaster, container, label := newModeToasterForPageModeTest(t, factory)
797- expectPageModeToastShown(container, label)
796+ toaster, container, label := newModeToasterForVimModeTest(t, factory)
797+ expectVimModeToastShown(container, label)
798798 // 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()
800800 container.EXPECT().SetVisible(false).Maybe()
801801
802802 app := &App{runtimeConfig: runtimeConfigStateFromSnapshotForTest(entity.RuntimeConfigSnapshot{
@@ -808,7 +808,7 @@ func TestPageMode_ToasterRemainsVisibleUntilModeExit(t *testing.T) {
808808 app.browserWindows = map[string]*browserWindow{bw.id: bw}
809809 app.lastFocusedWindowID = bw.id
810810
811- app.updateModeIndicatorToaster(context.Background(), bw, input.ModePage)
811+ app.updateModeIndicatorToaster(context.Background(), bw, input.ModeVim)
812812 require.True(t, toaster.IsVisible())
813813
814814 deadline := time.Now().Add(time.Duration(component.ToastBriefDurationMs+100) * time.Millisecond)
@@ -820,15 +820,15 @@ func TestPageMode_ToasterRemainsVisibleUntilModeExit(t *testing.T) {
820820 }
821821 }
822822
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")
824824 }
825825
826-func TestPageMode_ModeExitHidesToaster(t *testing.T) {
826+func TestVimMode_ModeExitHidesToaster(t *testing.T) {
827827 factory := mocks.NewMockWidgetFactory(t)
828- toaster, container, label := newModeToasterForPageModeTest(t, factory)
829- expectPageModeToastShown(container, label)
828+ toaster, container, label := newModeToasterForVimModeTest(t, factory)
829+ expectVimModeToastShown(container, label)
830830 container.EXPECT().RemoveCssClass("toast-custom").Once()
831- container.EXPECT().RemoveCssClass("toast-pane-mode").Once()
831+ container.EXPECT().RemoveCssClass("toast-vim-mode").Once()
832832 container.EXPECT().SetVisible(false).Once()
833833
834834 app := &App{runtimeConfig: runtimeConfigStateFromSnapshotForTest(entity.RuntimeConfigSnapshot{
@@ -838,32 +838,32 @@ func TestPageMode_ModeExitHidesToaster(t *testing.T) {
838838 })}
839839 bw := &browserWindow{id: "win-1", modeToaster: toaster}
840840
841- app.updateModeIndicatorToaster(context.Background(), bw, input.ModePage)
841+ app.updateModeIndicatorToaster(context.Background(), bw, input.ModeVim)
842842 require.True(t, toaster.IsVisible())
843843 app.updateModeIndicatorToaster(context.Background(), bw, input.ModeNormal)
844844
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")
846846 }
847847
848-func TestPageMode_DisabledModeIndicatorPreferenceSuppressesToaster(t *testing.T) {
848+func TestVimMode_DisabledModeIndicatorPreferenceSuppressesToaster(t *testing.T) {
849849 factory := mocks.NewMockWidgetFactory(t)
850- toaster, _, _ := newModeToasterForPageModeTest(t, factory)
850+ toaster, _, _ := newModeToasterForVimModeTest(t, factory)
851851 app := &App{runtimeConfig: runtimeConfigStateFromSnapshotForTest(entity.RuntimeConfigSnapshot{})}
852852 bw := &browserWindow{id: "win-1", modeToaster: toaster}
853853
854- app.updateModeIndicatorToaster(context.Background(), bw, input.ModePage)
854+ app.updateModeIndicatorToaster(context.Background(), bw, input.ModeVim)
855855
856856 assert.False(t, toaster.IsVisible())
857857 }
858858
859-func TestPageMode_MultiWindowToastersExitIndependently(t *testing.T) {
859+func TestVimMode_MultiWindowToastersExitIndependently(t *testing.T) {
860860 factory := mocks.NewMockWidgetFactory(t)
861- toaster1, container1, label1 := newModeToasterForPageModeTest(t, factory)
862- toaster2, container2, label2 := newModeToasterForPageModeTest(t, factory)
863- expectPageModeToastShown(container1, label1)
864- expectPageModeToastShown(container2, label2)
861+ toaster1, container1, label1 := newModeToasterForVimModeTest(t, factory)
862+ toaster2, container2, label2 := newModeToasterForVimModeTest(t, factory)
863+ expectVimModeToastShown(container1, label1)
864+ expectVimModeToastShown(container2, label2)
865865 container1.EXPECT().RemoveCssClass("toast-custom").Once()
866- container1.EXPECT().RemoveCssClass("toast-pane-mode").Once()
866+ container1.EXPECT().RemoveCssClass("toast-vim-mode").Once()
867867 container1.EXPECT().SetVisible(false).Once()
868868
869869 app := &App{runtimeConfig: runtimeConfigStateFromSnapshotForTest(entity.RuntimeConfigSnapshot{
@@ -874,20 +874,20 @@ func TestPageMode_MultiWindowToastersExitIndependently(t *testing.T) {
874874 bw1 := &browserWindow{id: "win-1", modeToaster: toaster1}
875875 bw2 := &browserWindow{id: "win-2", modeToaster: toaster2}
876876
877- app.updateModeIndicatorToaster(context.Background(), bw1, input.ModePage)
878- app.updateModeIndicatorToaster(context.Background(), bw2, input.ModePage)
877+ app.updateModeIndicatorToaster(context.Background(), bw1, input.ModeVim)
878+ app.updateModeIndicatorToaster(context.Background(), bw2, input.ModeVim)
879879 app.updateModeIndicatorToaster(context.Background(), bw1, input.ModeNormal)
880880
881881 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")
883883 }
884884
885-func TestPageMode_RuntimePreferenceDisableHidesPersistentToaster(t *testing.T) {
885+func TestVimMode_RuntimePreferenceDisableHidesPersistentToaster(t *testing.T) {
886886 factory := mocks.NewMockWidgetFactory(t)
887- toaster, container, label := newModeToasterForPageModeTest(t, factory)
888- expectPageModeToastShown(container, label)
887+ toaster, container, label := newModeToasterForVimModeTest(t, factory)
888+ expectVimModeToastShown(container, label)
889889 container.EXPECT().RemoveCssClass("toast-custom").Once()
890- container.EXPECT().RemoveCssClass("toast-pane-mode").Once()
890+ container.EXPECT().RemoveCssClass("toast-vim-mode").Once()
891891 container.EXPECT().SetVisible(false).Once()
892892
893893 enabled := entity.RuntimeConfigSnapshot{
@@ -895,26 +895,26 @@ func TestPageMode_RuntimePreferenceDisableHidesPersistentToaster(t *testing.T) {
895895 ModeIndicatorToasterEnabled: true,
896896 }}},
897897 }
898- keyboardHandler := newKeyboardHandlerInPageMode(t)
898+ keyboardHandler := newKeyboardHandlerInVimMode(t)
899899 bw := &browserWindow{id: "win-1", modeToaster: toaster, keyboardHandler: keyboardHandler}
900900 app := &App{
901901 runtimeConfig: runtimeConfigStateFromSnapshotForTest(enabled),
902902 browserWindows: map[string]*browserWindow{bw.id: bw},
903903 }
904904
905- app.updateModeIndicatorToaster(context.Background(), bw, input.ModePage)
905+ app.updateModeIndicatorToaster(context.Background(), bw, input.ModeVim)
906906 require.True(t, toaster.IsVisible())
907907 app.applyRuntimeConfigChange(context.Background(), entity.RuntimeConfigSnapshot{})
908908
909909 assert.False(t, toaster.IsVisible(), "runtime preference disable must reconcile an existing persistent toast")
910910 }
911911
912-func TestPageMode_RuntimePreferenceEnableShowsPersistentToaster(t *testing.T) {
912+func TestVimMode_RuntimePreferenceEnableShowsPersistentToaster(t *testing.T) {
913913 factory := mocks.NewMockWidgetFactory(t)
914- toaster, container, label := newModeToasterForPageModeTest(t, factory)
915- expectPageModeToastShown(container, label)
914+ toaster, container, label := newModeToasterForVimModeTest(t, factory)
915+ expectVimModeToastShown(container, label)
916916
917- keyboardHandler := newKeyboardHandlerInPageMode(t)
917+ keyboardHandler := newKeyboardHandlerInVimMode(t)
918918 bw := &browserWindow{id: "win-1", modeToaster: toaster, keyboardHandler: keyboardHandler}
919919 app := &App{
920920 runtimeConfig: runtimeConfigStateFromSnapshotForTest(entity.RuntimeConfigSnapshot{}),
@@ -928,14 +928,14 @@ func TestPageMode_RuntimePreferenceEnableShowsPersistentToaster(t *testing.T) {
928928
929929 app.applyRuntimeConfigChange(context.Background(), enabled)
930930
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")
932932 }
933933
934934 // ============================================================================
935-// BorderManager — Page mode must NOT use the global border overlay
935+// BorderManager — Vim mode must NOT use the global border overlay
936936 // ============================================================================
937937
938-func TestPageMode_GlobalBorderOverlayStaysOff(t *testing.T) {
938+func TestVimMode_GlobalBorderOverlayStaysOff(t *testing.T) {
939939 mockFactory := mocks.NewMockWidgetFactory(t)
940940 mockBox := mocks.NewMockBoxWidget(t)
941941
@@ -949,113 +949,113 @@ func TestPageMode_GlobalBorderOverlayStaysOff(t *testing.T) {
949949 bm := focus.NewBorderManager(mockFactory)
950950
951951 mockBox.EXPECT().SetVisible(false).Once()
952- bm.OnModeChange(context.Background(), input.ModeNormal, input.ModePage)
952+ bm.OnModeChange(context.Background(), input.ModeNormal, input.ModeVim)
953953
954954 mockBox.EXPECT().SetVisible(false).Once()
955- bm.OnModeChange(context.Background(), input.ModePage, input.ModeNormal)
955+ bm.OnModeChange(context.Background(), input.ModeVim, input.ModeNormal)
956956 }
957957
958958 // ============================================================================
959959 // Pulse Debounce
960960 // ============================================================================
961961
962-func TestPageMode_Pulse_DebounceSkipsRapidConsecutiveCalls(t *testing.T) {
963- f := newSingleWindowPageModeFixture(t)
964- enterPageMode(t, f.app, f.overlay1A, f.bw1)
962+func TestVimMode_Pulse_DebounceSkipsRapidConsecutiveCalls(t *testing.T) {
963+ f := newSingleWindowVimModeFixture(t)
964+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
965965
966966 // Set up expectations for exactly ONE normal pulse.
967967 setUpNormalPulse(f.overlay1A)
968968
969969 // First call fires the pulse (debounce timer is zero).
970- f.app.triggerPageModePulse(context.Background(), false)
970+ f.app.triggerVimModePulse(context.Background(), false)
971971
972972 // Second call within the same frame should be debounced — no
973973 // 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)
975975 }
976976
977-func TestPageMode_Pulse_DebounceAllowsSpacedCalls(t *testing.T) {
977+func TestVimMode_Pulse_DebounceAllowsSpacedCalls(t *testing.T) {
978978 // 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.
980980 // The exit-and-re-enter path is covered by the
981- // TestPageMode_Pulse_DebounceResetOnClearOwnership test below.
981+ // TestVimMode_Pulse_DebounceResetOnClearOwnership test below.
982982
983- f := newSingleWindowPageModeFixture(t)
983+ f := newSingleWindowVimModeFixture(t)
984984
985985 // Fresh app: debounce timer is zero.
986- assert.True(t, f.app.pageModePulseLastTime.IsZero(),
986+ assert.True(t, f.app.vimModePulseLastTime.IsZero(),
987987 "fresh app should have zero pulse timer")
988988
989- enterPageMode(t, f.app, f.overlay1A, f.bw1)
989+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
990990
991991 // Pulse #1 fires because timer is zero (time.Since(zero) is huge).
992992 setUpNormalPulse(f.overlay1A)
993- f.app.triggerPageModePulse(context.Background(), false)
993+ f.app.triggerVimModePulse(context.Background(), false)
994994
995995 // 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(),
997997 "pulse timer should be non-zero after first pulse")
998998 }
999999
1000-func TestPageMode_Pulse_DebounceResetOnClearOwnership(t *testing.T) {
1001- // Verify that clearPageModeOwnership resets the debounce timer
1002- // so a subsequent pulse in a new page mode session is not skipped.
1000+func TestVimMode_Pulse_DebounceResetOnClearOwnership(t *testing.T) {
1001+ // Verify that clearVimModeOwnership resets the debounce timer
1002+ // so a subsequent pulse in a new vim mode session is not skipped.
10031003 // We check the timer state directly rather than triggering a second
10041004 // pulse (which would need pulse-cycle alternation handling).
10051005
1006- f := newSingleWindowPageModeFixture(t)
1007- enterPageMode(t, f.app, f.overlay1A, f.bw1)
1006+ f := newSingleWindowVimModeFixture(t)
1007+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
10081008
10091009 // Pulse #1 fires and sets the timer.
10101010 setUpNormalPulse(f.overlay1A)
1011- f.app.triggerPageModePulse(context.Background(), false)
1012- assert.False(t, f.app.pageModePulseLastTime.IsZero(),
1011+ f.app.triggerVimModePulse(context.Background(), false)
1012+ assert.False(t, f.app.vimModePulseLastTime.IsZero(),
10131013 "pulse timer should be non-zero after first pulse")
10141014
10151015 // Clear ownership — this resets the debounce timer.
1016- expectPageModeAccentHidden(f.overlay1A)
1017- f.app.clearPageModeOwnership(context.Background(), f.bw1)
1018- assert.Empty(t, f.bw1.pageModePaneID)
1016+ expectVimModeAccentHidden(f.overlay1A)
1017+ f.app.clearVimModeOwnership(context.Background(), f.bw1)
1018+ assert.Empty(t, f.bw1.vimModePaneID)
10191019
10201020 // Debounce timer should now be back to zero.
1021- assert.True(t, f.app.pageModePulseLastTime.IsZero(),
1022- "pulse timer should be reset after clearPageModeOwnership")
1021+ assert.True(t, f.app.vimModePulseLastTime.IsZero(),
1022+ "pulse timer should be reset after clearVimModeOwnership")
10231023 }
10241024
1025-func TestPageMode_Pulse_DebounceFirstPulseAlwaysFires(t *testing.T) {
1026- // Even if triggerPageModePulse is called rapidly on a fresh
1027- // App (e.g., first keystroke after page mode entry), the first
1025+func TestVimMode_Pulse_DebounceFirstPulseAlwaysFires(t *testing.T) {
1026+ // Even if triggerVimModePulse is called rapidly on a fresh
1027+ // App (e.g., first keystroke after vim mode entry), the first
10281028 // call must always fire. This is guaranteed by the zero value
10291029 // of time.Time producing a time.Since result much larger than
10301030 // the debounce interval.
1031- f := newSingleWindowPageModeFixture(t)
1032- enterPageMode(t, f.app, f.overlay1A, f.bw1)
1031+ f := newSingleWindowVimModeFixture(t)
1032+ enterVimMode(t, f.app, f.overlay1A, f.bw1)
10331033
10341034 // Rapid double pulse — only first should fire regardless of order.
10351035 // Set up just one set of expectations.
10361036 setUpNormalPulse(f.overlay1A)
1037- f.app.triggerPageModePulse(context.Background(), false)
1038- f.app.triggerPageModePulse(context.Background(), false)
1037+ f.app.triggerVimModePulse(context.Background(), false)
1038+ f.app.triggerVimModePulse(context.Background(), false)
10391039 }
10401040
10411041 // ============================================================================
1042-// UI Scale: Page mode CSS uses relative (em) units
1042+// UI Scale: Vim mode CSS uses relative (em) units
10431043 // ============================================================================
10441044
1045-func TestPageMode_CSSUsesEms(t *testing.T) {
1045+func TestVimMode_CSSUsesEms(t *testing.T) {
10461046 css := theme.GenerateCSS(theme.DefaultDarkPalette())
10471047
10481048 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")
10501050 assert.Contains(t, css, "0.2em",
1051- "page mode overlay pulse glow radius uses em")
1051+ "vim mode overlay pulse glow radius uses em")
10521052 assert.Contains(t, css, "0.26em",
1053- "page mode overlay fast-pulse glow radius uses em")
1054- assert.NotContains(t, css, "page-mode-indicator",
1053+ "vim mode overlay fast-pulse glow radius uses em")
1054+ assert.NotContains(t, css, "vim-mode-indicator",
10551055 "removed PAGE label styling must not remain")
10561056 }
10571057
1058-func TestPageMode_CSSPulseTimingUsesDefaultTransitionDuration(t *testing.T) {
1058+func TestVimMode_CSSPulseTimingUsesDefaultTransitionDuration(t *testing.T) {
10591059 // The pulse animation durations should be derived from the
10601060 // default transition duration (normal = 3x, fast = 6x).
10611061 // Default = 120ms → normal = 360ms, fast = 720ms.
@@ -1067,7 +1067,7 @@ func TestPageMode_CSSPulseTimingUsesDefaultTransitionDuration(t *testing.T) {
10671067 "fast pulse duration should be 6x transition duration (720ms for 120ms base)")
10681068 }
10691069
1070-func TestPageMode_CSSWithCustomTransitionDuration(t *testing.T) {
1070+func TestVimMode_CSSWithCustomTransitionDuration(t *testing.T) {
10711071 // Custom transition duration (e.g., 200ms) should produce
10721072 // adjusted pulse timings: normal = 600ms, fast = 1200ms.
10731073 css := theme.GenerateCSSFullWithTiming(
M internal/ui/browser_window.go +3 -3
@@ -53,8 +53,8 @@ type browserWindow struct {
5353 sidebarVisible bool
5454 activeSidebarKind nativeSidebarKind
5555
56- // pageModePaneID tracks this window's pane-local Page Mode accent/pulse owner.
57- pageModePaneID entity.PaneID
56+ // vimModePaneID tracks this window's pane-local Vim Mode accent/pulse owner.
57+ vimModePaneID entity.PaneID
5858 }
5959
6060 func (bw *browserWindow) detachInputForDestroy() {
@@ -111,7 +111,7 @@ func (bw *browserWindow) clearShellState() {
111111 bw.favoritesSidebar = nil
112112 bw.historySidebarReloader = nil
113113 bw.activeSidebarKind = nativeSidebarNone
114- bw.pageModePaneID = ""
114+ bw.vimModePaneID = ""
115115 }
116116
117117 func (bw *browserWindow) initChrome(ctx context.Context, a *App) {
M internal/ui/component/pane_view.go +35 -35
@@ -15,26 +15,26 @@ const (
1515 // CSS class applied to active pane's border overlay
1616 activePaneClass = "pane-active"
1717
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.
1919 // It produces a subtle local border accent using the pane mode color.
20- pageModeActiveClass = "page-mode-active"
20+ vimModeActiveClass = "vim-mode-active"
2121
22- // pageModePulseClass triggers a normal scroll pulse on the pane overlay.
22+ // vimModePulseClass triggers a normal scroll pulse on the pane overlay.
2323 // Repeated calls must remove-then-add to restart the CSS animation.
24- pageModePulseClass = "page-mode-pulse"
24+ vimModePulseClass = "vim-mode-pulse"
2525
26- // pageModeFastPulseClass triggers a fast/stronger scroll pulse.
27- pageModeFastPulseClass = "page-mode-pulse-fast"
26+ // vimModeFastPulseClass triggers a fast/stronger scroll pulse.
27+ vimModeFastPulseClass = "vim-mode-pulse-fast"
2828
2929 // Alternate equivalent animations so repeated scrolls reliably restart
3030 // the GTK CSS animation.
31- pageModePulseCycleClassA = "page-mode-pulse-cycle-a"
32- pageModePulseCycleClassB = "page-mode-pulse-cycle-b"
31+ vimModePulseCycleClassA = "vim-mode-pulse-cycle-a"
32+ vimModePulseCycleClassB = "vim-mode-pulse-cycle-b"
3333
3434 // Exported aliases for tests and external inspection.
35- PageModeActiveClass = pageModeActiveClass
36- PageModePulseClass = pageModePulseClass
37- PageModeFastPulseClass = pageModeFastPulseClass
35+ VimModeActiveClass = vimModeActiveClass
36+ VimModePulseClass = vimModePulseClass
37+ VimModeFastPulseClass = vimModeFastPulseClass
3838 )
3939
4040 // PaneView is a container for a single WebView with active state indication.
@@ -52,8 +52,8 @@ type PaneView struct {
5252 paneID entity.PaneID
5353 isActive bool
5454
55- // Page Mode pane-local visual state.
56- pageMode bool
55+ // Vim Mode pane-local visual state.
56+ vimMode bool
5757 pulseCycle bool
5858
5959 onFocusIn func(paneID entity.PaneID)
@@ -117,7 +117,7 @@ func NewPaneView(ctx context.Context, factory layout.WidgetFactory, paneID entit
117117 loading: loading,
118118 paneID: paneID,
119119 isActive: false,
120- pageMode: false,
120+ vimMode: false,
121121 }
122122 }
123123
@@ -546,39 +546,39 @@ func (pv *PaneView) HideLinkStatus() {
546546 }
547547 }
548548
549-// SetPageMode activates or deactivates the pane-local Page Mode accent.
550-func (pv *PaneView) SetPageMode(active bool) {
549+// SetVimMode activates or deactivates the pane-local Vim Mode accent.
550+func (pv *PaneView) SetVimMode(active bool) {
551551 pv.mu.Lock()
552552 defer pv.mu.Unlock()
553553
554- if pv.pageMode == active {
554+ if pv.vimMode == active {
555555 return
556556 }
557557
558- pv.pageMode = active
558+ pv.vimMode = active
559559
560560 if active {
561- pv.overlay.AddCssClass(pageModeActiveClass)
561+ pv.overlay.AddCssClass(vimModeActiveClass)
562562 } else {
563- pv.overlay.RemoveCssClass(pageModeActiveClass)
563+ pv.overlay.RemoveCssClass(vimModeActiveClass)
564564 }
565565 }
566566
567-// IsPageMode returns whether Page mode is currently active on this pane.
568-func (pv *PaneView) IsPageMode() bool {
567+// IsVimMode returns whether Vim mode is currently active on this pane.
568+func (pv *PaneView) IsVimMode() bool {
569569 pv.mu.RLock()
570570 defer pv.mu.RUnlock()
571571
572- return pv.pageMode
572+ return pv.vimMode
573573 }
574574
575-// TriggerPageModePulse triggers a normal scroll pulse on the pane overlay.
576-func (pv *PaneView) TriggerPageModePulse() {
575+// TriggerVimModePulse triggers a normal scroll pulse on the pane overlay.
576+func (pv *PaneView) TriggerVimModePulse() {
577577 pv.triggerOverlayPulse(false)
578578 }
579579
580-// TriggerPageModePulseFast triggers a stronger, longer pane-overlay pulse.
581-func (pv *PaneView) TriggerPageModePulseFast() {
580+// TriggerVimModePulseFast triggers a stronger, longer pane-overlay pulse.
581+func (pv *PaneView) TriggerVimModePulseFast() {
582582 pv.triggerOverlayPulse(true)
583583 }
584584
@@ -589,18 +589,18 @@ func (pv *PaneView) triggerOverlayPulse(fast bool) {
589589 pv.mu.Lock()
590590 defer pv.mu.Unlock()
591591
592- pv.overlay.RemoveCssClass(pageModePulseClass)
593- pv.overlay.RemoveCssClass(pageModeFastPulseClass)
594- pv.overlay.RemoveCssClass(pageModePulseCycleClassA)
595- pv.overlay.RemoveCssClass(pageModePulseCycleClassB)
592+ pv.overlay.RemoveCssClass(vimModePulseClass)
593+ pv.overlay.RemoveCssClass(vimModeFastPulseClass)
594+ pv.overlay.RemoveCssClass(vimModePulseCycleClassA)
595+ pv.overlay.RemoveCssClass(vimModePulseCycleClassB)
596596 if fast {
597- pv.overlay.AddCssClass(pageModeFastPulseClass)
597+ pv.overlay.AddCssClass(vimModeFastPulseClass)
598598 } else {
599- pv.overlay.AddCssClass(pageModePulseClass)
599+ pv.overlay.AddCssClass(vimModePulseClass)
600600 }
601- cycle := pageModePulseCycleClassA
601+ cycle := vimModePulseCycleClassA
602602 if pv.pulseCycle {
603- cycle = pageModePulseCycleClassB
603+ cycle = vimModePulseCycleClassB
604604 }
605605 pv.pulseCycle = !pv.pulseCycle
606606 pv.overlay.AddCssClass(cycle)
M internal/ui/component/pane_view_test.go +43 -43
@@ -682,7 +682,7 @@ func setupPaneViewMocksNoWebView(
682682 mockOverlay.EXPECT().SetMeasureOverlay(mockBorderBox, false).Once()
683683 }
684684
685-func TestSetPageMode_TrueAddsPaneAccentWithoutCreatingLabel(t *testing.T) {
685+func TestSetVimMode_TrueAddsPaneAccentWithoutCreatingLabel(t *testing.T) {
686686 mockFactory := mocks.NewMockWidgetFactory(t)
687687 mockOverlay := mocks.NewMockOverlayWidget(t)
688688 mockBorderBox := mocks.NewMockBoxWidget(t)
@@ -690,13 +690,13 @@ func TestSetPageMode_TrueAddsPaneAccentWithoutCreatingLabel(t *testing.T) {
690690 setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
691691 pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
692692
693- mockOverlay.EXPECT().AddCssClass("page-mode-active").Once()
694- pv.SetPageMode(true)
693+ mockOverlay.EXPECT().AddCssClass("vim-mode-active").Once()
694+ pv.SetVimMode(true)
695695
696- require.True(t, pv.IsPageMode())
696+ require.True(t, pv.IsVimMode())
697697 }
698698
699-func TestSetPageMode_FalseRemovesPaneAccent(t *testing.T) {
699+func TestSetVimMode_FalseRemovesPaneAccent(t *testing.T) {
700700 mockFactory := mocks.NewMockWidgetFactory(t)
701701 mockOverlay := mocks.NewMockOverlayWidget(t)
702702 mockBorderBox := mocks.NewMockBoxWidget(t)
@@ -704,15 +704,15 @@ func TestSetPageMode_FalseRemovesPaneAccent(t *testing.T) {
704704 setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
705705 pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
706706
707- mockOverlay.EXPECT().AddCssClass("page-mode-active").Once()
708- pv.SetPageMode(true)
709- mockOverlay.EXPECT().RemoveCssClass("page-mode-active").Once()
710- pv.SetPageMode(false)
707+ mockOverlay.EXPECT().AddCssClass("vim-mode-active").Once()
708+ pv.SetVimMode(true)
709+ mockOverlay.EXPECT().RemoveCssClass("vim-mode-active").Once()
710+ pv.SetVimMode(false)
711711
712- require.False(t, pv.IsPageMode())
712+ require.False(t, pv.IsVimMode())
713713 }
714714
715-func TestSetPageMode_NoChangeWhenSameState(t *testing.T) {
715+func TestSetVimMode_NoChangeWhenSameState(t *testing.T) {
716716 mockFactory := mocks.NewMockWidgetFactory(t)
717717 mockOverlay := mocks.NewMockOverlayWidget(t)
718718 mockBorderBox := mocks.NewMockBoxWidget(t)
@@ -720,12 +720,12 @@ func TestSetPageMode_NoChangeWhenSameState(t *testing.T) {
720720 setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
721721 pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
722722
723- pv.SetPageMode(false)
723+ pv.SetVimMode(false)
724724
725- require.False(t, pv.IsPageMode())
725+ require.False(t, pv.IsVimMode())
726726 }
727727
728-func TestSetPageMode_TrueThenTrueIsIdempotent(t *testing.T) {
728+func TestSetVimMode_TrueThenTrueIsIdempotent(t *testing.T) {
729729 mockFactory := mocks.NewMockWidgetFactory(t)
730730 mockOverlay := mocks.NewMockOverlayWidget(t)
731731 mockBorderBox := mocks.NewMockBoxWidget(t)
@@ -733,27 +733,27 @@ func TestSetPageMode_TrueThenTrueIsIdempotent(t *testing.T) {
733733 setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
734734 pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
735735
736- mockOverlay.EXPECT().AddCssClass("page-mode-active").Once()
737- pv.SetPageMode(true)
738- pv.SetPageMode(true)
736+ mockOverlay.EXPECT().AddCssClass("vim-mode-active").Once()
737+ pv.SetVimMode(true)
738+ pv.SetVimMode(true)
739739
740- require.True(t, pv.IsPageMode())
740+ require.True(t, pv.IsVimMode())
741741 }
742742
743-func expectPageModeOverlayPulse(overlay *mocks.MockOverlayWidget, fast bool, cycle string) {
744- overlay.EXPECT().RemoveCssClass("page-mode-pulse").Once()
745- overlay.EXPECT().RemoveCssClass("page-mode-pulse-fast").Once()
746- overlay.EXPECT().RemoveCssClass("page-mode-pulse-cycle-a").Once()
747- overlay.EXPECT().RemoveCssClass("page-mode-pulse-cycle-b").Once()
743+func expectVimModeOverlayPulse(overlay *mocks.MockOverlayWidget, fast bool, cycle string) {
744+ overlay.EXPECT().RemoveCssClass("vim-mode-pulse").Once()
745+ overlay.EXPECT().RemoveCssClass("vim-mode-pulse-fast").Once()
746+ overlay.EXPECT().RemoveCssClass("vim-mode-pulse-cycle-a").Once()
747+ overlay.EXPECT().RemoveCssClass("vim-mode-pulse-cycle-b").Once()
748748 if fast {
749- overlay.EXPECT().AddCssClass("page-mode-pulse-fast").Once()
749+ overlay.EXPECT().AddCssClass("vim-mode-pulse-fast").Once()
750750 } else {
751- overlay.EXPECT().AddCssClass("page-mode-pulse").Once()
751+ overlay.EXPECT().AddCssClass("vim-mode-pulse").Once()
752752 }
753753 overlay.EXPECT().AddCssClass(cycle).Once()
754754 }
755755
756-func TestTriggerPageModePulse_PulsesOnlyOverlay(t *testing.T) {
756+func TestTriggerVimModePulse_PulsesOnlyOverlay(t *testing.T) {
757757 mockFactory := mocks.NewMockWidgetFactory(t)
758758 mockOverlay := mocks.NewMockOverlayWidget(t)
759759 mockBorderBox := mocks.NewMockBoxWidget(t)
@@ -761,11 +761,11 @@ func TestTriggerPageModePulse_PulsesOnlyOverlay(t *testing.T) {
761761 setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
762762 pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
763763
764- expectPageModeOverlayPulse(mockOverlay, false, "page-mode-pulse-cycle-a")
765- pv.TriggerPageModePulse()
764+ expectVimModeOverlayPulse(mockOverlay, false, "vim-mode-pulse-cycle-a")
765+ pv.TriggerVimModePulse()
766766 }
767767
768-func TestTriggerPageModePulseFast_PulsesOnlyOverlay(t *testing.T) {
768+func TestTriggerVimModePulseFast_PulsesOnlyOverlay(t *testing.T) {
769769 mockFactory := mocks.NewMockWidgetFactory(t)
770770 mockOverlay := mocks.NewMockOverlayWidget(t)
771771 mockBorderBox := mocks.NewMockBoxWidget(t)
@@ -773,11 +773,11 @@ func TestTriggerPageModePulseFast_PulsesOnlyOverlay(t *testing.T) {
773773 setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
774774 pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
775775
776- expectPageModeOverlayPulse(mockOverlay, true, "page-mode-pulse-cycle-a")
777- pv.TriggerPageModePulseFast()
776+ expectVimModeOverlayPulse(mockOverlay, true, "vim-mode-pulse-cycle-a")
777+ pv.TriggerVimModePulseFast()
778778 }
779779
780-func TestTriggerPageModePulse_RepeatedCallsReTriggerOverlayAnimation(t *testing.T) {
780+func TestTriggerVimModePulse_RepeatedCallsReTriggerOverlayAnimation(t *testing.T) {
781781 mockFactory := mocks.NewMockWidgetFactory(t)
782782 mockOverlay := mocks.NewMockOverlayWidget(t)
783783 mockBorderBox := mocks.NewMockBoxWidget(t)
@@ -785,15 +785,15 @@ func TestTriggerPageModePulse_RepeatedCallsReTriggerOverlayAnimation(t *testing.
785785 setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
786786 pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
787787
788- expectPageModeOverlayPulse(mockOverlay, false, "page-mode-pulse-cycle-a")
789- pv.TriggerPageModePulse()
790- expectPageModeOverlayPulse(mockOverlay, true, "page-mode-pulse-cycle-b")
791- pv.TriggerPageModePulseFast()
792- expectPageModeOverlayPulse(mockOverlay, false, "page-mode-pulse-cycle-a")
793- pv.TriggerPageModePulse()
788+ expectVimModeOverlayPulse(mockOverlay, false, "vim-mode-pulse-cycle-a")
789+ pv.TriggerVimModePulse()
790+ expectVimModeOverlayPulse(mockOverlay, true, "vim-mode-pulse-cycle-b")
791+ pv.TriggerVimModePulseFast()
792+ expectVimModeOverlayPulse(mockOverlay, false, "vim-mode-pulse-cycle-a")
793+ pv.TriggerVimModePulse()
794794 }
795795
796-func TestPageMode_CleanupHasNoIndicatorOverlay(t *testing.T) {
796+func TestVimMode_CleanupHasNoIndicatorOverlay(t *testing.T) {
797797 mockFactory := mocks.NewMockWidgetFactory(t)
798798 mockOverlay := mocks.NewMockOverlayWidget(t)
799799 mockBorderBox := mocks.NewMockBoxWidget(t)
@@ -801,15 +801,15 @@ func TestPageMode_CleanupHasNoIndicatorOverlay(t *testing.T) {
801801 setupPaneViewMocks(t, mockFactory, mockOverlay, mockBorderBox, mockWebView)
802802 pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
803803
804- mockOverlay.EXPECT().AddCssClass("page-mode-active").Once()
805- pv.SetPageMode(true)
804+ mockOverlay.EXPECT().AddCssClass("vim-mode-active").Once()
805+ pv.SetVimMode(true)
806806 mockOverlay.EXPECT().RemoveOverlay(mock.Anything).Once()
807807 mockOverlay.EXPECT().SetChild(nil).Once()
808808
809809 pv.Cleanup()
810810 }
811811
812-func TestPageMode_NewPaneViewInactiveByDefault(t *testing.T) {
812+func TestVimMode_NewPaneViewInactiveByDefault(t *testing.T) {
813813 mockFactory := mocks.NewMockWidgetFactory(t)
814814 mockOverlay := mocks.NewMockOverlayWidget(t)
815815 mockBorderBox := mocks.NewMockBoxWidget(t)
@@ -818,5 +818,5 @@ func TestPageMode_NewPaneViewInactiveByDefault(t *testing.T) {
818818
819819 pv := component.NewPaneView(context.Background(), mockFactory, entity.PaneID("pane-1"), mockWebView)
820820
821- require.False(t, pv.IsPageMode())
821+ require.False(t, pv.IsVimMode())
822822 }
M internal/ui/coordinator/navigation.go +1 -1
@@ -304,7 +304,7 @@ func (c *NavigationCoordinator) ScrollWebView(ctx context.Context, wv port.WebVi
304304 return c.pageScrollUC.Scroll(ctx, wv, cmd)
305305 }
306306
307-// ScrollWebViewContinuous applies one autonomous held-key page-scroll step.
307+// ScrollWebViewContinuous applies one autonomous held-key vim-scroll step.
308308 func (c *NavigationCoordinator) ScrollWebViewContinuous(ctx context.Context, wv port.WebView, cmd usecase.PageScrollCommand) error {
309309 if err := requireWebView(wv); err != nil {
310310 return err
M internal/ui/dispatcher/keyboard.go +37 -37
@@ -52,7 +52,7 @@ type KeyboardDispatcher struct {
5252 onMovePaneToTab func(ctx context.Context, paneID entity.PaneID) error
5353 onMovePaneToNext func(ctx context.Context, paneID entity.PaneID) error
5454 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)
5656 onToggleHistorySidebar func(ctx context.Context) error
5757 onToggleFavoritesSidebar func(ctx context.Context) error
5858 onToggleCurrentFavorite func(ctx context.Context) error
@@ -157,19 +157,19 @@ func (d *KeyboardDispatcher) SetOnOpenFloatingTarget(fn func(ctx context.Context
157157 d.onOpenFloating = fn
158158 }
159159
160-// SetOnPageModePulse registers a callback for pane-local Page mode pulse
160+// SetOnVimModePulse registers a callback for pane-local Vim mode pulse
161161 // visual feedback triggered after a page scroll action.
162162 // The cb receives a bool indicating whether the pulse should be "fast"
163163 // (stronger/longer for up-fast/down-fast).
164164 // If nil, pulse callbacks are no-ops.
165-func (d *KeyboardDispatcher) SetOnPageModePulse(cb func(ctx context.Context, fast bool)) {
166- d.onPageModePulse = cb
165+func (d *KeyboardDispatcher) SetOnVimModePulse(cb func(ctx context.Context, fast bool)) {
166+ d.onVimModePulse = cb
167167 }
168168
169169 // triggerPulse calls the registered pulse callback if set, or no-ops otherwise.
170170 func (d *KeyboardDispatcher) triggerPulse(ctx context.Context, fast bool) {
171- if d.onPageModePulse != nil {
172- d.onPageModePulse(ctx, fast)
171+ if d.onVimModePulse != nil {
172+ d.onVimModePulse(ctx, fast)
173173 }
174174 }
175175
@@ -266,13 +266,13 @@ func (d *KeyboardDispatcher) initActionHandlers() {
266266 input.ActionZoomIn: func(ctx context.Context) error { return d.handleZoom(ctx, "in") },
267267 input.ActionZoomOut: func(ctx context.Context) error { return d.handleZoom(ctx, "out") },
268268 input.ActionZoomReset: func(ctx context.Context) error { return d.handleZoom(ctx, "reset") },
269- // Page mode scroll actions — single mapping drives tap pulse + command.
270- input.ActionPageScrollLeft: d.pageScrollTapHandler(input.ActionPageScrollLeft),
271- input.ActionPageScrollRight: d.pageScrollTapHandler(input.ActionPageScrollRight),
272- input.ActionPageScrollUp: d.pageScrollTapHandler(input.ActionPageScrollUp),
273- input.ActionPageScrollDown: d.pageScrollTapHandler(input.ActionPageScrollDown),
274- input.ActionPageScrollUpFast: d.pageScrollTapHandler(input.ActionPageScrollUpFast),
275- input.ActionPageScrollDownFast: d.pageScrollTapHandler(input.ActionPageScrollDownFast),
269+ // Vim mode scroll actions — single mapping drives tap pulse + command.
270+ input.ActionVimScrollLeft: d.vimScrollTapHandler(input.ActionVimScrollLeft),
271+ input.ActionVimScrollRight: d.vimScrollTapHandler(input.ActionVimScrollRight),
272+ input.ActionVimScrollUp: d.vimScrollTapHandler(input.ActionVimScrollUp),
273+ input.ActionVimScrollDown: d.vimScrollTapHandler(input.ActionVimScrollDown),
274+ input.ActionVimScrollUpFast: d.vimScrollTapHandler(input.ActionVimScrollUpFast),
275+ input.ActionVimScrollDownFast: d.vimScrollTapHandler(input.ActionVimScrollDownFast),
276276 // UI
277277 input.ActionOpenOmnibox: d.navCoord.OpenOmnibox,
278278 input.ActionOpenFind: d.handleFindOpen,
@@ -322,50 +322,50 @@ func (d *KeyboardDispatcher) initActionHandlers() {
322322 }
323323 }
324324
325-// DispatchPageScrollLifecycle routes autonomous held-key page scroll events.
325+// DispatchVimScrollLifecycle routes autonomous held-key page scroll events.
326326 // Stop deliberately avoids the normal action map so it emits neither another
327327 // scroll tap nor visual pulse.
328-func (d *KeyboardDispatcher) DispatchPageScrollLifecycle(ctx context.Context, action input.Action, phase input.PageScrollPhase) error {
329- spec, ok := pageScrollSpec(action)
328+func (d *KeyboardDispatcher) DispatchVimScrollLifecycle(ctx context.Context, action input.Action, phase input.VimScrollPhase) error {
329+ spec, ok := vimScrollSpec(action)
330330 if !ok {
331331 return nil
332332 }
333333 return d.withActiveWebView(ctx, "page scroll lifecycle", func(wv port.WebView) error {
334- if phase == input.PageScrollStop {
334+ if phase == input.VimScrollStop {
335335 return d.navCoord.StopPageScroll(ctx, wv)
336336 }
337337 return d.navCoord.ScrollWebViewContinuous(ctx, wv, spec.cmd)
338338 })
339339 }
340340
341-type pageScrollActionSpec struct {
341+type vimScrollActionSpec struct {
342342 cmd usecase.PageScrollCommand
343343 fast bool
344344 }
345345
346-// pageScrollSpec is the single source of truth for Page Mode action→command
346+// vimScrollSpec is the single source of truth for Vim Mode action→command
347347 // 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) {
349349 switch action {
350- case input.ActionPageScrollLeft:
351- return pageScrollActionSpec{cmd: usecase.PageScrollLeft, fast: false}, true
352- case input.ActionPageScrollRight:
353- return pageScrollActionSpec{cmd: usecase.PageScrollRight, fast: false}, true
354- case input.ActionPageScrollUp:
355- return pageScrollActionSpec{cmd: usecase.PageScrollUp, fast: false}, true
356- case input.ActionPageScrollDown:
357- return pageScrollActionSpec{cmd: usecase.PageScrollDown, fast: false}, true
358- case input.ActionPageScrollUpFast:
359- return pageScrollActionSpec{cmd: usecase.PageScrollUpFast, fast: true}, true
360- case input.ActionPageScrollDownFast:
361- return pageScrollActionSpec{cmd: usecase.PageScrollDownFast, fast: true}, true
350+ case input.ActionVimScrollLeft:
351+ return vimScrollActionSpec{cmd: usecase.PageScrollLeft, fast: false}, true
352+ case input.ActionVimScrollRight:
353+ return vimScrollActionSpec{cmd: usecase.PageScrollRight, fast: false}, true
354+ case input.ActionVimScrollUp:
355+ return vimScrollActionSpec{cmd: usecase.PageScrollUp, fast: false}, true
356+ case input.ActionVimScrollDown:
357+ return vimScrollActionSpec{cmd: usecase.PageScrollDown, fast: false}, true
358+ case input.ActionVimScrollUpFast:
359+ return vimScrollActionSpec{cmd: usecase.PageScrollUpFast, fast: true}, true
360+ case input.ActionVimScrollDownFast:
361+ return vimScrollActionSpec{cmd: usecase.PageScrollDownFast, fast: true}, true
362362 default:
363- return pageScrollActionSpec{}, false
363+ return vimScrollActionSpec{}, false
364364 }
365365 }
366366
367-func (d *KeyboardDispatcher) pageScrollTapHandler(action input.Action) func(context.Context) error {
368- spec, ok := pageScrollSpec(action)
367+func (d *KeyboardDispatcher) vimScrollTapHandler(action input.Action) func(context.Context) error {
368+ spec, ok := vimScrollSpec(action)
369369 if !ok {
370370 return func(context.Context) error { return nil }
371371 }
@@ -381,8 +381,8 @@ func (d *KeyboardDispatcher) pageScrollTapHandler(action input.Action) func(cont
381381 }
382382 }
383383
384-func pageScrollCommand(action input.Action) (usecase.PageScrollCommand, bool) {
385- spec, ok := pageScrollSpec(action)
384+func vimScrollCommand(action input.Action) (usecase.PageScrollCommand, bool) {
385+ spec, ok := vimScrollSpec(action)
386386 if !ok {
387387 return 0, false
388388 }
M internal/ui/dispatcher/keyboard_test.go +28 -28
@@ -220,7 +220,7 @@ func (wv *lifecycleScrollableWebView) CancelPageScroll(context.Context) {
220220 wv.cancelCalls++
221221 }
222222
223-func TestKeyboardDispatcher_PageModeActionsRouteToCorrectScrollCommand(t *testing.T) {
223+func TestKeyboardDispatcher_VimModeActionsRouteToCorrectScrollCommand(t *testing.T) {
224224 ctx := context.Background()
225225
226226 tests := []struct {
@@ -229,12 +229,12 @@ func TestKeyboardDispatcher_PageModeActionsRouteToCorrectScrollCommand(t *testin
229229 expectedDx int
230230 expectedDy int
231231 }{
232- {input.ActionPageScrollLeft, usecase.PageScrollLeft, -80, 0},
233- {input.ActionPageScrollRight, usecase.PageScrollRight, 80, 0},
234- {input.ActionPageScrollUp, usecase.PageScrollUp, 0, -80},
235- {input.ActionPageScrollDown, usecase.PageScrollDown, 0, 80},
236- {input.ActionPageScrollUpFast, usecase.PageScrollUpFast, 0, -320},
237- {input.ActionPageScrollDownFast, usecase.PageScrollDownFast, 0, 320},
232+ {input.ActionVimScrollLeft, usecase.PageScrollLeft, -80, 0},
233+ {input.ActionVimScrollRight, usecase.PageScrollRight, 80, 0},
234+ {input.ActionVimScrollUp, usecase.PageScrollUp, 0, -80},
235+ {input.ActionVimScrollDown, usecase.PageScrollDown, 0, 80},
236+ {input.ActionVimScrollUpFast, usecase.PageScrollUpFast, 0, -320},
237+ {input.ActionVimScrollDownFast, usecase.PageScrollDownFast, 0, 320},
238238 }
239239
240240 for _, tc := range tests {
@@ -268,7 +268,7 @@ func TestKeyboardDispatcher_PageModeActionsRouteToCorrectScrollCommand(t *testin
268268 }
269269 }
270270
271-func TestKeyboardDispatcher_PageScrollLifecycleRoutesContinuousAndStop(t *testing.T) {
271+func TestKeyboardDispatcher_VimScrollLifecycleRoutesContinuousAndStop(t *testing.T) {
272272 ctx := context.Background()
273273 base := mocks.NewMockWebView(t)
274274 scroller := mocks.NewMockPageScrollable(t)
@@ -282,12 +282,12 @@ func TestKeyboardDispatcher_PageScrollLifecycleRoutesContinuousAndStop(t *testin
282282 scroller.EXPECT().ScrollPage(ctx, port.PageScrollRequest{
283283 Command: port.PageScrollCommandDown, FallbackDY: 80, Continuous: true,
284284 }).Return(nil).Once()
285- require.NoError(t, d.DispatchPageScrollLifecycle(ctx, input.ActionPageScrollDown, input.PageScrollContinuous))
286- require.NoError(t, d.DispatchPageScrollLifecycle(ctx, input.ActionPageScrollDown, input.PageScrollStop))
285+ require.NoError(t, d.DispatchVimScrollLifecycle(ctx, input.ActionVimScrollDown, input.VimScrollContinuous))
286+ require.NoError(t, d.DispatchVimScrollLifecycle(ctx, input.ActionVimScrollDown, input.VimScrollStop))
287287 assert.Equal(t, 1, wv.cancelCalls)
288288 }
289289
290-func TestKeyboardDispatcher_PageModeNoopWhenNoActiveWebView(t *testing.T) {
290+func TestKeyboardDispatcher_VimModeNoopWhenNoActiveWebView(t *testing.T) {
291291 ctx := context.Background()
292292
293293 navCoord := &coordinator.NavigationCoordinator{}
@@ -304,11 +304,11 @@ func TestKeyboardDispatcher_PageModeNoopWhenNoActiveWebView(t *testing.T) {
304304 )
305305
306306 // No ActiveWebView set — dispatcher should no-op cleanly
307- err := d.Dispatch(ctx, input.ActionPageScrollDown)
307+ err := d.Dispatch(ctx, input.ActionVimScrollDown)
308308 require.NoError(t, err)
309309 }
310310
311-func TestKeyboardDispatcher_PageModeNoopWhenActiveWebViewReturnsNil(t *testing.T) {
311+func TestKeyboardDispatcher_VimModeNoopWhenActiveWebViewReturnsNil(t *testing.T) {
312312 ctx := context.Background()
313313
314314 navCoord := &coordinator.NavigationCoordinator{}
@@ -326,37 +326,37 @@ func TestKeyboardDispatcher_PageModeNoopWhenActiveWebViewReturnsNil(t *testing.T
326326 func(context.Context) entity.PaneID { return "" },
327327 )
328328
329- err := d.Dispatch(ctx, input.ActionPageScrollDown)
329+ err := d.Dispatch(ctx, input.ActionVimScrollDown)
330330 require.NoError(t, err)
331331 }
332332
333-func TestPageScrollSpec_IsSingleSourceForCommandAndPulse(t *testing.T) {
333+func TestVimScrollSpec_IsSingleSourceForCommandAndPulse(t *testing.T) {
334334 tests := []struct {
335335 action input.Action
336336 cmd usecase.PageScrollCommand
337337 fast bool
338338 }{
339- {input.ActionPageScrollLeft, usecase.PageScrollLeft, false},
340- {input.ActionPageScrollRight, usecase.PageScrollRight, false},
341- {input.ActionPageScrollUp, usecase.PageScrollUp, false},
342- {input.ActionPageScrollDown, usecase.PageScrollDown, false},
343- {input.ActionPageScrollUpFast, usecase.PageScrollUpFast, true},
344- {input.ActionPageScrollDownFast, usecase.PageScrollDownFast, true},
339+ {input.ActionVimScrollLeft, usecase.PageScrollLeft, false},
340+ {input.ActionVimScrollRight, usecase.PageScrollRight, false},
341+ {input.ActionVimScrollUp, usecase.PageScrollUp, false},
342+ {input.ActionVimScrollDown, usecase.PageScrollDown, false},
343+ {input.ActionVimScrollUpFast, usecase.PageScrollUpFast, true},
344+ {input.ActionVimScrollDownFast, usecase.PageScrollDownFast, true},
345345 }
346346 for _, tc := range tests {
347- spec, ok := pageScrollSpec(tc.action)
347+ spec, ok := vimScrollSpec(tc.action)
348348 require.True(t, ok, string(tc.action))
349349 assert.Equal(t, tc.cmd, spec.cmd)
350350 assert.Equal(t, tc.fast, spec.fast)
351- cmd, ok := pageScrollCommand(tc.action)
351+ cmd, ok := vimScrollCommand(tc.action)
352352 require.True(t, ok)
353353 assert.Equal(t, tc.cmd, cmd)
354354 }
355- _, ok := pageScrollSpec(input.ActionQuit)
355+ _, ok := vimScrollSpec(input.ActionQuit)
356356 assert.False(t, ok)
357357 }
358358
359-func TestKeyboardDispatcher_PageScrollTapUsesSharedSpecPulse(t *testing.T) {
359+func TestKeyboardDispatcher_VimScrollTapUsesSharedSpecPulse(t *testing.T) {
360360 ctx := context.Background()
361361 base := mocks.NewMockWebView(t)
362362 scroller := mocks.NewMockPageScrollable(t)
@@ -368,7 +368,7 @@ func TestKeyboardDispatcher_PageScrollTapUsesSharedSpecPulse(t *testing.T) {
368368 }, func(context.Context) entity.PaneID { return "" })
369369
370370 var pulses []bool
371- d.SetOnPageModePulse(func(_ context.Context, fast bool) {
371+ d.SetOnVimModePulse(func(_ context.Context, fast bool) {
372372 pulses = append(pulses, fast)
373373 })
374374
@@ -380,7 +380,7 @@ func TestKeyboardDispatcher_PageScrollTapUsesSharedSpecPulse(t *testing.T) {
380380 Command: port.PageScrollCommandDownFast, FallbackDY: 320,
381381 }).Return(nil).Once()
382382
383- require.NoError(t, d.Dispatch(ctx, input.ActionPageScrollDown))
384- require.NoError(t, d.Dispatch(ctx, input.ActionPageScrollDownFast))
383+ require.NoError(t, d.Dispatch(ctx, input.ActionVimScrollDown))
384+ require.NoError(t, d.Dispatch(ctx, input.ActionVimScrollDownFast))
385385 assert.Equal(t, []bool{false, true}, pulses)
386386 }
M internal/ui/focus/borders.go +6 -6
@@ -61,8 +61,8 @@ func (bm *BorderManager) OnModeChange(ctx context.Context, from, to input.Mode)
6161 bm.borderOverlay.RemoveCssClass(tabModeClass)
6262 case input.ModeSession:
6363 bm.borderOverlay.RemoveCssClass(sessionModeClass)
64- case input.ModePage:
65- // Page mode never activates the global border overlay.
64+ case input.ModeVim:
65+ // Vim mode never activates the global border overlay.
6666 // No class to remove.
6767 }
6868
@@ -77,10 +77,10 @@ func (bm *BorderManager) OnModeChange(ctx context.Context, from, to input.Mode)
7777 case input.ModeSession:
7878 bm.borderOverlay.AddCssClass(sessionModeClass)
7979 bm.borderOverlay.SetVisible(true)
80- case input.ModePage:
81- // Page mode must not activate the workspace-wide border overlay.
82- // The pane-local page-mode-active CSS class and indicator are
83- // managed by PaneView.SetPageMode / app handleModeChange.
80+ case input.ModeVim:
81+ // Vim mode must not activate the workspace-wide border overlay.
82+ // The pane-local vim-mode-active CSS class and indicator are
83+ // managed by PaneView.SetVimMode / app handleModeChange.
8484 bm.borderOverlay.SetVisible(false)
8585 case input.ModeNormal:
8686 bm.borderOverlay.SetVisible(false)
M internal/ui/input/global_shortcuts.go +4 -4
@@ -114,7 +114,7 @@ func NewGlobalShortcutHandler(
114114
115115 // Register app-reserved shortcuts (Ctrl+L, Ctrl+F, other mode activations, etc.)
116116 // so they work even when WebView has focus.
117- // Page mode activation is intentionally excluded here: it must flow through
117+ // Vim mode activation is intentionally excluded here: it must flow through
118118 // the normal KeyboardHandler path so editable page contexts can pass Ctrl+Y
119119 // through instead of having a global shortcut controller consume it first.
120120 shortcuts := NewShortcutSet(ctx, workspace, session)
@@ -496,11 +496,11 @@ func appendGlobalShortcutEventFields(evt *zerolog.Event, binding KeyBinding, act
496496 }
497497
498498 func shouldRegisterGTKGlobalShortcut(action Action) bool {
499- return action != ActionEnterPageMode
499+ return action != ActionEnterVimMode
500500 }
501501
502502 func shouldIgnoreGlobalShortcutInMode(mode Mode, action Action) bool {
503- return mode == ModePage && action != ActionEnterPageMode
503+ return mode == ModeVim && action != ActionEnterVimMode
504504 }
505505
506506 func globalShortcutActionMap() map[string]Action {
@@ -840,7 +840,7 @@ func formatEventType(eventType gdk.EventType) string {
840840
841841 func isModeAction(action Action) bool {
842842 switch action {
843- case ActionEnterTabMode, ActionEnterPaneMode, ActionEnterSessionMode, ActionEnterResizeMode, ActionEnterPageMode, ActionExitMode:
843+ case ActionEnterTabMode, ActionEnterPaneMode, ActionEnterSessionMode, ActionEnterResizeMode, ActionEnterVimMode, ActionExitMode:
844844 return true
845845 default:
846846 return false
M internal/ui/input/global_shortcuts_test.go +9 -9
@@ -10,24 +10,24 @@ import (
1010 "github.com/bnema/puregotk/v4/gtk"
1111 )
1212
13-func TestShouldRegisterGTKGlobalShortcut_ExcludesPageModeActivation(t *testing.T) {
14- if shouldRegisterGTKGlobalShortcut(ActionEnterPageMode) {
15- t.Fatal("ActionEnterPageMode should stay off the GTK global shortcut controller")
13+func TestShouldRegisterGTKGlobalShortcut_ExcludesVimModeActivation(t *testing.T) {
14+ if shouldRegisterGTKGlobalShortcut(ActionEnterVimMode) {
15+ t.Fatal("ActionEnterVimMode should stay off the GTK global shortcut controller")
1616 }
1717 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")
1919 }
2020 }
2121
22-func TestShouldIgnoreGlobalShortcutInMode_PageModeBlocksNonToggleActions(t *testing.T) {
23- if !shouldIgnoreGlobalShortcutInMode(ModePage, ActionOpenOmnibox) {
24- t.Fatal("Page mode should ignore app-global shortcuts like omnibox")
22+func TestShouldIgnoreGlobalShortcutInMode_VimModeBlocksNonToggleActions(t *testing.T) {
23+ if !shouldIgnoreGlobalShortcutInMode(ModeVim, ActionOpenOmnibox) {
24+ t.Fatal("Vim mode should ignore app-global shortcuts like omnibox")
2525 }
2626 if shouldIgnoreGlobalShortcutInMode(ModeNormal, ActionOpenOmnibox) {
2727 t.Fatal("normal mode should not ignore app-global shortcuts")
2828 }
29- if shouldIgnoreGlobalShortcutInMode(ModePage, ActionEnterPageMode) {
30- t.Fatal("page mode toggle should not be blocked by the mode guard")
29+ if shouldIgnoreGlobalShortcutInMode(ModeVim, ActionEnterVimMode) {
30+ t.Fatal("vim mode toggle should not be blocked by the mode guard")
3131 }
3232 }
3333
M internal/ui/input/keyboard.go +116 -116
@@ -36,22 +36,22 @@ import (
3636 // Return an error if the action fails.
3737 type ActionHandler func(ctx context.Context, action Action) error
3838
39-// PageScrollPhase identifies held-scroll lifecycle events. It is internal input
39+// VimScrollPhase identifies held-scroll lifecycle events. It is internal input
4040 // state, not a configurable action or shortcut.
41-type PageScrollPhase uint8
41+type VimScrollPhase uint8
4242
4343 const (
44- PageScrollContinuous PageScrollPhase = iota
45- PageScrollStop
44+ VimScrollContinuous VimScrollPhase = iota
45+ VimScrollStop
4646 )
4747
48-// PageScrollLifecycleHandler receives autonomous held-scroll ticks and the
48+// VimScrollLifecycleHandler receives autonomous held-scroll ticks and the
4949 // matching stop event without changing the normal configurable action API.
50-type PageScrollLifecycleHandler func(ctx context.Context, action Action, phase PageScrollPhase) error
50+type VimScrollLifecycleHandler func(ctx context.Context, action Action, phase VimScrollPhase) error
5151
5252 const (
53- pageScrollHoldDelay = 250 * time.Millisecond
54- pageScrollCadence = 16 * time.Millisecond
53+ vimScrollHoldDelay = 250 * time.Millisecond
54+ vimScrollCadence = 16 * time.Millisecond
5555 )
5656
5757 // AccentHandler handles long-press accent detection.
@@ -77,8 +77,8 @@ type KeyboardHandler struct {
7777 session *entity.SessionConfig
7878
7979 // Action handler callbacks.
80- onAction ActionHandler
81- onPageScrollLifecycle PageScrollLifecycleHandler
80+ onAction ActionHandler
81+ onVimScrollLifecycle VimScrollLifecycleHandler
8282 // Optional routing callback that determines how a key should be handled.
8383 // Returns RouteHandleShortcuts (default), RoutePassToWidget (let focused
8484 // widget handle it), or RouteAccentDetection (long-press accent for GTK entries).
@@ -87,9 +87,9 @@ type KeyboardHandler struct {
8787 accentHandler AccentHandler
8888 // Optional escape hook for app-level overlays
8989 onEscape func(ctx context.Context) bool
90- // Optional hook that blocks Page mode activation and lets the original key
90+ // Optional hook that blocks Vim mode activation and lets the original key
9191 // event pass through to the focused widget/page instead.
92- pageModeActivationPassthrough func() bool
92+ vimModeActivationPassthrough func() bool
9393
9494 // GTK controller (nil until attached)
9595 controller *gtk.EventControllerKey
@@ -105,15 +105,15 @@ type KeyboardHandler struct {
105105 ctx context.Context
106106 mu sync.RWMutex
107107
108- pageScrollRepeatAction Action
109- pageScrollRepeatKeyval uint
110- pageScrollRepeatTimer uint
111- pageScrollRepeatGeneration uint64
112- pageScrollRepeatPressedAt time.Time
113- pageScrollRepeatCb glib.SourceFunc
114- pageScrollRepeatAdd func(intervalMS uint, cb *glib.SourceFunc) uint
115- pageScrollRepeatRemove func(id uint) bool
116- pageScrollNow func() time.Time
108+ vimScrollRepeatAction Action
109+ vimScrollRepeatKeyval uint
110+ vimScrollRepeatTimer uint
111+ vimScrollRepeatGeneration uint64
112+ vimScrollRepeatPressedAt time.Time
113+ vimScrollRepeatCb glib.SourceFunc
114+ vimScrollRepeatAdd func(intervalMS uint, cb *glib.SourceFunc) uint
115+ vimScrollRepeatRemove func(id uint) bool
116+ vimScrollNow func() time.Time
117117 }
118118
119119 // NewKeyboardHandler creates a new keyboard handler.
@@ -129,11 +129,11 @@ func NewKeyboardHandler(ctx context.Context, workspace *entity.WorkspaceConfig,
129129 session: session,
130130 activePressedActions: make(map[Action]uint),
131131 ctx: ctx,
132- pageScrollRepeatAdd: func(intervalMS uint, cb *glib.SourceFunc) uint {
132+ vimScrollRepeatAdd: func(intervalMS uint, cb *glib.SourceFunc) uint {
133133 return glib.TimeoutAdd(intervalMS, cb, 0)
134134 },
135- pageScrollRepeatRemove: glib.SourceRemove,
136- pageScrollNow: time.Now,
135+ vimScrollRepeatRemove: glib.SourceRemove,
136+ vimScrollNow: time.Now,
137137 }
138138 h.SetOnModeChange(nil)
139139
@@ -147,26 +147,26 @@ func (h *KeyboardHandler) SetOnAction(fn ActionHandler) {
147147 h.onAction = fn
148148 }
149149
150-// SetOnPageScrollLifecycle sets the callback for autonomous held-scroll ticks
150+// SetOnVimScrollLifecycle sets the callback for autonomous held-scroll ticks
151151 // and stop notification. Tap actions continue through SetOnAction.
152-func (h *KeyboardHandler) SetOnPageScrollLifecycle(fn PageScrollLifecycleHandler) {
152+func (h *KeyboardHandler) SetOnVimScrollLifecycle(fn VimScrollLifecycleHandler) {
153153 h.mu.Lock()
154154 defer h.mu.Unlock()
155- h.onPageScrollLifecycle = fn
155+ h.onVimScrollLifecycle = fn
156156 }
157157
158-// SetPageModeActivationPassthrough sets the callback that can block Page mode
158+// SetVimModeActivationPassthrough sets the callback that can block Vim mode
159159 // 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) {
161161 h.mu.Lock()
162162 defer h.mu.Unlock()
163- h.pageModeActivationPassthrough = fn
163+ h.vimModeActivationPassthrough = fn
164164 }
165165
166166 // ReloadShortcuts rebuilds the shortcut set from new config values.
167167 // This enables hot-reloading of keybindings without restarting.
168168 func (h *KeyboardHandler) ReloadShortcuts(ctx context.Context, workspace *entity.WorkspaceConfig, session *entity.SessionConfig) {
169- h.stopPageScrollRepeat()
169+ h.stopVimScrollRepeat()
170170 h.mu.Lock()
171171 defer h.mu.Unlock()
172172
@@ -189,8 +189,8 @@ func (h *KeyboardHandler) SetOnModeChange(fn func(from, to Mode)) {
189189 } else if from == ModeNormal {
190190 h.setControllerPhase(gtk.PhaseCaptureValue)
191191 }
192- if to != ModePage {
193- h.stopPageScrollRepeat()
192+ if to != ModeVim {
193+ h.stopVimScrollRepeat()
194194 }
195195 // Forward to app-level callback
196196 if fn != nil {
@@ -315,7 +315,7 @@ func (h *KeyboardHandler) DetachForDestroy() {
315315 }
316316
317317 func (h *KeyboardHandler) detach(removeController bool) {
318- h.stopPageScrollRepeat()
318+ h.stopVimScrollRepeat()
319319 h.mu.Lock()
320320 if h.controller != nil && h.keyPressedHandlerID != 0 {
321321 h.controller.DisconnectSignal(h.keyPressedHandlerID)
@@ -367,8 +367,8 @@ func (h *KeyboardHandler) handleKeyPress(keyval, keycode uint, state gdk.Modifie
367367 if mode != ModeNormal && modifiers == 0 {
368368 switch keyval {
369369 case uint(gdk.KEY_Escape), uint(gdk.KEY_Return), uint(gdk.KEY_KP_Enter):
370- if mode == ModePage {
371- h.stopPageScrollRepeat()
370+ if mode == ModeVim {
371+ h.stopVimScrollRepeat()
372372 }
373373 h.modal.ExitMode(h.ctx)
374374 return true
@@ -424,17 +424,17 @@ func (h *KeyboardHandler) handleShortcutLookupResult(
424424 modifiers Modifier,
425425 ) bool {
426426 if !found {
427- if shouldPassthroughNativePageModeNavigation(mode, keyval, modifiers) {
428- log.Trace().Uint("keyval", keyval).Msg("routing native page navigation key to focused widget in page mode")
427+ if shouldPassthroughNativeVimModeNavigation(mode, keyval, modifiers) {
428+ log.Trace().Uint("keyval", keyval).Msg("routing native page navigation key to focused widget in vim mode")
429429 return false
430430 }
431431 return mode != ModeNormal // Consume unrecognized keys in modal mode
432432 }
433- if h.shouldPassthroughPageModeActivation(action, mode) {
433+ if h.shouldPassthroughVimModeActivation(action, mode) {
434434 return false
435435 }
436- if mode == ModePage && isPageScrollAction(action) {
437- return h.handlePageScrollAction(log, action, mode, keyval)
436+ if mode == ModeVim && isVimScrollAction(action) {
437+ return h.handleVimScrollAction(log, action, mode, keyval)
438438 }
439439 if h.suppressHeldAction(action, keyval) {
440440 log.Trace().
@@ -445,105 +445,105 @@ func (h *KeyboardHandler) handleShortcutLookupResult(
445445 return h.dispatchAction(action, mode)
446446 }
447447
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 {
449449 h.mu.RLock()
450- sameHeldKey := h.pageScrollRepeatAction == action &&
451- h.pageScrollRepeatKeyval == keyval && h.pageScrollRepeatTimer != 0
450+ sameHeldKey := h.vimScrollRepeatAction == action &&
451+ h.vimScrollRepeatKeyval == keyval && h.vimScrollRepeatTimer != 0
452452 h.mu.RUnlock()
453453 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")
455455 return true
456456 }
457457
458- h.stopPageScrollRepeat()
458+ h.stopVimScrollRepeat()
459459 if !h.dispatchAction(action, mode) {
460460 return false
461461 }
462- h.startPageScrollRepeat(action, keyval)
462+ h.startVimScrollRepeat(action, keyval)
463463 return true
464464 }
465465
466-func (h *KeyboardHandler) startPageScrollRepeat(action Action, keyval uint) {
466+func (h *KeyboardHandler) startVimScrollRepeat(action Action, keyval uint) {
467467 h.mu.Lock()
468- if h.pageScrollRepeatTimer != 0 || h.pageScrollRepeatAdd == nil {
468+ if h.vimScrollRepeatTimer != 0 || h.vimScrollRepeatAdd == nil {
469469 h.mu.Unlock()
470470 return
471471 }
472- h.pageScrollRepeatGeneration++
473- generation := h.pageScrollRepeatGeneration
474- h.pageScrollRepeatAction = action
475- h.pageScrollRepeatKeyval = keyval
476- h.pageScrollRepeatPressedAt = h.pageScrollNow()
472+ h.vimScrollRepeatGeneration++
473+ generation := h.vimScrollRepeatGeneration
474+ h.vimScrollRepeatAction = action
475+ h.vimScrollRepeatKeyval = keyval
476+ h.vimScrollRepeatPressedAt = h.vimScrollNow()
477477 cb := glib.SourceFunc(func(_ uintptr) bool {
478- return h.pageScrollRepeatTick(action, keyval, generation)
478+ return h.vimScrollRepeatTick(action, keyval, generation)
479479 })
480- h.pageScrollRepeatCb = cb
481- add := h.pageScrollRepeatAdd
482- id := add(uint(pageScrollCadence/time.Millisecond), &h.pageScrollRepeatCb)
480+ h.vimScrollRepeatCb = cb
481+ add := h.vimScrollRepeatAdd
482+ id := add(uint(vimScrollCadence/time.Millisecond), &h.vimScrollRepeatCb)
483483 if id == 0 {
484- h.clearPageScrollRepeatLocked()
484+ h.clearVimScrollRepeatLocked()
485485 h.mu.Unlock()
486486 return
487487 }
488- h.pageScrollRepeatTimer = id
488+ h.vimScrollRepeatTimer = id
489489 h.mu.Unlock()
490490 }
491491
492-func (h *KeyboardHandler) pageScrollRepeatTick(action Action, keyval uint, generation uint64) bool {
492+func (h *KeyboardHandler) vimScrollRepeatTick(action Action, keyval uint, generation uint64) bool {
493493 h.mu.RLock()
494- active := h.pageScrollRepeatAction == action && h.pageScrollRepeatKeyval == keyval &&
495- h.pageScrollRepeatTimer != 0 && h.pageScrollRepeatGeneration == generation
496- pressedAt := h.pageScrollRepeatPressedAt
497- now := h.pageScrollNow
494+ active := h.vimScrollRepeatAction == action && h.vimScrollRepeatKeyval == keyval &&
495+ h.vimScrollRepeatTimer != 0 && h.vimScrollRepeatGeneration == generation
496+ pressedAt := h.vimScrollRepeatPressedAt
497+ now := h.vimScrollNow
498498 h.mu.RUnlock()
499- if !active || h.modal.Mode() != ModePage {
499+ if !active || h.modal.Mode() != ModeVim {
500500 return false
501501 }
502- if now().Sub(pressedAt) < pageScrollHoldDelay {
502+ if now().Sub(pressedAt) < vimScrollHoldDelay {
503503 return true
504504 }
505- // Continuous dispatch bypasses dispatchAction, so refresh Page Mode timeout here.
505+ // Continuous dispatch bypasses dispatchAction, so refresh Vim Mode timeout here.
506506 h.modal.ResetTimeout(h.ctx)
507- h.dispatchPageScrollLifecycle(action, PageScrollContinuous)
507+ h.dispatchVimScrollLifecycle(action, VimScrollContinuous)
508508 return true
509509 }
510510
511-func (h *KeyboardHandler) stopPageScrollRepeat() {
511+func (h *KeyboardHandler) stopVimScrollRepeat() {
512512 h.mu.Lock()
513- action := h.pageScrollRepeatAction
514- timerID := h.pageScrollRepeatTimer
515- remove := h.pageScrollRepeatRemove
516- h.clearPageScrollRepeatLocked()
513+ action := h.vimScrollRepeatAction
514+ timerID := h.vimScrollRepeatTimer
515+ remove := h.vimScrollRepeatRemove
516+ h.clearVimScrollRepeatLocked()
517517 h.mu.Unlock()
518518 if timerID != 0 && remove != nil {
519519 remove(timerID)
520520 }
521521 if action != "" {
522- h.dispatchPageScrollLifecycle(action, PageScrollStop)
522+ h.dispatchVimScrollLifecycle(action, VimScrollStop)
523523 }
524524 }
525525
526-func (h *KeyboardHandler) clearPageScrollRepeatLocked() {
527- h.pageScrollRepeatGeneration++
528- h.pageScrollRepeatAction = ""
529- h.pageScrollRepeatKeyval = 0
530- h.pageScrollRepeatTimer = 0
531- h.pageScrollRepeatPressedAt = time.Time{}
532- h.pageScrollRepeatCb = nil
526+func (h *KeyboardHandler) clearVimScrollRepeatLocked() {
527+ h.vimScrollRepeatGeneration++
528+ h.vimScrollRepeatAction = ""
529+ h.vimScrollRepeatKeyval = 0
530+ h.vimScrollRepeatTimer = 0
531+ h.vimScrollRepeatPressedAt = time.Time{}
532+ h.vimScrollRepeatCb = nil
533533 }
534534
535-func (h *KeyboardHandler) dispatchPageScrollLifecycle(action Action, phase PageScrollPhase) {
535+func (h *KeyboardHandler) dispatchVimScrollLifecycle(action Action, phase VimScrollPhase) {
536536 h.mu.RLock()
537- handler := h.onPageScrollLifecycle
537+ handler := h.onVimScrollLifecycle
538538 h.mu.RUnlock()
539539 if handler == nil {
540- if phase == PageScrollContinuous {
541- h.dispatchAction(action, ModePage)
540+ if phase == VimScrollContinuous {
541+ h.dispatchAction(action, ModeVim)
542542 }
543543 return
544544 }
545545 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")
547547 }
548548 }
549549
@@ -575,8 +575,8 @@ func normalizeKeyval(keyval uint) uint {
575575 return keyval
576576 }
577577
578-func shouldPassthroughNativePageModeNavigation(mode Mode, keyval uint, modifiers Modifier) bool {
579- if mode != ModePage || modifiers != ModNone {
578+func shouldPassthroughNativeVimModeNavigation(mode Mode, keyval uint, modifiers Modifier) bool {
579+ if mode != ModeVim || modifiers != ModNone {
580580 return false
581581 }
582582 switch keyval {
@@ -607,7 +607,7 @@ func (h *KeyboardHandler) dispatchAction(action Action, mode Mode) bool {
607607 if mode == ModeResize && isResizeAction(action) {
608608 h.modal.ResetTimeout(h.ctx)
609609 }
610- if mode == ModePage && isPageScrollAction(action) {
610+ if mode == ModeVim && isVimScrollAction(action) {
611611 h.modal.ResetTimeout(h.ctx)
612612 }
613613
@@ -644,7 +644,7 @@ func isRepeatedKeyboardActionSuppressed(action Action) bool {
644644 ActionEnterPaneMode,
645645 ActionEnterSessionMode,
646646 ActionEnterResizeMode,
647- ActionEnterPageMode,
647+ ActionEnterVimMode,
648648 ActionNewTab,
649649 ActionRenameTab,
650650 ActionSplitRight,
@@ -721,14 +721,14 @@ func isResizeAction(action Action) bool {
721721 }
722722 }
723723
724-func isPageScrollAction(action Action) bool {
724+func isVimScrollAction(action Action) bool {
725725 switch action {
726- case ActionPageScrollLeft,
727- ActionPageScrollDown,
728- ActionPageScrollUp,
729- ActionPageScrollRight,
730- ActionPageScrollDownFast,
731- ActionPageScrollUpFast:
726+ case ActionVimScrollLeft,
727+ ActionVimScrollDown,
728+ ActionVimScrollUp,
729+ ActionVimScrollRight,
730+ ActionVimScrollDownFast,
731+ ActionVimScrollUpFast:
732732 return true
733733 default:
734734 return false
@@ -774,21 +774,21 @@ func (h *KeyboardHandler) handleModeAction(action Action) bool {
774774 }
775775 h.modal.EnterResizeMode(h.ctx, time.Duration(ms)*time.Millisecond)
776776 return true
777- case ActionEnterPageMode:
778- if h.modal.Mode() == ModePage {
779- h.stopPageScrollRepeat()
777+ case ActionEnterVimMode:
778+ if h.modal.Mode() == ModeVim {
779+ h.stopVimScrollRepeat()
780780 h.modal.ExitMode(h.ctx)
781781 return true
782782 }
783783 var pgms int
784784 if workspace != nil {
785- pgms = workspace.PageMode.TimeoutMilliseconds
785+ pgms = workspace.VimMode.TimeoutMilliseconds
786786 }
787- h.modal.EnterPageMode(h.ctx, time.Duration(pgms)*time.Millisecond)
787+ h.modal.EnterVimMode(h.ctx, time.Duration(pgms)*time.Millisecond)
788788 return true
789789 case ActionExitMode:
790- if h.modal.Mode() == ModePage {
791- h.stopPageScrollRepeat()
790+ if h.modal.Mode() == ModeVim {
791+ h.stopVimScrollRepeat()
792792 }
793793 h.modal.ExitMode(h.ctx)
794794 return true
@@ -836,17 +836,17 @@ func (h *KeyboardHandler) EnterSessionMode() {
836836 h.modal.EnterSessionMode(h.ctx, time.Duration(ms)*time.Millisecond)
837837 }
838838
839-// EnterPageMode programmatically enters page scrolling mode.
839+// EnterVimMode programmatically enters Vim-style scrolling mode.
840840 // Useful for testing or programmatic mode changes.
841-func (h *KeyboardHandler) EnterPageMode() {
841+func (h *KeyboardHandler) EnterVimMode() {
842842 h.mu.RLock()
843843 workspace := h.workspace
844844 h.mu.RUnlock()
845845 var ms int
846846 if workspace != nil {
847- ms = workspace.PageMode.TimeoutMilliseconds
847+ ms = workspace.VimMode.TimeoutMilliseconds
848848 }
849- h.modal.EnterPageMode(h.ctx, time.Duration(ms)*time.Millisecond)
849+ h.modal.EnterVimMode(h.ctx, time.Duration(ms)*time.Millisecond)
850850 }
851851
852852 // ExitMode programmatically exits modal mode.
@@ -855,12 +855,12 @@ func (h *KeyboardHandler) ExitMode() {
855855 h.modal.ExitMode(h.ctx)
856856 }
857857
858-func (h *KeyboardHandler) shouldPassthroughPageModeActivation(action Action, mode Mode) bool {
859- if action != ActionEnterPageMode || mode != ModeNormal {
858+func (h *KeyboardHandler) shouldPassthroughVimModeActivation(action Action, mode Mode) bool {
859+ if action != ActionEnterVimMode || mode != ModeNormal {
860860 return false
861861 }
862862 h.mu.RLock()
863- passthrough := h.pageModeActivationPassthrough
863+ passthrough := h.vimModeActivationPassthrough
864864 h.mu.RUnlock()
865865 return passthrough != nil && passthrough()
866866 }
@@ -870,7 +870,7 @@ func (h *KeyboardHandler) shouldPassthroughPageModeActivation(action Action, mod
870870 // to the registered action handler. It returns whether the action was consumed.
871871 func (h *KeyboardHandler) DispatchAction(action Action) bool {
872872 mode := h.modal.Mode()
873- if h.shouldPassthroughPageModeActivation(action, mode) {
873+ if h.shouldPassthroughVimModeActivation(action, mode) {
874874 return false
875875 }
876876 h.dispatchAction(action, mode)
@@ -889,10 +889,10 @@ func (h *KeyboardHandler) handleKeyRelease(keyval uint) {
889889 }
890890 }
891891 }
892- stopPageScroll := h.pageScrollRepeatKeyval == keyval
892+ stopVimScroll := h.vimScrollRepeatKeyval == keyval
893893 h.mu.Unlock()
894- if stopPageScroll {
895- h.stopPageScrollRepeat()
894+ if stopVimScroll {
895+ h.stopVimScrollRepeat()
896896 }
897897
898898 if accentHandler == nil {
M internal/ui/input/keyboard_test.go +159 -159
@@ -369,10 +369,10 @@ func TestIsRepeatedKeyboardActionSuppressed_AllowsResizeStepRepeats(t *testing.T
369369 }
370370 }
371371
372-func TestHandleKeyPress_EnterPageMode(t *testing.T) {
372+func TestHandleKeyPress_EnterVimMode(t *testing.T) {
373373 ctx := context.Background()
374374 workspace := newTestWorkspace()
375- workspace.PageMode = entity.PageModeConfig{
375+ workspace.VimMode = entity.VimModeConfig{
376376 ActivationShortcut: "ctrl+y",
377377 }
378378
@@ -382,25 +382,25 @@ func TestHandleKeyPress_EnterPageMode(t *testing.T) {
382382 return nil
383383 })
384384
385- // Ctrl+Y should enter page mode
385+ // Ctrl+Y should enter vim mode
386386 result := h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)
387387 if !result {
388388 t.Fatal("Ctrl+Y should be consumed")
389389 }
390- if h.Mode() != ModePage {
391- t.Fatalf("mode = %v, want ModePage", h.Mode())
390+ if h.Mode() != ModeVim {
391+ t.Fatalf("mode = %v, want ModeVim", h.Mode())
392392 }
393393 }
394394
395-func TestHandleKeyPress_PageModeStaysActiveAfterScroll(t *testing.T) {
395+func TestHandleKeyPress_VimModeStaysActiveAfterScroll(t *testing.T) {
396396 ctx := context.Background()
397397 workspace := newTestWorkspace()
398- workspace.PageMode = entity.PageModeConfig{
398+ workspace.VimMode = entity.VimModeConfig{
399399 ActivationShortcut: "ctrl+y",
400400 Actions: map[string]entity.ActionBinding{
401- "page-scroll-down": {Keys: []string{"j"}},
402- "page-scroll-up": {Keys: []string{"k"}},
403- "cancel": {Keys: []string{"escape"}},
401+ "vim-scroll-down": {Keys: []string{"j"}},
402+ "vim-scroll-up": {Keys: []string{"k"}},
403+ "cancel": {Keys: []string{"escape"}},
404404 },
405405 }
406406
@@ -411,50 +411,50 @@ func TestHandleKeyPress_PageModeStaysActiveAfterScroll(t *testing.T) {
411411 return nil
412412 })
413413
414- // Enter page mode
414+ // Enter vim mode
415415 result := h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)
416- if !result || h.Mode() != ModePage {
417- t.Fatal("failed to enter page mode")
416+ if !result || h.Mode() != ModeVim {
417+ t.Fatal("failed to enter vim mode")
418418 }
419419
420420 var repeatTick glib.SourceFunc
421421 repeatNow := time.Unix(2_000, 0)
422- h.pageScrollNow = func() time.Time { return repeatNow }
423- h.pageScrollRepeatAdd = func(_ uint, cb *glib.SourceFunc) uint {
422+ h.vimScrollNow = func() time.Time { return repeatNow }
423+ h.vimScrollRepeatAdd = func(_ uint, cb *glib.SourceFunc) uint {
424424 repeatTick = *cb
425425 return 1
426426 }
427- h.pageScrollRepeatRemove = func(uint) bool { return true }
427+ h.vimScrollRepeatRemove = func(uint) bool { return true }
428428
429- // Scroll actions stay in page mode; the first press arms the smooth
429+ // Scroll actions stay in vim mode; the first press arms the smooth
430430 // repeater and later physical auto-repeat events are ignored.
431431 for i := 0; i < 2; i++ {
432432 scrolled := h.handleKeyPress(uint('j'), 0, 0)
433433 if !scrolled {
434434 t.Fatalf("scroll down iteration %d: key not consumed", i)
435435 }
436- if h.Mode() != ModePage {
437- t.Fatalf("scroll down iteration %d: mode = %v, want ModePage", i, h.Mode())
436+ if h.Mode() != ModeVim {
437+ t.Fatalf("scroll down iteration %d: mode = %v, want ModeVim", i, h.Mode())
438438 }
439439 }
440440 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")
442442 }
443- repeatNow = repeatNow.Add(pageScrollHoldDelay)
443+ repeatNow = repeatNow.Add(vimScrollHoldDelay)
444444 for i := 0; i < 3; i++ {
445445 if !repeatTick(0) {
446446 t.Fatalf("repeat tick %d stopped unexpectedly", i)
447447 }
448- if h.Mode() != ModePage {
449- t.Fatalf("repeat tick %d: mode = %v, want ModePage", i, h.Mode())
448+ if h.Mode() != ModeVim {
449+ t.Fatalf("repeat tick %d: mode = %v, want ModeVim", i, h.Mode())
450450 }
451451 }
452452 h.handleKeyRelease(uint('j'))
453453
454- // Scroll up also stays in page mode
454+ // Scroll up also stays in vim mode
455455 result = h.handleKeyPress(uint('k'), 0, 0)
456- if !result || h.Mode() != ModePage {
457- t.Fatal("scroll up should stay in page mode")
456+ if !result || h.Mode() != ModeVim {
457+ t.Fatal("scroll up should stay in vim mode")
458458 }
459459
460460 // Ensure scroll actions were dispatched
@@ -463,15 +463,15 @@ func TestHandleKeyPress_PageModeStaysActiveAfterScroll(t *testing.T) {
463463 }
464464 }
465465
466-func TestHandleKeyPress_PageModeTimeoutReset(t *testing.T) {
466+func TestHandleKeyPress_VimModeTimeoutReset(t *testing.T) {
467467 ctx := context.Background()
468468 workspace := newTestWorkspace()
469- workspace.PageMode = entity.PageModeConfig{
469+ workspace.VimMode = entity.VimModeConfig{
470470 ActivationShortcut: "ctrl+y",
471471 TimeoutMilliseconds: 100,
472472 Actions: map[string]entity.ActionBinding{
473- "page-scroll-down": {Keys: []string{"j"}},
474- "cancel": {Keys: []string{"escape"}},
473+ "vim-scroll-down": {Keys: []string{"j"}},
474+ "cancel": {Keys: []string{"escape"}},
475475 },
476476 }
477477
@@ -485,34 +485,34 @@ func TestHandleKeyPress_PageModeTimeoutReset(t *testing.T) {
485485 return nil
486486 })
487487
488- // Enter page mode with timeout.
488+ // Enter vim mode with timeout.
489489 h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)
490- if h.Mode() != ModePage {
491- t.Fatal("failed to enter page mode")
490+ if h.Mode() != ModeVim {
491+ t.Fatal("failed to enter vim mode")
492492 }
493493 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)
495495 }
496496
497497 // Scroll action should reset the timeout by starting a replacement timer.
498498 h.handleKeyPress(uint('j'), 0, 0)
499499
500- if h.Mode() != ModePage {
501- t.Fatalf("mode after scroll = %v, want ModePage", h.Mode())
500+ if h.Mode() != ModeVim {
501+ t.Fatalf("mode after scroll = %v, want ModeVim", h.Mode())
502502 }
503503 if timerStarts != 2 {
504504 t.Fatalf("timer starts after scroll reset = %d, want 2", timerStarts)
505505 }
506506 }
507507
508-func TestPageScrollRepeatTick_RefreshesConfiguredPageModeTimeout(t *testing.T) {
508+func TestVimScrollRepeatTick_RefreshesConfiguredVimModeTimeout(t *testing.T) {
509509 ctx := context.Background()
510510 workspace := newTestWorkspace()
511- workspace.PageMode = entity.PageModeConfig{
511+ workspace.VimMode = entity.VimModeConfig{
512512 ActivationShortcut: "ctrl+y",
513513 TimeoutMilliseconds: 100,
514514 Actions: map[string]entity.ActionBinding{
515- "page-scroll-down": {Keys: []string{"j"}},
515+ "vim-scroll-down": {Keys: []string{"j"}},
516516 },
517517 }
518518
@@ -523,8 +523,8 @@ func TestPageScrollRepeatTick_RefreshesConfiguredPageModeTimeout(t *testing.T) {
523523 return &fakeModalTimer{}
524524 }
525525 var continuous int
526- h.SetOnPageScrollLifecycle(func(_ context.Context, _ Action, phase PageScrollPhase) error {
527- if phase == PageScrollContinuous {
526+ h.SetOnVimScrollLifecycle(func(_ context.Context, _ Action, phase VimScrollPhase) error {
527+ if phase == VimScrollContinuous {
528528 continuous++
529529 }
530530 return nil
@@ -532,16 +532,16 @@ func TestPageScrollRepeatTick_RefreshesConfiguredPageModeTimeout(t *testing.T) {
532532
533533 var repeatTick glib.SourceFunc
534534 repeatNow := time.Unix(3_000, 0)
535- h.pageScrollNow = func() time.Time { return repeatNow }
536- h.pageScrollRepeatAdd = func(_ uint, cb *glib.SourceFunc) uint {
535+ h.vimScrollNow = func() time.Time { return repeatNow }
536+ h.vimScrollRepeatAdd = func(_ uint, cb *glib.SourceFunc) uint {
537537 repeatTick = *cb
538538 return 1
539539 }
540- h.pageScrollRepeatRemove = func(uint) bool { return true }
540+ h.vimScrollRepeatRemove = func(uint) bool { return true }
541541
542542 h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)
543- if h.Mode() != ModePage {
544- t.Fatal("failed to enter page mode")
543+ if h.Mode() != ModeVim {
544+ t.Fatal("failed to enter vim mode")
545545 }
546546 enterStarts := timerStarts
547547 h.handleKeyPress(uint('j'), 0, 0)
@@ -553,7 +553,7 @@ func TestPageScrollRepeatTick_RefreshesConfiguredPageModeTimeout(t *testing.T) {
553553 t.Fatalf("tap should refresh timeout: enter=%d afterTap=%d", enterStarts, afterTap)
554554 }
555555
556- repeatNow = repeatNow.Add(pageScrollHoldDelay)
556+ repeatNow = repeatNow.Add(vimScrollHoldDelay)
557557 if !repeatTick(0) {
558558 t.Fatal("continuous tick stopped unexpectedly")
559559 }
@@ -561,21 +561,21 @@ func TestPageScrollRepeatTick_RefreshesConfiguredPageModeTimeout(t *testing.T) {
561561 t.Fatalf("continuous dispatches=%d, want 1", continuous)
562562 }
563563 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)
565565 }
566- if h.Mode() != ModePage {
567- t.Fatalf("mode after continuous tick = %v, want ModePage", h.Mode())
566+ if h.Mode() != ModeVim {
567+ t.Fatalf("mode after continuous tick = %v, want ModeVim", h.Mode())
568568 }
569569 }
570570
571-func TestHandleKeyPress_PageModeEscapeExits(t *testing.T) {
571+func TestHandleKeyPress_VimModeEscapeExits(t *testing.T) {
572572 ctx := context.Background()
573573 workspace := newTestWorkspace()
574- workspace.PageMode = entity.PageModeConfig{
574+ workspace.VimMode = entity.VimModeConfig{
575575 ActivationShortcut: "ctrl+y",
576576 Actions: map[string]entity.ActionBinding{
577- "page-scroll-down": {Keys: []string{"j"}},
578- "cancel": {Keys: []string{"escape"}},
577+ "vim-scroll-down": {Keys: []string{"j"}},
578+ "cancel": {Keys: []string{"escape"}},
579579 },
580580 }
581581
@@ -584,13 +584,13 @@ func TestHandleKeyPress_PageModeEscapeExits(t *testing.T) {
584584 return nil
585585 })
586586
587- // Enter page mode
587+ // Enter vim mode
588588 h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)
589- if h.Mode() != ModePage {
590- t.Fatal("failed to enter page mode")
589+ if h.Mode() != ModeVim {
590+ t.Fatal("failed to enter vim mode")
591591 }
592592
593- // Escape should exit page mode
593+ // Escape should exit vim mode
594594 result := h.handleKeyPress(uint(gdk.KEY_Escape), 0, 0)
595595 if !result {
596596 t.Fatal("Escape should be consumed")
@@ -600,10 +600,10 @@ func TestHandleKeyPress_PageModeEscapeExits(t *testing.T) {
600600 }
601601 }
602602
603-func TestHandleKeyPress_PageModeEnterExits(t *testing.T) {
603+func TestHandleKeyPress_VimModeEnterExits(t *testing.T) {
604604 ctx := context.Background()
605605 workspace := newTestWorkspace()
606- workspace.PageMode = entity.PageModeConfig{
606+ workspace.VimMode = entity.VimModeConfig{
607607 ActivationShortcut: "ctrl+y",
608608 Actions: map[string]entity.ActionBinding{
609609 "confirm": {Keys: []string{"enter"}},
@@ -615,13 +615,13 @@ func TestHandleKeyPress_PageModeEnterExits(t *testing.T) {
615615 return nil
616616 })
617617
618- // Enter page mode
618+ // Enter vim mode
619619 h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)
620- if h.Mode() != ModePage {
621- t.Fatal("failed to enter page mode")
620+ if h.Mode() != ModeVim {
621+ t.Fatal("failed to enter vim mode")
622622 }
623623
624- // Enter should exit page mode
624+ // Enter should exit vim mode
625625 result := h.handleKeyPress(uint(gdk.KEY_Return), 0, 0)
626626 if !result {
627627 t.Fatal("Enter should be consumed")
@@ -631,10 +631,10 @@ func TestHandleKeyPress_PageModeEnterExits(t *testing.T) {
631631 }
632632 }
633633
634-func TestHandleKeyPress_PageModeActivationTogglesOff(t *testing.T) {
634+func TestHandleKeyPress_VimModeActivationTogglesOff(t *testing.T) {
635635 ctx := context.Background()
636636 workspace := newTestWorkspace()
637- workspace.PageMode = entity.PageModeConfig{
637+ workspace.VimMode = entity.VimModeConfig{
638638 ActivationShortcut: "ctrl+y",
639639 }
640640
@@ -643,8 +643,8 @@ func TestHandleKeyPress_PageModeActivationTogglesOff(t *testing.T) {
643643 if !h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue) {
644644 t.Fatal("first Ctrl+Y should be consumed")
645645 }
646- if h.Mode() != ModePage {
647- t.Fatalf("mode after first Ctrl+Y = %v, want ModePage", h.Mode())
646+ if h.Mode() != ModeVim {
647+ t.Fatalf("mode after first Ctrl+Y = %v, want ModeVim", h.Mode())
648648 }
649649
650650 h.handleKeyRelease(uint('y'))
@@ -656,13 +656,13 @@ func TestHandleKeyPress_PageModeActivationTogglesOff(t *testing.T) {
656656 }
657657 }
658658
659-func TestHandleKeyPress_PageModeEscapeExitsWithoutCancelBinding(t *testing.T) {
659+func TestHandleKeyPress_VimModeEscapeExitsWithoutCancelBinding(t *testing.T) {
660660 ctx := context.Background()
661661 workspace := newTestWorkspace()
662- workspace.PageMode = entity.PageModeConfig{
662+ workspace.VimMode = entity.VimModeConfig{
663663 ActivationShortcut: "ctrl+y",
664664 Actions: map[string]entity.ActionBinding{
665- "page-scroll-down": {Keys: []string{"j"}},
665+ "vim-scroll-down": {Keys: []string{"j"}},
666666 },
667667 }
668668
@@ -670,8 +670,8 @@ func TestHandleKeyPress_PageModeEscapeExitsWithoutCancelBinding(t *testing.T) {
670670 if !h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue) {
671671 t.Fatal("Ctrl+Y should be consumed")
672672 }
673- if h.Mode() != ModePage {
674- t.Fatal("failed to enter page mode")
673+ if h.Mode() != ModeVim {
674+ t.Fatal("failed to enter vim mode")
675675 }
676676
677677 if !h.handleKeyPress(uint(gdk.KEY_Escape), 0, 0) {
@@ -682,10 +682,10 @@ func TestHandleKeyPress_PageModeEscapeExitsWithoutCancelBinding(t *testing.T) {
682682 }
683683 }
684684
685-func TestHandleKeyPress_PageModeActivationNotInPassThrough(t *testing.T) {
685+func TestHandleKeyPress_VimModeActivationNotInPassThrough(t *testing.T) {
686686 ctx := context.Background()
687687 workspace := newTestWorkspace()
688- workspace.PageMode = entity.PageModeConfig{
688+ workspace.VimMode = entity.VimModeConfig{
689689 ActivationShortcut: "ctrl+y",
690690 }
691691
@@ -696,55 +696,55 @@ func TestHandleKeyPress_PageModeActivationNotInPassThrough(t *testing.T) {
696696 return RoutePassToWidget
697697 })
698698
699- // Ctrl+Y should NOT activate page mode because route says pass to widget
699+ // Ctrl+Y should NOT activate vim mode because route says pass to widget
700700 result := h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)
701701 if result {
702702 t.Fatal("Ctrl+Y should NOT be consumed when routed to widget")
703703 }
704704 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())
706706 }
707707 }
708708
709-func TestHandleKeyPress_PageModeActivationPassesThroughWhenBlocked(t *testing.T) {
709+func TestHandleKeyPress_VimModeActivationPassesThroughWhenBlocked(t *testing.T) {
710710 ctx := context.Background()
711711 workspace := newTestWorkspace()
712- workspace.PageMode = entity.PageModeConfig{
712+ workspace.VimMode = entity.VimModeConfig{
713713 ActivationShortcut: "ctrl+y",
714714 }
715715
716716 h := NewKeyboardHandler(ctx, workspace, newTestSession())
717- h.SetPageModeActivationPassthrough(func() bool { return true })
717+ h.SetVimModeActivationPassthrough(func() bool { return true })
718718
719719 result := h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)
720720 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")
722722 }
723723 if h.Mode() != ModeNormal {
724724 t.Fatalf("mode = %v, want ModeNormal", h.Mode())
725725 }
726726 }
727727
728-func TestDispatchAction_PageModeActivationPassesThroughWhenBlocked(t *testing.T) {
728+func TestDispatchAction_VimModeActivationPassesThroughWhenBlocked(t *testing.T) {
729729 ctx := context.Background()
730730 workspace := newTestWorkspace()
731- workspace.PageMode = entity.PageModeConfig{
731+ workspace.VimMode = entity.VimModeConfig{
732732 ActivationShortcut: "ctrl+y",
733733 }
734734
735735 h := NewKeyboardHandler(ctx, workspace, newTestSession())
736- h.SetPageModeActivationPassthrough(func() bool { return true })
736+ h.SetVimModeActivationPassthrough(func() bool { return true })
737737
738- consumed := h.DispatchAction(ActionEnterPageMode)
738+ consumed := h.DispatchAction(ActionEnterVimMode)
739739 if consumed {
740- t.Fatal("DispatchAction should not consume blocked page mode activation")
740+ t.Fatal("DispatchAction should not consume blocked vim mode activation")
741741 }
742742 if h.Mode() != ModeNormal {
743743 t.Fatalf("mode = %v, want ModeNormal", h.Mode())
744744 }
745745 }
746746
747-func TestHandleKeyPress_PageModeFastScrollLookup(t *testing.T) {
747+func TestHandleKeyPress_VimModeFastScrollLookup(t *testing.T) {
748748 tests := []struct {
749749 name string
750750 configAction string
@@ -753,15 +753,15 @@ func TestHandleKeyPress_PageModeFastScrollLookup(t *testing.T) {
753753 }{
754754 {
755755 name: "scroll down fast",
756- configAction: "page-scroll-down-fast",
756+ configAction: "vim-scroll-down-fast",
757757 key: uint('d'),
758- expected: ActionPageScrollDownFast,
758+ expected: ActionVimScrollDownFast,
759759 },
760760 {
761761 name: "scroll up fast",
762- configAction: "page-scroll-up-fast",
762+ configAction: "vim-scroll-up-fast",
763763 key: uint('u'),
764- expected: ActionPageScrollUpFast,
764+ expected: ActionVimScrollUpFast,
765765 },
766766 }
767767
@@ -769,7 +769,7 @@ func TestHandleKeyPress_PageModeFastScrollLookup(t *testing.T) {
769769 t.Run(tt.name, func(t *testing.T) {
770770 ctx := context.Background()
771771 workspace := newTestWorkspace()
772- workspace.PageMode = entity.PageModeConfig{
772+ workspace.VimMode = entity.VimModeConfig{
773773 ActivationShortcut: "ctrl+y",
774774 Actions: map[string]entity.ActionBinding{
775775 tt.configAction: {Keys: []string{"ctrl+" + string(rune(tt.key))}},
@@ -785,35 +785,35 @@ func TestHandleKeyPress_PageModeFastScrollLookup(t *testing.T) {
785785 return nil
786786 })
787787
788- // Enter page mode.
788+ // Enter vim mode.
789789 h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)
790- if h.Mode() != ModePage {
791- t.Fatal("failed to enter page mode")
790+ if h.Mode() != ModeVim {
791+ t.Fatal("failed to enter vim mode")
792792 }
793793
794- // Fast-scroll shortcut in page mode should dispatch the expected action.
794+ // Fast-scroll shortcut in vim mode should dispatch the expected action.
795795 result := h.handleKeyPress(tt.key, 0, gdk.ControlMaskValue)
796796 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")
798798 }
799799 if actionCalls != 1 || lastAction != tt.expected {
800800 t.Fatalf("expected %s, got action=%s calls=%d", tt.expected, lastAction, actionCalls)
801801 }
802- if h.Mode() != ModePage {
803- t.Fatal("should stay in page mode after scroll action")
802+ if h.Mode() != ModeVim {
803+ t.Fatal("should stay in vim mode after scroll action")
804804 }
805805 })
806806 }
807807 }
808808
809-func TestHandleKeyPress_PageModeNoAutoExitForScrollActions(t *testing.T) {
809+func TestHandleKeyPress_VimModeNoAutoExitForScrollActions(t *testing.T) {
810810 ctx := context.Background()
811811 workspace := newTestWorkspace()
812- workspace.PageMode = entity.PageModeConfig{
812+ workspace.VimMode = entity.VimModeConfig{
813813 ActivationShortcut: "ctrl+y",
814814 Actions: map[string]entity.ActionBinding{
815- "page-scroll-left": {Keys: []string{"h"}},
816- "page-scroll-right": {Keys: []string{"l"}},
815+ "vim-scroll-left": {Keys: []string{"h"}},
816+ "vim-scroll-right": {Keys: []string{"l"}},
817817 },
818818 }
819819
@@ -824,22 +824,22 @@ func TestHandleKeyPress_PageModeNoAutoExitForScrollActions(t *testing.T) {
824824 return nil
825825 })
826826
827- // Enter page mode
827+ // Enter vim mode
828828 h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)
829- if h.Mode() != ModePage {
830- t.Fatal("failed to enter page mode")
829+ if h.Mode() != ModeVim {
830+ t.Fatal("failed to enter vim mode")
831831 }
832832
833833 // Scroll left should not auto-exit
834834 h.handleKeyPress(uint('h'), 0, 0)
835- if h.Mode() != ModePage {
836- t.Fatal("scroll left should not exit page mode")
835+ if h.Mode() != ModeVim {
836+ t.Fatal("scroll left should not exit vim mode")
837837 }
838838
839839 // Scroll right should not auto-exit
840840 h.handleKeyPress(uint('l'), 0, 0)
841- if h.Mode() != ModePage {
842- t.Fatal("scroll right should not exit page mode")
841+ if h.Mode() != ModeVim {
842+ t.Fatal("scroll right should not exit vim mode")
843843 }
844844
845845 if actionCalls != 2 {
@@ -847,13 +847,13 @@ func TestHandleKeyPress_PageModeNoAutoExitForScrollActions(t *testing.T) {
847847 }
848848 }
849849
850-func TestHandleKeyPress_PageModeArrowKeysPassThroughNatively(t *testing.T) {
850+func TestHandleKeyPress_VimModeArrowKeysPassThroughNatively(t *testing.T) {
851851 ctx := context.Background()
852852 workspace := newTestWorkspace()
853- workspace.PageMode = entity.PageModeConfig{
853+ workspace.VimMode = entity.VimModeConfig{
854854 ActivationShortcut: "ctrl+y",
855855 Actions: map[string]entity.ActionBinding{
856- "page-scroll-down": {Keys: []string{"j"}},
856+ "vim-scroll-down": {Keys: []string{"j"}},
857857 },
858858 }
859859
@@ -865,32 +865,32 @@ func TestHandleKeyPress_PageModeArrowKeysPassThroughNatively(t *testing.T) {
865865 })
866866
867867 h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue)
868- if h.Mode() != ModePage {
869- t.Fatal("failed to enter page mode")
868+ if h.Mode() != ModeVim {
869+ t.Fatal("failed to enter vim mode")
870870 }
871871
872872 for _, keyval := range []uint{uint(gdk.KEY_Left), uint(gdk.KEY_Right), uint(gdk.KEY_Up), uint(gdk.KEY_Down)} {
873873 consumed := h.handleKeyPress(keyval, 0, 0)
874874 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)
876876 }
877- if h.Mode() != ModePage {
878- t.Fatalf("arrow key %d should keep page mode active", keyval)
877+ if h.Mode() != ModeVim {
878+ t.Fatalf("arrow key %d should keep vim mode active", keyval)
879879 }
880880 }
881881
882882 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)
884884 }
885885 }
886886
887-func TestHandleKeyPress_PageModeBlocksGlobalShortcutFallback(t *testing.T) {
887+func TestHandleKeyPress_VimModeBlocksGlobalShortcutFallback(t *testing.T) {
888888 ctx := context.Background()
889889 workspace := newTestWorkspace()
890- workspace.PageMode = entity.PageModeConfig{
890+ workspace.VimMode = entity.VimModeConfig{
891891 ActivationShortcut: "ctrl+y",
892892 Actions: map[string]entity.ActionBinding{
893- "page-scroll-down": {Keys: []string{"j"}},
893+ "vim-scroll-down": {Keys: []string{"j"}},
894894 },
895895 }
896896
@@ -906,28 +906,28 @@ func TestHandleKeyPress_PageModeBlocksGlobalShortcutFallback(t *testing.T) {
906906 if !h.handleKeyPress(uint('y'), 0, gdk.ControlMaskValue) {
907907 t.Fatal("Ctrl+Y should be consumed")
908908 }
909- if h.Mode() != ModePage {
910- t.Fatal("failed to enter page mode")
909+ if h.Mode() != ModeVim {
910+ t.Fatal("failed to enter vim mode")
911911 }
912912
913913 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")
915915 }
916916 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)
918918 }
919- if h.Mode() != ModePage {
920- t.Fatalf("mode after blocked Ctrl+L = %v, want ModePage", h.Mode())
919+ if h.Mode() != ModeVim {
920+ t.Fatalf("mode after blocked Ctrl+L = %v, want ModeVim", h.Mode())
921921 }
922922 }
923923
924-func TestPageScrollRepeat_FirstKeyDownDispatchesTapAndArmsHold(t *testing.T) {
925- h, now, tick := newPageScrollRepeatTestHandler(t)
924+func TestVimScrollRepeat_FirstKeyDownDispatchesTapAndArmsHold(t *testing.T) {
925+ h, now, tick := newVimScrollRepeatTestHandler(t)
926926 actionCalls := 0
927927 h.SetOnAction(func(_ context.Context, action Action) error {
928928 actionCalls++
929- if action != ActionPageScrollDown {
930- t.Fatalf("action = %s, want %s", action, ActionPageScrollDown)
929+ if action != ActionVimScrollDown {
930+ t.Fatalf("action = %s, want %s", action, ActionVimScrollDown)
931931 }
932932 return nil
933933 })
@@ -942,7 +942,7 @@ func TestPageScrollRepeat_FirstKeyDownDispatchesTapAndArmsHold(t *testing.T) {
942942 t.Fatal("first keydown must arm the hold source without an OS repeat")
943943 }
944944
945- *now = now.Add(pageScrollHoldDelay - time.Millisecond)
945+ *now = now.Add(vimScrollHoldDelay - time.Millisecond)
946946 if !(*tick)(0) {
947947 t.Fatal("hold source should remain armed before the delay")
948948 }
@@ -951,20 +951,20 @@ func TestPageScrollRepeat_FirstKeyDownDispatchesTapAndArmsHold(t *testing.T) {
951951 }
952952 }
953953
954-func TestPageScrollRepeat_HoldStartsWithoutOSRepeat(t *testing.T) {
955- h, now, tick := newPageScrollRepeatTestHandler(t)
954+func TestVimScrollRepeat_HoldStartsWithoutOSRepeat(t *testing.T) {
955+ h, now, tick := newVimScrollRepeatTestHandler(t)
956956 continuous := 0
957957 h.SetOnAction(func(context.Context, Action) error { return nil })
958- h.SetOnPageScrollLifecycle(func(_ context.Context, action Action, phase PageScrollPhase) error {
959- if action != ActionPageScrollDown || phase != PageScrollContinuous {
960- t.Fatalf("lifecycle = (%s, %v), want (%s, continuous)", action, phase, ActionPageScrollDown)
958+ h.SetOnVimScrollLifecycle(func(_ context.Context, action Action, phase VimScrollPhase) error {
959+ if action != ActionVimScrollDown || phase != VimScrollContinuous {
960+ t.Fatalf("lifecycle = (%s, %v), want (%s, continuous)", action, phase, ActionVimScrollDown)
961961 }
962962 continuous++
963963 return nil
964964 })
965965
966966 h.handleKeyPress(uint('j'), 0, 0)
967- *now = now.Add(pageScrollHoldDelay)
967+ *now = now.Add(vimScrollHoldDelay)
968968 if !(*tick)(0) {
969969 t.Fatal("hold source stopped at the delay")
970970 }
@@ -973,19 +973,19 @@ func TestPageScrollRepeat_HoldStartsWithoutOSRepeat(t *testing.T) {
973973 }
974974 }
975975
976-func TestPageScrollRepeat_FixedCadenceIgnoresDisplayAndOSRepeatRates(t *testing.T) {
976+func TestVimScrollRepeat_FixedCadenceIgnoresDisplayAndOSRepeatRates(t *testing.T) {
977977 const simulatedHold = time.Second
978978 simulate := func(t *testing.T, externalHz int) int {
979979 t.Helper()
980- h, now, tick := newPageScrollRepeatTestHandler(t)
980+ h, now, tick := newVimScrollRepeatTestHandler(t)
981981 tapDistance := 0
982982 continuousDistance := 0
983983 h.SetOnAction(func(context.Context, Action) error {
984984 tapDistance += 80
985985 return nil
986986 })
987- h.SetOnPageScrollLifecycle(func(_ context.Context, _ Action, phase PageScrollPhase) error {
988- if phase == PageScrollContinuous {
987+ h.SetOnVimScrollLifecycle(func(_ context.Context, _ Action, phase VimScrollPhase) error {
988+ if phase == VimScrollContinuous {
989989 continuousDistance += 80
990990 }
991991 return nil
@@ -994,7 +994,7 @@ func TestPageScrollRepeat_FixedCadenceIgnoresDisplayAndOSRepeatRates(t *testing.
994994
995995 start := *now
996996 nextExternal := time.Second / time.Duration(externalHz)
997- for elapsed := pageScrollCadence; elapsed <= simulatedHold; elapsed += pageScrollCadence {
997+ for elapsed := vimScrollCadence; elapsed <= simulatedHold; elapsed += vimScrollCadence {
998998 *now = start.Add(elapsed)
999999 for elapsed >= nextExternal {
10001000 h.handleKeyPress(uint('j'), 0, 0) // ignored OS auto-repeat
@@ -1017,7 +1017,7 @@ func TestPageScrollRepeat_FixedCadenceIgnoresDisplayAndOSRepeatRates(t *testing.
10171017 }
10181018 }
10191019
1020-func TestPageScrollRepeat_ReleaseAndEscapeStopImmediately(t *testing.T) {
1020+func TestVimScrollRepeat_ReleaseAndEscapeStopImmediately(t *testing.T) {
10211021 for _, tc := range []struct {
10221022 name string
10231023 stop func(*KeyboardHandler)
@@ -1026,19 +1026,19 @@ func TestPageScrollRepeat_ReleaseAndEscapeStopImmediately(t *testing.T) {
10261026 {name: "escape", stop: func(h *KeyboardHandler) { h.handleKeyPress(uint(gdk.KEY_Escape), 0, 0) }},
10271027 } {
10281028 t.Run(tc.name, func(t *testing.T) {
1029- h, now, tick := newPageScrollRepeatTestHandler(t)
1029+ h, now, tick := newVimScrollRepeatTestHandler(t)
10301030 removed := 0
10311031 stops := 0
1032- h.pageScrollRepeatRemove = func(uint) bool { removed++; return true }
1032+ h.vimScrollRepeatRemove = func(uint) bool { removed++; return true }
10331033 h.SetOnAction(func(context.Context, Action) error { return nil })
1034- h.SetOnPageScrollLifecycle(func(_ context.Context, _ Action, phase PageScrollPhase) error {
1035- if phase == PageScrollStop {
1034+ h.SetOnVimScrollLifecycle(func(_ context.Context, _ Action, phase VimScrollPhase) error {
1035+ if phase == VimScrollStop {
10361036 stops++
10371037 }
10381038 return nil
10391039 })
10401040 h.handleKeyPress(uint('j'), 0, 0)
1041- *now = now.Add(pageScrollHoldDelay)
1041+ *now = now.Add(vimScrollHoldDelay)
10421042 (*tick)(0)
10431043
10441044 tc.stop(h)
@@ -1052,24 +1052,24 @@ func TestPageScrollRepeat_ReleaseAndEscapeStopImmediately(t *testing.T) {
10521052 }
10531053 }
10541054
1055-func newPageScrollRepeatTestHandler(t *testing.T) (*KeyboardHandler, *time.Time, *glib.SourceFunc) {
1055+func newVimScrollRepeatTestHandler(t *testing.T) (*KeyboardHandler, *time.Time, *glib.SourceFunc) {
10561056 t.Helper()
10571057 workspace := newTestWorkspace()
1058- workspace.PageMode = entity.PageModeConfig{
1059- Actions: map[string]entity.ActionBinding{"page-scroll-down": {Keys: []string{"j"}}},
1058+ workspace.VimMode = entity.VimModeConfig{
1059+ Actions: map[string]entity.ActionBinding{"vim-scroll-down": {Keys: []string{"j"}}},
10601060 }
10611061 h := NewKeyboardHandler(context.Background(), workspace, newTestSession())
1062- h.EnterPageMode()
1062+ h.EnterVimMode()
10631063 now := time.Unix(1_000, 0)
10641064 var tick glib.SourceFunc
1065- h.pageScrollNow = func() time.Time { return now }
1066- h.pageScrollRepeatAdd = func(intervalMS uint, cb *glib.SourceFunc) uint {
1067- if intervalMS != uint(pageScrollCadence/time.Millisecond) {
1068- t.Fatalf("cadence = %dms, want %dms", intervalMS, pageScrollCadence/time.Millisecond)
1065+ h.vimScrollNow = func() time.Time { return now }
1066+ h.vimScrollRepeatAdd = func(intervalMS uint, cb *glib.SourceFunc) uint {
1067+ if intervalMS != uint(vimScrollCadence/time.Millisecond) {
1068+ t.Fatalf("cadence = %dms, want %dms", intervalMS, vimScrollCadence/time.Millisecond)
10691069 }
10701070 tick = *cb
10711071 return 99
10721072 }
1073- h.pageScrollRepeatRemove = func(uint) bool { return true }
1073+ h.vimScrollRepeatRemove = func(uint) bool { return true }
10741074 return h, &now, &tick
10751075 }
M internal/ui/input/modal.go +9 -9
@@ -26,8 +26,8 @@ const (
2626 ModeSession
2727 // ModeResize is the modal pane resizing mode.
2828 ModeResize
29- // ModePage is the modal page scrolling mode.
30- ModePage
29+ // ModeVim is the modal Vim-style scrolling mode.
30+ ModeVim
3131 )
3232
3333 // String returns a human-readable mode name.
@@ -43,8 +43,8 @@ func (m Mode) String() string {
4343 return "session"
4444 case ModeResize:
4545 return "resize"
46- case ModePage:
47- return "page"
46+ case ModeVim:
47+ return "vim"
4848 default:
4949 return "unknown"
5050 }
@@ -61,8 +61,8 @@ func (m Mode) DisplayName() string {
6161 return "SESSION MODE"
6262 case ModeResize:
6363 return "RESIZE MODE"
64- case ModePage:
65- return "PAGE MODE"
64+ case ModeVim:
65+ return "VIM MODE"
6666 default:
6767 return ""
6868 }
@@ -101,9 +101,9 @@ func NewModalState(ctx context.Context) *ModalState {
101101 }
102102 }
103103
104-// EnterPageMode switches to page scrolling mode with an optional timeout.
105-func (m *ModalState) EnterPageMode(ctx context.Context, timeout time.Duration) {
106- m.enterMode(ctx, ModePage, timeout)
104+// EnterVimMode switches to Vim-style scrolling mode with an optional timeout.
105+func (m *ModalState) EnterVimMode(ctx context.Context, timeout time.Duration) {
106+ m.enterMode(ctx, ModeVim, timeout)
107107 }
108108
109109 // SetMainThreadScheduler sets the function used to dispatch timer callbacks
M internal/ui/input/shortcuts.go +44 -44
@@ -203,14 +203,14 @@ const (
203203 // Application
204204 ActionQuit Action = "quit"
205205
206- // Page mode actions
207- ActionEnterPageMode Action = "enter_page_mode"
208- ActionPageScrollLeft Action = "page_scroll_left"
209- ActionPageScrollDown Action = "page_scroll_down"
210- ActionPageScrollUp Action = "page_scroll_up"
211- ActionPageScrollRight Action = "page_scroll_right"
212- ActionPageScrollDownFast Action = "page_scroll_down_fast"
213- ActionPageScrollUpFast Action = "page_scroll_up_fast"
206+ // Vim mode actions
207+ ActionEnterVimMode Action = "enter_vim_mode"
208+ ActionVimScrollLeft Action = "vim_scroll_left"
209+ ActionVimScrollDown Action = "vim_scroll_down"
210+ ActionVimScrollUp Action = "vim_scroll_up"
211+ ActionVimScrollRight Action = "vim_scroll_right"
212+ ActionVimScrollDownFast Action = "vim_scroll_down_fast"
213+ ActionVimScrollUpFast Action = "vim_scroll_up_fast"
214214 )
215215
216216 const floatingProfileActionPrefix = "open_floating_profile:"
@@ -231,8 +231,8 @@ type ShortcutSet struct {
231231 SessionMode ShortcutTable
232232 // ResizeMode shortcuts are only active in resize mode.
233233 ResizeMode ShortcutTable
234- // PageMode shortcuts are only active in page mode.
235- PageMode ShortcutTable
234+ // VimMode shortcuts are only active in vim mode.
235+ VimMode ShortcutTable
236236 }
237237
238238 // NewShortcutSet creates a ShortcutSet from workspace and session configuration.
@@ -246,7 +246,7 @@ func NewShortcutSet(ctx context.Context, workspace *entity.WorkspaceConfig, sess
246246 PaneMode: make(ShortcutTable),
247247 SessionMode: make(ShortcutTable),
248248 ResizeMode: make(ShortcutTable),
249- PageMode: make(ShortcutTable),
249+ VimMode: make(ShortcutTable),
250250 }
251251
252252 set.buildGlobalShortcutsFromParts(ctx, workspace, session)
@@ -254,7 +254,7 @@ func NewShortcutSet(ctx context.Context, workspace *entity.WorkspaceConfig, sess
254254 set.buildTabModeShortcuts(ctx, workspace)
255255 set.buildPaneModeShortcuts(ctx, workspace)
256256 set.buildResizeModeShortcuts(ctx, workspace)
257- set.buildPageModeShortcuts(ctx, workspace)
257+ set.buildVimModeShortcuts(ctx, workspace)
258258 }
259259 if session != nil {
260260 set.buildSessionModeShortcuts(ctx, session)
@@ -266,7 +266,7 @@ func NewShortcutSet(ctx context.Context, workspace *entity.WorkspaceConfig, sess
266266 Int("pane", len(set.PaneMode)).
267267 Int("resize", len(set.ResizeMode)).
268268 Int("session", len(set.SessionMode)).
269- Int("page", len(set.PageMode)).
269+ Int("page", len(set.VimMode)).
270270 Msg("shortcuts registered")
271271
272272 return set
@@ -302,19 +302,19 @@ func (s *ShortcutSet) buildResizeModeShortcuts(ctx context.Context, cfg *entity.
302302 s.buildModeShortcuts(ctx, cfg.ResizeMode.GetKeyBindings(), s.ResizeMode, "resize")
303303 }
304304
305-// buildPageModeShortcuts populates page mode shortcuts from config.
306-func (s *ShortcutSet) buildPageModeShortcuts(ctx context.Context, cfg *entity.WorkspaceConfig) {
305+// buildVimModeShortcuts populates vim mode shortcuts from config.
306+func (s *ShortcutSet) buildVimModeShortcuts(ctx context.Context, cfg *entity.WorkspaceConfig) {
307307 log := logging.FromContext(ctx)
308- s.buildModeShortcuts(ctx, cfg.PageMode.GetKeyBindings(), s.PageMode, "page")
309- if binding, ok := ParseKeyString(cfg.PageMode.ActivationShortcut); ok {
310- s.PageMode[binding] = ActionEnterPageMode
308+ s.buildModeShortcuts(ctx, cfg.VimMode.GetKeyBindings(), s.VimMode, "page")
309+ if binding, ok := ParseKeyString(cfg.VimMode.ActivationShortcut); ok {
310+ s.VimMode[binding] = ActionEnterVimMode
311311 log.Trace().
312- Str("shortcut", cfg.PageMode.ActivationShortcut).
312+ Str("shortcut", cfg.VimMode.ActivationShortcut).
313313 Uint("keyval", binding.Keyval).
314314 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")
316316 } 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")
318318 }
319319 }
320320
@@ -338,15 +338,15 @@ func (s *ShortcutSet) registerActivationShortcutsFromParts(
338338 }
339339 return
340340 }
341- if binding, ok := ParseKeyString(workspace.PageMode.ActivationShortcut); ok {
342- s.Global[binding] = ActionEnterPageMode
341+ if binding, ok := ParseKeyString(workspace.VimMode.ActivationShortcut); ok {
342+ s.Global[binding] = ActionEnterVimMode
343343 log.Trace().
344- Str("shortcut", workspace.PageMode.ActivationShortcut).
344+ Str("shortcut", workspace.VimMode.ActivationShortcut).
345345 Uint("keyval", binding.Keyval).
346346 Uint("mod", uint(binding.Modifiers)).
347- Msg("page mode activation registered")
347+ Msg("vim mode activation registered")
348348 } 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")
350350 }
351351 if binding, ok := ParseKeyString(workspace.TabMode.ActivationShortcut); ok {
352352 s.Global[binding] = ActionEnterTabMode
@@ -683,19 +683,19 @@ var configActionToAction = map[string]Action{
683683 // Session actions
684684 "session-manager": ActionOpenSessionManager,
685685
686- // Page mode scroll actions
687- "page_scroll_left": ActionPageScrollLeft,
688- "page-scroll-left": ActionPageScrollLeft,
689- "page_scroll_down": ActionPageScrollDown,
690- "page-scroll-down": ActionPageScrollDown,
691- "page_scroll_up": ActionPageScrollUp,
692- "page-scroll-up": ActionPageScrollUp,
693- "page_scroll_right": ActionPageScrollRight,
694- "page-scroll-right": ActionPageScrollRight,
695- "page_scroll_down_fast": ActionPageScrollDownFast,
696- "page-scroll-down-fast": ActionPageScrollDownFast,
697- "page_scroll_up_fast": ActionPageScrollUpFast,
698- "page-scroll-up-fast": ActionPageScrollUpFast,
686+ // Vim mode scroll actions
687+ "vim_scroll_left": ActionVimScrollLeft,
688+ "vim-scroll-left": ActionVimScrollLeft,
689+ "vim_scroll_down": ActionVimScrollDown,
690+ "vim-scroll-down": ActionVimScrollDown,
691+ "vim_scroll_up": ActionVimScrollUp,
692+ "vim-scroll-up": ActionVimScrollUp,
693+ "vim_scroll_right": ActionVimScrollRight,
694+ "vim-scroll-right": ActionVimScrollRight,
695+ "vim_scroll_down_fast": ActionVimScrollDownFast,
696+ "vim-scroll-down-fast": ActionVimScrollDownFast,
697+ "vim_scroll_up_fast": ActionVimScrollUpFast,
698+ "vim-scroll-up-fast": ActionVimScrollUpFast,
699699 }
700700
701701 // FloatingProfileTarget carries the session identity and URL for a floating profile action.
@@ -846,8 +846,8 @@ func stringToKeyval(s string) (uint, bool) {
846846
847847 // Lookup finds an action for the given key binding in the appropriate table.
848848 // 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-contained
850-// so app-global shortcuts do not interrupt held page scrolling.
849+// the current mode is Vim Mode. Vim Mode intentionally stays self-contained
850+// so app-global shortcuts do not interrupt held Vim-style scrolling.
851851 func (s *ShortcutSet) Lookup(binding KeyBinding, mode Mode) (Action, bool) {
852852 // Normalize the binding modifiers
853853 binding.Modifiers &= modifierMask
@@ -863,15 +863,15 @@ func (s *ShortcutSet) Lookup(binding KeyBinding, mode Mode) (Action, bool) {
863863 modeTable = s.ResizeMode
864864 case ModeSession:
865865 modeTable = s.SessionMode
866- case ModePage:
867- modeTable = s.PageMode
866+ case ModeVim:
867+ modeTable = s.VimMode
868868 }
869869
870870 if modeTable != nil {
871871 if action, ok := modeTable[binding]; ok {
872872 return action, true
873873 }
874- if mode == ModePage {
874+ if mode == ModeVim {
875875 return "", false
876876 }
877877 }
M internal/ui/input/shortcuts_test.go +35 -35
@@ -465,11 +465,11 @@ func TestGlobalShortcutActionMap_ConsumeOrExpelHyphenAliases(t *testing.T) {
465465 }
466466 }
467467
468-func TestNewShortcutSet_PageModeActivationShortcut(t *testing.T) {
468+func TestNewShortcutSet_VimModeActivationShortcut(t *testing.T) {
469469 workspace := &entity.WorkspaceConfig{
470470 TabMode: entity.TabModeConfig{ActivationShortcut: "ctrl+t"},
471471 PaneMode: entity.PaneModeConfig{ActivationShortcut: "ctrl+p"},
472- PageMode: entity.PageModeConfig{ActivationShortcut: "ctrl+y"},
472+ VimMode: entity.VimModeConfig{ActivationShortcut: "ctrl+y"},
473473 }
474474
475475 set := NewShortcutSet(context.Background(), workspace, nil)
@@ -481,26 +481,26 @@ func TestNewShortcutSet_PageModeActivationShortcut(t *testing.T) {
481481
482482 action, found := set.Global[binding]
483483 if !found {
484- t.Fatal("page mode activation shortcut not found in Global")
484+ t.Fatal("vim mode activation shortcut not found in Global")
485485 }
486- if action != ActionEnterPageMode {
487- t.Fatalf("Global shortcut action = %s, want %s", action, ActionEnterPageMode)
486+ if action != ActionEnterVimMode {
487+ t.Fatalf("Global shortcut action = %s, want %s", action, ActionEnterVimMode)
488488 }
489489 }
490490
491-func TestShortcutSet_Lookup_PageMode(t *testing.T) {
491+func TestShortcutSet_Lookup_VimMode(t *testing.T) {
492492 set := &ShortcutSet{
493493 Global: ShortcutTable{
494494 {uint(gdk.KEY_q), ModCtrl}: ActionQuit,
495495 },
496- PageMode: ShortcutTable{
497- {uint('h'), ModNone}: ActionPageScrollLeft,
498- {uint('j'), ModNone}: ActionPageScrollDown,
499- {uint('k'), ModNone}: ActionPageScrollUp,
500- {uint('l'), ModNone}: ActionPageScrollRight,
496+ VimMode: ShortcutTable{
497+ {uint('h'), ModNone}: ActionVimScrollLeft,
498+ {uint('j'), ModNone}: ActionVimScrollDown,
499+ {uint('k'), ModNone}: ActionVimScrollUp,
500+ {uint('l'), ModNone}: ActionVimScrollRight,
501501 {uint(gdk.KEY_Escape), ModNone}: ActionExitMode,
502502 {uint(gdk.KEY_Return), ModNone}: ActionExitMode,
503- {uint('y'), ModCtrl}: ActionEnterPageMode,
503+ {uint('y'), ModCtrl}: ActionEnterVimMode,
504504 },
505505 }
506506
@@ -513,47 +513,47 @@ func TestShortcutSet_Lookup_PageMode(t *testing.T) {
513513 {
514514 name: "scroll left",
515515 binding: KeyBinding{uint('h'), ModNone},
516- want: ActionPageScrollLeft,
516+ want: ActionVimScrollLeft,
517517 wantOk: true,
518518 },
519519 {
520520 name: "scroll down",
521521 binding: KeyBinding{uint('j'), ModNone},
522- want: ActionPageScrollDown,
522+ want: ActionVimScrollDown,
523523 wantOk: true,
524524 },
525525 {
526526 name: "scroll up",
527527 binding: KeyBinding{uint('k'), ModNone},
528- want: ActionPageScrollUp,
528+ want: ActionVimScrollUp,
529529 wantOk: true,
530530 },
531531 {
532532 name: "scroll right",
533533 binding: KeyBinding{uint('l'), ModNone},
534- want: ActionPageScrollRight,
534+ want: ActionVimScrollRight,
535535 wantOk: true,
536536 },
537537 {
538- name: "escape exits page mode",
538+ name: "escape exits vim mode",
539539 binding: KeyBinding{uint(gdk.KEY_Escape), ModNone},
540540 want: ActionExitMode,
541541 wantOk: true,
542542 },
543543 {
544- name: "enter exits page mode",
544+ name: "enter exits vim mode",
545545 binding: KeyBinding{uint(gdk.KEY_Return), ModNone},
546546 want: ActionExitMode,
547547 wantOk: true,
548548 },
549549 {
550- name: "activation shortcut still toggles page mode",
550+ name: "activation shortcut still toggles vim mode",
551551 binding: KeyBinding{uint('y'), ModCtrl},
552- want: ActionEnterPageMode,
552+ want: ActionEnterVimMode,
553553 wantOk: true,
554554 },
555555 {
556- name: "global shortcut does not fire inside page mode",
556+ name: "global shortcut does not fire inside vim mode",
557557 binding: KeyBinding{uint(gdk.KEY_q), ModCtrl},
558558 want: "",
559559 wantOk: false,
@@ -562,7 +562,7 @@ func TestShortcutSet_Lookup_PageMode(t *testing.T) {
562562
563563 for _, tt := range tests {
564564 t.Run(tt.name, func(t *testing.T) {
565- got, ok := set.Lookup(tt.binding, ModePage)
565+ got, ok := set.Lookup(tt.binding, ModeVim)
566566 if ok != tt.wantOk {
567567 t.Errorf("Lookup() ok = %v, want %v", ok, tt.wantOk)
568568 return
@@ -574,24 +574,24 @@ func TestShortcutSet_Lookup_PageMode(t *testing.T) {
574574 }
575575 }
576576
577-func TestMapConfigAction_PageScrollActions(t *testing.T) {
577+func TestMapConfigAction_VimScrollActions(t *testing.T) {
578578 tests := []struct {
579579 name string
580580 config string
581581 expected Action
582582 }{
583- {name: "page_scroll_left", config: "page_scroll_left", expected: ActionPageScrollLeft},
584- {name: "page-scroll-left", config: "page-scroll-left", expected: ActionPageScrollLeft},
585- {name: "page_scroll_down", config: "page_scroll_down", expected: ActionPageScrollDown},
586- {name: "page-scroll-down", config: "page-scroll-down", expected: ActionPageScrollDown},
587- {name: "page_scroll_up", config: "page_scroll_up", expected: ActionPageScrollUp},
588- {name: "page-scroll-up", config: "page-scroll-up", expected: ActionPageScrollUp},
589- {name: "page_scroll_right", config: "page_scroll_right", expected: ActionPageScrollRight},
590- {name: "page-scroll-right", config: "page-scroll-right", expected: ActionPageScrollRight},
591- {name: "page_scroll_down_fast", config: "page_scroll_down_fast", expected: ActionPageScrollDownFast},
592- {name: "page-scroll-down-fast", config: "page-scroll-down-fast", expected: ActionPageScrollDownFast},
593- {name: "page_scroll_up_fast", config: "page_scroll_up_fast", expected: ActionPageScrollUpFast},
594- {name: "page-scroll-up-fast", config: "page-scroll-up-fast", expected: ActionPageScrollUpFast},
583+ {name: "vim_scroll_left", config: "vim_scroll_left", expected: ActionVimScrollLeft},
584+ {name: "vim-scroll-left", config: "vim-scroll-left", expected: ActionVimScrollLeft},
585+ {name: "vim_scroll_down", config: "vim_scroll_down", expected: ActionVimScrollDown},
586+ {name: "vim-scroll-down", config: "vim-scroll-down", expected: ActionVimScrollDown},
587+ {name: "vim_scroll_up", config: "vim_scroll_up", expected: ActionVimScrollUp},
588+ {name: "vim-scroll-up", config: "vim-scroll-up", expected: ActionVimScrollUp},
589+ {name: "vim_scroll_right", config: "vim_scroll_right", expected: ActionVimScrollRight},
590+ {name: "vim-scroll-right", config: "vim-scroll-right", expected: ActionVimScrollRight},
591+ {name: "vim_scroll_down_fast", config: "vim_scroll_down_fast", expected: ActionVimScrollDownFast},
592+ {name: "vim-scroll-down-fast", config: "vim-scroll-down-fast", expected: ActionVimScrollDownFast},
593+ {name: "vim_scroll_up_fast", config: "vim_scroll_up_fast", expected: ActionVimScrollUpFast},
594+ {name: "vim-scroll-up-fast", config: "vim-scroll-up-fast", expected: ActionVimScrollUpFast},
595595 }
596596
597597 for _, tt := range tests {
M internal/ui/theme/css.go +25 -20
@@ -111,8 +111,8 @@ func GenerateCSSFullWithTiming(p Palette, _ float64, fonts FontConfig, modeColor
111111 sb.WriteString(generateStackedPaneCSS(p))
112112 sb.WriteString("\n")
113113
114- // Page mode styling
115- sb.WriteString(generatePageModeCSS(transitionDurationMs))
114+ // Vim mode styling
115+ sb.WriteString(generateVimModeCSS(transitionDurationMs))
116116 sb.WriteString("\n")
117117
118118 // Progress bar styling
@@ -593,65 +593,65 @@ entry.find-bar-entry:focus-visible {
593593
594594 // generatePaneCSS creates pane border styles.
595595 // 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.
597597 // It reuses --pane-mode-color (from workspace.styling.pane_mode_color).
598-func generatePageModeCSS(transitionDurationMs int) string {
598+func generateVimModeCSS(transitionDurationMs int) string {
599599 if transitionDurationMs < 0 {
600600 transitionDurationMs = defaultTransitionDurationMs
601601 }
602602 normalPulseMs := transitionDurationMs * 3
603603 fastPulseMs := transitionDurationMs * 6
604- return fmt.Sprintf(`/* ===== Page Mode Styling ===== */
604+ return fmt.Sprintf(`/* ===== Vim Mode Styling ===== */
605605
606-/* Page mode active — subtle local border accent on the pane overlay.
606+/* Vim mode active — subtle local border accent on the pane overlay.
607607 Uses the existing pane mode color token via CSS variable.
608608 This is independent of the workspace-level pane-mode border overlay. */
609-.page-mode-active {
609+.vim-mode-active {
610610 box-shadow: inset 0 0 0 0.125em alpha(var(--pane-mode-color), 0.35);
611611 border-radius: 0;
612612 }
613613
614614 /* Pane overlay pulse keyframes — start/end at the active accent state so the
615- pane keeps its Page mode border while briefly flaring brighter. */
616-@keyframes page-mode-overlay-pulse-anim-a {
615+ pane keeps its Vim mode border while briefly flaring brighter. */
616+@keyframes vim-mode-overlay-pulse-anim-a {
617617 0%% { box-shadow: inset 0 0 0 0.125em alpha(var(--pane-mode-color), 0.35); }
618618 35%% { box-shadow: inset 0 0 0 0.2em alpha(var(--pane-mode-color), 0.78); }
619619 100%% { box-shadow: inset 0 0 0 0.125em alpha(var(--pane-mode-color), 0.35); }
620620 }
621621
622-@keyframes page-mode-overlay-pulse-anim-b {
622+@keyframes vim-mode-overlay-pulse-anim-b {
623623 0%% { box-shadow: inset 0 0 0 0.125em alpha(var(--pane-mode-color), 0.35); }
624624 35%% { box-shadow: inset 0 0 0 0.2em alpha(var(--pane-mode-color), 0.78); }
625625 100%% { box-shadow: inset 0 0 0 0.125em alpha(var(--pane-mode-color), 0.35); }
626626 }
627627
628-@keyframes page-mode-overlay-pulse-fast-anim-a {
628+@keyframes vim-mode-overlay-pulse-fast-anim-a {
629629 0%% { box-shadow: inset 0 0 0 0.125em alpha(var(--pane-mode-color), 0.35); }
630630 25%% { box-shadow: inset 0 0 0 0.26em alpha(var(--pane-mode-color), 0.95); }
631631 100%% { box-shadow: inset 0 0 0 0.125em alpha(var(--pane-mode-color), 0.35); }
632632 }
633633
634-@keyframes page-mode-overlay-pulse-fast-anim-b {
634+@keyframes vim-mode-overlay-pulse-fast-anim-b {
635635 0%% { box-shadow: inset 0 0 0 0.125em alpha(var(--pane-mode-color), 0.35); }
636636 25%% { box-shadow: inset 0 0 0 0.26em alpha(var(--pane-mode-color), 0.95); }
637637 100%% { box-shadow: inset 0 0 0 0.125em alpha(var(--pane-mode-color), 0.35); }
638638 }
639639
640640 /* Pane overlay scroll pulse — derived from transition_duration. */
641-.page-mode-pulse.page-mode-pulse-cycle-a {
642- animation: page-mode-overlay-pulse-anim-a %dms ease-in-out;
641+.vim-mode-pulse.vim-mode-pulse-cycle-a {
642+ animation: vim-mode-overlay-pulse-anim-a %dms ease-in-out;
643643 }
644644
645-.page-mode-pulse.page-mode-pulse-cycle-b {
646- animation: page-mode-overlay-pulse-anim-b %dms ease-in-out;
645+.vim-mode-pulse.vim-mode-pulse-cycle-b {
646+ animation: vim-mode-overlay-pulse-anim-b %dms ease-in-out;
647647 }
648648
649-.page-mode-pulse-fast.page-mode-pulse-cycle-a {
650- animation: page-mode-overlay-pulse-fast-anim-a %dms ease-in-out;
649+.vim-mode-pulse-fast.vim-mode-pulse-cycle-a {
650+ animation: vim-mode-overlay-pulse-fast-anim-a %dms ease-in-out;
651651 }
652652
653-.page-mode-pulse-fast.page-mode-pulse-cycle-b {
654- animation: page-mode-overlay-pulse-fast-anim-b %dms ease-in-out;
653+.vim-mode-pulse-fast.vim-mode-pulse-cycle-b {
654+ animation: vim-mode-overlay-pulse-fast-anim-b %dms ease-in-out;
655655 }
656656 `, normalPulseMs, normalPulseMs, fastPulseMs, fastPulseMs)
657657 }
@@ -1214,6 +1214,11 @@ func generateToasterCSS(p Palette) string {
12141214 color: #ffffff;
12151215 }
12161216
1217+.toast-vim-mode {
1218+ background-color: var(--pane-mode-color);
1219+ color: #ffffff;
1220+}
1221+
12171222 .toast-tab-mode {
12181223 background-color: var(--tab-mode-color);
12191224 color: #ffffff;
M internal/ui/theme/css_test.go +5 -5
@@ -34,14 +34,14 @@ func TestGenerateCSS_StandaloneOmniboxWindowUsesTransparentBackground(t *testing
3434 }
3535 }
3636
37-func TestGenerateCSSFullWithTiming_PageModePulseUsesConfiguredTransitionDuration(t *testing.T) {
38- // Page Mode overlay pulse timings scale from the base transition duration:
37+func TestGenerateCSSFullWithTiming_VimModePulseUsesConfiguredTransitionDuration(t *testing.T) {
38+ // Vim Mode overlay pulse timings scale from the base transition duration:
3939 // normal pulse = 3× (150ms -> 450ms), fast pulse = 6× (150ms -> 900ms).
4040 css := GenerateCSSFullWithTiming(DefaultDarkPalette(), 1.0, DefaultFontConfig(), DefaultModeColors(), 150)
4141
42- assert.Contains(t, css, "animation: page-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;")
44- assert.NotContains(t, css, "page-mode-indicator")
42+ assert.Contains(t, css, "animation: vim-mode-overlay-pulse-anim-a 450ms 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, "vim-mode-indicator")
4545 }
4646
4747 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) {
6565 }
6666
6767 // 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.
6969 func (m *Manager) SetTransitionDuration(ms int) {
7070 if ms < 0 {
7171 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