caretline

Messages, effects and keys

Every input to the engine is a Msg, and every piece of work it hands back is an Effect. Both are plain values with a JSON form. Use the JSON form in --msgs files, traces and the protocol.

The JSON form

Messages are tagged with "msg" and effects with "effect". Names are snake_case. Variants without fields are just the tag:

{"msg":"undo"}
{"msg":"insert_text","text":"hi"}
{"msg":"move","dir":"backward","by":"word","extend":true}
{"effect":"clipboard_set","text":"hi"}

A message file is one message per line (blank lines and // comments are skipped), or a single JSON array.

Messages

Editing

Every edit applies at every range of the selection. An edit with a selection replaces or deletes exactly the selection.

MsgJSONDoes
InsertText { text }{"msg":"insert_text","text":"hi"}Types text at every caret, replacing selections. Line breaks are converted to the document’s line ending
InsertNewline{"msg":"insert_newline"}Inserts the document’s line ending
DeleteBackward{"msg":"delete_backward"}Deletes one grapheme back, or the selection
DeleteForward{"msg":"delete_forward"}Deletes one grapheme forward, or the selection
DeleteWordBackward{"msg":"delete_word_backward"}Deletes back to the previous word start. At a line start, only the line break
DeleteWordForward{"msg":"delete_word_forward"}Deletes forward to the next word end. At a line end, only the line break
DeleteToLineStart{"msg":"delete_to_line_start"}Deletes back to the start of the visual row. At the row start, one grapheme
DeleteToLineEnd{"msg":"delete_to_line_end"}Deletes forward to the end of the visual row. At the row end, one grapheme
KillLine{"msg":"kill_line"}Deletes to the end of the document line, or the line break when already there

Motion and selection

MsgJSONDoes
Move { dir, by, extend }{"msg":"move","dir":"forward","by":"word","extend":false}Moves every caret. extend (default false) keeps the anchors, so the selection grows or shrinks
Click { col, row, extend }{"msg":"click","col":4,"row":0}Places the caret at a screen cell, or extends to it. Leaves one range
Scroll { rows }{"msg":"scroll","rows":-3}Scrolls the view (negative is up). The caret moves only if it would leave the view
SelectAll{"msg":"select_all"}Selects the whole document
Collapse{"msg":"collapse"}Collapses every range to its caret

dir is backward or forward. by is one of:

byMoves to
graphemeThe next or previous grapheme cluster
wordThe end of the next word (forward) or the start of the previous one (backward)
lineThe same column one document line up or down, ignoring soft wrap
visual_lineThe goal column one visual (wrapped) row up or down
line_startThe start of the caret’s visual row (dir is ignored)
line_endThe end of the caret’s visual row (dir is ignored)
pageA screenful of visual rows; the view scrolls by the same amount
doc_start, doc_endThe start or end of the document (dir is ignored)
blockIn an outline: the next block’s content start, or back to this block’s (then the previous one’s). Elsewhere, as line

Collapse rules. A motion without extend on a non-empty selection collapses it instead of moving from the caret. Backward goes to the selection’s start and forward to its end. Up and down start from that edge. ↑ on the first row goes to the document start and ↓ on the last row to the end, as in a macOS text field.

Clipboard and history

MsgJSONDoes
Copy{"msg":"copy"}Copies the selection to the register and returns clipboard_set. Several ranges join with the line ending
Cut{"msg":"cut"}Copy, then delete the selection. One undo step. With one range, the register keeps the marks the cut removed
Paste { text }{"msg":"paste","text":"x"} or {"msg":"paste"}Pastes text, or the internal register when text is absent. Outside text is converted to the document’s line ending; the register is pasted as is. Pasting the register, or text equal to what it put on the system clipboard, brings its marks back where their ids aren’t in use
Undo{"msg":"undo"}Steps back one revision, restoring text and selection
Redo{"msg":"redo"}Steps forward one revision

Files, lifecycle and the runtime

MsgJSONDoes
Save{"msg":"save"}Returns write_file when the state has a path; otherwise sets a status message
Saved{"msg":"saved"}The runtime finished a save: marks that revision as saved
SaveFailed { err }{"msg":"save_failed","err":"disk full"}The runtime couldn’t save: shows the error
Quit{"msg":"quit"}Returns quit. With unsaved changes, the first quit only warns and a second one quits
Resize { width, height }{"msg":"resize","width":80,"height":24}Sets the viewport (clamped to at least 1 × 1)
Tick { now_ms }{"msg":"tick","now_ms":1000}Reports the time. Undo grouping uses it
ShowStatus { text }{"msg":"show_status","text":"hi"}Shows a one-line message in the status bar (the first line of text)
Frame { now_ms }{"msg":"frame","now_ms":1008}A display frame from the runtime’s frame clock. Advances the clock as tick does; animation state advances from it
FrameClock { fps }{"msg":"frame_clock","fps":120}Asks the runtime for a frame clock: a frame message fps times a second (0 turns it off). Stored in the view as frame_clock

Outline documents

These act on outline documents (state.doc.outline set). Elsewhere they only set the status message only in outline documents, except soft_break (a line break), select_word_at and paste_plain (a paste). In an outline, Enter, Backspace, Delete, the word and line deletes, typing, copy, cut and paste also follow the outline’s rules (see outline.md).

MsgJSONDoes
SoftBreak{"msg":"soft_break"}A line break inside the block (in a paragraph, as Enter)
Indent, Outdent{"msg":"indent"}Nests the caret’s block, or every block the selection touches, one level deeper or shallower
TaskCycle{"msg":"task_cycle"}Text → open task → done → text, on the caret’s block or the selected blocks. Inside a multi-line paragraph, splits the selected lines out as tasks
SetStatus { id, ch }{"msg":"set_status","id":3,"ch":"x"}Sets a task’s box character (a click on the box)
MoveBlock { dir }{"msg":"move_block","dir":"backward"}Swaps the caret’s block and its children with the previous or next sibling
SelectBlock { id }{"msg":"select_block","id":3}Selects a block’s content
SelectWordAt { pos }{"msg":"select_word_at","pos":12}Selects the word at a char position; a click with extend right after extends by words
InsertBlocks { after, blocks }{"msg":"insert_blocks","after":3,"blocks":[{"kind":"task","status":" ","text":"Call Ana"}]}Inserts host blocks after a block, or at the start without after. One undo step
PastePlain { text }{"msg":"paste_plain","text":"a\nb"}Pastes as paragraphs with their line breaks kept

A block is named by its mark id, a number. A NewBlock is {"depth":0,"kind":"para"|"bullet"|"task","status":" ","text":"…","gap":true,"mark":7}; everything but kind and text is optional.

Views and folds

MsgJSONDoes
ScrollView { rows }{"msg":"scroll_view","rows":5}Scrolls the view without moving the caret (scroll moves it when it would leave the view). The view stays put until the next caret motion or edit
Fold { id }{"msg":"fold","id":3}Hides a block’s children in this view (outline documents). A caret inside them moves to the block’s end. A block without children doesn’t fold
Unfold { id }, ToggleFold { id }{"msg":"toggle_fold","id":3}Shows them again, or toggles

Folds belong to a view: another view of the same document still shows the children. Hidden lines take no rows, so ↓ steps over them and → at the block’s end goes past them. A fold drops when its block goes.

Changes from elsewhere

External { changes } applies changes made outside the editor (another device, a daemon, an agent) to the document, in order, outside the undo history: every view is mapped through them, and undo takes back only local edits around them. It is passive, and a read-only view takes it too.

{"msg":"external","changes":[
  {"change":"replace_content","id":3,"text":"Call Ana\nabout the desk"},
  {"change":"set_shape","id":4,"depth":1,"kind":"task","status":"x"},
  {"change":"insert_block","after":4,"block":{"kind":"bullet","text":"new","mark":12}},
  {"change":"remove_block","id":5},
  {"change":"set_gap","id":6,"gap":true},
  {"change":"replace","from":0,"to":5,"text":"Hello"}]}
ChangeDoes
replace_content { id, text }A block’s content after its marker (\n for soft breaks). Only the chars that differ change
set_shape { id, depth, kind, status }Rewrites a block’s indentation and list marker. A number, heading or quote marker stays, as content
set_gap { id, gap }A block’s blank row (null: the default)
insert_block { after, block }A NewBlock after a block’s own lines, or first without after. block.mark gives it that id when free
remove_block { id }A block’s lines, its continuations included, not its children
replace { from, to, text }Chars [from, to) of the current text (any document)

A change that names a missing block is skipped with a notice effect. See architecture.md for the history transform.

tick, frame, frame_clock, resize, saved, save_failed, show_status and external are passive. They don’t clear the status message, don’t end a typing run and don’t disarm a pending quit.

Effects

EffectJSONThe runtime should
WriteFile { path, text }{"effect":"write_file","path":"notes.md","text":"…"}Write the file, then send saved or save_failed
ClipboardSet { text }{"effect":"clipboard_set","text":"…"}Put the text on the system clipboard
Quit{"effect":"quit"}Exit
Notice { text }{"effect":"notice","text":"nothing to nest under"}Show a message: an outline document’s status message when the status bar is off
Completed { id }{"effect":"completed","id":3}Nothing required. A task reached done in an outline (a host may save at once)
Restored{"effect":"restored"}Nothing required. Undo or redo changed an outline (a host re-reads what it keeps per block)
BlockLeft { from, to }{"effect":"block_left","from":2,"to":3}Nothing required. The caret moved to another block of an outline
Refused{"effect":"refused"}Nothing required. An editing message reached a read-only view and changed nothing

Effect is #[non_exhaustive]: new kinds may come, so a match needs a wildcard arm (a runtime can ignore kinds it doesn’t know).

The keymap

keymap(&Key) -> Option<Msg> is pure. It follows macOS text-field keys, with Ctrl twins for terminals that don’t forward Cmd (d- in key scripts). Adding Shift to any motion extends the selection.

KeyMsg
← →move by grapheme
⌥← ⌥→, Ctrl-← Ctrl-→, Alt-B Alt-Fmove by word
↑ ↓move by visual_line
⌘↑ ⌘↓, Ctrl-Home Ctrl-End, ⌘Home ⌘Endmove to doc_start / doc_end
Home End, ⌘← ⌘→, Ctrl-A Ctrl-Emove to line_start / line_end
PgUp PgDnmove by page
Esccollapse
⌘A, Alt-Aselect_all
Backspace, Ctrl-Hdelete_backward
Deletedelete_forward
⌥Backspace, Ctrl-Backspace, Ctrl-Wdelete_word_backward
⌥Delete, Ctrl-Delete, Alt-Ddelete_word_forward
⌘Backspace, Ctrl-Udelete_to_line_start
⌘Deletedelete_to_line_end
Ctrl-Kkill_line
⌘C ⌘X ⌘V, Ctrl-C Ctrl-X Ctrl-Vcopy, cut, paste (with no text)
⌘Z, Ctrl-Zundo
⇧⌘Z, Ctrl-Shift-Z, ⌘Y, Ctrl-Y, ⌘R, Ctrl-Rredo
⌘S, Ctrl-Ssave
⌘Q, Ctrl-Qquit
Enterinsert_newline
Tabinsert_text with "\t"
Any other characterinsert_text with that character

Unbound: Shift-Tab, Alt-↑/Alt-↓, Ctrl-↑/Ctrl-↓, and Cmd or Ctrl with any letter not listed. Moving by document line has no key; send the message.

An outline document uses outline_keymap, which adds Tab/Shift-Tab (indent/outdent), Ctrl-T (task_cycle), Shift-Enter and Ctrl-J (soft_break), Alt-↑/Alt-↓ (move_block), Ctrl-↑/Ctrl-↓ (move by block) and Alt-V (paste_plain). keymap_for(outline, key) picks the right one; key scripts, Session::keys and the protocol’s keys op use the state’s.

Key scripts

--keys, script_to_msgs, the tests and the protocol’s keys op share one notation. Literal characters type themselves. <…> is a special key or a modifier chord. Keys go through the keymap, so a script tests the bindings too.

TokenKey
<cr> <enter> <ret> <return>Enter
<bs> <backspace>Backspace
<del> <delete>Delete
<tab> <s-tab>Tab, Shift-Tab
<esc>Escape
<space>A space
<left> <right> <up> <down>Arrows
<home> <end> <pgup> <pageup> <pgdn> <pagedown>Home, End, Page Up, Page Down
<lt> <gt>A literal < or >
<wait:MS>Advances the clock by MS milliseconds (a tick)

Modifier prefixes combine in any order:

PrefixModifier
s-Shift
c-Ctrl
a- or m-Alt / Option
d-Cmd (Super)

Examples: <s-left> (extend left), <a-left> (word left), <c-s-z> (redo), <d-a> (select all), <s-a-right> (extend a word right). A newline or tab character in the script is Enter or Tab. A < that doesn’t start a token is a literal <. An unknown token such as <nope> or an unknown modifier such as <q-x> is an error.

$ caretline --state s.json --keys 'one<wait:2000> two<c-z>' --snapshot 40x6

Here <wait:2000> puts ” two” in its own undo step, so <c-z> removes only ” two”.

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