Files
cumulus/DESIGN.md
T
skkeyeandClaude Opus 5.5 c587c42115 Add the outline and Note views, splits, popups and the bars
The outline shows the tree as an indented list: a row per node with its
fold marker, tree lines in the parent's color, tags, a ¶ for a body and
+N for what a fold hides. zb shows every body under its title, to read a
document like a page. j/k go row by row, h/l to the parent and the first
child, a click on a marker folds, and titles are edited in their row,
rapid entry included. In move mode, j/k swap the node with its siblings,
h takes it out a level and l puts it under the one above.

The Note view shows one node at full size, as parts: the title, the
tags, the body, each link and each child. j/k step through them, Enter
or i edits the part (or goes to that link or child), and h/l turn the
page to the sibling before or after. Bodies and tags are edited with the
same editor as titles: Enter starts a line in a body, Tab types spaces,
and tags that don't parse come back as they were. cc, C and s now start
typing on an empty line too, as in vim.

Panes split the screen the way vim's windows do (Ctrl-w v s h j k l w c
o =, :split, :vsplit, :only), each in a box with its view and document,
and any pane can show any open document (gt, gT, :b, the documents
picker, a click on the document bar). The status bar has two lines: the
mode, the view, the breadcrumb or a message, the keys typed; then key
hints for what you're doing, which <Space>w hides with which-key.

Popups are a mode of their own, built on one list: ? lists every key and
command, / searches it and Enter runs what you pick; the right-click menu
lists what you can do with a node (with the keys that do the same) or
adds a new idea on the empty canvas; the pickers set a node's color,
border, style or tags. K opens the body in a card beside the node, where
i edits it in place.

The example now opens two documents (the garden, and the kitchen its
portal leads to) and its tests drive all of the above end to end.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-01 19:10:09 -04:00

34 KiB
Raw Blame History

cumulus: design

This is the design approved on 2026-10-01, after six rounds of questions. It's the reference while cumulus is being built: when a decision changes, this file changes with it.

What cumulus is

A keyboard-driven brainstorming app for the terminal, in Rust with ratatui. The aim is "the idea node graph to end them all": quick idea dumps, mind maps, whiteboard-style planning and linked notes, in one tool. It runs on Windows and Linux, with vim keys first and full mouse support.

Scope

v1

  • The canvas: nodes, branches, cross-links with every style, folding and focus, zoom, the K card, the mouse.
  • The Outline and Note views, splits and the document bar.
  • The key engine: medium vim, which-key, help and status-bar hints.
  • Visual, move and connect modes; text editing with vim normal mode.
  • The command line, with completion and history.
  • Portals, with the jump list, backlinks and breadcrumb.
  • Rapid entry and the Inbox.
  • RON documents with autosave, reload, backups and undo; file names that follow the root's title.
  • The startup rules and cumulus new.
  • TOML config, key and theme files, with named styles and styles set by tags.

v1.1: the Board view · the Search picker across documents · :tidy and align · branch out · export (Markdown, plain text) and import (indented lists).

Later: peeking into portals and pulling a document back in · branches that keep themselves tidy · the Links map · backlinks inside a document · picking single nodes in visual mode · Alt+hjkl · opening a body in $EDITOR · persistent undo · sessions · operator + motion and macros · a tutorial document · checkbox, link and date fields · Graphviz and Mermaid export · a Settings view · vim tab pages.

Not planned unless asked: f hint jumps · a minimap · straight (braille) lines.

Decisions

Documents and structure

  • Each document has one main root, and the document's name is the root's title. Loose top-level nodes float around it.
  • A tree plus cross-links: each node has at most one parent, and cross-links join any two nodes, at most one link per pair.
  • A node holds a title (one line, never empty), a body, tags and a color. A new node left empty disappears.
  • A portal node shows another document's root, live (its title, color and node count), and leads there. Portals always land on the root.
  • Backlinks are stored in the target document and synced on save, so they travel with the files.
  • Files live anywhere: there's no workspace folder. Portal paths are relative, with / on every OS.
  • Each file carries a random uid, so "where you left off" survives renames and a portal can tell when its file was replaced by another document.
  • File names follow the root's title, and a rename fixes the portals pointing at the file. A folder's default document keeps its name (ideas.cum).

