A browser Vim tutor, inspired by vim-hero's short-lesson format — small lessons of 1–4 keys, each with one practice
challenge — that goes deeper: registers, macros, ex commands, regex, windows, quickfix, and the
Neovim plugins people actually run. The lesson plan is in CURRICULUM.md.
npm install
npm run dev # http://localhost:5317
npm test # engine + every lesson's reference solution
npm run build # typecheck + production bundle in dist/src/vim/— a Vim engine written from scratch in TypeScript (modes, operators, text objects, registers, macros, dot-repeat, undo, Vim regex, ex commands, splits, quickfix, folds) plus plugin emulations insrc/vim/plugins/.src/lessons/— lesson content (sections/*.tsx), the challenge runtime, and the validator. Authoring guide:docs/LESSONS.md; plugin API:docs/PLUGINS.md.src/components/— the UI.
npm run check:nvim (needs nvim on PATH) replays lessons and ad-hoc cases in nvim --clean --headless:
ok N bad M— every plain-text round's reference solution must leave the goal text in Neovim. This is the gate: the script fails whenbadis not 0.extended: ok N bad M— the engine's own result for each round against Neovim's: cursor (Neovim defaults,nostartofline), the unnamed register's text and type, any register the goal names, and the text/cursor after follow-up probes (x, pluspwhen the register is non-empty) fed after the solution.cases: ok N bad M— the same engine-vs-Neovim comparison forscripts/nvimcheck/cases.json, a list of{id, text, keys, cursor?, name?, options?}repros that are not lesson rounds. Append to it freely.
All three sections gate the run (strict by default); NVIMCHECK_STRICT=0 npm run check:nvim relaxes it to the text gate only.
Browsers reserve Ctrl-W/N/T/Q. The practice editor maps Alt-W/N/T/Q to them, and its "full screen" button uses the Keyboard Lock API to capture the real keys (Chromium).
The Go backend in server/ handles GitHub sign-in, syncs lesson runs to SQLite,
and serves the built SPA. It needs Go 1.26+ and no cgo.
cd server
cp .env.example .env # fill in GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET
set -a; . ./.env; set +a
go run ./cmd/vimchi- Dev: run
npm run devalongside it; Vite proxies/apiand/authto:8080. Set the GitHub OAuth app's callback to$VIMCHI_BASE_URL/auth/github/callback(the Vite origin). - Prod:
npm run build, then run the server withVIMCHI_STATICpointing atdist/,VIMCHI_BASE_URLset to the public origin andVIMCHI_SECURE_COOKIES=1if TLS terminates at a proxy.
Tests: cd server && go test ./...
The frontend also works on its own as a static site (dist/): without the server, progress
stays in the browser's localStorage and sign-in is unavailable.
The only instance is the public one, https://vimchi.dev, on a DigitalOcean droplet: see
deploy/do/README.md. deploy/do/deploy.sh builds the image here (Dockerfile builds the SPA
with node and the server as a static Go binary into one image), ships it over SSH, restarts and
smoke-tests. GET /healthz is the uptime probe (200 {"ok":true} when SQLite answers).
docker/docker-compose.yml + docker/up.sh run the same image locally on the
nginx-proxy-manager_default network with SQLite bind-mounted at var/ (no host port). The
tailnet instance that used them (vimchi.nrsil.io) was torn down on 2026-09-30; they remain as a
local/staging recipe. up.sh injects VIMCHI_GITHUB_CLIENT_ID / VIMCHI_GITHUB_CLIENT_SECRET
from keys when they exist; without them the tutor runs with sign-in disabled.
The plugin lessons emulate the default keymaps (LazyVim's where the starters differ, such as
mini.surround on gsa / gsd / gsr; flash.nvim is on in every lesson) of mini.surround, mini.ai and
nvim-treesitter-textobjects, flash.nvim, telescope.nvim, snacks.explorer and neo-tree, gitsigns.nvim, lazygit,
grug-far.nvim and a LuaSnip/blink.cmp-style completion and snippet flow; their code is not
included. Fonts (IBM Plex Sans, JetBrains Mono, Space Grotesk) load from Google Fonts under
the SIL Open Font License.
Challenge files (src/challenges/corpus/) are short excerpts from these repos, used under their
licenses, attributed per file, with their license texts shipped at /THIRD-PARTY-NOTICES.txt
(public/THIRD-PARTY-NOTICES.txt): TheAlgorithms/Go (MIT), TheAlgorithms/TypeScript (MIT), charmbracelet/lipgloss (MIT), echasnovski/mini.nvim (MIT), google/uuid (BSD-3-Clause), lewis6991/gitsigns.nvim (MIT), stevearc/oil.nvim (MIT), unjs/ufo (MIT).
MIT — see LICENSE.
