Skip to content

Architecture

CtrlUserKnown edited this page Aug 18, 2026 · 1 revision

Architecture

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

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

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

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.

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.

Clone this wiki locally