Canvas

  • Free placement: a new node appears next to its parent, and nothing moves unless you move it. Moving a node brings its branch along.
  • Folding: each branch is Open, Auto (open while the cursor is inside) or Closed (a jump inside opens it until you leave). New branches start Open; a focus toggle makes Open branches act like Auto.
  • Zoom levels: Normal (boxes), Far (one line per node: the same width, half the height) and Overview (colored dots: a quarter of the width, a third of the height). Far keeps the width because one-line titles collided when distances were halved both ways (changed during stage 2). Body text never shows on the canvas: K pops it up, and the Note view shows all of it.
  • Right-angle lines with proper junctions (├ ┬ ┼); lines pass under nodes.
  • A node's color goes on its border. Each node can pick its border, and the theme's named styles and tag rules can set color and border together.
  • Connections can set arrowheads (off, end, start, both), a line (solid, dashed, dotted, thick, double), a color (a palette color or the node's own) and a label, or use a named style. Branches start solid without arrowheads, in their parent's color, so a node's color flows down the lines to its children (dim when the parent has no color). Cross-links start as dim dashed arrows.

Keys

  • Medium vim: counts, operators with node targets, registers (with "+ for the system clipboard), marks (capital marks across documents), . to repeat.
  • Tab and Ctrl-i jump forward, as in vim, so a adds a child.
  • dd deletes a node with its branch; x deletes just the node, and its children move up.
  • A count on dd, yy, >> or << takes in the next siblings: 3dd deletes this node and the next two, with their branches, as vim's 3dd takes three lines. Which-key says how many as you type (2d3: 6 nodes).
  • Rapid entry is what happens after o, O or a: Enter starts the next sibling, Tab / Shift-Tab indent and outdent the idea being typed, and Enter on an empty idea stops (the empty idea goes away). Esc switches to vim keys inside the text; a second Esc leaves. Inside a title, o and O start a new sibling too.
  • >> puts the node under the one above it, as its last child; << takes it out a level, right after its old parent. Both place the node below its new neighbors, and keep its branch with it.
  • C (connect mode) edits the connection to another node. Aimed at your own parent, it styles or cuts the branch line; cutting makes the node loose.
  • M (move mode) moves the branch; M again switches to just the node.
  • gh gj gk gl move through the tree in every view; plain hjkl follow each view's layout (on the canvas: the nearest node that way).
  • Folds: za zo zc zR zM, plus zf (Auto) and zi (focus).
  • q stays free for macros; quit with :q, :qa or ZZ.
  • Which-key works like skyshift's; the leader menu pops up at once. The status bar shows key hints, and ? opens searchable help.
  • Keys can be changed in keys.toml, in vim notation.

Editing text (settled in stage 6)

  • A title is typed right in its box on the canvas (zoomed to Normal while it's edited). The box grows with the text, the terminal's cursor sits in it, and the border takes the insert color while you type.
  • The cursor's shape follows the mode, as in vim: a bar while typing (and in the command line), a block on the letter with vim keys, and the terminal's own shape elsewhere.
  • The document follows the text as you type, so every view stays current. The whole edit is one undo step (in rapid entry, one per idea); inside the text, u and Ctrl-r undo and redo the edit's own changes.
  • A title left empty comes back as it was, with a note saying so; a new idea left empty goes away.
  • Registers are shared by the whole app, as in vim: text yanked in one title pastes into another. "+ stays inside cumulus until stage 8 connects it to the system clipboard.
  • . inside the text repeats the last change there, typing included. . on nodes comes with node yank and paste, in stage 8.
  • As in vim: cw changes to the end of the word, dw stops at the end of the line, j and k move by lines of text rather than wrapped rows, Esc steps back onto the last letter typed.
  • Tab and Shift-Tab only move a new idea; on an existing title they say that >> and << do it.
  • The mouse while editing: a click in the text moves the text cursor; a click anywhere else keeps the text, finishes the edit and does what a click does; a double-click on a title starts editing at that letter.
  • A portal's title is its document's name, so it isn't edited in place: rename that document instead.
  • Bodies are edited in the Note view and in the K card, with the same editor: Enter starts a new line, Tab types four spaces. cc, C and s start typing on an empty line too, as in vim (bodies are often empty).

Views, panes and popups (settled in stage 7)

  • The Note view is made of parts: the title, the tags, the body, each link (out, then in) and each child. j and k step through them, with a bar marking the one you're on; Enter, i or A edits it (I from its start, cc S anew), or goes to that link (a jump: '' comes back) or child. h and l turn the page: the sibling before or after. Node keys (dd, za, o…) act on the note's node. The note follows the cursor, starting at the title of each node it comes to.
  • The outline has a row per node: its fold marker (▾ ▸), tree lines in the parent's color (as branches are on the canvas), its tags, when it has a body, +N for what a fold hides. zb shows every body under its title, where its children hang, to read the document like a page. j and k go row by row, h and l to the parent and the first child; a click on a marker folds. Titles are edited in their row, rapid entry included.
  • Move mode in the outline and the Note view moves the node through the tree: j and k swap it with its siblings (J and K five places), h takes it out a level, l puts it under the one above.
  • The status bar has two lines: the mode, the view, the breadcrumb (or a message in its place) and the keys typed so far; then the key hints for what you're doing. <Space>w hides the hint line along with which-key.
  • Help (?, <Space>?, :help [topic]) lists every key of the focused view and every : command, under which-key's headings. / searches (rows that start with what's typed first, then fuzzy matches), and Enter runs the row: a key that waits for more (m) is typed for you to finish, and a : command that needs an argument opens the command line with it typed.
  • The K card opens beside the node (in the middle when the node isn't on screen). j k Ctrl-d Ctrl-u scroll it; i, A or Enter edits the body right there (I from its start), and when that's done the card stays; K, q or Esc closes it.
  • Right-click opens a menu of what you can do with the node, each with the keys that do the same; on the empty canvas: a new idea right there (a loose node), zoom in and out. <Space>c <Space>B <Space>s <Space>t open the color, border, style and tags pickers, which start on what the node has now. In the tags picker Enter puts a tag on or off and the picker stays open; a search that matches no tag, then Enter, adds it as a new one.
  • Every pane has a box with its view and document in the top border, the focused one in the accent color. A new pane comes first (left or above) and takes the focus, as in vim, and closing the last pane quits (q, ZZ, :q). A click focuses the pane under it; a text edited in the old one is done first.
  • The document bar is always at the top, the focused pane's document standing out; a click on a name shows that document in the pane. Each pane can show any open document.
  • Popups (the help, the menu, the pickers, the card) are a mode of their own: keys and clicks go to them, a click outside a list closes it, and they stay above the status bar.

Files and saving

  • Documents are RON files ending in .cum. Configs are TOML: config.toml, keys.toml and themes/<name>.toml.
  • Autosave about a second after a change (autosave = false for vim-style :w). A file changed outside cumulus is reloaded; if you also had unsaved changes, cumulus asks which to keep. Daily backups go to the data folder.
  • Undo per document, while cumulus runs: one step per command, per text-editing session, and per node in rapid entry.
  • Each document reopens where you left it (cursor, zoom, scroll).

Screen

  • Vim-style splits; panes on the same document share one cursor.
  • Open documents are buffers, listed in a bar at the top; the status bar is at the bottom.

Project

  • A library plus a thin main.rs, so build stages can run as examples.
  • Rust pinned to 1.90.0; nothing needs nightly.

Data model (model/: pure data, no UI, no files)

// Ids and units: newtypes, so they can't be mixed up.
NodeId(u32)                      // unique inside a document, never reused while it's open
BufferId(u32)                    // an open document, like vim's buffer numbers (:ls, :b 3)
PaneId(u32)                      // a split
DocUid(u64)                      // stored in the file, survives renames
CanvasPos { x: i32, y: i32 }     // cells at Normal zoom, may be negative
ScreenPos { col: u16, row: u16 } // a terminal cell
Camera { origin: CanvasPos, zoom: Zoom }   // converts between the two
enum Zoom { Normal, Far, Overview }

// Text, checked once when it comes in.
Title    // one line, never empty
Body     // any text
Tag      // lowercase, no spaces, stored without '#'
Label    // a connection label: one short line
ColorName, StyleName   // names looked up in the theme

// A node and its place in the tree.
struct Node  { content: Content, tags: Tags, look: NodeLook, pos: CanvasPos, fold: Fold }
enum Content { Idea(Idea), Portal(Portal) }
enum Fold    { Open, Auto, Closed }
enum Place   { Root, Loose, Child { parent: NodeId, branch: LineLook } }
struct NodeLook { color: Option<ColorName>, border: Option<Border>, style: Option<StyleName> }
enum Border  { Rounded, Square, Double, Thick, Dashed, Hidden }

// Cross-links. Branch lines use the same LineLook.
struct Link     { from: NodeId, to: NodeId, label: Option<Label>, look: LineLook }
struct LineLook { arrows: Option<Arrows>, line: Option<Stroke>, color: Option<LineColor>, style: Option<StyleName> }
enum Arrows     { Off, End, Start, Both }
enum Stroke     { Solid, Dashed, Dotted, Thick, Double }
enum LineColor  { Named(ColorName), FollowNode }

// Between documents.
struct Portal   { path: LinkPath, uid: DocUid, title: Title }  // title: kept in case the file goes missing
struct Backlink { from: LinkPath, uid: DocUid, node: NodeId }

// The document. Its fields are private, so only its methods can change it.
struct Document { uid, root: NodeId, entries: BTreeMap<NodeId, Entry>, loose: Vec<NodeId>,
                  links: BTreeMap<LinkKey, Link>, backlinks: Vec<Backlink>, next_id }
struct Entry    { node: Node, place: Place, children: Vec<NodeId> }

The Option fields in NodeLook and LineLook are overrides. A look is built up in layers: the theme default, then the tag style, then the named style, then the node's own fields. Each layer only states what it changes.

Rules the Document methods guarantee

  • There is exactly one root, and it can't be deleted or given a parent.
  • Each node has at most one parent, and there are no cycles: a move checks ancestors first.
  • Children keep their order, and the file lists nodes in outline order.
  • A link never joins a node to itself, both of its ends always exist, and deleting a node removes its links.

Undo. Every change runs as a transaction: doc.edit(|e| { e.add(…)?; e.update(…)?; Ok(()) }). The transaction records the old value of everything it touches. If a step fails, it rolls back; if it succeeds, that record becomes one undo step. Undo writes the old values back, redo the new ones. No command needs its own "reverse" code, and the records are plain data, ready for a persistent undo file later.

Folding is one pure function of the document, the cursor and the focus toggle, so every view folds the same way.

Portals. A cache holds each target's root title, color and node count, refreshed when files change. When a root's title changes, the next save renames the file, then fixes the portal paths in every document listed in its backlinks, and the backlink paths in every document it points to.

Screen and views

 cumulus  Garden plans [+] │ Kitchen │ Reading list
┌ Outline ──────────────┐┌ Canvas ───────────────────────────────────────────┐
│ ▾ Garden plans        ││                      ┌──────┐                     │
│   ├ Soil              ││                   ┌──┤ Soil │                     │
│   ├ [Raised beds]     ││                   │  └──────┘                     │
│   │ └ Cedar           ││ ╭──────────────╮  │  ┏━━━━━━━━━━━━━┓    ┌───────┐ │
│   └ Watering          ││ │ Garden plans ├──┼──┨ Raised beds ┠────┤ Cedar │ │
│ ◦ ask about compost   ││ ╰──────────────╯  │  ┗━━━━━━━━━━━━━┛    └───────┘ │
│                       ││                   │  ┌──────────┐                 │
│                       ││                   └──┤ Watering │                 │
│                       ││                      └──────────┘                 │
└───────────────────────┘└───────────────────────────────────────────────────┘
 NORMAL │ Canvas │ Garden plans › Raised beds                               2g
View What it's for
Canvas The 2D map: free placement, 3 zoom levels, right-angle lines and the mouse.
Outline Reorganizing and reading: an indented tree with fold markers, +N for hidden nodes and » for portals, with loose nodes listed at the end.
Note Writing: one node's title, tags, color and body (with vim motions inside), plus its links in and out and its children. It follows the cursor.
Board (v1.1) Sorting: columns by tag or color. Moving a card to another column changes its tag.
  • Pickers: documents, colors, borders, styles and tags in v1; Search in v1.1.
  • Popups: which-key (bottom right, as in skyshift), ? help, the K card, the right-click menu, confirmations, and the command line with its completion list.
  • Top bar: the open documents. Bottom bar: the mode, the view, the breadcrumb (or a message), and the keys typed so far (2g above); then a line of key hints, which <Space>w hides.

Mouse (on the canvas; other views get click, double-click and scroll): click selects, double-click edits the title (from the letter clicked), dragging a node moves it with its branch, dragging empty space pans, scrolling pans, Ctrl+drag selects a rectangle, Ctrl+click adds or removes one node, right-click opens a menu built from the same actions as the keys. Zoom is on the + and - keys.

Modes and keys

enum Mode {
    Normal,
    Text(TextEdit),       // a title or body being edited: typing (INSERT) or vim
                          // keys inside it (TEXT); the editor knows which, so the
                          // two can't disagree. Rapid entry lives here too.
    Visual(Visual),       // anchor + Rectangle or Branches
    Move(Grab),           // a node or a branch being moved
    Connect(Connect),     // the source node and the link being styled
    Command(CommandLine), // `:` with completion
    Popup(Popup),         // the help, the menu, a picker or the K card (which
                          // holds its own TextEdit while the body is edited)
}

Each mode has its own key tables. Which-key, ? help and keys.toml all read those same tables. The engine handles counts, registers, operator + target (d + ab), and keys that take a character after them (ma, fx inside text).

Group Keys
Move (every view) hjkl or arrows · gh gj gk gl tree moves · w b next / previous in reading order · gg root · G last · / n N search · Ctrl-o Ctrl-i (Tab) jump back / forward · m{a-z} '{a-z} marks (A–Z across documents) · Enter or gf through a portal · gd follow a cross-link
View zz center · Ctrl-e Ctrl-y zh zl pan · Ctrl-d Ctrl-u half a screen · + - zoom · za zo zc zR zM folds · zf Auto · zi focus · K body card
Create and edit o O sibling below / above · a child · i A Enter edit the title · I edit from its start · cc S retype
Operators dd or dab delete branch · dib delete children · x delete just the node · yy yab yib yank · p P paste after / before · ]p paste as child · >> << indent / outdent · "a–"z and "+ registers · . repeat · u Ctrl-r undo / redo
Modes v V visual · M move · C connect · : command · Esc back to normal
Leader <Space> c color · s style · B border · t tags · p new portal · i inbox · b documents · v then c/o/n for canvas / outline / note · w which-key on/off · ? help
Panes, documents Ctrl-w v Ctrl-w s split · Ctrl-w hjkl focus · Ctrl-w w the next pane · Ctrl-w c close · Ctrl-w o only · Ctrl-w = even sizes · gt gT ]b [b next / previous document · ZZ close pane (the last one quits)
Outline j k rows · h l parent / first child · zb every body · M then j k (J K: 5) swap with siblings, h out, l under the one above
Note view j k parts · Enter i A edit the part (I from its start, cc S anew), or go to that link or child · h l the sibling before / after
Lists (help, menu, pickers) j k gg G Ctrl-d Ctrl-u move · / search · Enter pick or run · Esc or q close
K card j k Ctrl-d Ctrl-u scroll · i A Enter edit the body (I from its start) · K q Esc close
Typing Backspace Delete · Ctrl-w delete word · Ctrl-u delete line · arrows, Home, End · Enter (title: done, or after o O a the next idea; body: new line) · Tab Shift-Tab (a new idea: under the one above / out a level) · Esc to vim keys
Inside text h j k l w b e 0 ^ $ gg G (and arrows, Home, End) · f t F T ; , · d c y with any motion and iw aw i" a" i( a( · x X s S D C Y · r ~ J p P · u Ctrl-r . · i a I A back to typing · o O (title: a new sibling; body: a new line) · Esc done
Move mode hjkl 1 cell · HJKL 5 cells · M branch ↔ node · Enter or Esc done
Connect mode move to the target as usual · Enter link · d remove · > attach as child · <Space> style menu (s named, a arrows, l line, c color, t label) · Esc cancel
Visual d y > < M C and <Space>c <Space>s <Space>t act on the whole selection · o swaps ends

Files

Documents (.cum, RON). A backlinks: [...] list appears once another document has a portal pointing here.

// cumulus document. Hand edits are fine: cumulus reloads it when it changes.
(
    format: 1,
    uid: "7f3c9a1e52b04d18",
    root: 1,
    nodes: [
        (
            id: 1,
            title: "Garden plans",
            pos: (0, 4),
            color: "green",
        ),
        (
            id: 2,
            parent: 1,
            title: "Raised beds",
            pos: (22, 4),
            tags: "#todo",
            fold: Auto,
            body: "Cedar, three of them.
40 cm high.",
        ),
        (
            id: 3,
            parent: 1,
            portal: (
                path: "kitchen.cum",
                uid: "c01d5e7aa0f3b912",
                title: "Kitchen",
            ),
            pos: (22, 8),
        ),
        (
            id: 4,
            title: "ask about compost",
            pos: (0, 14),
        ),
    ],
    links: [
        (
            from: 2,
            to: 4,
            label: "needs",
            line: Dashed,
            arrows: End,
        ),
    ],
)

Config (TOML) lives in %APPDATA%\cumulus (~/.config/cumulus on Linux); cumulus --paths prints the folders. :config writes these files with a comment on every option, and cumulus reloads them when they change.

# config.toml
theme = "default"            # a file in the themes folder
default_document = "ideas"   # what `cumulus` looks for at startup
autosave = true
mouse = true
which_key = true
which_key_delay = "300ms"
max_node_width = 30
glyphs = "unicode"           # "ascii" for fonts without box-drawing characters
completion_popup = "as-you-type"  # "on-tab": only after Tab, like vim's wildmenu

# keys.toml: sections motions, normal, canvas, outline, note, visual, move,
# connect, list, card, text (vim keys inside text), insert (typing: one key each,
# the others type themselves); commands by name (src/input/command.rs);
# "nothing" unbinds
leader = "<Space>"
[normal]
"<Space>x" = "delete-branch"
"x" = "nothing"
[canvas]
"H" = "previous-document"

# themes/default.toml
accent = "cyan"
[palette]                    # terminal names, 0-255, or "#rrggbb"
red = 167
orange = 208
green = 108                  # … yellow, teal, blue, purple, pink, brown, gray
[modes]
insert = "green"
visual = "magenta"
[styles.node.question]
color = "yellow"
border = "dashed"
[styles.link.blocks]
color = "red"
line = "dashed"
arrows = "end"
label = "blocks"
[tags]
question = "question"        # a #question node gets the "question" style

Data folder: where you left off in each document (keyed by uid), command history, capital marks, recent documents, daily backups, and lock files. If a second cumulus opens a document that's already open, it opens it read-only.

Command line

  • cumulus opens ./ideas.cum, or ~/ideas.cum if the current folder doesn't have one.
  • cumulus garden.cum opens that file; cumulus some/folder opens that folder's default document.
  • cumulus new creates ./ideas.cum, and cumulus new "Garden plans" creates ./garden-plans.cum. If the file already exists, cumulus opens it and says so.
  • cumulus --paths, --version and --help.

Module layout and flow

cumulus/
├─ Cargo.toml  rust-toolchain.toml  build.rs  rustfmt.toml  .cargo/config.toml
├─ README.md  DESIGN.md  .gitignore  .gitattributes
├─ examples/       runnable checkpoints during the build (canvas.rs, …)
└─ src/
   ├─ main.rs      command-line arguments, then app::run
   ├─ lib.rs       the module list
   ├─ model/       ids, text, geometry, look, node, portal, link, document, edit, op (edits as
   │               data), history, fold
   ├─ input/       key (vim notation), command (Command enum + names), bindings (default tables),
   │               keymap (+ keys.toml), engine (counts, registers, operators), mouse
   ├─ command/     registry, parse, complete, history
   ├─ view/        mod.rs (the View trait), canvas/ (camera, boxes, lines, route, scene, render,
   │               navigate, placement, view), outline, note; later board
   ├─ ui/          text_input (a line of text and its cursor), text_editor/ (vim inside text:
   │               motion, object, layout), list (the popup list help, menus and pickers
   │               share), card (the K card), status_bar, document_bar, which_key,
   │               command_line, theme, glyphs
   ├─ config/      paths, settings, keys_file, theme_file (TOML)
   ├─ storage/     document_file (RON), atomic save, backups, lock, state, watch (change polling)
   └─ app/         action, mode, buffer, commands (keys every view shares), calls (`:` commands),
                   text_edit (a node's text being edited, rapid entry), registers, panes (the
                   split tree), popup (help, menu, pickers, card); later App, jumps, portals
                   (cache, backlinks, renames), update, render, terminal (setup, teardown,
                   panic hook)

Flow: a terminal event comes in → the key engine turns keys into a typed Command (with its count and register) → the focused view answers it with Actions, and app::commands::shared answers the commands the view leaves (undo, marks, adding, deleting, folds, modes…), so a view can give a key its own meaning → a : line is parsed into a Call and app::calls answers it → update carries out the actions in order, stopping at the first problem: document actions go to the document's Buffer (the only place a document changes: the edit, its undo step, the cursor and the marks), the rest change the mode or the app → the screen is drawn from the state.

While a text is edited, keys go to the edit (app::text_edit) instead: when typing, a key the insert table binds is a command (Enter, Tab, Backspace…) and any other key types itself; with vim keys, the key engine reads the text table, where operators take motions as targets too (dw, c$, dfx). The edit answers with actions like everything else. While a popup is open (app::popup), keys and clicks go to it the same way: through the list or card table, or straight into its search. The help and the menu run what you pick as if its keys were typed (AppAction::Run).

The panes (app::panes) are a split tree, apart from what each pane shows: a view and one of the open documents. Only the focused pane is in the mode (the others are drawn as in normal mode), and its document is the one the actions change; a click focuses the pane under it first.

Edits travel as data (EditOp), so views never touch the document. A Group makes several edits one undo step: a whole move-mode session, a mouse drag, a text edit, an idea in rapid entry. A group whose edits cancel out (an idea added, then left empty) leaves no step at all.

trait View {   // settled in stage 5
    fn kind(&self) -> ViewKind;
    fn contexts(&self, mode: &Mode) -> &'static [Context];   // its key tables, most specific first
    fn layout(&mut self, area: Rect, ctx: &ViewContext);     // fit to the area, follow the cursor
    fn render(&self, frame: &mut Frame, ctx: &ViewContext);  // draws the layout, changes nothing
    fn command(&mut self, typed: &Typed, ctx: &ViewContext) -> Option<Vec<Action>>; // None: not mine
    fn mouse(&mut self, gesture: Gesture, ctx: &ViewContext) -> Vec<Action>;
    // Added in stage 7, with defaults:
    fn status(&self) -> String;                                 // its name in the status bar
    fn cursor_area(&self, ctx: &ViewContext) -> Option<Rect>;  // where the K card goes beside
}
// ViewContext: the document, the cursor, the mode, the theme and the view settings.

What changed from the first sketch: layout comes before render, because a view keeps the frame it drew (the canvas matches clicks against it) and moves its camera to follow the cursor, and drawing should change nothing; command takes the whole Typed (counts matter) and returns None for commands it leaves to the shared ones.

Build order

Hardest parts first, so they shape a simple interface that every view shares and the core ends up thin. The model comes first because every hard part reads it.

# Stage What you can run
0 Skeleton: Cargo.toml, toolchain, build.rs, rustfmt, aliases, README, DESIGN.md cumulus --version
1 Model: types, document rules, transactions and undo, folding, RON round trip cargo test
2 Canvas engine: camera, boxes, a line layer with junctions, right-angle routing, arrows and labels, zoom levels, hit-testing, nearest-node moves, placement cargo run --example canvas
3 Key engine: notation, tables, counts, registers, operator + target, keys that take a character, which-key, keys.toml the example gets real keys and which-key
4 Command line: registry, parser, completion (popup + ghost text), history : in the example
5 The shared View interface, Action and Mode, pulled out of stages 2–4 the example runs through it
6 Text editor: typing, plus vim normal mode inside text editing titles in the example
7 Outline and Note views; splits (the split tree); status bar, document bar, help, K card, right-click menu, pickers (documents, colors, borders, styles, tags) the example with splits and two documents
8 App core: main loop, terminal setup and panic hook, buffers and panes (from the example into the app), visual and connect modes, jumps, marks, registers, files (load, autosave, reload, backups, locks, where you left off), config/theme/keys, portals, backlinks and renames, startup rules cumulus: v1
9 v1.1: Board, Search picker, :tidy and align, branch out, export/import

Each stage ends with cargo build, cargo lint and cargo test passing, and one commit.

Crates and tooling

  • Crates: ratatui 0.30 (with the crossterm 0.29 it ships with) · serde, ron 0.12 and toml 1 · directories 6 · thiserror 2 and anyhow · arboard 3 (with Wayland support) · unicode-width and unicode-segmentation. No testing crates. Each crate is added in the stage that first needs it.
  • Toolchain: rust-toolchain.toml pins 1.90.0 with clippy, rustfmt and the musl target.
  • build.rs: embeds the git commit for cumulus --version.
  • Windows first: only key Press and Repeat events count; a panic hook restores the terminal (raw mode, the screen, the mouse); paths come from directories.
  • Same as skyshift: edition 2024, rustfmt at 110 columns, cargo lint and cargo linux, the static C runtime on Windows, LF line endings, unsafe forbidden.
  • Doc comments on public types and functions, written where they explain something. (The missing_docs lint was dropped: it demands a comment on every field and enum variant, which is the comment noise we don't want.)

Tests

  • Keys: notation, sequences, counts, registers, operator + target, overrides, and no binding hiding another.
  • Command line: parsing, abbreviations, completion ranking, history.
  • Model: tree rules, undo/redo round trips, folding.
  • Canvas: camera conversions, routing and junctions (small text snapshots), hit-testing, nearest-node moves.
  • Text editing: vim keys inside text driven key by key (motions, operators, text objects, ., undo), rapid entry, empty titles, registers, bodies and tags.
  • Views: the outline and the Note view drawn on a test terminal (rows, parts, drafts being edited), and driven by keys and clicks.
  • Panes and popups: splitting, closing and moving the focus in the split tree; lists (moving, searching, clicks), the help, the menu, the pickers and the card, driven by their key tables.
  • The example: its tests drive the whole loop on a test terminal, keys and mouse in and the screen out, so what a stage says works is checked end to end.
  • Files: RON round trip, load errors, config parsing, and backlink and rename fix-ups, in temporary folders.