caretline

The caretline command

caretline is the interactive editor and a headless tool for states, snapshots and replay. It comes from the caretline-app crate. Every output on this page is from a real run.

Install

cargo install --locked --path crates/caretline-app   # installs `caretline`
cargo run -p caretline-app -- notes.md                # or run it from the checkout

Commands at a glance

CommandDoes
caretline [FILE]Edit FILE interactively (created on first save)
caretline FILE --trace T.jsonlEdit, recording the session to a trace
caretline FILE --no-mouseEdit without capturing the mouse
caretline --new-state FILE [--size WxH]Print an initial state for FILE as JSON
caretline --state S.jsonEdit a saved state interactively
caretline --state S.json --keys SCRIPT …Apply a key script, headless
caretline --state S.json --msgs M.jsonl …Apply messages, headless (- reads stdin)
caretline --replay T.jsonl …Start from a trace with all its messages applied, headless
… --snapshot WxH [--format text|ansi]Print the rendered frame
… --dump-state OUT.jsonWrite the final state (- for stdout)
… --effectsPrint the effects update returned, one JSON per line
… --size WxHResize before applying messages
caretline serve, caretline send, --listenThe state protocol, see protocol.md
caretline benchProtocol throughput and latency (build with --release)
caretline --outline FILEEdit FILE as an outline: lists, tasks and blocks with their own keys, Markdown in and out (also for --new-state and serve)
caretline --layout FILEAs --outline, with the outline layout: markers in a hang with plain glyphs, a column per depth (also for serve)
… --no-status-barHide the status bar (config.status_bar = false): every row shows text
… --max-fps FPSEditor: repaint at most this many times a second, coalescing changes in between (default 120; 0 repaints after every batch of input). See performance.md
… --frame-clock FPSEditor: start with a frame clock (a frame message FPS times a second). Off by default
… --statsEditor: print the repaint count and mean repaint time on exit
… --trace-limit LINESEditor and serve: bound the in-memory trace trace.get serves (default 100,000 lines; see protocol.md)

A run is headless when any of --snapshot, --dump-state, --msgs, --keys or --replay is given. Headless runs never perform effects: no file is written and the clipboard isn’t touched. --effects shows what would have happened.

Edit a file

caretline notes.md

The keys are macOS text-field keys with Ctrl twins (see messages.md). Ctrl-S saves and Ctrl-Q quits; with unsaved changes, press Ctrl-Q twice. Cmd (⌘) keys need a terminal that speaks the kitty keyboard protocol (kitty, WezTerm, Ghostty, iTerm2 with the option on). caretline turns it on when the terminal supports it. Copy also writes the system clipboard (pbcopy, wl-copy, xclip or xsel, else OSC 52), and paste reads it when it can.

Saving writes a temporary sibling file and renames it over the target, so a failed write never leaves half a file.

The state, snapshot and replay workflow

This walks through every headless flag on a small file.

1. Capture a state

$ printf '# Plan\n\nShip the editor docs.\n' > plan.md
$ caretline --new-state plan.md --size 40x6 > s0.json
$ caretline --state s0.json --snapshot 40x6
# Plan

Ship the editor docs.


 plan.md                            1:1

--new-state uses 80x24 unless you pass --size. The state is pretty-printed JSON: the text, the selection, the scroll, the viewport, the undo history and the config. You can edit it by hand; loading repairs anything out of range.

You can also write a state from scratch. Only text is needed; everything else takes --new-state’s defaults (a caret at 0, 80x24, a fresh history, a clean document):

$ cat > min.json <<'EOF'
{"text": "hello\nworld\n", "selection": {"ranges": [{"anchor": 0, "head": 5}]}, "viewport": {"width": 20, "height": 4}}
EOF
$ caretline --state min.json --keys 'bye' --snapshot 20x4
bye
world

 [scratch] [+]  1:4

See architecture.md for every default.

2. Drive it with keys, and save the result

$ caretline --state s0.json --keys '<d-down>Then review them.' --snapshot 40x6 --dump-state s1.json
# Plan

Ship the editor docs.
Then review them.

 plan.md [+]                       4:18

<d-down> is ⌘↓ (document end). The status bar shows the file name, [+] for unsaved changes, and line:col.

3. Select, undo, inspect

$ caretline --state s1.json --keys '<s-a-left><s-a-left>' --snapshot 40x6
# Plan

Ship the editor docs.
Then review them.

 plan.md [+]                12 sel  4:6
$ caretline --state s1.json --keys '<c-z>' --snapshot 40x6
# Plan

Ship the editor docs.


 plan.md                            4:1

The undo history is part of the state, so s1.json can still undo the typing it recorded. After the undo, the document matches the saved revision again, so [+] is gone.

4. See effects instead of performing them

$ caretline --state s1.json --keys '<c-s>' --effects --dump-state /dev/null
{"effect":"write_file","path":"plan.md","text":"# Plan\n\nShip the editor docs.\nThen review them."}
$ cat plan.md
# Plan

Ship the editor docs.

The file is unchanged: headless runs report effects and never perform them.

5. Apply a message file

$ cat m.jsonl
{"msg":"move","dir":"backward","by":"word","extend":true}
{"msg":"copy"}
{"msg":"move","dir":"forward","by":"doc_end"}
{"msg":"insert_newline"}
{"msg":"paste"}
$ caretline --state s1.json --msgs m.jsonl --effects --snapshot 40x6
{"effect":"clipboard_set","text":"them."}
# Plan

