feat(vim): show visual and hint sub-mode in the mode indicator
The Vim Mode toast reads VIM MODE · VISUAL or VIM MODE · HINTS while a page interaction owns the keys, and visual selection is painted with the theme accent.
- commit
- 2bc63567e163e305922b12be78d359330919740e
- author
- brice · · 2026-09-26 13:47 UTC
- committer
- brice · · 2026-10-10 08:27 UTC
- parent
- a94e48a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +63 -17
M docs/reference/keybindings.md +1 -1
| @@ -66,7 +66,7 @@ CEF and WebKit execute Vim Mode scroll commands (`h/j/k/l`, `Shift+J/K`) with th | |||
| 66 | 66 | ||
| 67 | 67 | Link hints label the visible targets in the viewport; type a label to pick one, `Backspace` to undo a letter, and `Escape` to close the hints and stay in Vim Mode. `f` follows a link or activates a control, `F` opens a link in a new pane, and `yf` copies a link URL. | |
| 68 | 68 | ||
| 69 | - | `v` starts a visual selection from the current page selection, or from the first visible text. Motions extend it: `h/j/k/l`, `w/b/e`, `0`/`^`/`$`, `(`/`)` for sentences, `{`/`}` for paragraphs, `gg`/`G` for the document edges, and a count prefix such as `3w`. `o` swaps the selection ends, `y` or `Enter` copies and ends the selection, and `v` or `Escape` cancels it. | |
| 69 | + | `v` starts a visual selection from the current page selection, or from the first visible text. The mode indicator reads `VIM MODE · VISUAL` (or `· HINTS` while hints are shown) and the selection uses the theme accent. Motions extend it: `h/j/k/l`, `w/b/e`, `0`/`^`/`$`, `(`/`)` for sentences, `{`/`}` for paragraphs, `gg`/`G` for the document edges, and a count prefix such as `3w`. `o` swaps the selection ends, `y` or `Enter` copies and ends the selection, and `v` or `Escape` cancels it. | |
| 70 | 70 | ||
| 71 | 71 | Text-object yanks copy one block without selecting it: `yah` copies the current section (the heading nearest the top of the viewport through the next heading of the same or a higher level), and `yap`, `yac`, `yat`, and `yal` copy the first visible paragraph, code block, table, or list. The copied block flashes with the theme accent. Hints, visual selection, and yanks use the CEF engine path. | |
| 72 | 72 | ||
| @@ -66,7 +66,7 @@ CEF and WebKit execute Vim Mode scroll commands (`h/j/k/l`, `Shift+J/K`) with th | |||||
| 66 | 66 | ||||
| 67 | Link hints label the visible targets in the viewport; type a label to pick one, `Backspace` to undo a letter, and `Escape` to close the hints and stay in Vim Mode. `f` follows a link or activates a control, `F` opens a link in a new pane, and `yf` copies a link URL. | 67 | Link hints label the visible targets in the viewport; type a label to pick one, `Backspace` to undo a letter, and `Escape` to close the hints and stay in Vim Mode. `f` follows a link or activates a control, `F` opens a link in a new pane, and `yf` copies a link URL. | ||
| 68 | 68 | ||||
| 69 | - | `v` starts a visual selection from the current page selection, or from the first visible text. Motions extend it: `h/j/k/l`, `w/b/e`, `0`/`^`/`$`, `(`/`)` for sentences, `{`/`}` for paragraphs, `gg`/`G` for the document edges, and a count prefix such as `3w`. `o` swaps the selection ends, `y` or `Enter` copies and ends the selection, and `v` or `Escape` cancels it. | 69 | + | `v` starts a visual selection from the current page selection, or from the first visible text. The mode indicator reads `VIM MODE · VISUAL` (or `· HINTS` while hints are shown) and the selection uses the theme accent. Motions extend it: `h/j/k/l`, `w/b/e`, `0`/`^`/`$`, `(`/`)` for sentences, `{`/`}` for paragraphs, `gg`/`G` for the document edges, and a count prefix such as `3w`. `o` swaps the selection ends, `y` or `Enter` copies and ends the selection, and `v` or `Escape` cancels it. |
| 70 | 70 | ||||
| 71 | Text-object yanks copy one block without selecting it: `yah` copies the current section (the heading nearest the top of the viewport through the next heading of the same or a higher level), and `yap`, `yac`, `yat`, and `yal` copy the first visible paragraph, code block, table, or list. The copied block flashes with the theme accent. Hints, visual selection, and yanks use the CEF engine path. | 71 | Text-object yanks copy one block without selecting it: `yah` copies the current section (the heading nearest the top of the viewport through the next heading of the same or a higher level), and `yap`, `yac`, `yat`, and `yal` copy the first visible paragraph, code block, table, or list. The copied block flashes with the theme accent. Hints, visual selection, and yanks use the CEF engine path. | ||
| 72 | 72 | ||||
M internal/infrastructure/cef/vim_page_runtime.js +13 -2
| @@ -206,7 +206,17 @@ | |||
| 206 | 206 | return null; | |
| 207 | 207 | } | |
| 208 | 208 | ||
| 209 | - | function startVisual() { | |
| 209 | + | // Visual mode paints the selection with the accent so it stands out even on | |
| 210 | + | // pages that restyle ::selection; the style is removed when visual ends. | |
| 211 | + | function styleVisual(color) { | |
| 212 | + | const style = document.createElement("style"); | |
| 213 | + | style.setAttribute("data-dumber-vim-visual", ""); | |
| 214 | + | style.textContent = "::selection { background: " + color + " !important; color: #111 !important; }"; | |
| 215 | + | document.documentElement.appendChild(style); | |
| 216 | + | state.root = style; | |
| 217 | + | } | |
| 218 | + | ||
| 219 | + | function startVisual(color) { | |
| 210 | 220 | const selection = window.getSelection(); | |
| 211 | 221 | if (!selection) return false; | |
| 212 | 222 | if (selection.rangeCount === 0 || selection.isCollapsed) { | |
| @@ -217,6 +227,7 @@ | |||
| 217 | 227 | } | |
| 218 | 228 | state.count = ""; | |
| 219 | 229 | state.pendingG = false; | |
| 230 | + | styleVisual(color); | |
| 220 | 231 | return true; | |
| 221 | 232 | } | |
| 222 | 233 | ||
| @@ -350,7 +361,7 @@ | |||
| 350 | 361 | return; | |
| 351 | 362 | } | |
| 352 | 363 | state = { kind: request.kind, token }; | |
| 353 | - | const started = request.kind === "visual" ? startVisual() : startHints(request.kind, color); | |
| 364 | + | const started = request.kind === "visual" ? startVisual(color) : startHints(request.kind, color); | |
| 354 | 365 | if (!started) finish({ type: "end" }); | |
| 355 | 366 | }, | |
| 356 | 367 | key(token, key) { | |
| @@ -206,7 +206,17 @@ | |||||
| 206 | return null; | 206 | return null; | ||
| 207 | } | 207 | } | ||
| 208 | 208 | ||||
| 209 | - | function startVisual() { | 209 | + | // Visual mode paints the selection with the accent so it stands out even on |
| 210 | + | // pages that restyle ::selection; the style is removed when visual ends. | |||
| 211 | + | function styleVisual(color) { | |||
| 212 | + | const style = document.createElement("style"); | |||
| 213 | + | style.setAttribute("data-dumber-vim-visual", ""); | |||
| 214 | + | style.textContent = "::selection { background: " + color + " !important; color: #111 !important; }"; | |||
| 215 | + | document.documentElement.appendChild(style); | |||
| 216 | + | state.root = style; | |||
| 217 | + | } | |||
| 218 | + | ||||
| 219 | + | function startVisual(color) { | |||
| 210 | const selection = window.getSelection(); | 220 | const selection = window.getSelection(); | ||
| 211 | if (!selection) return false; | 221 | if (!selection) return false; | ||
| 212 | if (selection.rangeCount === 0 || selection.isCollapsed) { | 222 | if (selection.rangeCount === 0 || selection.isCollapsed) { | ||
| @@ -217,6 +227,7 @@ | |||||
| 217 | } | 227 | } | ||
| 218 | state.count = ""; | 228 | state.count = ""; | ||
| 219 | state.pendingG = false; | 229 | state.pendingG = false; | ||
| 230 | + | styleVisual(color); | |||
| 220 | return true; | 231 | return true; | ||
| 221 | } | 232 | } | ||
| 222 | 233 | ||||
| @@ -350,7 +361,7 @@ | |||||
| 350 | return; | 361 | return; | ||
| 351 | } | 362 | } | ||
| 352 | state = { kind: request.kind, token }; | 363 | state = { kind: request.kind, token }; | ||
| 353 | - | const started = request.kind === "visual" ? startVisual() : startHints(request.kind, color); | 364 | + | const started = request.kind === "visual" ? startVisual(color) : startHints(request.kind, color); |
| 354 | if (!started) finish({ type: "end" }); | 365 | if (!started) finish({ type: "end" }); | ||
| 355 | }, | 366 | }, | ||
| 356 | key(token, key) { | 367 | key(token, key) { | ||
M internal/ui/app.go +26 -4
| @@ -2230,7 +2230,7 @@ func (a *App) navigateVimSequenceAction(ctx context.Context, bw *browserWindow, | |||
| 2230 | 2230 | return | |
| 2231 | 2231 | } | |
| 2232 | 2232 | if outcome.CapturePageKeys { | |
| 2233 | - | a.captureVimPageKeys(ctx, bw, wv) | |
| 2233 | + | a.captureVimPageKeys(ctx, bw, wv, vimPageInteractionLabel(action)) | |
| 2234 | 2234 | } | |
| 2235 | 2235 | if bw != nil && (action == "heading-next" || action == "heading-prev") { | |
| 2236 | 2236 | if _, ok := wv.(port.SemanticNavigable); ok { | |
| @@ -2248,11 +2248,12 @@ func (a *App) navigateVimSequenceAction(ctx context.Context, bw *browserWindow, | |||
| 2248 | 2248 | ||
| 2249 | 2249 | // captureVimPageKeys routes this window's Vim Mode keys to the in-page | |
| 2250 | 2250 | // interaction running in wv until the page reports its end. | |
| 2251 | - | func (a *App) captureVimPageKeys(ctx context.Context, bw *browserWindow, wv port.WebView) { | |
| 2251 | + | func (a *App) captureVimPageKeys(ctx context.Context, bw *browserWindow, wv port.WebView, label string) { | |
| 2252 | 2252 | if bw == nil || bw.keyboardHandler == nil || wv == nil { | |
| 2253 | 2253 | return | |
| 2254 | 2254 | } | |
| 2255 | 2255 | bw.vimPageInteractionWebView = wv | |
| 2256 | + | a.showVimModeToast(ctx, bw, label) | |
| 2256 | 2257 | bw.keyboardHandler.SetPageKeyCapture(func(key string) { | |
| 2257 | 2258 | if err := a.vimNavigationUseCase().SendPageKey(ctx, wv, key); err != nil { | |
| 2258 | 2259 | logging.FromContext(ctx).Debug().Err(err).Msg("vim page key forwarding failed") | |
| @@ -2263,7 +2264,7 @@ func (a *App) captureVimPageKeys(ctx context.Context, bw *browserWindow, wv port | |||
| 2263 | 2264 | ||
| 2264 | 2265 | // handleVimPageInteractionEnded releases key capture when the page reports | |
| 2265 | 2266 | // that its hint or visual interaction finished. | |
| 2266 | - | func (a *App) handleVimPageInteractionEnded(paneID entity.PaneID) { | |
| 2267 | + | func (a *App) handleVimPageInteractionEnded(ctx context.Context, paneID entity.PaneID) { | |
| 2267 | 2268 | bw := a.browserWindowForAnyPane(paneID) | |
| 2268 | 2269 | if bw == nil || bw.vimPageInteractionWebView == nil || a.contentCoord == nil { | |
| 2269 | 2270 | return | |
| @@ -2274,6 +2275,24 @@ func (a *App) handleVimPageInteractionEnded(paneID entity.PaneID) { | |||
| 2274 | 2275 | bw.vimPageInteractionWebView = nil | |
| 2275 | 2276 | if bw.keyboardHandler != nil { | |
| 2276 | 2277 | bw.keyboardHandler.ClearPageKeyCapture() | |
| 2278 | + | if bw.keyboardHandler.Mode() == input.ModeVim { | |
| 2279 | + | a.showVimModeToast(ctx, bw, "") | |
| 2280 | + | } | |
| 2281 | + | } | |
| 2282 | + | } | |
| 2283 | + | ||
| 2284 | + | // vimPageInteractionLabel names the key-capturing sub-mode shown in the Vim | |
| 2285 | + | // Mode indicator so users can tell hints or visual selection are active. | |
| 2286 | + | func vimPageInteractionLabel(action string) string { | |
| 2287 | + | switch action { | |
| 2288 | + | case "visual": | |
| 2289 | + | return "VISUAL" | |
| 2290 | + | case "hint-follow-new": | |
| 2291 | + | return "HINTS · NEW PANE" | |
| 2292 | + | case "hint-yank-url": | |
| 2293 | + | return "HINTS · YANK URL" | |
| 2294 | + | default: | |
| 2295 | + | return "HINTS" | |
| 2277 | 2296 | } | |
| 2278 | 2297 | } | |
| 2279 | 2298 | ||
| @@ -2290,6 +2309,9 @@ func (a *App) endVimPageInteraction(ctx context.Context, bw *browserWindow) { | |||
| 2290 | 2309 | if wv == nil { | |
| 2291 | 2310 | return | |
| 2292 | 2311 | } | |
| 2312 | + | if bw.keyboardHandler != nil && bw.keyboardHandler.Mode() == input.ModeVim { | |
| 2313 | + | a.showVimModeToast(ctx, bw, "") | |
| 2314 | + | } | |
| 2293 | 2315 | if err := a.vimNavigationUseCase().CancelPageInteraction(ctx, wv); err != nil { | |
| 2294 | 2316 | logging.FromContext(ctx).Debug().Err(err).Msg("failed to cancel vim page interaction") | |
| 2295 | 2317 | } | |
| @@ -3477,7 +3499,7 @@ func (a *App) initCoordinators(ctx context.Context) { | |||
| 3477 | 3499 | a.handlePageEditableFocusChanged(ctx, paneID, editable) | |
| 3478 | 3500 | }) | |
| 3479 | 3501 | a.contentCoord.SetOnVimPageInteractionEnded(func(paneID entity.PaneID) { | |
| 3480 | - | a.handleVimPageInteractionEnded(paneID) | |
| 3502 | + | a.handleVimPageInteractionEnded(ctx, paneID) | |
| 3481 | 3503 | }) | |
| 3482 | 3504 | ||
| 3483 | 3505 | // JS dialogs are shown in the owning pane's overlay, wherever that pane lives. | |
| @@ -2230,7 +2230,7 @@ func (a *App) navigateVimSequenceAction(ctx context.Context, bw *browserWindow, | |||||
| 2230 | return | 2230 | return | ||
| 2231 | } | 2231 | } | ||
| 2232 | if outcome.CapturePageKeys { | 2232 | if outcome.CapturePageKeys { | ||
| 2233 | - | a.captureVimPageKeys(ctx, bw, wv) | 2233 | + | a.captureVimPageKeys(ctx, bw, wv, vimPageInteractionLabel(action)) |
| 2234 | } | 2234 | } | ||
| 2235 | if bw != nil && (action == "heading-next" || action == "heading-prev") { | 2235 | if bw != nil && (action == "heading-next" || action == "heading-prev") { | ||
| 2236 | if _, ok := wv.(port.SemanticNavigable); ok { | 2236 | if _, ok := wv.(port.SemanticNavigable); ok { | ||
| @@ -2248,11 +2248,12 @@ func (a *App) navigateVimSequenceAction(ctx context.Context, bw *browserWindow, | |||||
| 2248 | 2248 | ||||
| 2249 | // captureVimPageKeys routes this window's Vim Mode keys to the in-page | 2249 | // captureVimPageKeys routes this window's Vim Mode keys to the in-page | ||
| 2250 | // interaction running in wv until the page reports its end. | 2250 | // interaction running in wv until the page reports its end. | ||
| 2251 | - | func (a *App) captureVimPageKeys(ctx context.Context, bw *browserWindow, wv port.WebView) { | 2251 | + | func (a *App) captureVimPageKeys(ctx context.Context, bw *browserWindow, wv port.WebView, label string) { |
| 2252 | if bw == nil || bw.keyboardHandler == nil || wv == nil { | 2252 | if bw == nil || bw.keyboardHandler == nil || wv == nil { | ||
| 2253 | return | 2253 | return | ||
| 2254 | } | 2254 | } | ||
| 2255 | bw.vimPageInteractionWebView = wv | 2255 | bw.vimPageInteractionWebView = wv | ||
| 2256 | + | a.showVimModeToast(ctx, bw, label) | |||
| 2256 | bw.keyboardHandler.SetPageKeyCapture(func(key string) { | 2257 | bw.keyboardHandler.SetPageKeyCapture(func(key string) { | ||
| 2257 | if err := a.vimNavigationUseCase().SendPageKey(ctx, wv, key); err != nil { | 2258 | if err := a.vimNavigationUseCase().SendPageKey(ctx, wv, key); err != nil { | ||
| 2258 | logging.FromContext(ctx).Debug().Err(err).Msg("vim page key forwarding failed") | 2259 | logging.FromContext(ctx).Debug().Err(err).Msg("vim page key forwarding failed") | ||
| @@ -2263,7 +2264,7 @@ func (a *App) captureVimPageKeys(ctx context.Context, bw *browserWindow, wv port | |||||
| 2263 | 2264 | ||||
| 2264 | // handleVimPageInteractionEnded releases key capture when the page reports | 2265 | // handleVimPageInteractionEnded releases key capture when the page reports | ||
| 2265 | // that its hint or visual interaction finished. | 2266 | // that its hint or visual interaction finished. | ||
| 2266 | - | func (a *App) handleVimPageInteractionEnded(paneID entity.PaneID) { | 2267 | + | func (a *App) handleVimPageInteractionEnded(ctx context.Context, paneID entity.PaneID) { |
| 2267 | bw := a.browserWindowForAnyPane(paneID) | 2268 | bw := a.browserWindowForAnyPane(paneID) | ||
| 2268 | if bw == nil || bw.vimPageInteractionWebView == nil || a.contentCoord == nil { | 2269 | if bw == nil || bw.vimPageInteractionWebView == nil || a.contentCoord == nil { | ||
| 2269 | return | 2270 | return | ||
| @@ -2274,6 +2275,24 @@ func (a *App) handleVimPageInteractionEnded(paneID entity.PaneID) { | |||||
| 2274 | bw.vimPageInteractionWebView = nil | 2275 | bw.vimPageInteractionWebView = nil | ||
| 2275 | if bw.keyboardHandler != nil { | 2276 | if bw.keyboardHandler != nil { | ||
| 2276 | bw.keyboardHandler.ClearPageKeyCapture() | 2277 | bw.keyboardHandler.ClearPageKeyCapture() | ||
| 2278 | + | if bw.keyboardHandler.Mode() == input.ModeVim { | |||
| 2279 | + | a.showVimModeToast(ctx, bw, "") | |||
| 2280 | + | } | |||
| 2281 | + | } | |||
| 2282 | + | } | |||
| 2283 | + | ||||
| 2284 | + | // vimPageInteractionLabel names the key-capturing sub-mode shown in the Vim | |||
| 2285 | + | // Mode indicator so users can tell hints or visual selection are active. | |||
| 2286 | + | func vimPageInteractionLabel(action string) string { | |||
| 2287 | + | switch action { | |||
| 2288 | + | case "visual": | |||
| 2289 | + | return "VISUAL" | |||
| 2290 | + | case "hint-follow-new": | |||
| 2291 | + | return "HINTS · NEW PANE" | |||
| 2292 | + | case "hint-yank-url": | |||
| 2293 | + | return "HINTS · YANK URL" | |||
| 2294 | + | default: | |||
| 2295 | + | return "HINTS" | |||
| 2277 | } | 2296 | } | ||
| 2278 | } | 2297 | } | ||
| 2279 | 2298 | ||||
| @@ -2290,6 +2309,9 @@ func (a *App) endVimPageInteraction(ctx context.Context, bw *browserWindow) { | |||||
| 2290 | if wv == nil { | 2309 | if wv == nil { | ||
| 2291 | return | 2310 | return | ||
| 2292 | } | 2311 | } | ||
| 2312 | + | if bw.keyboardHandler != nil && bw.keyboardHandler.Mode() == input.ModeVim { | |||
| 2313 | + | a.showVimModeToast(ctx, bw, "") | |||
| 2314 | + | } | |||
| 2293 | if err := a.vimNavigationUseCase().CancelPageInteraction(ctx, wv); err != nil { | 2315 | if err := a.vimNavigationUseCase().CancelPageInteraction(ctx, wv); err != nil { | ||
| 2294 | logging.FromContext(ctx).Debug().Err(err).Msg("failed to cancel vim page interaction") | 2316 | logging.FromContext(ctx).Debug().Err(err).Msg("failed to cancel vim page interaction") | ||
| 2295 | } | 2317 | } | ||
| @@ -3477,7 +3499,7 @@ func (a *App) initCoordinators(ctx context.Context) { | |||||
| 3477 | a.handlePageEditableFocusChanged(ctx, paneID, editable) | 3499 | a.handlePageEditableFocusChanged(ctx, paneID, editable) | ||
| 3478 | }) | 3500 | }) | ||
| 3479 | a.contentCoord.SetOnVimPageInteractionEnded(func(paneID entity.PaneID) { | 3501 | a.contentCoord.SetOnVimPageInteractionEnded(func(paneID entity.PaneID) { | ||
| 3480 | - | a.handleVimPageInteractionEnded(paneID) | 3502 | + | a.handleVimPageInteractionEnded(ctx, paneID) |
| 3481 | }) | 3503 | }) | ||
| 3482 | 3504 | ||||
| 3483 | // JS dialogs are shown in the owning pane's overlay, wherever that pane lives. | 3505 | // JS dialogs are shown in the owning pane's overlay, wherever that pane lives. | ||
M internal/ui/mode_frame_test.go +7 -0
| @@ -156,3 +156,10 @@ func TestShouldStartLinger(t *testing.T) { | |||
| 156 | 156 | require.False(t, shouldStartLinger(true, true, true, true, 15, 25, rect)) | |
| 157 | 157 | require.False(t, shouldStartLinger(false, false, true, true, 15, 25, rect)) | |
| 158 | 158 | } | |
| 159 | + | ||
| 160 | + | func TestVimPageInteractionLabel(t *testing.T) { | |
| 161 | + | require.Equal(t, "VISUAL", vimPageInteractionLabel("visual")) | |
| 162 | + | require.Equal(t, "HINTS", vimPageInteractionLabel("hint-follow")) | |
| 163 | + | require.Equal(t, "HINTS · NEW PANE", vimPageInteractionLabel("hint-follow-new")) | |
| 164 | + | require.Equal(t, "HINTS · YANK URL", vimPageInteractionLabel("hint-yank-url")) | |
| 165 | + | } | |
| @@ -156,3 +156,10 @@ func TestShouldStartLinger(t *testing.T) { | |||||
| 156 | require.False(t, shouldStartLinger(true, true, true, true, 15, 25, rect)) | 156 | require.False(t, shouldStartLinger(true, true, true, true, 15, 25, rect)) | ||
| 157 | require.False(t, shouldStartLinger(false, false, true, true, 15, 25, rect)) | 157 | require.False(t, shouldStartLinger(false, false, true, true, 15, 25, rect)) | ||
| 158 | } | 158 | } | ||
| 159 | + | ||||
| 160 | + | func TestVimPageInteractionLabel(t *testing.T) { | |||
| 161 | + | require.Equal(t, "VISUAL", vimPageInteractionLabel("visual")) | |||
| 162 | + | require.Equal(t, "HINTS", vimPageInteractionLabel("hint-follow")) | |||
| 163 | + | require.Equal(t, "HINTS · NEW PANE", vimPageInteractionLabel("hint-follow-new")) | |||
| 164 | + | require.Equal(t, "HINTS · YANK URL", vimPageInteractionLabel("hint-yank-url")) | |||
| 165 | + | } | |||
M internal/ui/vim_mode.go +16 -10
| @@ -323,25 +323,31 @@ func (a *App) showPendingSequence(ctx context.Context, bw *browserWindow, pendin | |||
| 323 | 323 | if bw.modeToaster == nil { | |
| 324 | 324 | return | |
| 325 | 325 | } | |
| 326 | - | log := logging.FromContext(ctx) | |
| 326 | + | ||
| 327 | + | a.showVimModeToast(ctx, bw, pending) | |
| 328 | + | logging.FromContext(ctx).Debug(). | |
| 329 | + | Str("window_id", bw.id). | |
| 330 | + | Str("pending", pending). | |
| 331 | + | Msg("vim mode pending toaster updated") | |
| 332 | + | } | |
| 333 | + | ||
| 334 | + | // showVimModeToast shows "VIM MODE" with an optional " · detail" suffix on the | |
| 335 | + | // per-window mode toaster, honoring the indicator toggle. | |
| 336 | + | func (a *App) showVimModeToast(ctx context.Context, bw *browserWindow, detail string) { | |
| 337 | + | if bw == nil || bw.modeToaster == nil { | |
| 338 | + | return | |
| 339 | + | } | |
| 327 | 340 | if !a.runtimeConfigSnapshot().UI.Workspace.Styling.ModeIndicatorToasterEnabled { | |
| 328 | 341 | bw.modeToaster.Hide() | |
| 329 | - | log.Debug().Str("window_id", bw.id).Msg("vim mode pending toaster hidden (disabled)") | |
| 330 | 342 | return | |
| 331 | 343 | } | |
| 332 | - | ||
| 333 | 344 | text := input.ModeVim.DisplayName() | |
| 334 | - | if pending != "" { | |
| 335 | - | text = text + " · " + pending | |
| 345 | + | if detail != "" { | |
| 346 | + | text = text + " · " + detail | |
| 336 | 347 | } | |
| 337 | 348 | bw.modeToaster.Show(ctx, text, component.ToastInfo, | |
| 338 | 349 | component.WithDuration(0), | |
| 339 | 350 | component.WithPosition(component.ToastPositionBottomLeft), | |
| 340 | 351 | component.WithModeClass(getModeToastClass(input.ModeVim)), | |
| 341 | 352 | ) | |
| 342 | - | log.Debug(). | |
| 343 | - | Str("window_id", bw.id). | |
| 344 | - | Str("pending", pending). | |
| 345 | - | Str("toast_text", text). | |
| 346 | - | Msg("vim mode pending toaster updated") | |
| 347 | 353 | } | |
| @@ -323,25 +323,31 @@ func (a *App) showPendingSequence(ctx context.Context, bw *browserWindow, pendin | |||||
| 323 | if bw.modeToaster == nil { | 323 | if bw.modeToaster == nil { | ||
| 324 | return | 324 | return | ||
| 325 | } | 325 | } | ||
| 326 | - | log := logging.FromContext(ctx) | 326 | + | |
| 327 | + | a.showVimModeToast(ctx, bw, pending) | |||
| 328 | + | logging.FromContext(ctx).Debug(). | |||
| 329 | + | Str("window_id", bw.id). | |||
| 330 | + | Str("pending", pending). | |||
| 331 | + | Msg("vim mode pending toaster updated") | |||
| 332 | + | } | |||
| 333 | + | ||||
| 334 | + | // showVimModeToast shows "VIM MODE" with an optional " · detail" suffix on the | |||
| 335 | + | // per-window mode toaster, honoring the indicator toggle. | |||
| 336 | + | func (a *App) showVimModeToast(ctx context.Context, bw *browserWindow, detail string) { | |||
| 337 | + | if bw == nil || bw.modeToaster == nil { | |||
| 338 | + | return | |||
| 339 | + | } | |||
| 327 | if !a.runtimeConfigSnapshot().UI.Workspace.Styling.ModeIndicatorToasterEnabled { | 340 | if !a.runtimeConfigSnapshot().UI.Workspace.Styling.ModeIndicatorToasterEnabled { | ||
| 328 | bw.modeToaster.Hide() | 341 | bw.modeToaster.Hide() | ||
| 329 | - | log.Debug().Str("window_id", bw.id).Msg("vim mode pending toaster hidden (disabled)") | |||
| 330 | return | 342 | return | ||
| 331 | } | 343 | } | ||
| 332 | - | ||||
| 333 | text := input.ModeVim.DisplayName() | 344 | text := input.ModeVim.DisplayName() | ||
| 334 | - | if pending != "" { | 345 | + | if detail != "" { |
| 335 | - | text = text + " · " + pending | 346 | + | text = text + " · " + detail |
| 336 | } | 347 | } | ||
| 337 | bw.modeToaster.Show(ctx, text, component.ToastInfo, | 348 | bw.modeToaster.Show(ctx, text, component.ToastInfo, | ||
| 338 | component.WithDuration(0), | 349 | component.WithDuration(0), | ||
| 339 | component.WithPosition(component.ToastPositionBottomLeft), | 350 | component.WithPosition(component.ToastPositionBottomLeft), | ||
| 340 | component.WithModeClass(getModeToastClass(input.ModeVim)), | 351 | component.WithModeClass(getModeToastClass(input.ModeVim)), | ||
| 341 | ) | 352 | ) | ||
| 342 | - | log.Debug(). | |||
| 343 | - | Str("window_id", bw.id). | |||
| 344 | - | Str("pending", pending). | |||
| 345 | - | Str("toast_text", text). | |||
| 346 | - | Msg("vim mode pending toaster updated") | |||
| 347 | } | 353 | } | ||