The crate layout, cargo commands, and the Makefile.
This page is for working on ctrlvim itself, or building it without the install script. See Installation if you just want the cvi binary on your system.
Rust 1.80 or newer, and a C compiler, since ctrlvim vendors Lua 5.4 and tree sitter. See the Requirements section of Installation for platform specific notes.
ctrlvim is a Cargo workspace of twelve library crates plus the ctrlvim binary crate that produces cvi. Each crate handles one concern; see Architecture for what each one owns.
shcargo run -p ctrlvim # launch the editor
cargo run -p ctrlvim-core # headless demo, exercises the engine with no UI
cargo test --workspace # run every crate's test suite
cargo clippy --workspace --all-targets
cargo run -p ctrlvim is a debug build, which is the right choice while iterating: the dev profile keeps ctrlvim's own crates unoptimized so breakpoints and backtraces still work, but raises the optimization level for dependencies. Ropey and the tree sitter grammars are the two places nearly all the per keystroke cost lives, and an unoptimized build of those is three to four times slower for no debugging benefit, which is why the workspace overrides [profile.dev.package."*"] rather than leaving the whole dev profile unoptimized.
shmake # cargo build --release --package ctrlvim, produces target/release/cvi
make test # cargo test --workspace
make lint # cargo clippy --workspace --all-targets
The Makefile exists mainly to wire up installation on top of the plain cargo build; see Installation for make install and the macOS universal binary targets.
The ctrlvim-tui crate's render tests draw the UI into an in memory Ratatui TestBackend and assert against the resulting text grid, rather than mocking the renderer. That means a rendering regression, not just a logic regression, gets caught by cargo test --workspace.
The project site's docs page is a rendered copy of this wiki, generated at build time rather than served live, since GitHub Pages runs no server side code and the wiki is a separate repository:
shgit clone https://github.com/CtrlUserKnown/ctrlvim.wiki.git ~/development/wikis/ctrlvim.wiki
make site-docs WIKI=~/development/wikis/ctrlvim.wiki
make site-docs needs Node installed and writes into site/, which is what the deploy-site GitHub Actions workflow publishes.
The rest of this page is a tour of how the codebase itself is organized — where each crate's responsibilities begin and end.
ctrlvim is a Cargo workspace of twelve library crates plus the ctrlvim binary crate that produces cvi. Each crate handles one concern, so the dependency graph reads roughly bottom up: text storage, then editing logic on top of it, then language integration, then the terminal frontend on top of all of it.
| Crate | Purpose |
|---|---|
ctrlvim-types | Shared value types used across the engine |
ctrlvim-text | Rope backed buffers, marks, the undo tree, registers |
ctrlvim-options | Vim style options ('number', 'wrap', ...) and :set |
ctrlvim-regex | A Vim regex engine written from scratch: magic levels, backreferences, lookaround, \zs / \ze |
ctrlvim-editor | Motions, operators, text objects, window splits |
ctrlvim-vimscript | The Vimscript interpreter backing vim.fn and user functions |
ctrlvim-treesitter | Tree sitter integration and the highlights.scm to styled span highlighter |
ctrlvim-markdown | Markdown parsing for live rendering in the TUI |
ctrlvim-api-macro / ctrlvim-api | The #[ctrlvim_api] macro and the dispatch table it generates, exposing engine functionality to Lua and RPC uniformly |
ctrlvim-lua | Lua embedding (mlua) and the vim.* API compatibility layer plugins run against |
ctrlvim-async | The Tokio event loop and msgpack-RPC server |
ctrlvim-term | Terminal emulation backing the pseudoterminal panel |
ctrlvim-lsp | The LSP client: JSON-RPC transport, request and response parsing |
ctrlvim-core | Ties the engine crates together behind one Ctrlvim facade |
ctrlvim-tui | The Ratatui terminal UI: dashboard, file editor, overlays, plugin manager |
ctrlvim-core::Ctrlvim is the single entry point the frontend, and any headless caller, drives. It owns a Session (editor plus modal state, the same object interactive key input and the Lua host operate on), and a separate Editor that Lua plugin state, such as floating windows and their buffers, lives in. Keeping those separate is deliberate: a plugin that opens and focuses its own floating window must not be able to silently clobber the buffer you are actually editing.
Ctrlvim::run_lua and Ctrlvim::run_lua_as run a chunk of Lua and then sync two things back out to the frontend afterward: any mapping the chunk registered through vim.keymap.set, and any command it registered through vim.api.ctrlvim_create_user_command. This is the mechanism that lets a plugin register the rest of its mappings and commands lazily, on first use, rather than needing to declare everything up front.
ctrlvim-tui crate| Module | Role |
|---|---|
app | App state and the Action enum that both the keymap and mouse hit testing dispatch through; owns the real ctrlvim_core::Ctrlvim engine |
input | Keyboard handling: editor focus routes straight to the engine, the shell keymap handles everything else |
model | Domain types and static UI data |
icons | Nerd Font detection and the per filetype icon table |
theme | The color palette |
data | Real project data gathering: recent files, git status, session list, lines of code, all off the actual project on disk, gathered on a worker thread so a large project never blocks the first frame |
config | config.toml parsing, defaults, and the format preserving write path the Settings tab uses |
lsp_config | Reads lsp.lua's declared servers for the Settings tab's Language Servers list |
ui/* | Rendering: the shell chrome, dashboard, plugin manager, file editor, and overlays |
A file buffer in the TUI is a live editor window rather than a static text view: keystrokes are translated into ctrlvim_core::Key values and fed to the engine's Session::feed, and what renders is the engine's real buffer text, its real cursor position, and its real mode, not a copy the frontend maintains independently.
Gathering project data, recent files, git status, and a line count, involves a full recursive directory walk, a stat per file, several git subprocess calls, and reading every source file to count lines. That is disk bound and, on a large checkout, slow enough to make the editor look hung on launch if done before the first frame draws. Project::load instead starts that work on a background thread immediately and returns an empty snapshot right away; the event loop's idle tick polls for the result and fills the dashboard's panels in a moment after the first frame, rather than holding up startup for it.