A buffer-native video experiment for Neovim, backed by a small Rust engine. It does not run a terminal UI inside Neovim and does not require Deno, Bun, or ffmpeg during playback.
The project currently provides:
bav-format: the indexed BAV2 keyframe and XOR-run delta format;bav-encode: a dependency-free P5 PGM sequence encoder;bav-engine: real-time scaling, Braille rendering, and changed-row output;- a Lua client that applies row patches to a regular Neovim scratch buffer.
For development:
- Neovim 0.11 or newer
- Rust and Cargo
For playback:
- Neovim 0.11 or newer
curlandffmpegduring the one-time local media generation
Normal playback after that only needs Neovim and the locally generated assets. It does not require Rust, ffmpeg, Deno, Bun, Node.js, or a compression tool.
With lazy.nvim:
{
"satotek/bad-apple.nvim",
config = true,
}Run the player:
:BadAppleOn first use, the plugin downloads the platform-specific Rust player and
encoder, then downloads the source PV to your machine and uses ffmpeg to
generate the high-resolution BAV2 movie and MP3 audio locally. No movie or
audio asset is distributed in this repository or its GitHub Releases. Files
are stored under stdpath("data")/bad-apple.nvim, outside the plugin checkout.
Remove that directory to force a fresh automatic installation.
Build the engine:
cargo build -p bav-engineLoad a local checkout with lazy.nvim:
{
dir = vim.fn.expand("~/ghq/github.com/satotek/bad-apple.nvim"),
config = function()
require("bad-apple").setup({
engine_path = vim.fn.expand("~/path/to/bav-engine"),
movie_path = vim.fn.expand("~/path/to/movie.bav"),
})
end,
}During local development, target/debug/bav-engine is detected automatically
when neither engine_path nor a bav-engine executable on PATH is found.
:BadApplePlay ~/.local/share/bad-apple/movie.bav
:checkhealth bad-appleInside the player buffer:
<Space>pauses or resumes playback.mtoggles audio mute.hseeks backward five seconds.lseeks forward five seconds.rrestarts from the beginning.qstops playback and closes the buffer.
bav-encode intentionally accepts simple binary PGM images rather than video.
This keeps video codecs and media tools outside the core format and player.
cargo run -p bav-encode -- movie.bav 30 frames/*.pgmEvery input image must use the P5 format, 8-bit samples, and the same size. Pixels with a value of at least 128 become lit bits.
The file starts with fixed metadata and ends with an index table. Each indexed one-second chunk is compressed independently with zstd, starts with a full keyframe, and stores subsequent frames as XOR runs relative to the previous frame. Seeking therefore decompresses at most one small chunk instead of scanning the movie from the beginning.
The Rust engine reconstructs source frames, scales them directly from packed 1-bit pixels, converts each 2x4 dot group to one Unicode Braille character, and sends only changed UTF-8 rows to Neovim over a length-prefixed protocol. The release movie is generated at 480x360 from the source PV. Audio is decoded inside the Rust engine, and its playback position drives video frame selection. If no audio device is available, playback continues silently. The Lua client also tracks the player window size and asks the engine to rerender the current frame when the window changes.
cargo fmt --all -- --check
cargo test --workspace
cargo build -p bav-engine
BAD_APPLE_TEST_ENGINE=$PWD/target/debug/bav-engine \
BAD_APPLE_TEST_MOVIE=/path/to/test.bav \
nvim --headless -u NONE -l tests/player.luaTagged releases publish only the Rust player and encoder binaries for Apple Silicon macOS, x86-64 Linux, and ARM64 Linux. The source PV, derived BAV2 movie, and MP3 audio are never included. Media conversion happens on the user's own machine during the first run.
See NOTICE.md for source and attribution information.