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