iPad app for learning Neovim
nvim-tutor
An iPad app that teaches Vim motions by running real Neovim on the iPad, with a SwiftUI game layer on top.
nvim-tutor teaches Neovim on an iPad by running the real editor locally, in an emulated Alpine Linux that ships inside the app. You type dw, Neovim deletes the word, and the app scores what actually happened to the buffer.
01 / 05 · mission briefing
Motivation
Learning Vim on an iPad usually means SSH into another machine or a minigame that fakes the motions. I wanted the real editor, on the device, with no network, and a learning loop that feels like a game instead of the builtin :Tutor.
So the bet was a local Linux runtime in the style of Litter and iSH, a small dedicated Neovim profile, and SwiftUI for missions, hints, scoring and review. Neovim stays the source of truth for every edit. AI coaching can come later, but the core loop has to work offline and deterministically.
Capabilities
- Boots an Alpine 3.21 aarch64 filesystem through a vendored iSH ARM64 emulator and opens a real PTY inside the app.
- Renders the live terminal with a vendored Ghostty renderer, with a native cell-grid fallback.
- First run installs
git,bash,neovim,ripgrep,fdand friends from 41 bundled APKs, checksum-verified and installed with no network. - 20 lessons across Motion, Words, Search, Operators and Objects, from
5jand/beacontod2w,di"andca{. - Each submitted command plays inside the live Neovim session, and a generated Lua module checks the buffer lines and cursor before the result reaches SwiftUI.
- Scores, streaks, ranks, achievements, per-lane mastery, retry drills, a spaced review queue and a campaign map.
- Route coaching that splits a command into count, operator and motion, and explains exactly where a miss went wrong.
- In-app runtime evidence, a keyboard audit and a physical-validation checklist for the release gates.
Key decisions
Real Neovim owns editing
Why
SwiftUI never reimplements a Vim motion. Neovim processes the keys and changes the buffer, and the overlay only observes, guides and scores.
What it cost
Grading dw needs an emulator, a PTY, a renderer and a bridge, and the lessons depend on the runtime booting cleanly.
The app owns its runtime
Why
Litter and litter-ish were references, never build inputs. The app links its own Rust wrapper over a vendored iSH tree, its own Ghostty archives and its own Alpine filesystem, and a guard script fails the build if a forbidden reference path sneaks back in.
What it cost
The repo carries large Ghostty archives in Git LFS and a vendored C emulator that has to be re-vendored by hand.
A locked, offline package core
Why
The first-run packages are pinned by a lock digest, checked by SHA-256 and installed with apk --no-network. There is no repository URL and no untrusted-install fallback.
What it cost
Those APKs include copyleft packages, so shipping them means delivering their source too, which isn't solved yet.
Ship it as GPLv3
Why
Statically linking an iSH-derived emulator makes the app a GPL combined work, and iOS offers no useful process split. iSH's App Store covenant only covers otherwise compliant distribution.
What it cost
Undoing this means replacing the runtime and re-running every compatibility check against the new one.
Architecture
Commands go from the SwiftUI console into a live PTY on the emulated Alpine system, where Neovim runs them. A Lua bridge writes the verdict back as JSONL, and Ghostty draws the terminal.
Limits and what's next
- Proven in the iOS simulator. Physical iPad validation, including hardware keyboard input, is the remaining release gate.
- Not distributable yet. The bundled copyleft packages need a source-delivery path first, and a build gate enforces that.
vim-be-goodisn't bundled because I couldn't confirm its license, so the app ships its own small Neovim game instead.- System clipboard integration inside the runtime is future work.
Notable
It boots Linux to grade dw
dw, the app types :TutorSubmit dw into a real Neovim running on an emulated ARM64 Alpine system inside the iPad app. A Lua function then compares the actual buffer lines and cursor with the lesson's expected state, and SwiftUI only shows the verdict. A wrong answer on the counted lesson really does delete one word, and the coach explains why that missed.