Vim-flavoured modal editor with the parts a writer actually needs and a few features vim never had. Single static binary, sub-10 ms startup, soft-wrap by default, Claude Code in the editor, syntax highlighting, hunspell spellcheck, Goyo-style reading mode, persistent registers shared across sessions.
Part of the Fe₂O₃ Rust terminal suite. Built on crust and highlight.
Vim has a thousand features. A writer needs about thirty of them. Scribe is "vim minus 90 % minus the programming subsystem, plus a handful of writer-first niceties":
- Modal core —
hjkl, motions, operators, text-objects, registers, marks, macros, dot-repeat, undo tree. - Soft-wrap by default — long prose lines wrap at the pane edge with a continuation indicator. Live resize via
SIGWINCH. - Claude integration in the prompt —
:claude {prompt}runsclaude -pover your selection / paragraph / buffer and splices the response back. Oneureverses an entire turn.:chatorCtrl-Afor a full interactive session. - Reading mode —
:read(orzr) for Goyo-style centered text, optional Limelight-style paragraph dim. Prose without chrome. - Email-mode rendering —
.emlfiles and kastrup compose tempfiles get header / quote-level / signature colors that match kastrup's right pane 1-for-1. Inline email addresses + URLs highlighted everywhere. - Syntax highlighting for ~18 source languages plus dedicated HyperList / Markdown / LaTeX renderers via the shared
highlightcrate. Multi-line block comments and string literals keep their color across line breaks. - Inline colour, font + office export —
\Ccolours a Visual selection (prism picks fg/bg) and\Fsets its font + size (thefontspicker), both stored as live HTML spans;\Mconceals the span markup (revealing it only on the cursor's line) so styled prose reads clean.\xd/\xoexport the buffer to docx / odt via LibreOffice with colour, highlight, font and size preserved. The styling lives in the text, so a Markdown / HTML / plain save keeps it. - Spellcheck via hunspell — auto-on for email mode, opt-in elsewhere via
:set spell. Configurable language and underline color.]s/[snavigation,z=suggestions,zgto add to personal dict. - Persistent registers — yanks and macros live in
~/.config/scribe/registers.jsonand update on every yank, so they survive restarts AND share live across concurrent scribe sessions. - No LSP / debugger / quickfix /
:make— writers don't compile. - Block paste that actually pastes a block —
Ctrl-vselection lays each row at the same column on consecutive lines. Oneureverses the whole thing.
v0.1.63 — daily-driveable for prose AND HyperList. Inline colour / font / markup-conceal and office export are in. Full feature reference below.
HyperList editing — full hyperlist.vim parity with the new \-grouped keymap. Syntax highlighting tracks the spec: red Properties, blue Operators, green Qualifiers, magenta References, teal Comments / Quotes, yellow Hashtags, multi-line + markers, underlined State/Transition items.
Reading mode + Limelight — :read (or zr) for distraction-free prose, :set pdim to dim every paragraph except the cursor's. Header collapses, gutter hides, focus follows you.
Long-form Markdown — soft-wrap by default, syntax highlighting, sub-10 ms startup. The same modal core that powers HyperList editing handles prose, code, email, and source.
| Keys | Action |
|---|---|
h j k l |
left / down / up / right, by whole lines |
↑ / ↓ |
one visible row: on a soft-wrapped paragraph, one screen line, the same in Normal and Insert |
0 ^ $ |
line start / first non-blank / line end |
+ - |
first non-blank of the next / previous line |
gg G (also HOME END; count: 12G) |
first / last line / line N |
w b e W B |
next/prev word, end of word; capital = WORD (whitespace-delimited) |
f{c} F{c} t{c} T{c} |
jump on/before next/prev c on the current line |
Ctrl-D Ctrl-U (PgUp/PgDn) |
half-page / full-page scroll |
n N |
next / prev search match |
* # |
search forward / backward for word under cursor |
| Counts | prefix any motion: 5j, 12G, 3w |
| Keys | Action |
|---|---|
i a |
insert before / after cursor |
I A |
insert at line start / end |
o O |
open new line below / above |
s S |
substitute char (s) or line (S) and enter Insert |
Esc |
back to Normal. The cursor steps back onto the last character you typed, as in vim, so x / r / ~ act on it. At column 0 it stays put |
| Arrows + HOME/END | work in Insert too |
Ctrl-Y |
insert character from the same column on the line above |
Ctrl-E |
insert character from the same column on the line below |
Ctrl-P Ctrl-N |
complete the word you are typing from the rest of the buffer. Ctrl-P takes the nearest match above the cursor, Ctrl-N the nearest below; press again to walk through the list, and once more past the end to get your own typing back |
| Bracketed paste | the entire pasted blob is a single undo node — instant on big pastes |
d c y > < gq over any motion or text-object. Examples: 5dw, d3w, cgg, yG, c$, >ap, gqap. Linewise doubling: dd cc yy >> << gqq. Vertical motions are linewise as in vim, so dj, dk and d<Down> take both whole lines. Linewise shortcuts: D C Y (delete / change / yank to end of line). All record last_change for . repeat.
iw aw i" a" i' a' i` a` i( a( i[ a[ i{ a{ i< a< ip ap. Aliases: ib/iB for () / {}. Use after any operator: ci", dap, >i{.
| Keys | Action |
|---|---|
x X |
delete char forward / backward into the register, so xp swaps two chars and . repeats |
r{c} |
replace char under cursor with c |
J |
join with line below |
~ |
toggle case under cursor |
p P |
paste after / before |
Alt-X Ctrl-X |
increment / decrement number under cursor — supports ISO 8601 dates YYYY-MM-DD with month-end / leap-year rollover (2024-02-28 + 1 = 2024-02-29; 2025-02-28 + 1 = 2025-03-01). Counts work (30 Alt-X). Ctrl-A, vim's increment, is the Claude key here as in every Fe₂O₃ app. Zero-padding preserved on integers. |
Ctrl-Up Ctrl-Down |
swap current line with the one above / below. Counts work (5 Ctrl-Down). One compound undo node per swap. |
. |
dot-repeat — replays the last change (operator + motion + inserted text, replace, paste, increment, line move). A plain insert replays its command, so . after A! appends ! at the end of whatever line you are on, and after o foo opens another line |
v charwise, V linewise, Ctrl-v block. Operate with any operator. Live selection stats in the status line: sel: 3l 47w 230c updates as you extend.
| Slot | Use |
|---|---|
"a … "z, "0-"9 |
named slots; "ay$ yanks into a |
"" |
unnamed (last yank/delete; default for p/P) |
"0 |
last yank only (delete doesn't touch it) |
"+ "* |
system clipboard via OSC 52 |
The system clipboard. A yank or delete of a visual selection goes to the system clipboard as well, and so does a yank into "+ or "*. Ctrl-y copies the whole file there. A plain x, dw, dd or yy in Normal mode stays in scribe's own registers. A deleted letter never replaces what you copied in another program. Changing a selection with c, or pasting over it with p, leaves the clipboard alone too.
Persistent. Named slots are written to ~/.config/scribe/registers.json on every yank / cut. They survive restarts AND share live across concurrent scribe sessions: yank in scribe A, "ap in scribe B without going through the OS clipboard. The system-clipboard slots ("+, "*) are not persisted.
Yank/cut feedback. Status line confirms every register write: 5 lines yanked, 23 chars yanked into "a, 3 lines deleted. Yank green, cut/change orange.
Inspector. :reg (also :registers) opens a popup showing every set register with kind tag (c/l/b) and a 60-char preview. \n rendered literally so multiline yanks and macros stay one row. ESC closes.
| Pattern | Action |
|---|---|
/pat ?pat |
regex forward / backward |
n N |
next / prev |
* # |
jump on word under cursor |
:s/pat/rep/[gi] |
substitute on current line |
:%s/pat/rep/[gi] |
substitute on whole buffer (one undo node) |
u undo, Ctrl-R redo. The buffer keeps an undo tree in memory (not a linear stack), and the cursor follows the edit site so you can see what changed.
Two ways to drop a number into the text:
- Insert mode
Ctrl-R =— type a quick arithmetic expression, the result is inserted. Good for1920/2 + 8. - Normal mode
=— hands the screen to rpnx, the full RPN / XRPN scientific calculator. Do the calculation, quit withQ, and the X register lands at the cursor.Ctrl-Cin rpnx inserts nothing.
M{reg} starts recording into register reg; M again stops. @{reg} replays. @@ replays the last-played macro. Macros and yanks share the same registers, so:
Ma, type some keys,M— recorded."appastes the captured key sequence as readable text (<Esc>,<C-Up>,<CR>, …).- Edit the pasted text, select it, yank it back into a register:
"ay$(or any yank). @anow replays the edited sequence.
m is left free for marks. Recursion is bounded at depth 4.
m{a-z} sets a mark at the cursor's byte offset. Jump back with:
'a— first non-blank of the line containing marka.`a— exact column.
Marks are session-local (not persisted).
| Command / key | Action |
|---|---|
:set spell / :set nospell |
toggle |
:spell LANG / :spell! LANG |
switch dictionary and turn spell on / the same as a toggle, off if already on in that language (map a key to it) |
:set spelllang=NAME (or :set lang=NAME) |
switch dictionary (en_US, nb_NO, nn_NO, de_DE, …); drops + re-spawns hunspell |
:set spellcolor=N (rcfile only) |
xterm-256 palette index for the underline / text color |
]s / zn |
next misspelling |
[s / zp |
previous misspelling |
z= |
suggestions (numbered list, type number to pick) |
zg |
add word at cursor to personal dict (~/.config/scribe/spell.add) |
Auto-on for .eml and kastrup compose tempfiles. Visual: misspelled words rendered in the spell color (default 196 / red) with a curly underline where supported (kitty, wezterm); on terminals that don't support extended-SGR underline color, the colored text alone makes the spelling signal clear.
For long-form prose. Distraction-free rendering, optionally Goyo-style.
| Command / key | Action |
|---|---|
:read / zr |
toggle reading mode |
:noread |
exit |
:set readingwidth=80 (or rw=80) |
centered text column width; 0 = full pane |
:set paragraphdim (or pdim) |
Limelight-style dim of every paragraph except the cursor's |
:set nopdim |
turn dim off |
Header collapses to a thin dim divider, footer hides except for transient status, line numbers force-off. With paragraphdim, source-mode syntax colors are dropped on dimmed lines so the dim is uniform.
Suggested rcfile combo:
read = true
readingwidth = 80
paragraphdim = true
:set textwidth=72 (or :set tw=72) — typing a space past column 72 breaks the line at the last preceding whitespace. :set tw=0 disables. Preserves leading indent. No-ops when no break point exists (one giant word).
| Command | Action |
|---|---|
:set theme=NAME |
monokai / solarized / nord / dracula / gruvbox / plain |
--theme=NAME (CLI) |
one-session override |
:set syntax=NAME (vim aliases :set ft= / :set filetype=) |
force the buffer's filetype: plain / email / rust / md / html / py / sh / js / ts / c / cpp / go / rb / lua / tex / hl (HyperList) / … |
Source highlight handles multi-line state: block comments and string literals keep their color across line breaks. Email mode mirrors kastrup's right-pane palette 1-for-1.
Per-span styling is stored as inline HTML, so it lives in the text (a Markdown / HTML / plain save keeps it) and exports to office formats with the styling intact.
| Key | Action |
|---|---|
\C |
colour the Visual selection — prism picks fg/bg → <span style="color:…"> |
\F |
set its font — the fonts picker chooses the family, then scribe asks for a point size (Enter = none) → <span style="font-family:'…'[;font-size:…pt]"> |
\M |
hide the span markup — tags conceal on every line except the cursor's, so styled prose reads clean but stays editable where the cursor sits |
\xd / \xo |
export to docx / odt via LibreOffice headless, with colour, background, font and size preserved |
The styled inner text renders live (colour applied, <span> tags dimmed) in Markdown, HTML, and plain-text buffers alike. \C / \F each launch their picker full-screen, then return to scribe with the choice wrapped around the selection.
In a Markdown note, a line that is one picture link and nothing else shows the picture under it:

- The path counts from the folder the note is in.
:img <file>copies a picture intoimg/beside the note and puts the link on a new line below the cursor. Shift-Tab in the prompt picks the file in pointer.- A picture takes at most half the height of the text, and is never enlarged.
- It needs a terminal with the kitty graphics protocol (glass, kitty, WezTerm) and ImageMagick. Anywhere else the link stays a line of text.
pictures = falsein scriberc, oriin:config, turns it off.
The nomad scribe app for Android writes its notes the same way, so a folder of notes shared with Syncthing reads the same on the phone and here.
go shows the text files in the folder of the file you are in, as cards: the name, the first lines, the tags and the first picture.
hjklor the arrows move.gandGgo to the first and the last card.Enteropens the card.qorEscgoes back to the file you were in./keeps the cards with that text in the name or the note, so/#workkeeps one tag.Escshows them all again.:overview(or:ov) does the same asgo, and:ov ~/notesshows another folder. So doesscribe ~/notes.- Notes tagged
#pinnedcome first, then the newest. Hidden files and files that are not text get no card. - The folder is read once, when the overview opens. Nothing runs while it waits for a key.
| Command | Action |
|---|---|
:set number (:set nu) |
absolute line numbers |
:set relativenumber (:set rnu) |
distance from cursor (cursor line stays absolute) |
:set nonumber (:set nonu) |
gutter off |
:config opens a modal popup for on-the-fly preferences:
tcycle thementoggle line numbersrtoggle relative numbersptoggle match pairs: with the cursor on a LaTeX\begin{x}or\end{x}, both ends get an orange backgrounditoggle pictures in Markdown notesstoggle spell on/offlprompt for spell languagecprompt for spell color (0–255)Wsave current settings to~/.config/scribe/scriberc(preserves comments and unknown keys)ESCclose
\?/g?— searchable help index: a live-filtered list of every binding, command, and feature. Type to filter (matches across key, description, and category),↑↓/PgUp-PgDnscroll,Esccloses.:help <query>— open the index pre-filtered, e.g.:help font,:help export,:help :set,:help fold.:help/:help hl— open the full bundled README / HyperList reference in a popup; scroll withj/k,Esccloses.
Cursor position + scroll for every file you edit are saved to ~/.config/scribe/sessions.json on quit and restored next time you open the same path. CLI +N overrides the saved position. The store is capped at 200 entries.
scribe a.md b.md opens two files, each in its own half of the screen.
In a running scribe, :vs FILE opens a second file to the right of yours.
| Key / command | Does |
|---|---|
:vs FILE (or :vsplit) |
open FILE in the right half and go to it |
Ctrl-W w (or Ctrl-W Ctrl-W) |
go to the other file |
Ctrl-W h / Ctrl-W l |
go to the left / the right file |
q, :wq, Q |
close the file you are in; the other gets the whole screen |
:only / Ctrl-W o |
close the other file and keep this one |
- Yank in one file and paste in the other: the registers are shared.
- Each file has its own undo, folds, marks, cursor and recovery file.
- The header names both files, with the one you are in in bold.
- Two files is the limit, and one file cannot be open in both halves.
- Pictures in Markdown notes are not shown while two files are open.
- The file you are not in is drawn once and left alone until the screen is cleared or resized, so typing costs the same as with one file.
Text you have typed and not saved is written to a recovery file when you stop typing for four seconds, or after 200 changes in one go. A crash, a closed terminal or a dead battery then costs a few seconds of typing.
- The next time you open the file, scribe says it has unsaved text for it:
rtakes it back,ddeletes it, any other key leaves it for later. - After
r,ureturns to the file as it is on disk, and:wkeeps the recovered text. - A scribe started with no file offers the newest text that never had one.
- A save or a quit removes the recovery file,
:q!too. - The files are in
~/.config/scribe/recover/, readable by you alone, and never beside your file. An encrypted file gets an encrypted recovery file. - An idle scribe does no work for this: with nothing new to write, it waits for the next key and nothing else.
recover = falsein scriberc turns it off.
Drop a [keymap] section into scriberc to define your own bindings. Format per line: MODE LHS RHS.
[keymap]
normal zr :read
normal zz :wq
insert jk <Esc>
normal qq :q!
MODEisnormal,insert, orvisual.LHSis 1- or 2-key — single chars or vim-style escapes (<Esc>,<C-Space>,<CR>,<C-Up>).RHSstarting with:runs as an ex command. Anything else is fed back through the input layer as if you typed it.
User maps take precedence over scribe's built-in zr/zz/zn/zp shortcuts (which stay as defaults if you don't define your own).
| Keys | Action |
|---|---|
zr |
toggle reading mode |
zz |
save + quit (= :wq) |
zq |
quit, discard unsaved changes (= Q) |
zn |
next misspelling |
zp |
previous misspelling |
z= |
spell suggestions |
zg |
add word to dict |
Up / Down at the : prompt recalls past commands. Persisted in ~/.config/scribe/cmdhistory (capped at 100).
Always shows:
- Mode badge (NORMAL / INSERT / VISUAL / …)
- Transient status messages or
:cmdline - Word + char count (full buffer normally, selection live in Visual)
spell:LANG(green when on) /spell:off(grey)line:colposition, and how far into the document you are as a percentage (first line 0%, last 100%)scribe vN.N.N
| Key / command | Behavior |
|---|---|
q |
quit when buffer is clean; refuses + warns if dirty |
Q / zq |
quit, discard unsaved changes |
zz / :wq / :x |
save + quit |
:q! |
quit without saving |
Every save first writes <path>.scribe-bak so an accidental :wq after a destructive :claude is recoverable.
On a panic, scribe restores the terminal, writes a timestamped backtrace to ~/.config/scribe/scribe.log, and prints a one-line summary to stderr. RUST_BACKTRACE=1 is set automatically (unless you've set =0 yourself).
:claude runs claude -p with your text on stdin and splices the response back. The scoping is deliberately conservative — whole-buffer replacement requires an explicit selection so a stray prompt can't silently destroy your file:
:claude rewrite this paragraph in plainer English
→ with a Visual selection: replace the selection with the response
→ without selection: replace the CURRENT PARAGRAPH (text-object `ap`)
:claude what's a tighter version of this?
→ same scoping rules: selection > current paragraph
:claude grammar
→ shorthand: "Fix grammar, spelling, punctuation. Preserve meaning + tone."
:claude tighten
→ shorthand: "Rewrite to be more concise."
:claude plain
→ shorthand: "Rewrite in plainer English."
:claude continue
→ input = buffer up to cursor; INSERT response at cursor (no replace)
To rewrite the whole buffer, select it first: ggVG:claude ….
The whole turn is one compound undo node, so u reverses the change in one step. The status line shows claude: NNN chars (u to undo) after a successful turn.
If Claude has rewritten code into prose (or vice versa) and the highlighter looks wrong, swap it with :set syntax=plain / :set syntax=markdown / :set syntax=rust.
For multi-turn discussion, :chat suspends scribe and opens a regular interactive Claude Code session in the same terminal. The current buffer (including unsaved edits) is snapshotted to /tmp/scribe-chat-<pid>.txt:
:chat
→ scribe yields the terminal; you're in `claude` interactively.
→ ask anything, paste excerpts, iterate. The buffer's tempfile path
is in claude's first message — read it via /file or just ask claude
to read it.
→ /exit (or claude's normal quit) returns you to scribe, buffer
untouched.
Use :claude {prompt} for surgical one-shot edits where you want the response spliced back; use :chat when you want a real conversation.
Requires claude on PATH (both commands).
K (or \w) explains the word under the cursor, or a selection, as it is used in the text around it, in a popup. y there copies the explanation, so p pastes it into the document.
~/.config/scribe/scriberc — simple key = value per line, # comments. Full key reference:
# Appearance
theme = dracula # monokai | solarized | nord | dracula | gruvbox | plain
number = false # absolute line numbers in the gutter
relativenumber = false # relative line numbers (forces number=true)
pairs = false # light up both ends of a LaTeX \begin{x} ... \end{x}
pictures = true # show the picture under a picture link in a Markdown note
recover = true # keep unsaved text in a recovery file while you type
# Spell
spell = false # auto-on (email is always on regardless)
lang = en_US # hunspell dict tag (en_US, nb_NO, nn_NO, de_DE, …)
spellcolor = 196 # xterm-256 palette index for the spell color
# Reading
read = false # enter reading mode at startup
readingwidth = 80 # centered column width (0 = full pane)
paragraphdim = true # Limelight-style dim of non-current paragraphs
--theme NAME overrides the rcfile for one session. Runtime :set commands stay in-session — the rcfile is the hand-edited source of truth, except for the W key in :config which writes managed keys back while preserving comments.
| Path | Contents |
|---|---|
~/.config/scribe/scriberc |
persistent settings |
~/.config/scribe/cmdhistory |
: command history (capped at 100) |
~/.config/scribe/registers.json |
persisted yank/macro registers |
~/.config/scribe/sessions.json |
per-file cursor + scroll positions |
~/.config/scribe/spell.add |
personal dictionary for zg |
~/.config/scribe/scribe.log |
error log (panics + backtraces) |
<file>.scribe-bak |
one-step backup written before every save |
~/.config/scribe/recover/ |
unsaved text, kept until it is saved or given up |
/tmp/scribe-chat-<pid>.txt |
buffer snapshot during :chat |
git clone https://github.com/isene/scribe
cd scribe
PATH="/usr/bin:$PATH" cargo build --release
ln -sf "$PWD/target/release/scribe" ~/bin/scribeOr grab the binary from the latest release.
export EDITOR=scribeScribe accepts +N for line-jump (vim convention), so kastrup's compose flow drops the cursor straight on the body.
Scribe ships with full hyperlist.vim parity for .hl and .woim files: indent folding, autonumbering, encryption (dotfile auto-encrypt, reading both the Ruby hyperlist app's ENC: envelope and the vim plugin's openssl aes-256-cbc -pbkdf2 -salt form, and preserving whichever a file uses on save), references, presentation mode, calendar export, HTML / LaTeX / Markdown export, and more.
All HyperList commands live behind the \ leader. Highlights:
| Group | Keys |
|---|---|
| Folding | \0..\9 level · \a open all · <SPACE> / <C-SPACE> toggle |
| Items | \v checkbox · \V +stamp · \o in-progress · \n autonumber |
| Nav | \r reference jump (in-buffer / file / URL) · \p presentation toggle |
| Filter | \S / \H / \N show / hide / reset (also zs / zh / z0) |
| Look | \h limelight · \u state/transition underline |
| Export | \xh HTML · \xl LaTeX · \xm Markdown · \xp PDF · \xd docx · \xo odt |
| Crypto | \ee encrypt · \ed decrypt · \ek rekey |
| Colour | \C colour the Visual selection (prism picks fg/bg) |
| Other | \s sort · \c complexity · \g calendar add |
| Help | \? cheatsheet popup (ESC dismisses) |
Run scribe on the bundled HyperListTestSuite.hl to learn (and verify) every feature interactively. Full reference: HYPERLIST.md or :h hl inside scribe.
- HyperList Tab fold/unfold via Tab key (currently SPACE) — minor ergonomics tweak.
:earlier 5m— time-based undo navigation.
A writer's editor. Not a programmer's editor. Not an "everything" editor. Built specifically because every other editor is bloated with features for a job the user doesn't have — and is missing the one feature a writer in 2026 actually wants: AI in the loop without leaving the buffer.
Public domain (Unlicense).


