Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

27 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

fingers.nvim

Watches how you actually edit, and tells you — in one buffer, when you ask — where there was a shorter way. Your config says how you mean to edit; this measures how you actually do.

fingers.vim.pro · :h fingers

Everything is observed live. Nothing interrupts you: findings surface only in the fingers buffer, which you open with :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
  <leader>xx          diagnostics list

beyond the leader — plugins & remaps   0 of 1 pressed

  never fired · 1
  ys          add surround

counted locally; nothing has left this machine

The never-fired lists are the point. Everything else you already knew — except perhaps the pause: a mapping you stop in front of every time has a recall cost its fire-count can't see.

Local by default, and complete that way

No account. No server. Nothing automatic ever touches the network, and nothing is ever sent. Two calls exist, both explicit and both download-only: :Fingers census and :Fingers practice, each a GET of a public page on vim.pro (below).

Counts live in a single JSON file in your own data directory:

:lua= require("fingers.store").path()

That file holds mapping names, counts, dates, timings, and short normal-mode command phrases (the mining table). It does not hold insert-mode text, the files you opened, or anything about your projects — printable characters in insert mode are dropped at the watch layer's gate before any other module can see them, a rule the test suite enforces rather than documents. Read the file, diff it, delete it — deleting it erases your history completely, and the plugin will start over without complaint.

Install

Any plugin manager, no configuration required.

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

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

Then edit normally for a week and run :Fingers.

Usage

command what it does
:Fingers the fingers buffer (q closes it)
:Fingers all the same, with the never-fired cap lifted
:Fingers export the report as a markdown buffer, for sharing
:Fingers census fetch the vim.pro mapping census (a public aggregate)
:Fingers practice fetch your vim.pro practice record, to see what transferred
:Fingers live per-session whispers after a finding — same command off
:Fingers rescan re-read your mappings (after a lazy-loaded plugin lands)
:Fingers reset forget every count and finding, start over
:Fingers flush write to disk now (otherwise: on exit)

scripts/fingers renders the findings half in a terminal, from the counts file alone.

Lowercase :fingers works too. Neovim reserves lowercase command names, so it's a command-line abbreviation rather than a command of its own: it expands only when it's the whole line, and nothing built-in answers to that word (:fin[ish] stops at finish). Set abbrev = false to leave it alone.

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, findings only
  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

:checkhealth fingers explains the usual problem, which is a lazy-loaded config whose mappings didn't exist yet when counting started.

What it will and won't say

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.

The current inventory, every entry naming its shorter form: runs of j k h l x X w b n N and the arrow keys in normal mode; runs of <BS> and arrows inside insert mode (seen through the control-keys-only gate); retyping f{char} for the target ; already remembers; and v + one motion + operator where operator + motion is a key shorter.

Some things are counted but never coached, because no honest arithmetic applies: mouse clicks and scrolling (for anyone leaving the mouse behind, the truest metric is that line shrinking on its own), and sightings of native techniques like ciw and . — evidence of practice, not a homework list.

The phrases section is the census inverted: recurring normal-mode phrases with no name yet, each earning its place by (keys − 2) × count — what a two-key mapping would have saved. Phrases you already bound are excluded; noticing you never use them is the census's job.

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.

It watches the key stream (vim.on_key) and matches it against the mappings your editor reports. Your mappings are never wrapped or re-registered, so nothing here can change what they do or break an expr or <Plug> mapping.

Things that deliberately do not count:

  • Replayed macros. A macro is one decision, however many keys it emits. Counting the replay would inflate exactly the mappings you automated.
  • Dot-repeats. . replays the change, not your keystrokes, so repeating a mapping's effect doesn't credit the mapping again. Press <leader>x then . and the count is 1: your fingers reached the mapping once.
  • Keys a mapping produced. If <leader>x expands to ciwfoo<Esc>, the ciw inside it is not a technique you typed — one decision, already counted.
  • Ambiguous prefixes, until they resolve. If you have both <leader>g and <leader>gs, typing <leader>gs credits only <leader>gs — the same rule vim itself uses via timeoutlen.
  • nvim's own stock mappings. The census is what you added. The baseline is measured against a bare nvim --clean and subtracted, not guessed from metadata.
  • Insert-mode text, ever.

The census cross-reference

Your editor can say "you never press <leader>gd". Only a population can finish the sentence. :Fingers census fetches the vim.pro mapping census — one GET of a public aggregate, nothing sent — and from then on the never-fired list carries facts like:

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

Delete or learn: the data is stated, the verdict stays yours. The result is cached in the counts file, so the annotation works offline until you refresh.

Practice transfer

A drill teaches you a technique. Whether it stuck is a separate question, and a harder one to answer.

:Fingers practice reads your public vim.pro profile — which techniques you drilled, when, and the key pattern each one looks like — and from then on watches for those patterns in your real editing. The practice record comes from the site; the keys are counted here:

drilled, then   2 of 5 reached your editing

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

Only sightings on or after the day you drilled count. One from before means you already knew it, and calling that transfer would take credit for the drill.

Download only. The pattern comes down, the match runs here. Nothing about your editing is sent, and there is no code in the plugin that could send it. Set login and run it:

require("fingers").setup({ login = "your-vim-pro-login" })

What it can't watch, and says so. Techniques typed on the command line — :g, :s, :norm, :sort — can never match, because fingers never reads what you type after :. Those rows say not watched with the reason rather than showing a zero, because silence from a blind spot is not evidence of failure.

Known limits

  • A mapping installed after counting starts is invisible until the first idle rescan (or :Fingers rescan). Counting begins at VimEnter.
  • Buffer-local mappings (an LSP on_attach, a filetype plugin) are not in the census — nvim_get_keymap reads the global table.
  • A mapping that overrides one of nvim's stock lhs (say, rebinding gcc) is subtracted with the stock vocabulary. The count still accumulates; the row doesn't render yet.
  • Counts are held in memory and written on exit — atomically, and merged, so two instances no longer clobber each other; a hard crash still loses the current session rather than corrupting the file.

Connecting to vim.pro

Two connections exist — the census cross-reference and practice transfer — and both are the same shape on purpose: an explicit command, a GET of a public page, nothing sent, cached locally. The site hands down what it knows and every join runs on your machine.

This plugin is useful with the network unplugged; that's the design, not a staging post. What's still missing is what an editor cannot know on its own:

  • History. 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.
  • Your editing, read by the coach that already reads your config.

Both would need an account, and neither would change the rule above: nothing leaves without you asking, every time.

The direction of travel is fixed: 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.

Development

./scripts/test          # every spec, headless
./scripts/test watch    # just the matching ones

tests/integration_spec.lua drives a real child nvim over RPC and types at it. It's there because a full unit suite once passed while the plugin counted nothing at all: the matcher was right, the storage was right, and the key handler read the wrong argument. Only real keystrokes catch that.

License

MIT

About

Your config says how you mean to edit; fingers.nvim measures how you actually do. Local-first, no account, nothing sent.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages