Skip to content

az-pz/vimdose

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Vimdose

Vimium-style click hints for every app on Windows.

Press a global hotkey and Vimdose overlays letter hints on every clickable element across all the windows currently visible on your screen. Type a hint and that element is clicked — no mouse required. It's the Vimium "follow link" experience, but for native Windows apps instead of just the browser.

CI Release License: MIT .NET 9 Platform: Windows


Table of contents


Demo

┌──────────────────────────────  Notepad  ──────────────────────────────┐
│ [sa] File   [sd] Edit   [sf] View            …                         │
│                                                                        │
│   The quick brown fox jumps over the lazy dog.                         │
│                                                                        │
│                                                                        │
│ [jk] Add New Tab                                                       │
└────────────────────────────────────────────────────────────────────────┘
        ▲ press Ctrl+Alt+Space, then type "jk" to click "Add New Tab"

Press the hotkey → hints appear → type the letters → Vimdose invokes the element. Press Esc any time to cancel, Backspace to undo a keystroke.


Features

  • System-wide. Works in any app that exposes UI Automation — Explorer, Settings, Notepad, Office, WinUI/WPF/WinForms apps, Electron apps, browsers, and more.
  • Every visible window at once. Hints are generated for all on-screen windows in a single pass — not just the focused one — with front-to-back occlusion filtering so elements hidden behind another window are skipped.
  • Keyboard-only clicking. Home-row hint labels (Vimium's sadfjklewcmpgh alphabet), generated prefix-free so no hint is ever ambiguous.
  • Non-intrusive overlay. A single topmost, click-through, no-activate layer draws the hints without stealing focus, so menus and dropdowns stay open while you aim.
  • Real invocation, not just a mouse move. Prefers the element's UI Automation pattern (Invoke / Toggle / SelectionItem / ExpandCollapse) and falls back to a synthesized click at the element's clickable point — restoring your cursor afterward.
  • Incremental filtering. Type one letter to narrow the set; matching hints shrink until a single element remains and fires automatically.
  • DPI-aware. Per-monitor-v2 aware; hint placement is exact on any single-monitor scale.
  • Tiny, layered, dependency-free codebase built on the standard .NET 9 Windows stack.
  • Configurable hotkey, alphabet, hint cap, font size, and cursor behavior via a JSON file.

Requirements

  • Windows 10 (1809+) or Windows 11, x64.
  • To run a release build: nothing extra for the self-contained download; the framework-dependent download needs the .NET 9 Desktop Runtime.
  • To build from source: the .NET 9 SDK.

Install

Option A — download a release (recommended)

  1. Go to the Releases page.
  2. Download one of:
    • Vimdose-<version>-win-x64-selfcontained.zipno .NET needed, larger (~150 MB).
    • Vimdose-<version>-win-x64-framework.zip — tiny, requires the .NET 9 Desktop Runtime.
  3. Unzip and run Vimdose.exe. A tray icon appears; you're ready.

Windows SmartScreen may warn about an unsigned binary the first time — choose More info → Run anyway. See Troubleshooting.

Option B — build it yourself

See Building from source.


Usage

  1. Launch Vimdose.exe. It runs in the background with a tray icon (right-click for Edit config… and Exit).
  2. Arrange the windows you want to act on so they're visible on screen.
  3. Press the trigger chord — Ctrl+Alt+Space by default.
  4. Hint labels appear over clickable elements across every visible window.
  5. Type the hint to click. As you type, non-matching hints disappear; when one hint remains it fires automatically.
Key Action
Trigger chord Scan all visible windows and show hints
Letter keys Filter hints; the last match is clicked
Backspace Remove the last typed letter
Esc or trigger chord Cancel and hide the overlay

While hints are shown Vimdose is modal: it swallows keystrokes so your typing selects a hint instead of leaking into the underlying app.

Single instance (last one wins). Starting Vimdose.exe automatically terminates any previously running instance, so only the newest build stays resident and just one global keyboard hook is ever active. When installing an update, simply run the new build — no need to exit the old one first.


Configuration

Vimdose runs with sensible defaults. To customize, right-click the tray icon and choose Edit config… — this writes and opens:

%APPDATA%\Vimdose\config.json

Edit, save, and restart Vimdose to apply. Invalid JSON silently falls back to defaults.

{
  // Characters used to build hint labels (home-row by default).
  "HintCharacters": "sadfjklewcmpgh",
  // Modifiers for the trigger chord: any of Ctrl, Alt, Shift, Win.
  "Modifiers": ["Ctrl", "Alt"],
  // Main key of the chord (named key, e.g. Space, Semicolon, F).
  "Key": "Space",
  // Safety cap on the number of hints shown at once.
  "MaxHints": 500,
  // Restore the mouse cursor after a synthesized (fallback) click.
  "RestoreCursor": true,
  // Hint label font size, in device-independent pixels.
  "FontSize": 12.0
}
Key Type Default Description
HintCharacters string "sadfjklewcmpgh" Alphabet for hint labels. Duplicates/case are normalized; min 2 chars.
Modifiers string[] ["Ctrl","Alt"] Modifier keys held for the trigger. Any of Ctrl, Alt, Shift, Win.
Key string "Space" The chord's main key (named, e.g. Space, Semicolon, F).
MaxHints int 500 Upper bound on hints per scan (guards pathological windows).
RestoreCursor bool true Move the cursor back after a fallback mouse click.
FontSize double 12.0 Hint label font size in DIPs (min 6).

Command-line interface

Vimdose.exe doubles as a small diagnostic tool. These modes print to the console and exit.

Command What it does
Vimdose Run in the background with the tray icon (normal mode).
Vimdose --help Show usage. (-h, /? also work.)
Vimdose --selftest Validate the hint-label generator (unique + prefix-free). CI gate.
Vimdose --dump [--spawn <exe>] Scan a single window and print every clickable element with its hint and bounds.
Vimdose --dumpall Scan all visible windows and print their clickable elements, grouped by window.
Vimdose --click --match <text> [--spawn <exe>] Scan, click the first element whose name contains <text>, and re-scan to prove the effect.

With --spawn <exe> Vimdose launches the app first (e.g. --spawn notepad); without it you get 3 seconds to focus a window. Examples:

# List everything clickable in a fresh Notepad window
Vimdose --dump --spawn notepad

# Prove clicking works end-to-end
Vimdose --click --spawn notepad --match "Add New Tab"

# List clickable elements across every visible window, grouped by window
Vimdose --dumpall

# Validate hint generation
Vimdose --selftest

Note for scripts/CI: run the managed DLL through the console-subsystem host (dotnet bin/Release/net9.0-windows/Vimdose.dll --selftest) so output is captured. The GUI-subsystem Vimdose.exe apphost detaches from an existing console.


How it works

flowchart LR
    A[Global keyboard hook<br/>WH_KEYBOARD_LL] -->|trigger chord| B[Enumerate all visible<br/>top-level windows]
    B -->|UI Automation FindAll<br/>per window, front-to-back| C[Clickable elements]
    C -->|drop occluded elements| D[Assign prefix-free hints]
    D --> E[Topmost click-through overlay]
    E -->|type hint| F{Match?}
    F -->|unique| G[Invoke via UIA pattern<br/>or synthesized click]
    F -->|partial| E
    G --> H[Overlay closes, back to Idle]
Loading
  1. Global input. A low-level keyboard hook (WH_KEYBOARD_LL) watches for the trigger chord without stealing focus, so the foreground app — and any open menu — stays put. While hints are visible the hook runs a small Idle → Scanning → Hinting state machine and swallows keys so they select hints rather than leak through. A background watchdog keeps the hook armed and force-resets the state machine if a scan or overlay ever gets stuck, so the hotkey can't silently stop working (see Logging & reliability).
  2. Scan. Vimdose enumerates every visible top-level window (front-to-back in Z order, skipping minimized, DWM-cloaked, and tool windows) and, using UI Automation, does a single cached FindAll over each for elements that expose the Invoke / Toggle / SelectionItem / ExpandCollapse patterns or are common clickable control types (Button, Hyperlink, MenuItem, TabItem, ListItem, TreeItem, CheckBox, RadioButton, ComboBox, SplitButton, Edit). Offscreen, disabled, zero-size, duplicate, and occluded elements — those whose centre is hidden behind a window nearer the front — are filtered out, and a window fully covered by a nearer one is skipped entirely. The scan runs off the UI thread to keep the hook responsive.
  3. Hints. Labels are generated with Vimium's breadth-first, prefix-free algorithm over the configured alphabet (14 chars → 14 one-letter hints, then two-letter up to 196, etc.), guaranteeing no label is a prefix of another.
  4. Overlay. A single topmost, layered, click-through, no-activate WPF window spanning the whole virtual desktop draws the labels on a canvas. Physical pixels are converted to DIPs using the target window's per-monitor DPI scale.
  5. Click. On a unique match Vimdose invokes the element via its UIA pattern (Invoke → Select → Toggle → ExpandCollapse); if none is available it synthesizes a left click at the element's clickable point with SendInput, optionally restoring the cursor.

Project layout

vimdose/
├─ Vimdose.csproj              # net9.0-windows, WPF + WinForms interop, single StartupObject
├─ app.manifest               # PerMonitorV2 DPI awareness, asInvoker
├─ src/
│  ├─ Program.cs              # STA entry point; dispatches CLI modes or runs the app
│  ├─ App.cs                  # Tray icon, hook wiring, state machine + watchdog, logging
│  ├─ Config.cs               # Defaults + %APPDATA%\Vimdose\config.json loader
│  ├─ SingleInstance.cs       # Last-one-wins: terminates previous instances on startup
│  ├─ Interop/
│  │  └─ NativeMethods.cs     # P/Invoke: hook, window styles, virtual screen, SendInput
│  ├─ Input/
│  │  ├─ KeyboardHook.cs      # WH_KEYBOARD_LL wrapper (supervised: reinstallable)
│  │  ├─ KeyMap.cs            # Key-name → virtual-key mapping, modifier checks
│  │  └─ ModalStateMachine.cs # Pure Idle/Scanning/Hinting state machine + watchdog (unit-tested)
│  ├─ Automation/
│  │  ├─ ClickableElement.cs  # Scanned element model (bounds, name, control type, label)
│  │  ├─ WindowEnumerator.cs  # Lists visible top-level windows in Z order
│  │  ├─ ClickableScanner.cs  # Cached UIA FindAll across all visible windows + occlusion
│  │  └─ ElementClicker.cs    # Pattern invocation + synthesized-click fallback
│  ├─ Hints/
│  │  ├─ HintLabelGenerator.cs# Prefix-free Vimium label algorithm
│  │  └─ FilterResult.cs      # Filter outcome (None/Partial/Unique)
│  ├─ Overlay/
│  │  └─ OverlayWindow.cs     # Topmost click-through hint layer
│  ├─ Diagnostics/
│  │  └─ Logger.cs            # Thread-safe file log (%LOCALAPPDATA%\Vimdose\logs)
│  └─ Cli/
│     └─ CliMode.cs           # --selftest (+ state-machine tests) / --dump / --dumpall / --click / --help
├─ .github/workflows/         # ci.yml (PRs) and release.yml (main)
├─ LICENSE                    # MIT
└─ README.md

Logging & reliability

Vimdose is a background app with no window, so it writes a log you can inspect when something looks off.

  • Location: %LOCALAPPDATA%\Vimdose\logs\vimdose-YYYYMMDD.log (one file per day). Open the folder quickly from the tray menu → Open log folder….
  • What's logged: startup, keyboard-hook install/reinstall, each trigger, scan start/finish (element count + duration), overlay show/close, hint activations, watchdog recoveries, exceptions, and a heartbeat every 30 s. Individual keystrokes are not logged.

Self-healing. Two mechanisms keep the hotkey working indefinitely:

  1. Supervised hook. A low-level keyboard hook can be silently dropped by Windows if a callback ever runs longer than the system LowLevelHooksTimeout (~300 ms). A 2-second watchdog re-arms the hook if it's gone and, while idle, proactively reinstalls it every 30 s. Slow hook callbacks are logged as warnings so you can spot the cause.
  2. Un-wedgeable state machine. The Idle → Scanning → Hinting logic lives in a pure, unit-tested ModalStateMachine with built-in timeouts. If a scan never returns or an overlay is left up too long, the watchdog forces everything back to Idle and tears down any stale overlay — the app recovers instead of appearing frozen.

These paths are covered by regression tests in Vimdose --selftest (the same gate CI runs), so a change that reintroduces a stuck state fails the build.


Building from source

git clone https://github.com/az-pz/vimdose.git
cd vimdose

# Build
dotnet build -c Release

# Run
dotnet run -c Release

Publishing a standalone binary

# Self-contained single file — no .NET install needed on the target machine
dotnet publish Vimdose.csproj -c Release -r win-x64 `
  --self-contained true `
  -p:PublishSingleFile=true `
  -p:IncludeNativeLibrariesForSelfExtract=true `
  -o dist/selfcontained

# Framework-dependent single file — tiny, needs the .NET 9 Desktop Runtime
dotnet publish Vimdose.csproj -c Release -r win-x64 `
  --self-contained false `
  -p:PublishSingleFile=true `
  -o dist/framework

The result is a single Vimdose.exe in the chosen output folder.

WPF apps don't support IL trimming or Native AOT, so the self-contained build bundles the full runtime (~150 MB). Use the framework-dependent build if size matters.


Continuous integration & releases

Two GitHub Actions workflows run on windows-latest:

  • ci.yml — on every pull request to main (and manual dispatch): restore, dotnet build -c Release, then run the --selftest gate.
  • release.yml — on every push to main (i.e. when a PR is merged) and on manual dispatch: build, self-test, read <Version> from Vimdose.csproj, publish both the self-contained and framework-dependent single-file binaries, zip each with the README and LICENSE, and publish a GitHub Release tagged v<version>.<run-number> with auto-generated notes and both zips attached.

To cut a new versioned release, bump <Version> in Vimdose.csproj and merge to main.


Troubleshooting

Hints stop appearing after a while (the hotkey goes dead). This should no longer happen: a background watchdog re-arms the keyboard hook and recovers any stuck state automatically. If you ever hit it, open the tray menu → Open log folder… and check the latest vimdose-*.log for WARN/ERROR lines (e.g. "Slow hook callback" or a watchdog recovery) and please attach them to a GitHub issue. See Logging & reliability for details.

No hints appear in a specific app. Some apps expose little or no UI Automation tree (certain custom-drawn or game UIs). Run Vimdose --dumpall (or --dump with that window focused) to see exactly what Vimdose can find.

A window I can see isn't getting hints. Vimdose skips elements whose centre is hidden behind a window nearer the front, and skips any window fully covered by another. Bring the window forward or move the covering window aside, or inspect what was detected with Vimdose --dumpall.

Nothing happens in an app running as administrator. Windows blocks a normal-privilege process from sending input to, or hooking keys for, an elevated window. Run Vimdose.exe as administrator to interact with elevated apps.

Hints look offset on a high-DPI or multi-monitor setup. Placement is exact on a single monitor at any scale. Mixed-DPI multi-monitor layouts are best-effort; please file an issue with your monitor/scale configuration.

SmartScreen or antivirus flags the download. The binary isn't code-signed, and a global keyboard hook can look suspicious to heuristics. Choose More info → Run anyway, or build from source if you prefer.

The hotkey conflicts with another app. Change Modifiers/Key in config.json (tray → Edit config…) and restart.


Roadmap

  • Smarter occlusion (per-pixel region subtraction) and per-monitor DPI-exact placement across mixed-DPI multi-monitor setups.
  • Right-click, middle-click, and hover activation modifiers.
  • Configurable overlay theme (colors, matched/unmatched styling).
  • Scrolling and text-selection ("caret") hint modes, closer to full Vimium parity.
  • Signed release binaries.

Contributing

Contributions are welcome! Please read CONTRIBUTING.md for build steps, the self-test gate, coding conventions, and commit-message guidance. Bug reports and feature requests can use the provided issue templates.


License

Released under the MIT License.

Vimdose is inspired by Vimium but is an independent project and not affiliated with it.

About

Vimium for Windows. Mouseless navigation for Windows using element hints.

Topics

Resources

License

Contributing

Stars

1 star

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages