Skip to content

feat(hint-mode): add Vim-style keyboard navigation - #202

Merged
h4yfans merged 18 commits into
mainfrom
worktree-vim-mode
Apr 14, 2026
Merged

feat(hint-mode): add Vim-style keyboard navigation#202
h4yfans merged 18 commits into
mainfrom
worktree-vim-mode

Conversation

@h4yfans

@h4yfans h4yfans commented Apr 14, 2026

Copy link
Copy Markdown
Collaborator

What

Add Vim/Vimium-style hint mode: press Alt+F (or F when no editor is focused) to overlay typed labels on every clickable element, then type a label to activate the target mouselessly.

Why

Users who prefer keyboard-driven workflows needed a way to reach any button, link, tab, or sidebar item without the mouse. Existing keyboard shortcuts covered only the most-used actions; this closes the gap for the long tail.

How

  • DOM scanner (lib/dom-scanner.ts) — queries clickable roles (button, link, tab, treeitem, menuitem, option, tabindex, data-hint), filters by enabled + visible + in-viewport + outside the hint overlay itself
  • Label assigner (lib/label-assigner.ts) — mnemonic-first: unique first letters get single-char labels (N for Notes); collisions try second-letter candidates (TA/TS); remaining get sequential two-char codes, with prefix-collision prevention so no single-char label is a prefix of any two-char label — the auto-click-on-unique-match UX depends on this guarantee
  • React context (contexts/hint-mode/context.tsx) — exposes activate/deactivate/typeChar/backspace; module-level hintModeActiveRef lets other keyboard hooks suspend themselves when hint mode is active
  • Portal overlay (components/hint-overlay/) — HintOverlay renders into document.body at zIndex: 2147483646; badges dim non-matching hints to 0.3 opacity as the user narrows; HintIndicator shows the status chip + typed chars
  • Activation hook (hooks/use-hint-activation.ts) — captures keydown at the window level; Alt+F always activates; bare F activates only when no editor is focused so it doesn't steal typing in notes/journals; Escape deactivates; Backspace undoes a char

Type

  • feat — new feature

Test plan

  • Unit tests added/updated — DOM scanner, label assigner, and provider (HintModeProvider) covered via Vitest (see earlier commits on branch)
  • Integration tests added/updated — HintModeProvider integration test (use-hint-activation.test.tsx)
  • E2E tests added — tests/e2e/hint-mode.e2e.ts covers 10 behaviors against the built Electron bundle: activation shortcuts, overlay/indicator rendering, prefix narrowing with opacity dimming, backspace undo, unique-full-match auto-click, non-matching ignored, editor-focus suppression of bare F, Alt+F override inside editors, disabled/off-screen exclusion
  • Manual testing — verified Alt+F → type label → target clicks; tried macOS, checked e.code === 'KeyF' works even when Alt produces ƒ

Checklist

  • Self-reviewed the diff
  • No hardcoded secrets or credentials
  • Files stay under ~500 LOC
  • Follows immutable data patterns

h4yfans added 18 commits April 14, 2026 18:51
- guard against IME composition eating CJK input
- detect BlockNote/ProseMirror editors via closest(), not activeElement
- drop wholesale key-swallowing — let DevTools/reload/menu accelerators through
- guard focus() with document.contains() after click
- aria-hidden=true on overlay for screen readers
- reset hintModeActiveRef on provider unmount
- filter out excess labels past 676-element ceiling instead of returning duplicate ZZ
…abels

- Flatten HintIndicator JSX: collapse inner wrapper div into outer, keep same visual result
- Swap local isEditorFocused helper for shared isInputFocused from use-keyboard-shortcuts
- Rewrite label-assigner final pass as explicit loop; drop redundant filter
Covers 10 behaviors end-to-end against the built Electron bundle:
- Alt+F activation renders overlay portal, HINT indicator, and badges
- Escape deactivates and clears overlay + indicator
- Typing a prefix dims non-matching badges; indicator echoes typed chars via kbd
- Backspace removes last typed char and restores full hint set opacity
- Typing a full unique label fires click and deactivates (tracked via window.__hintClicks)
- Non-matching chars are silently ignored
- F alone activates when no editor is focused
- F inside a focused textarea types normally (hint mode does not activate)
- Alt+F overrides editor focus (textarea does not receive f due to preventDefault)
- Disabled and off-screen elements are excluded by the DOM scanner

Tests inject controlled DOM so they don't depend on shifting sidebar labels,
and match badges to targets by screen rect so they survive label-assignment
algorithm changes. Opacity assertions use expect.poll to handle the 100ms
CSS transition deterministically.
@h4yfans
h4yfans merged commit d8d18eb into main Apr 14, 2026
2 checks passed
@h4yfans
h4yfans deleted the worktree-vim-mode branch April 14, 2026 19:57
h4yfans added a commit that referenced this pull request May 6, 2026
h4yfans added a commit that referenced this pull request May 6, 2026
feat(hint-mode): add Vim-style keyboard navigation
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant