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.:chatfor full interactive sessions. - 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. - 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.28 — daily-driveable for prose AND HyperList. Full feature reference below.
| Keys | Action |
|---|---|
h j k l (also arrows) |
left / down / up / right; arrows wrap across line boundaries in Insert |
0 ^ $ (HOME END) |
line start / first non-blank / line end |
gg G (with 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 |
| 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 |
| 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. 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 |
r{c} |
replace char under cursor with c |
J |
join with line below |
~ |
toggle case under cursor |
p P |
paste after / before |
Ctrl-A 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 Ctrl-A). 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) |
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 |
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.
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 |
: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 |
force the buffer's filetype: plain / email / rust / md / 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.
| 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 numbersstoggle spell on/offlprompt for spell languagecprompt for spell color (0–255)Wsave current settings to~/.config/scribe/scriberc(preserves comments and unknown keys)ESCclose
:help (or :h) opens the bundled README in the current buffer as a Markdown source — search with /, navigate with normal motions. Refuses if the current buffer is dirty (save or Q first). :e <file> returns to your work.
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.
Drop a [keymap] section into scriberc to define your own bindings. Format per line: MODE LHS RHS.
[keymap]
normal zr :read
normal zq :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/zq/zn/zp shortcuts (which stay as defaults if you don't define your own).
| Keys | Action |
|---|---|
zr |
toggle reading mode |
zq |
save + quit (= :wq) |
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:colpositionscribe vN.N.N
| Key / command | Behavior |
|---|---|
q |
quit when buffer is clean; refuses + warns if dirty |
Q |
quit, discard unsaved changes |
zq / :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).
~/.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)
# 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 |
/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 (incl. dotfile auto-encrypt with byte-for-byte compatibility with the Ruby hyperlist app's ENC: format), references, exports, and more. See HYPERLIST.md or :h hl inside scribe.
- HyperList Tab fold/unfold via Tab key (currently SPACE) — minor ergonomics tweak.
- General
:mapsystem — user-defined keymaps in scriberc instead of hardcoded built-ins. :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).