A mouse-first, VS Code-like terminal code editor in Rust, built on its own plugin system. Tabs, a clickable directory explorer, full mouse support, syntax highlighting, find/replace, project search, live file-sync, multi-cursor, an LSP client, an integrated terminal panel, and a sandboxed external plugin runtime.
Six crates (headless core, thin view — the Helix/VS Code split):
| Crate | Role |
|---|---|
editor-core |
Headless model: rope, normalized multi-cursor selections, reversible transactions/undo, motions, and the pure screen_to_char/char_to_screen coordinate mapping. No terminal deps. |
editor-syntax |
tree-sitter parsing + highlight-query → capture spans (cached, viewport-only). |
editor-lsp |
LSP client: JSON-RPC transport, UTF-16 position conversion, diagnostics. |
editor-plugin |
The contribution API (traits + registries + event bus), the Host surface, and the external plugin runtime — the kernel that hosts plugins. |
editor-builtins |
The core features implemented as plugins (the explorer, the PDF/hex file viewers, …). |
lumina |
The lumina binary: event loop, ratatui rendering, keymap, and wiring. |
Everything is a command; a document holds a set of selections; features are plugins; render is a pure function of state; all buffer mutation goes through the transaction API.
See docs/ARCHITECTURE.md for the full guide: the load-bearing
invariants, the crate dependency direction, the plugin kernel's ports-and-adapters seam,
and the conventions the doc comments reference.
Prebuilt lmn binaries are published for macOS, Windows and Linux on every tagged
release. Once installed, open a directory just like vim:
lmn . # open the current directory
lmn src/main.rs # open a single filemacOS / Linux (installs to ~/.local/bin):
curl -fsSL https://raw.githubusercontent.com/gmfc/lumina/main/install.sh | shWindows (PowerShell):
irm https://raw.githubusercontent.com/gmfc/lumina/main/install.ps1 | iexOverride the destination with LMN_INSTALL_DIR, or pin a version with
LMN_VERSION=v0.1.0. Supported targets: x86_64/aarch64 Linux, Intel/Apple-silicon
macOS, and x86_64 Windows.
From source (any platform with Rust ≥ 1.88):
cargo install --git https://github.com/gmfc/lumina lumina # installs `lmn`Updating — pull the newest release in place (safe to run while the editor is open):
lmn update # re-runs the installer for your OS, upgrading this binary
lmn --version # check what you're onRe-running the install one-liner above does the same thing. The installers replace the binary atomically, so a running instance keeps working until you restart it.
cargo build --workspace
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all
cargo run -p lumina -- <path> # or: cargo run --bin lmn -- <path>Ctrl+P quick-open (files; type > for the command palette) · Ctrl+F/Ctrl+H find/replace ·
Ctrl+Shift+F project search (Tab for replace, Alt+A to replace all) · Ctrl+B toggle sidebar · Alt+Z toggle word wrap ·
Ctrl+D add cursor at next match ·
Ctrl+F2 select all occurrences · Shift+Alt+I cursors to line ends · Alt+Click add cursor ·
Ctrl+G go to line · Ctrl+, settings · Ctrl+\ jump to matching bracket · Ctrl+S save · Ctrl+K S save all ·
Ctrl+K Ctrl+S save as · Ctrl+N new file · Ctrl+W close tab · Ctrl+K Ctrl+W close all ·
Ctrl+Shift+T reopen closed editor · Ctrl+K Ctrl+K delete line ·
Shift+Alt+Down/Shift+Alt+Up copy line down/up · Alt+Down/Alt+Up move line ·
Ctrl+Enter/Ctrl+Shift+Enter insert line below/above · Ctrl+/ toggle comment ·
Ctrl+K Ctrl+X trim trailing whitespace · F8/Shift+F8 next/prev diagnostic ·
Ctrl+Space completions · F12 go to definition · Ctrl+F12 go to implementation ·
View: Problems (palette) lists every diagnostic in the workspace ·
Shift+F12 find references · Ctrl+Shift+O document symbols · F2 rename ·
Alt+J/Alt+K next/prev git change · Source Control: Show Changes (palette) · Ctrl+J/Ctrl+` toggle terminal panel ·
Ctrl+PageUp/Ctrl+PageDown prev/next terminal · Ctrl+K Ctrl+H view file as hex · Ctrl+K Ctrl+T open as text ·
Ctrl+K Ctrl+R keyboard-shortcut reference · Ctrl+K Ctrl+N notifications · Ctrl+Q quit.
The explorer's file operations live in the command palette: Explorer: New File / New Folder / Rename / Delete. A new entry goes into the selected folder, and delete asks you to type the name back — it is the one operation with no undo.
You never have to keep this list: Ctrl+K Ctrl+R opens the same table generated from the
keymap actually in use, so it includes plugin chords, your own [keys] overrides, and a section
naming any binding an override displaced. The command palette shows each command's chord next to
it and floats what you actually reach for to the top, and an armed prefix like Ctrl+K lists what
may follow it in the status bar.
A note on Ctrl+Shift+… chords. Most terminals send identical bytes for Ctrl+P and
Ctrl+Shift+P — only the kitty keyboard
protocol can tell them apart, and lumina
enables it wherever the terminal advertises support (kitty, foot, WezTerm, Ghostty, recent
Alacritty). Elsewhere a Ctrl+Shift+<letter> chord arrives shiftless, and lumina falls back to the
shifted binding when the unshifted chord is free — so Ctrl+Shift+O still reaches Document Symbols.
Where both are bound the unshifted one wins, because the terminal genuinely cannot distinguish them:
on those terminals Ctrl+Shift+P opens quick-open, and the command palette is one > away (or
remap it under [keys]). Ctrl+K Ctrl+R always shows what is actually reachable.
A minimizable, tabbed terminal dock lives below the editor. Ctrl+J (or Ctrl+`)
opens and focuses it, spawning your shell on first use; press it again to close. Each tab is a
real PTY-backed shell session parsed by a VT100 emulator, so colors, cursor addressing, and
full-screen programs work. While the panel is focused every keystroke — including Ctrl+C —
goes to the shell; click the editor to return there, or use the terminal.* commands. The
header's ▾/▸ control minimizes and restores the dock, × closes a tab, and + opens a new
one. Mouse-wheel over the panel scrolls its history. It is built to grow (split panes, task
runners, and other bottom-dock contributions can hang off the same panel later).
Beyond the per-line change bar in the gutter, Source Control: Show Changes (command palette) opens a sidebar panel listing everything git reports for the repository: the branch, modified and untracked files, and what is already staged. Rows open the file; Stage File, Unstage File and Commit Staged do what they say, and the commit box refuses an empty message rather than letting git reject it.
Every git call runs on a worker thread — a status on a large repository takes long enough to drop
frames — and the panel re-reads the repository after each change, so the list can't drift from
what git actually thinks. Outside a repository (or with no git on PATH) the panel says so
rather than showing a convincing empty list.
Lumina is mouse-first and non-modal by default, but ships an optional Vim modal-editing
layer. Turn it on with vim = true under [settings], or toggle it live from the command
palette (Vim: Toggle Vim Mode, id vim.toggle / vim.enable / vim.disable). The active
mode shows as a -- NORMAL -- / -- INSERT -- / -- VISUAL -- badge in the status bar, along
with any pending count, register, or operator.
It implements the core of Vim's operator + motion + text-object grammar rather than a fixed list of shortcuts, so combinations compose:
- Modes — Normal, Insert, Visual (charwise) and Visual-Line, plus the
:command line and/?search lines. - Motions —
h j k l,w W b B e E ge gE,0 ^ $ g_,f t F Twith; ,,{ },%,gg G {n}G,H M L,|, andCtrl-D/Ctrl-U/Ctrl-F/Ctrl-Bscrolling. - Operators —
d c y > < gu gU g~, doubled for the current line (dd,yy,cc,>>), each combining with every motion and text object, and with counts that multiply (2d3w= delete six words). - Text objects —
iw aw iW aW,i( a( i{ a{ i[ a[ i< a<(andb/Baliases),i" a" i' a' ia, andip ap. - Edits —
i I a A o O gi s S c C d D x X r ~ J p P,u/Ctrl-R, and the dot command.which repeats the last change (recorded as keystrokes, sociwfoo<Esc>then.works). - Registers — the unnamed register, the yank register
"0, named"a–"z(uppercase appends), the system clipboard"+/"*, and the black hole"_. - Visual mode — motions and text objects extend the (inclusive) selection;
oswaps ends; operators (d c y > < u U ~ r J) act on it. - Ex commands —
:w :wq :x :q :q! :wa :qa,:{number}to jump to a line,:noh, and a literal:[%]s/old/new/[g]substitute.
Insert mode is otherwise the normal editor: auto-pairs, auto-indent, completion, and all the
Ctrl-shortcuts above keep working, and Esc (or Ctrl-[) returns to Normal. Un-owned Ctrl
chords fall through in Normal mode too, so Ctrl+S, Ctrl+P, Ctrl+Shift+P, etc. still do
their usual thing. Not (yet) implemented: macros (q/@), marks, jump/change lists, tag
objects (it/at), the =/gq reformat operators, and regex in :s (it's literal).
Opening a file no longer means reading it. lumina probes the header first — the size from the filesystem plus the first 8 KiB — and decides what kind of tab to give you:
- Text, under the limits → an ordinary buffer, as before.
- Text, at or over
large_file_mb(8 MB) → still a buffer, but in large-file mode: syntax highlighting, the git gutter, and the language server stay off. The status bar says so when the file opens, and carries aLARGEsegment for as long as the file is open, so the missing colors never read as a bug. A 200 MB log opens and scrolls instead of stalling the frame. - Text, over
max_file_size_mb(64 MB) → a tab explaining the limit, with Open Anyway (file.openAnyway, orEnter) if you meant it. - Binary → a tab naming the format ("PDF document", "PNG image", "ELF executable", …) and
its size. Binary refusals aren't overridable — those bytes can't round-trip through a text
buffer, so "open anyway" would corrupt the file on the first save.
Ctrl+K Ctrl+Hopens a hex view instead, which works for any file. - Text in an encoding lumina can't re-encode — a legacy 8-bit codepage (Latin-1, CP1252,
Shift-JIS) rather than UTF-8 or BOM-marked UTF-16 → a tab saying so. Also not overridable, and
for a sharper reason than binary: decoding it means turning every undecodable byte into
U+FFFD, and the first save would write those replacements over your file. Convert it (iconv -f latin1 -t utf-8) or read the bytes withCtrl+K Ctrl+H. - A file some plugin claims → that plugin's viewer.
.pdfopens in the built-in PDF viewer, which extracts the document's text page by page (.csv/.tsvtoo, if you install thecsvviewexample).Ctrl+K Ctrl+Topens it as text instead — a viewer can claim an extension, never hold it hostage.
A notice or viewer tab behaves like any other tab — switch, reorder, close, restore with the session — but holds no text buffer, so nothing can write it back over the file it is showing.
Viewers are a plugin contribution, not editor code: a plugin declares which extensions it
claims and publishes styled rows for its tab. Disabling the pdf plugin hands .pdf straight
back to the binary notice; nothing in lumina knows what a PDF is.
Every path that can throw a buffer away asks first, and every path that fails says what to do about it.
- Quitting with unsaved changes (
Ctrl+Q,:qa, the palette's Quit) opens a confirmation naming the files at risk: save all & quit, discard & quit, or cancel. Sessions restore paths, cursors, and scroll — not buffer contents — so nothing else would have brought that work back.:qa!still force-quits, because that is what the bang means. - Save As over an existing file asks before overwriting, shows the absolute path the name resolves to as you type it, and reports a missing directory in the box instead of failing after the fact.
- A file changed on disk under a modified buffer is never clobbered. The tab shows
⚠, the status bar showsCONFLICTfor as long as it holds, and you get two exits from the palette: File: Revert File (take the disk version — confirmed, since it discards your edits and this file's undo history) and File: Keep My Version (keep yours; the next save overwrites theirs). - Messages have a severity. A confirmation clears on the next keystroke; a warning or an error
stays on screen, tinted, until you replace it or press
Esc. Every message is also kept in a scrollback —Ctrl+K Ctrl+N, or View: Show Notifications — so nothing you blinked past is gone. Errors name their recovery with the chord that is actually bound to it.
Alt+Z (or View: Toggle Word Wrap, or line_wrap = true) soft-wraps long lines at word
boundaries instead of scrolling horizontally. It is off by default and applies to every open
tab at once.
Wrapping is purely a view: the buffer, its transactions, and every character offset are
untouched, so a wrapped file saves back byte-for-byte identical. Up/Down move by visual
row and preserve the goal column; Home/End snap to the visual row; PageUp/PageDown stay
logical.
Current limits: wrapped rows carry no continuation indent, wrapping happens at the pane width only (there is no fixed wrap column), and inline virtual text — inlay hints — is not drawn while wrap is on.
In Vim mode, j/k follow the visual row under wrap, the same as Up/Down; operator-pending
motions such as dj and cj still act on whole logical lines. Real Vim keeps j/k logical
and reserves gj/gk for display lines, so this is a known deviation rather than a design
choice — see issue #54.
Prefer a UI? Open the Settings tab with Ctrl+, (or the command palette →
Preferences: Open Settings). It renders as a normal editor tab with sections and typed
widgets — checkboxes, ‹ … › steppers/dropdowns, and text fields — for every [settings]
option plus a Plugins section to enable/disable each installed plugin. Navigate with the
arrows (or j/k), toggle with Space, adjust with ←/→, Enter to open a dropdown or
edit a field, or just click. Changes apply live and are written straight back to
config.toml (other sections you hand-wrote are preserved; comments are not). Plugin
enable/disable takes effect on the next launch.
Everything the Settings tab writes can also be edited by hand in
~/.config/lumina/config.toml. A project can override any of it from
<project>/.lumina/config.toml, alongside the .lumina/plugins folder — so a per-project
tab_width, keybinding, or language server has somewhere to live. The project file is layered
over the global one: [settings] keys and [lsp] entries win per key, [keys] entries are
applied last (so they win), and [plugins] can switch a plugin off but never force one on. Both
files hot-reload. The Settings tab writes the global file and says so when a project override
would beat the change you just made.
[settings]
tab_width = 4
sidebar_width = 30
follow_mode = true # auto-scroll to external edits as an agent writes files
poll_watch = false # set true on devcontainer/NFS mounts where inotify is unreliable
auto_pairs = true # auto-close brackets/quotes, type over closers, delete empty pairs
auto_indent = true # copy indent on newline (brace-aware); dedent on a closing bracket
trim_trailing_whitespace = false # on save, strip trailing spaces/tabs from every line
insert_final_newline = false # on save, ensure the file ends with a single newline
format_on_save = false # on save, run the language server's formatter before writing
autosave_ms = 0 # save a modified file after this long without typing (0 = off)
git_gutter = true # per-line add/modify/delete change bar in the gutter (vs HEAD)
line_wrap = false # soft-wrap long lines at word boundaries (Alt+Z toggles live)
max_file_size_mb = 64 # above this a file opens as a notice tab (0 = no limit)
large_file_mb = 8 # at this size a file still opens, but with no syntax/git/LSP (0 = never)
icons = false # Nerd Font file glyphs in the explorer (needs a patched font)
vim = false # start in Vim modal editing (Normal/Insert/Visual) — see "Vim mode"
terminal_height = 12 # rows the terminal panel occupies when expanded
# terminal_shell = "bash" # override the shell (default: $SHELL / /bin/sh, %ComSpec% on Windows)
[keys]
"ctrl+k ctrl+u" = "shout.line"
[lsp]
rust = "rust-analyzer" # diagnostics; inert unless configured
[theme] # override syntax colors by capture name
keyword = "#c678dd"The editor is built on its own plugin system: built-ins register through the same API as
third-party plugins. External plugins live in ~/.config/lumina/plugins/ or
<project>/.lumina/plugins/, each a folder with a plugin.toml manifest and a guest module.
Two substrates run through the same contribution API:
- Rhai script (default) — a
main.rhaireturning a list of host actions. - WebAssembly (
runtime = "wasm") — a sandboxed.wasm/.watguest with no host imports, fuel-metered against runaway loops, run on thewasmiengine.
Both are deny-by-default: a plugin declares capabilities (edit, ui, fs:read) and
can only take the actions it was granted. See plugins/ for worked examples — shout, todo,
inspector, csvview (Rhai) and wasm-hello (WebAssembly).
A panel is placed by its declaration, not by the editor recognising its id: declare
location = "sidebar" and it joins the sidebar rotation (View: Next Sidebar Panel cycles;
the heading is your title), or location = "bottom" and it shows in the results dock whenever
you publish rows to it. Rows carrying a payload are clickable, and the click comes back to your
plugin. The built-in explorer and project-search results go through exactly this path.
Alongside commands, panels, keybindings, and menu items, a plugin can contribute a file viewer: a tab that renders a file the text editor can't. It declares the extensions it claims and publishes styled rows; the editor owns the tab, the scrolling, and the file-IO policy.
[[viewers]]
id = "csvview.table"
title = "CSV Table"
extensions = ["csv", "tsv"]fn render_viewer(id, ctx) { // ctx.path always; ctx.text only with `fs:read`
ctx.text.split("\n")
}The built-in pdf and hexview plugins use exactly this contribution — there is no privileged
path for them.

