caretline
caretline— fps

The text editor
as a state machine.

A headless, deterministic text-editing engine you drive like a state machine: the whole editor is one JSON state, every input is a message, and any session can be saved, sent, replayed, or shared live with an agent.

cargo add caretline
frames

Every keystroke, replayable. Every frame, too.

To find the engine's ceiling, a script pushed full-screen ASCII animations into a live caretline editor: each frame a whole new state, 122×64 cells, with the brightest cells sent as selections. The engine applied between 835 and 1,674 full states a second. The terminal paints 60. These are the same scenes, ported to your browser and drawn on a frame clock.

Unthrottled state.set requests over the socket, Apple-silicon Mac, release build. All scenes interleaved ran at 1,155 a second. Busier frames, with more selection ranges, are slower. The numbers

one state

The whole editor is one state.

State in, message in, state out. update is the only place state changes and it reads no clock. view turns the state into a grid of cells. Below is the real engine, compiled to WebAssembly and running in this page. Click the frame and type, then drag the scrubber back.

view(state) → frameloading
rev 0··
msg streamupdate(state, msg)
    state.jsonone value
    1. State

      Everything that decides the screen: text, selections, scroll, viewport, the undo tree, the clock. Plain data that round-trips through JSON.

    2. Msg

      Every input: an edit, a motion, a click, a resize, a clock tick, the result of a save. Each one has a JSON form.

    3. update(&mut State, Msg) → Vec<Effect>

      The only place state changes. Pure: no I/O, no clock, no randomness. Work for the outside world comes back as effects.

    4. view(&State) → Frame

      A grid of cells and the caret’s cell. Pure, so the same state always draws the same frame.

    // The pieces, end to end (docs/caretline/architecture.md)
    use caretline::{keymap, update, view, Key, KeyCode, State, Viewport};
    
    let mut state = State::new("hi", None, Viewport { width: 20, height: 3 });
    let msg = keymap(&Key::plain(KeyCode::End)).unwrap();   // Move { forward, line_end }
    let effects = update(&mut state, msg);                   // no effects for a motion
    assert!(effects.is_empty());
    assert_eq!(view(&state).cursor, Some((2, 0)));          // the caret is after "hi"
    uses

    Edit together: you, your scripts, your agents.

    One engine, driven the same way by a keyboard, a test, a script or a model. What that makes easy:

    Agents editing beside people

    Attach to the editor a person is using and drive it over JSON lines. Every result carries a rev; pass if_rev and a write only lands if nothing changed since you read. Events say whether a change came from the terminal or a client.

    $ caretline notes.md --listen          # a person edits here
    $ caretline send keys '<d-down>from another shell'
    $ caretline send '{"op":"msgs","msgs":[{"msg":"undo"}],"if_rev":0}'
    {"error":{"kind":"stale","message":"rev is 12, the request expected 0"}}

    Replay and time travel

    A trace is a state line followed by every message, clock ticks included. Replaying it gives the identical state and the identical frame, so a bug report can be the session itself.

    $ caretline send trace.get all --raw > live.jsonl
    $ caretline --replay live.jsonl --snapshot 80x24

    Snapshot testing

    Because update and view are pure, a test is data: a state, some keys, the expected result. No terminal, no timing, no mocks. Goldens write the selection inline.

    #[test]
    fn e01_left_collapses_to_start() {
        golden("Hello ⟦wor▮⟧ld", "<left>", "Hello ▮world");
    }

    Embedding in a TUI

    caretline owns the editing rules; you own the loop, the terminal, files and the clipboard. Turn input into a Msg, call update, perform the effects, copy view’s cells to the screen. No terminal crate inside.

    let msg = keymap(&key).unwrap();          // a key → a Msg
    let effects = update(&mut state, msg);     // pure
    for fx in effects { run(fx) }              // files, clipboard, quit
    draw(view(&state));                        // a grid of cells

    Outliners and notes

    Outline documents read the text as blocks: paragraphs, bullets, numbered items and tasks. Each block keeps an id through every edit, undo and redo, and cut and paste, so a host can hang its own data on it.

    text                          marks         block
    ───────────────────────────   ───────────   ─────────────────────
    - [ ] Pay the deposit         1 @ line 2    1  task ' ', depth 0
      - ask Ana about her desk    2 @ line 3    2  bullet, depth 1
    compare

    How it compares.

    caretline puts Helix's editing model (a rope, multi-range selections, transactions, an undo tree, grapheme-correct motion and soft wrap) inside a strict Elm architecture. Against the usual ways to get an editor into a program:

    caretlineA textarea widgetA browser editor coreAn editor run as a server
    The whole editor serializes to one value yes no partly no
    Undo history, viewport and clock included yes no partly no
    Every input is a recorded message yes no partly partly
    Exact replay of a session, built in yes no no no
    Drive a live session from another process yes no no yes
    Runs in a terminal yes yes no yes

    Not there yet: syntax highlighting, search, multiple buffers, keys that add cursors, IME and bidirectional text. Multi-range selections work in the engine; no key creates them yet. The protocol's socket transport is Unix-only. What it covers

    performance

    The engine is never the bottleneck.

    Document size barely matters: a million-line document edits and scrolls as fast as a hundred-line one. The real ceilings are how fast the terminal paints and how fast a client can produce input.

    one edit, in process
    1.6 µs
    one edit over the socket
    13.4 µs
    load 1,000,000 lines (82 MB)
    202 ms
    a keystroke with 10,000 carets
    1.4 ms
    characters a second, live redraw
    72,000
    render an 80×24 frame
    0.13 ms

    Release builds on an Apple-silicon Mac. caretline bench reproduces the engine and protocol numbers. Every figure, and where the limits are

    Start with one state.

    Add the crate, build a State, send it messages. Or run caretline serve and talk to it from any language.