Skip to content

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.

Jun to Jul 2026Proven in the iOS simulator
9:41Wed Oct 21
Delete word
ReadyMission 10/20Operators

Delete word

Delete one word from the cursor.

Use an operator plus motion to remove stale.

dOperatorwMotion

Target

L1 C1

Cursor

L1 C1

Keys

2 keys

Reward

150 pts

Live iSHLIVE 58x10 · GHOSTTY · nvim-ipad-tutor
1stale token remains ← target
~
~
~
~
~
~
~
NORMALnvim-tutor://operators-dw-01[+]1,1All
:command
dwExpected dw0/2
d takes a motion. Real Neovim checks 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, fd and friends from 41 bundled APKs, checksum-verified and installed with no network.
  • 20 lessons across Motion, Words, Search, Operators and Objects, from 5j and /beacon to d2w, di" and ca{.
  • 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-good isn'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

When you submit 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.

Stack and numbers

SwiftSwiftUIRustiSH ARM64 emulatorAlpine Linux 3.21Neovim + LuaGhostty rendererXcodeGenXCTest
Commits
46
Days of work
14
App Swift
19
k lines
XCTest cases
341