a plugin for neovim

Where there was a shorter way.

Your config says how you mean to edit. fingers.nvim measures how you actually do — in one buffer, when you ask.

what you pressed
14 keys
j j j j j j j j j j j j j j
the shorter way
3 keys
1 4 j

14 against 3 is a sum anyone can check. That is the only kind of thing this plugin will say to you.

{ "vim-pro/fingers.nvim" } No configuration required. Then edit normally for a week and run :Fingers.
the whole interface

One buffer, when you ask for it.

Everything is observed live and nothing interrupts you: no notification, no virtual text, no flash at the moment you press the key. Being corrected mid-edit is how a coach becomes a thing you disable. It waits in the fingers buffer, which q closes.

fingers://you:Fingers
fingers.nvim   how you actually edit

this week, one thing

  j×14 in a row   ×9 this week
      try: {count}j, } , or <C-d>

what your editing looked like

  j×14 in a row   ×31   this wk 9 · last 22 ↘
      moving down a line at a time
      try: {count}j, } , or <C-d>

  f{char} retyped for the same target   ×8
      repeating a find by spelling it out again
      try: ; repeats the last f/t in one key (and , goes back)

  <BS>×11 in insert mode   ×6
      erasing a character at a time
      try: <Esc>, then db or daw — or <C-w> without leaving insert

drilled, then   2 of 5 reached your editing

  visual-block   drilled 9d ago   ×9 since
  macro          drilled 9d ago   ×2 since
  increment      drilled 1d ago   not once since
  global         drilled 9d ago   not watched — typed on the command line, which fingers never reads

phrases you keep typing   a mapping nobody wrote yet

  A;j  ×23   a 2-key mapping would have saved 23 keys

