Building from Source

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.

# Requirements

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.

# Layout

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.

# Everyday development

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.

# Release builds

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.

# Testing notes

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.

# Rendering the documentation site

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 map

CratePurpose
ctrlvim-typesShared value types used across the engine
ctrlvim-textRope backed buffers, marks, the undo tree, registers
ctrlvim-optionsVim style options ('number', 'wrap', ...) and :set
ctrlvim-regexA Vim regex engine written from scratch: magic levels, backreferences, lookaround, \zs / \ze
ctrlvim-editorMotions, operators, text objects, window splits
ctrlvim-vimscriptThe Vimscript interpreter backing vim.fn and user functions
ctrlvim-treesitterTree sitter integration and the highlights.scm to styled span highlighter
ctrlvim-markdownMarkdown parsing for live rendering in the TUI
ctrlvim-api-macro / ctrlvim-apiThe #[ctrlvim_api] macro and the dispatch table it generates, exposing engine functionality to Lua and RPC uniformly
ctrlvim-luaLua embedding (mlua) and the vim.* API compatibility layer plugins run against
ctrlvim-asyncThe Tokio event loop and msgpack-RPC server
ctrlvim-termTerminal emulation backing the pseudoterminal panel
ctrlvim-lspThe LSP client: JSON-RPC transport, request and response parsing
ctrlvim-coreTies the engine crates together behind one Ctrlvim facade
ctrlvim-tuiThe Ratatui terminal UI: dashboard, file editor, overlays, plugin manager

# The engine facade

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.

# The ctrlvim-tui crate

ModuleRole
appApp state and the Action enum that both the keymap and mouse hit testing dispatch through; owns the real ctrlvim_core::Ctrlvim engine
inputKeyboard handling: editor focus routes straight to the engine, the shell keymap handles everything else
modelDomain types and static UI data
iconsNerd Font detection and the per filetype icon table
themeThe color palette
dataReal 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
configconfig.toml parsing, defaults, and the format preserving write path the Settings tab uses
lsp_configReads 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.

# Startup performance

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.

On this page