← Back to Index
Architecture

KindaVim & Vim Mode

Vim-style navigation across every macOS app, powered by Kanata layers.

Navigate. Hint. Learn.

Vim motions as Kanata layers, a hint overlay that shows what each key does, and telemetry that tracks which motions the user discovers.
h l k j

Navigate

hjkl motions, word jumps, and line ops across all apps.

w word

Hint

Overlay shows available motions on each key.

Learn

Telemetry tracks which motions the user discovers.

The Complete Picture

Kanata provides the vim layer. KeyPath observes mode transitions and renders the hint overlay. Telemetry records what the user tries.
KANATA LAYER (deflayer vim ...) key events push-msg OBSERVER KindaVimStateAdapter mode snapshot key events SWIFTUI OVERLAY VimHintLayer LOCAL STORAGE KindaVimTelemetryStore SUB-STATE VimSequenceObserver mode resets USER INPUT Hold Caps Lock (enters vim layer) → hjkl / w / b / $ / ... → Release (returns to base layer) FILE WATCHER environment.json Kanata owns the layers. KeyPath only observes and renders. Blue arrows show data flow; dashed arrows show secondary signals.
Core invariant. KindaVim adds no new input mechanism. Every vim motion is a standard macOS keyboard shortcut (Cmd+Left, Option+Delete, etc.) remapped through Kanata layers. The hint layer is pure UI — removing it changes nothing about functionality.

Four Components, Four Jobs

Each component has a single responsibility. Together they deliver vim motions with visual feedback and learning telemetry.
KindaVimStateAdapter Observer
Bridges KindaVim runtime signals into KeyPath UI state. Watches environment.json for mode changes (insert, normal, visual, operatorPending). Emits a strict snapshot with mode, source, confidence, and isStale. De-noises duplicate unchanged states. Falls back to unknown when the file is missing.
@MainActor @Observable
final class KindaVimStateAdapter {
    enum Mode: String { case insert, normal, visual, operatorPending, unknown }
    enum Source: String { case json, karabiner, fallback }

    private(set) var state: Snapshot   // mode + source + confidence + isStale
    var isEnvironmentFilePresent: Bool
}
VimHintLayer SwiftUI View
Renders a hint label on every keycap that has a corresponding vim command. hjkl get large accent-coloured arrow glyphs; other motions render as a smaller chip. Advanced hints are gated behind the user's "Show all keys" toggle. Lives as a sibling layer in OverlayKeyboardView's ZStack. Uses PhysicalLayout for key geometry.
@MainActor
struct VimHintLayer: View {
    let layout: PhysicalLayout
    let scale: CGFloat
    let keyFrame: (PhysicalKey) -> CGRect

    // Renders when adapter mode is normal/visual/operatorPending
    // Suppressed in terminal apps unless explicitly enabled
}
KindaVimTelemetryStore Local Storage
Local-only, opt-in usage counters. Records aggregate frequencies (how many times h was pressed), never sequences or timestamps. Data stored at ~/Library/Application Support/KeyPath/kindavim-telemetry.json. Off by default, user-deletable, never transmitted. Powers the insights chart that tracks vim adoption over 30 days.
Kanata vim layer Config
The vim layer is a standard Kanata (deflayer vim ...) definition. Motions are macOS shortcuts remapped to vim keys: h sends Left, w sends Option+Right, $ sends Cmd+Right. Mode transitions use push-msg to notify the state adapter. No special runtime support needed — the same mechanism that powers every other Kanata layer.

One Keystroke, Full Loop

From holding Caps Lock to telemetry recording, every step is observable and stateless.
Hold Caps Lock
Kanata activates the vim layer via tap-hold. push-msg notifies mode transition.
Adapter observes
KindaVimStateAdapter reads environment.json, emits mode: .normal snapshot.
Hints appear
VimHintLayer renders motion labels on every keycap. hjkl get large arrows, others get small chips.
Motion keys
User presses w → Kanata sends Option+Right (word forward). Standard macOS shortcut.
Release Caps Lock
Kanata returns to base layer. Adapter emits mode: .insert. Hints disappear.
Telemetry records
KindaVimTelemetryStore increments the daily counter for each motion used. Aggregate only, no sequences.

Design Rules

Four constraints that keep vim mode simple and safe.
1
Vim mode is Kanata layers, not app-level key interception
Every vim motion is defined in (deflayer vim ...). KeyPath never intercepts keystrokes itself. The same Kanata engine that runs every other layer runs vim.
2
VimHintLayer only shows in terminal apps when explicitly enabled
Terminal apps have their own vim. The hint layer checks showHintsInTerminals and suppresses itself by default in terminals to avoid visual noise over real vim.
3
The state adapter observes layers — it does not control them
KindaVimStateAdapter is read-only. It watches environment.json and emits snapshots. It never sends commands to Kanata or modifies layer state.
4
Motions are macOS keyboard shortcuts mapped to vim keys
h = Left, w = Option+Right, dd = Cmd+Shift+K, $ = Cmd+Right. No custom text engine. Every motion works anywhere macOS shortcuts work.

Related Architecture