Ship the editor docs.
Then review them.
them.
 plan.md [+]                        5:6

--msgs - reads the messages from stdin. A JSON array works too.

6. Resize

--size resizes before the messages; --snapshot resizes after them if its size differs. At 20 columns the long line wraps:

$ caretline --state s1.json --size 20x6 --snapshot 20x6
# Plan

Ship the editor
docs.
Then review them.
 plan.md [+]   4:18

7. Record and replay a session

caretline notes.md --trace t.jsonl          # edit, then quit
caretline --replay t.jsonl --snapshot 80x24 # the final frame, exactly
caretline --replay t.jsonl --dump-state -   # the final state

--trace appends, so restarting into the same file adds a new state line and replay continues from it. The file gets every line, also past --trace-limit, which bounds only the in-memory trace. --replay reads any trace: a full file, the output of caretline send trace.get --raw (one segment), or of trace.get all; a state line in the middle (a state.set or checkpoint) restarts the replay from that state. The fixture session.trace.jsonl is a recorded session:

$ caretline --replay crates/caretline-app/fixtures/session.trace.jsonl --snapshot 36x8

Shift and the arrows grow one from
the caret's end,
and a plain arrow collapses it to
the edge.

arrows grow one from the caret's
 select.md [+]                 5:33

Snapshot formats

--format text (the default) prints the rows with trailing spaces trimmed. --format ansi adds styling: the selection in reverse colours, the status bar highlighted, and the caret as an underlined reverse cell (a snapshot has no terminal cursor).

$ caretline --state s1.json --keys '<s-a-left>' --snapshot 40x6 --format ansi | sed -n 4p | cat -v
^[[0mThen review ^[[0m^[[7;4mt^[[0m^[[30;46mhem.^[[0m                       ^[[0m

Here ^[[7;4m marks the caret cell and ^[[30;46m the rest of the selection.

Fixtures

crates/caretline-app/fixtures holds saved states with their text and ANSI snapshots. The CLI tests check that each one still renders the same.

FixtureShows
wrapped-paragraphA Markdown note soft-wrapped at 44 columns, caret on a wrapped row
emoji-lineEmoji (skin tone, ZWJ family, flags), combining accents and wide CJK, with a selection
mid-selectionA selection across lines, ending inside a wrapped row
after-undoAn edit undone, with the redo step still in the history
no-wrap-tableWrapping off: a long table row scrolled sideways
session.trace.jsonlA recorded session; session.snapshot.txt is its replay at 36x8
outline-triptrip.md opened as an outline: a heading, paragraphs, nested tasks, an image block and a numbered list, with blank rows drawn as virtual rows
outline-editedThe same after keys: a new nested task added and done, an item moved up
outline-splitA paragraph whose two selected lines the task cycle turned into tasks (standup.md)
$ caretline --state crates/caretline-app/fixtures/emoji-line.state.json --keys '<s-right><s-right>' --snapshot 40x6
Emoji 👍🏽 and family 👨‍👩‍👧, accents café
and résumé, wide 漢字かな, flags 🇫🇷🇯🇵.
Second line: ½ ⅓ → ✓


 emoji.md                   5 sel  1:19
$ caretline --state crates/caretline-app/fixtures/no-wrap-table.state.json --keys '<home>' --snapshot 48x6
| Name | Role | Notes |
|---|---|---|
| Ada | engine | Wrapping is off here, so long t
| Lin | review | Short row. |

 nowrap.md                                  3:1

The status bar’s N sel counts Unicode scalar values, while line:col counts graphemes.

Animate a live editor

crates/caretline-app/examples/scenes.rs pushes six ASCII scenes (donut, cube, tunnel, plasma, fire, warp) into a running editor with the frame op, paced against absolute deadlines, and reports the frames per second it achieved:

caretline notes.md --listen                               # in one terminal
cargo run --release -p caretline-app --example scenes     # in another
cargo run --release -p caretline-app --example scenes -- --fps 120 --scene donut,plasma --seconds 5
FlagDoes
--fps 60,120,0Target rates to play each scene at (0 is unthrottled). Default 60,120,0
--seconds SHow long each scene plays at each rate (default 3)
--scene NAMESSome of donut,cube,tunnel,plasma,fire,warp (default all)
--socket PATHThe editor’s socket (default: the newest live editor)
--frames N, --size WxH, --spin-ms MSFrames precomputed per scene (180), the scene size (the editor’s text area), how long before a deadline to stop sleeping and spin (1 ms)

For 120 fps on screen, the terminal must paint that fast too: in WezTerm set config.max_fps = 120, on a 120 Hz display.

Errors

Errors go to stderr with exit code 1:

$ caretline --state s0.json --snapshot 80
caretline: bad size "80": expected WIDTHxHEIGHT, like 80x24
$ caretline --state s0.json --keys '<nope>'
caretline: unknown key <nope>
$ echo '{"msg":"jump"}' | caretline --state s0.json --msgs -
caretline: line 1: unknown variant `jump`, expected one of `insert_text`, `insert_newline`, … at line 1 column 13

Limits

This page on GitHub: docs/caretline/cli.md