techniques in the wild

  .    ×48   dot repeat
  ciw  ×12   change inner word
  never sighted · 23   @@ ; , caw diw daw dap yiw ci( ci" …+13

the mouse

  clicks ×132 · scroll ×460   this wk 49 · last 126 ↘

your mappings   4 of 6 pressed

  <leader>ff    214   2026-07-26   find files
  <leader>gs     89   2026-07-27   git status
  <leader>ca     41   2026-07-26   code action   · 1.6s pause before it
  <leader>rn     12   2026-07-18   rename

  never fired · 2
  <leader>gd          git diff   · 3 of 5 census configs bind this
  <leader>xx          diagnostics list   · nobody on the census binds it either

beyond the leader — plugins & remaps   0 of 1 pressed

  never fired · 1
  ys          add surround   · nobody on the census binds it either

census: 5 configs, fetched 2026-07-27 — refresh with :Fingers census
counted locally; nothing has left this machine

Made-up mappings; the numbers are only ever your own. In your editor the buffer takes its colors from your colorscheme — it links to Title, String, Identifier, Number, DiagnosticWarn and Comment, all overridable.

The never-fired lists are the point.

Everything else you already knew — except perhaps the pause. A mapping you stop in front of for 1.6 seconds every time has a recall cost its fire-count can't see.

Mappings are read from the editor via nvim_get_keymap, never by parsing your config. Everything typable counts — a plugin's ys as much as your <leader>gs. Neovim's own defaults are excluded.

Counted honestly, or not at all.

  • Replayed macros don't count. A macro is one decision, however many keys it emits. Counting the replay would inflate exactly the mappings you automated.
  • Dot-repeats don't count. . replays the change, not your keystrokes. Press <leader>x then . and the count is 1: your fingers reached the mapping once.
  • Keys a mapping produced don't count. If <leader>x expands to ciwfoo<Esc>, that ciw is not a technique you typed — one decision, already counted.
  • Ambiguous prefixes wait to resolve. With both <leader>g and <leader>gs bound, typing <leader>gs credits only <leader>gs — the same rule vim itself uses via timeoutlen.
local by default

And complete that way.

No account. No server. Nothing automatic touches the network and nothing is ever sent. Two calls exist — :Fingers census and :Fingers practice — both explicit, both a GET of a public page, both download-only. :checkhealth fingers states it as a check rather than a promise.

one file

Counts live in your data directory

A single JSON file, and nowhere else. Read it, diff it, delete it.

:lua= require("fingers.store").path()
-- stdpath("data")/fingers/counts.json
what it holds

Mapping names, counts, dates, timings

Plus short normal-mode command phrases. Never insert-mode text, the files you opened, or anything about your projects.

deleting it

A complete uninstall of your history

Delete the file and your history is erased entirely; the plugin starts over without complaint. :Fingers reset does the same from inside the editor.

what it watches

Insert mode passes control keys only

Insert is where text lives, so its gate admits <BS>, arrows and control bytes — never a printable character. A backspace run can be counted without your prose ever being readable. The contents of a : or / line are never recorded.

It reads the key stream via vim.on_key. Your mappings are never wrapped or re-registered, so nothing here can change what they do, or break an expr or <Plug> mapping.

one rule governs every detector

The waste has to be arithmetic, not aesthetic.

“You pressed j fourteen times to move fourteen lines” is a fact, and 14 keys against 3 is a sum anyone can check.

“You use w more than f” is taste, it's usually wrong, and it will never appear here — both are correct for different jobs.

There is no score, no streak, and no efficiency rating. A shorter way to do the thing you just did is coaching; a percentage is a game, and this isn't one.

What it notices

Runs of a single key past run_threshold (five by default), where the alternative is named rather than gestured at:

jkhlxXwbnN

Plus runs of and arrows inside insert mode (through the control-keys-only gate); retyping f{char} for the target ; already remembers; and selecting before operating — v, one motion, then d/y/c, one key more than the operator and motion for the identical edit.

Some things are counted but never coached, because no honest arithmetic applies: mouse clicks and scrolling, and sightings of native techniques like ciw and ..

The phrases section points the rule at absence. A phrase you keep typing is worth (keys − 2) × count — what a two-key mapping nobody wrote would have saved.

install

Any plugin manager, no configuration.

-- lazy.nvim
{ "vim-pro/fingers.nvim" }

-- vim.pack
vim.pack.add({ "https://github.com/vim-pro/fingers.nvim" })

Commands

commandwhat it does
:Fingersthe fingers buffer (q closes it)
:Fingers allthe same, with the never-fired cap lifted
:Fingers exportthe report as a markdown buffer, for sharing
:Fingers censusfetch the vim.pro mapping census — a public aggregate
:Fingers practicefetch your vim.pro practice record, to see what transferred
:Fingers liveper-session whispers after a finding; same command turns them off
:Fingers rescanre-read your mappings after a lazy-loaded plugin lands
:Fingers resetforget every count and finding, start over
:Fingers flushwrite to disk now (otherwise: on exit)
scripts/fingersthe report in a terminal, from the counts file alone
:checkhealth fingerswhy it might be counting nothing — usually a lazy-loaded config

Lowercase :fingers works too — a command-line abbreviation, since Neovim reserves lowercase command names. Set abbrev = false to leave the command line alone.

Configuration

require("fingers").setup({
  modes = { "n", "x" },   -- modes counted; insert passes only control keys
  census = "all",         -- count every typable mapping; "leader" narrows it
  coach = true,           -- false = mapping counts only, no live observer
  techniques = true,      -- tally native idioms (ciw, ., *, ``…) seen in the wild
  mine = true,            -- mine recurring phrases: the mapping you don't have yet
  run_threshold = 5,      -- repeats of one key before it's worth mentioning
  weekly_note = false,    -- opt-in: one notify line, at most weekly
  site = "https://vim.pro", -- where census and practice read from
  login = "",             -- your vim.pro login, for :Fingers practice
  abbrev = true,          -- let `:fingers` expand to `:Fingers`
  enabled = true,         -- false loads the plugin without counting
})

require("fingers").stats() -- the numbers, for your own statusline
known limits

Stated here rather than discovered later.

Late mappings need a rescan

Counting begins at VimEnter and rescans once on the first idle moment, which covers most lazy-loading. For the rest, :Fingers rescan.

A hard crash loses the session

Counts are held in memory and written on exit — atomically and merged, so two open editors don't clobber each other. A crash costs the current session rather than corrupting the file. :Fingers flush writes immediately.

Buffer-local mappings aren't counted

An LSP on_attach or filetype-plugin mapping lives in a buffer's own table, which the census doesn't read. A mapping that overrides a Neovim default (a rebound gcc) is excluded with the defaults — it still counts, it just doesn't render.

opt-in

Connecting to vim.pro.

The plugin is useful without it — that's the design, not a staging post. The site knows things an editor cannot, and that asymmetry is the only reason to connect.

the population — built

:Fingers census fetches the public mapping aggregate (one GET, nothing sent) and your never-fired list gains its missing fact: “3 of 5 census configs bind this” — or “nobody on the census binds it either.” Delete or learn; the verdict stays yours.

practice transfer — built

A drill teaches you a technique; whether it stuck is a separate question. :Fingers practice reads which techniques you drilled and what each looks like as keys, then watches for them in real work. The practice record comes from the site; the keys are counted here.

history — planned

Your config's changelog lives on vim.pro, so a mapping can be dated. “Added fourteen months ago, never fired” and “added Tuesday” are different advice.

vim.pro hands down what it knows, the plugin computes everything locally, and only aggregates you explicitly agree to are ever uploaded — never keys, never phrases, never text.