The user wants to drag a card out between columns to make a new column, and to drag columns to reorder them or put them inside other columns. Their pick for where: the row of titles, the gaps between columns and the space past the last one are among the columns. Something dragged there becomes a column, before the column under the mouse on its left half and after it on its right half, after the one to the left in a gap. Anywhere else it goes into the column under the mouse by the card rules, so a column dragged down into another one becomes a card, its own cards its block. A column's title no longer means "its first card"; the top half of its first card does that. The root's column stays first and can't be dragged (the user's pick): it says so. The board holds still around a drag. Besides the kept widths, the room a dragged column took stays empty while it isn't a column (Drag.room): without it, the columns to its right slid left under the mouse each time it stopped being one, and the card landed a column too far. Dropping it back in that room makes it that column again. Co-Authored-By: Claude Opus 5.5 <[email protected]>
957 lines
57 KiB
Markdown
957 lines
57 KiB
Markdown
# 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 with keys (Ctrl+click does it with the mouse) ·
|
||
`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. `"+` is the system clipboard (part 8c).
|
||
- `.` inside the text repeats the last change there, typing included.
|
||
- 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).
|
||
|
||
**The rest of vim** (settled in part 8c)
|
||
- Visual mode takes in what lies between where it started and the cursor:
|
||
on the canvas, every node on show that the rectangle from one end's box to
|
||
the other's touches; in the outline and the Note view, the nodes in
|
||
between in reading order. `V` takes each with its whole branch; `v` in a
|
||
`V` selection (and `V` in a `v` one) switches, and the same key again
|
||
stops. Ctrl+click adds or takes out one node; Ctrl+drag on empty canvas
|
||
adds every node its rectangle touches. The selection is drawn thick in
|
||
the theme's visual color, and the status bar counts it.
|
||
- Operators take exactly what's selected (the user's pick): `d` deletes the
|
||
selected nodes, and with `v` their unselected children move up, as with
|
||
`x`; `y` yanks them with their shape among them; `>` and `<` move nodes
|
||
that share a parent; `M` moves them on the canvas (with their branches, or
|
||
just them after `M` again); the pickers set every one of them, as one undo
|
||
step.
|
||
- A register holds text or nodes, and either pastes anywhere: nodes pasted
|
||
into a text give their titles, text pasted on nodes becomes nodes. `yy`,
|
||
`yab`, `yib` yank nodes; every delete (`dd`, `x`, `dib`, `d` on a
|
||
selection) keeps what it took, so `dd` then `p` moves a branch. `p`
|
||
pastes as the node's last children and `P` as its next siblings (the
|
||
user's pick after trying part 8c: `p` goes under the node as `a` adds,
|
||
`P` below it as `o` adds), keeping the yanked layout, where a new node
|
||
would go; a count pastes copies. Links between
|
||
the pasted nodes come along, and so do links to nodes still there when
|
||
they go back into the document they came from.
|
||
- `"+` is the system clipboard. Nodes go there as Markdown, the user's
|
||
format: a heading per node (`##` for the nodes yanked, `###` for their
|
||
children, `####` for anything deeper), its tags as a list between two
|
||
`---` lines, then its body. While the clipboard still holds what cumulus
|
||
put there, `"+p` gives back everything (colors, bodies, links, the
|
||
layout); text from elsewhere becomes nodes (Markdown headings, or a node
|
||
per line nested by its indent). `clipboard = "unnamedplus"` makes every
|
||
yank, delete and paste use the clipboard, as vim's setting does.
|
||
- `.` repeats the last change made with keys in normal or visual mode: the
|
||
command with its count and register, plus everything typed in the text
|
||
edit it started (`aMulch` then Esc adds another Mulch). A value picked in a
|
||
picker repeats on the cursor's node. A change made on a selection repeats
|
||
on the cursor's node (`d` as `x`, or `dd` after `V`; `>` as `>>`).
|
||
- `/` opens a search in the middle of the screen, as Telescope does (the
|
||
user's pick): what's typed at the top, the matches below it, fuzzy, best
|
||
first (titles before tags before bodies), with the letters that matched
|
||
standing out, and the note of the match you're on beside them. Case only
|
||
matters once there's a capital (smartcase). Enter goes there, Esc goes
|
||
back; `n` and `N` then go through the same matches, going round, and
|
||
`:find` searches straight away.
|
||
- Each pane keeps a jump list, as vim's windows do: `gg`, `G`, marks, a
|
||
search, `gd`, a link followed in the Note view and switching documents are
|
||
jumps. `Ctrl-o` goes back (into another document, opened again if it was
|
||
closed), `Ctrl-i` or Tab forward. `''` still goes back to before the last
|
||
jump.
|
||
- Capital marks (`mA`, `'A`) lead into any document, opening it if it isn't
|
||
open; they're kept in the data folder's `memory.ron`, and the small ones
|
||
(`ma`) are kept there with each document, so both last from one run to
|
||
the next.
|
||
- `gd` follows the cursor's cross-link; with several, a list of them pops up
|
||
beside the node. `:marks`, `:jumps` and `:registers` list them; Enter goes
|
||
to the mark or the place, or pastes the register.
|
||
|
||
**Connect mode and portals** (settled in part 8d)
|
||
- `C` starts connect mode on the cursor's node: the cursor goes to another
|
||
node as usual (`hjkl`, `gh`…, `n` after a search, a click), the node
|
||
being connected is drawn thick in the visual color, and a dashed arrow in
|
||
that color follows the cursor. Enter links them (a line already there
|
||
stays) and connect mode is done, with the cursor back on the node `C`
|
||
started from (the user's pick). `d` removes the line between them: a link
|
||
goes; a branch (aimed at the node's parent, or at a child) is cut, and the
|
||
child comes loose with its own branch. `>` puts the node under the one
|
||
aimed at, as its last child, where it is on the canvas (a link between the
|
||
two goes: the branch takes its place).
|
||
- The `<Space>` keys style the line as you watch: `a` and `l` go through the
|
||
arrowheads (off, end, start, both) and the kinds of line (solid, dashed,
|
||
dotted, thick, double); `c` and `s` pick a color (or the node's own) and a
|
||
named line style; `t` a link's label, where typing a label that isn't
|
||
listed makes it. A line that isn't there yet is made, as a link. Everything
|
||
connect mode changes is one undo step, and Esc takes it all back.
|
||
- From visual mode, `C` connects every selected node (with `V`, each
|
||
branch's top) to the one the cursor goes to. Connect mode works in every
|
||
view, and stays in its document.
|
||
- A portal shows its document live: the root's title, its color (when the
|
||
portal has none of its own) and how many nodes it has, in the box's corner
|
||
(`?` and faded when the document isn't there). The portal remembers the
|
||
title, in case the file goes. Enter or `gf` goes through it, to the root:
|
||
a jump, so `Ctrl-o` comes back. The breadcrumb then starts with the
|
||
documents the pane came through (`Garden plans » Kitchen › Herbs`);
|
||
going back up one of them takes the trail back there.
|
||
- `<Space>p` opens the command line with `:portal ` typed (the user's
|
||
pick): a path as `:e` takes it, relative to the document's folder, with
|
||
the same completion. An existing document gets a portal under the
|
||
cursor's node, where `a` adds a child; when nothing's there, what was
|
||
typed is a new document's title, made next to this one as `:new` makes
|
||
it. A portal to its own document, or to a file that isn't a document, is
|
||
refused.
|
||
- Backlinks are put right whenever a document is opened or saved: each
|
||
document its portals lead to gets exactly one backlink per portal (an
|
||
open one in its buffer, written at once unless it has changes of its own
|
||
to save with them; a closed one in its file, unless another cumulus has
|
||
it). The root shows how many (`« 2`), the Note view lists them on the
|
||
root, and `gu` goes up to them (the user's pick): one straight there, on
|
||
the portal, several in a list, the document the pane came from first.
|
||
`:backlinks` always lists them.
|
||
- File names follow the root's title: when the title changed since the
|
||
file was read or written, the next save renames it (`-2`, `-3`… when the
|
||
name is taken; a folder's default document keeps its name). The portals
|
||
pointing at it take the new path (in the documents its backlinks name,
|
||
and any open one), as do the backlinks in the documents it leads to, the
|
||
recent documents, the capital marks and the jump lists.
|
||
- A portal whose file isn't where it says looks in that folder for its
|
||
document by uid (a file moved or renamed by hand), and keeps the path it
|
||
finds.
|
||
|
||
**Tidying and lining up** (settled in part 9a)
|
||
- `<Space>=` then `m`, `c` or `g` (the user's pick: leader menus), or
|
||
`:tidy [map|chart|grid]`, lays out the cursor's branch: its top node
|
||
stays where it is, and every node under it gets a place, folded away or
|
||
not (the user's pick: unfolding later shows it tidy too). One undo step.
|
||
What's outside the branch stays put, even under it, and the message says
|
||
how many nodes the branch covers now.
|
||
- A mind map puts each node in the middle of its children (the user's
|
||
pick), with their branches stacked to the right. An org chart puts the
|
||
children's branches side by side below, each node in the middle above
|
||
its own. A grid makes a block of cards beside the node (the user's pick),
|
||
filled left to right, about as many columns as rows; a card's own branch
|
||
goes along with it, as a mind map.
|
||
- The lines follow: a parent whose children are all below it sends its
|
||
branches down to a shared bend, and a new child of it goes beside the
|
||
last one. In a grid, a card behind another in its row isn't joined by a
|
||
line: the bit showing between the two cards would look like it joins
|
||
them.
|
||
- In visual mode, `<Space>a` then `h`, `l`, `k`, `j` or `c` lines the
|
||
selection up: left, right, top or bottom edges, or middles in a column,
|
||
on the box around them all. Each brings its branch along (with `V`, each
|
||
branch goes by its top). `<Space>=` tidies each selected branch.
|
||
- `:` in visual mode opens the command line on the selection (the status
|
||
bar keeps counting it): `:align` and `:tidy` take it, and so do `:color`,
|
||
`:border`, `:style`, `:tag` and `:fold`, setting every selected node as
|
||
one undo step, the way the pickers do.
|
||
|
||
**Branching out** (settled in part 9b)
|
||
- `<Space>o` (the user's pick), `:branchout [file]` or the right-click
|
||
menu moves the cursor's branch into a document of its own: next to this
|
||
one, named after the branch's title (`-2` when that's taken), or at the
|
||
path given. Its top node becomes the root, in the middle of the canvas,
|
||
and the nodes keep their ids, so the links among them, the marks on them
|
||
(small and capital) and the jump lists still find them.
|
||
- A portal to the new document takes the branch's place (the same spot in
|
||
the tree and on the canvas, the same branch line), as one undo step: `u`
|
||
brings the branch back, and the new file stays on disk, as after `:new`.
|
||
The pane then goes to the new document, on its root, as through the
|
||
portal (the user's pick): `Ctrl-o` comes back, and the breadcrumb starts
|
||
with the document it came from.
|
||
- Portals inside the branch move along (the user's pick), still leading
|
||
where they did from the new file's folder, and their documents'
|
||
backlinks follow. Links between the branch and the rest of the document
|
||
can't follow (links stay inside a document): the message counts them.
|
||
The root can't branch out (it's the whole document), and neither can a
|
||
portal (its document is one of its own already).
|
||
|
||
**Export and import** (settled in part 9c)
|
||
- `:export md` writes the document as Markdown headings, in the clipboard's
|
||
format (the user's pick): `#` for the root and each loose node, `##` for
|
||
their children, and so on down to `######`; the tags as a list between
|
||
two `---` lines, then the body. `:export txt` writes an outline: a title
|
||
per line, two spaces a level, its tags after it, and its body under it a
|
||
level further in, each line after a `| ` (the user's pick).
|
||
- The file goes next to the document and is named after it (`garden.md`),
|
||
or where `:export md file` says (`.md` or `.txt` is added). As vim's `:w`
|
||
to another file does, it leaves a file that's there alone unless it's
|
||
`:export!` (the user's pick).
|
||
- Typed in visual mode, `:export` writes just the selection (the user's
|
||
pick), named after its first branch (`raised-beds.md`): with `v` exactly
|
||
the nodes chosen, with `V` their branches.
|
||
- `:import file` reads Markdown headings, a bulleted list or indented lines
|
||
(what both exports write, and what the clipboard reads), and the nodes go
|
||
under the cursor's node as its last children, laid out as a mind map, as
|
||
one undo step (the user's pick). `#tags` at the end of a line are its
|
||
tags, and lines after a `| ` are the body of the node above. Looks and
|
||
cross-links stay out of both exports.
|
||
|
||
**Searching every document** (settled in part 9d)
|
||
- `<Space>/` (the user's pick), or `:search text` straight to the best
|
||
match, searches every document cumulus knows of: the open ones (the one
|
||
shown first), the ones opened lately, the `.cum` files in this
|
||
document's folder, and every document their portals and backlinks lead
|
||
to, however far (the user's pick; up to 200 documents). The open ones are
|
||
searched as they are, the others as their files have them.
|
||
- It's the `/` popup: one list, best first whatever the document (titles
|
||
before tags before bodies), each match with its document's name at the
|
||
end of its row (the user's pick), and the note of the one you're on
|
||
beside them. Enter goes there, opening its document if it's closed: a
|
||
jump, so `Ctrl-o` comes back.
|
||
- `n` and `N` then go through every match, from document to document (the
|
||
user's pick), going round, as vim's `:cnext` goes through a `:vimgrep`'s
|
||
list: the matches as they were when you searched. `/` and `:find` still
|
||
search the document shown, and `n` after them stays in it.
|
||
|
||
**The board** (settled in part 9e, the user's design)
|
||
- The board shows the document as lists and blocks, the way Scratch nests
|
||
blocks inside a loop: each node without a parent (the root, and each
|
||
loose node) is a column, with its title in the column's top border; its
|
||
children are cards in it, one under the other; and a card with children
|
||
on show wraps them in a "C": a `├` at its bottom left, a bracket down the
|
||
left past its cards (two cells further in), and a foot closing it. Blocks
|
||
nest as deep as the tree does. The columns by tag or by color the first
|
||
design had are dropped (the user's pick).
|
||
- A card is a node's box, and a column a node's box with the title in its
|
||
top border, drawn exactly as the canvas draws them (the user asked for
|
||
them to match): its border as its style says (rounded for the root,
|
||
dashed for a question, double…), in its color, thick in the accent color
|
||
for the cursor; the tags in its color and the badge in its bottom border.
|
||
The borders go through the canvas's line layer, so a block's "C" is in
|
||
its card's own line and color, joined where they meet (`├`, `┣`, `╠`).
|
||
Folds fold here too: a folded block is a card with `+N`, and a folded
|
||
column says `+N` in its bottom border.
|
||
- `j` and `k` go down and up a column in reading order (into the blocks),
|
||
`h` and `l` to the column beside, onto the card nearest the cursor's
|
||
height. Titles are typed right in their cards, and `<Space>vb` or
|
||
`:board` shows it.
|
||
- Move mode (`M`, the user's pick) moves the card instead: `h` and `l` to
|
||
the end of the column beside, `j` and `k` past the cards around it, into
|
||
a block (as its first card going down, its last going up) and out of one
|
||
at its end; a column's own node trades places with the column beside
|
||
(the root's stays first). Enter is done, one undo step. Every move goes
|
||
through the tree.
|
||
- A drag moves a card, or a column by its title, live, as the canvas does
|
||
(the user's pick); the whole drag is one undo step. The row of titles,
|
||
the gaps between columns and the space past the last one are among the
|
||
columns (the user's pick): there it becomes a column, before the column
|
||
under the mouse on its left half, after it on its right half, after the
|
||
one to the left in a gap. Anywhere else it goes into the column under
|
||
the mouse, as a card, by the row: on a card's top half before it; on its
|
||
bottom half after it, or into its block as the first card; on a block's
|
||
foot, after the block; below the cards, last. So a card dragged out
|
||
becomes a column, and a column dragged into another one becomes a card,
|
||
its cards its block. The root's column stays first and can't be dragged
|
||
(the user's pick: it's the document's root).
|
||
- The board holds still around a drag: the columns keep the widths they
|
||
had, and the room a dragged column took stays empty while it isn't one
|
||
(dropping it there makes it that column again). Otherwise the columns
|
||
beside would slide under the mouse, and the card would chase them.
|
||
- In visual mode the board selects in reading order, as the outline does.
|
||
|
||
**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.
|
||
Rows wider than the view scroll sideways (`zh` `zl`, the wheel sideways),
|
||
and the cursor's title comes back into view when the cursor moves.
|
||
- 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** (settled in part 8a)
|
||
- Documents are RON files ending in `.cum`. Configs are TOML: `config.toml`,
|
||
`keys.toml` and `themes/<name>.toml`.
|
||
- Autosave a second after the latest change, so typing isn't saved letter
|
||
by letter (`autosave = false` for vim-style `:w`). Undoing back to what's
|
||
on disk counts as no change. The document bar marks unsaved changes `[+]`.
|
||
Quitting saves first; without autosave, `:q` with unsaved changes refuses
|
||
(`:q!` drops them), and `ZZ` saves, then closes, as in vim.
|
||
- Every second, cumulus looks at each file's time and size. A file changed
|
||
outside cumulus is read again as an undo step (`u` brings back what was on
|
||
screen); never under a text being edited or a node being moved, which it
|
||
waits for. If you also had unsaved changes, cumulus asks which to keep
|
||
(Esc: later; `:w` keeps yours, `:e!` takes the file's). A file with a
|
||
mistake in it (a hand edit half done) stops autosave until it's fixed or
|
||
`:w` saves over it; a deleted or moved file waits for `:w` to write it
|
||
again.
|
||
- The day's first save copies the file to the data folder's backups first
|
||
(`backups/<uid>/2026-10-01.cum`, the last 30 days kept).
|
||
- A document open in another cumulus opens read-only (`[RO]` in the bar)
|
||
until that one lets go of it. The lock is the operating system's lock on
|
||
a file in the data folder, so a crash never leaves one behind.
|
||
- 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: the cursor, and the canvas's
|
||
zoom and scroll. The documents picker (`<Space>b`) lists the documents
|
||
opened lately after the open ones.
|
||
|
||
**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)
|
||
|
||
```rust
|
||
// 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) | Lists and blocks: a column per node without a parent, its children as cards, and a card with children wrapping them, as Scratch's loop blocks do. |
|
||
|
||
- **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, the wheel
|
||
pans up and down, and sideways with a wheel that tilts, a touchpad, or the
|
||
wheel with Shift or Alt held (Alt for terminals that keep Shift for selecting
|
||
text), 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. On the board, dragging a card, or a column by its
|
||
title, moves it through the tree as the mouse goes: before or after the
|
||
card under it (by which half), into a block, out at its foot, to a
|
||
column's end, or among the columns (on the titles' row or in a gap) as a
|
||
column of its own.
|
||
|
||
## Modes and keys
|
||
|
||
```rust
|
||
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 nodes being connected, where the cursor goes
|
||
// back, and the undo group Esc takes back
|
||
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 (`<Space>/` every document) · `Ctrl-o` `Ctrl-i` (Tab) jump back / forward · `m{a-z}` `'{a-z}` marks (`A–Z` across documents) · Enter or `gf` through a portal · `gu` up to the portals that lead here · `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` paste as children · `P` paste as siblings below · `>>` `<<` 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 · `=` tidy (`m` map, `c` chart, `g` grid) · `p` new portal (`:portal `) · `o` this branch out, to a document of its own · `/` search every document · `i` inbox · `b` documents · `v` then `c`/`o`/`n`/`b` for canvas / outline / note / board · `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 |
|
||
| **Board** | `j` `k` cards in a column · `h` `l` the column beside · `M` then `h` `l` to the column beside, `j` `k` down and up (into and out of blocks) · drag a card |
|
||
| **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 (or click it) · Enter link · `d` remove (a branch: cut) · `>` attach as child · `<Space>` style menu (`s` named, `a` arrows, `l` line, `c` color, `t` label) · Esc cancel (takes it all back) |
|
||
| **Visual** | `d` `x` `y` `>` `<` `M` and `<Space>c` `<Space>s` `<Space>B` `<Space>t` act on the whole selection · `<Space>a` then `h` `l` `k` `j` `c` line it up · `<Space>=` tidies each branch · `:` a command on the selection · `o` swaps ends · `v` `V` switch, or stop · Ctrl+click, Ctrl+drag pick with the mouse |
|
||
|
||
## Files
|
||
|
||
**Documents** (`.cum`, RON). A `backlinks: [...]` list appears once another
|
||
document has a portal pointing here.
|
||
|
||
```ron
|
||
// 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` (`~/.config/cumulus` on
|
||
Linux); `cumulus --paths` prints the folders. Each file only states what it
|
||
changes: a missing file changes nothing, and a mistake only drops its own line,
|
||
with a message in the status bar saying what's wrong. cumulus applies a file
|
||
again when it's saved. (Settled in part 8b.)
|
||
|
||
- `:config` lists config.toml, keys.toml and the theme's file, with the folder
|
||
they're in; Enter opens one in your editor (the `editor` setting, `$VISUAL`,
|
||
`$EDITOR`, else notepad on Windows and vi elsewhere), with the terminal
|
||
handed over until it's done. A file that isn't there yet is written first,
|
||
every option at its default behind a `#`, with what it does: keys.toml lists
|
||
every command by name.
|
||
- `:set` and `:theme` change things until cumulus quits, as in vim; the files
|
||
change them for good.
|
||
- A theme file is laid over the built-in theme (`default`): a theme that says
|
||
just `accent = "magenta"` is the built-in one in magenta, and
|
||
`themes/default.toml` changes the built-in theme itself. Screen colors are
|
||
terminal colors; the colors in looks and styles are palette names.
|
||
- The built-in theme comes with named styles, each put on by the tag of the
|
||
same name: `question` (yellow, dashed), `important` (red, thick) and `done`
|
||
(gray, its title faded); and two for links, `blocks` (red, dashed, "blocks")
|
||
and `supports` (green, "supports"). The user's pick in part 8b.
|
||
|
||
```toml
|
||
# config.toml
|
||
theme = "default" # or 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
|
||
editor = "" # what :config opens files in ("nvim", "code --wait")
|
||
clipboard = "cumulus" # "unnamedplus": plain y, d and p use the system clipboard
|
||
|
||
# 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; "nothing" unbinds
|
||
leader = "<Space>"
|
||
[normal]
|
||
"<Space>x" = "delete-branch"
|
||
"x" = "nothing"
|
||
[canvas]
|
||
"H" = "previous-document"
|
||
|
||
# themes/night.toml
|
||
accent = "cyan" # screen colors: terminal names, 0-255, or "#rrggbb"
|
||
muted = "darkgray"
|
||
[modes]
|
||
insert = "green"
|
||
visual = "magenta"
|
||
[palette] # the colors nodes and lines are given, by name
|
||
red = 167
|
||
orange = 208 # … yellow, green, teal, blue, purple, pink, brown, gray
|
||
[node] # every node, unless something says otherwise
|
||
border = "square"
|
||
[root]
|
||
border = "rounded"
|
||
[branch] # lines from a parent to its children
|
||
line = "solid"
|
||
arrows = "off"
|
||
color = "node" # the parent's own color
|
||
[link] # cross-links
|
||
line = "dashed"
|
||
arrows = "end"
|
||
[styles.node.question] # named node styles: <Space>s, and [tags]
|
||
color = "yellow" # a palette color
|
||
border = "dashed"
|
||
[styles.node.done]
|
||
color = "gray"
|
||
faded = true # the title in the muted color
|
||
[styles.link.blocks] # named line styles, for links
|
||
color = "red"
|
||
line = "dashed"
|
||
arrows = "end"
|
||
label = "blocks" # shown on a link without a label of its own
|
||
[tags]
|
||
question = "question" # a #question node gets the "question" style
|
||
```
|
||
|
||
**Data folder** (`%APPDATA%\cumulus\data`, `~/.local/share/cumulus` on Linux):
|
||
`memory.ron` (where you left off in each document, keyed by `uid`, with its
|
||
marks; the capital marks; the command history; recent documents), `backups/`
|
||
and `locks/`. 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. When neither exists, it starts a new `~/ideas.cum` (written once
|
||
something is added): one ideas document for everywhere (the user's pick in
|
||
part 8a).
|
||
- `cumulus garden.cum` opens that file (a new document if there's no such
|
||
file yet, as `:e` does); `cumulus some/folder` opens that folder's default
|
||
document. Several can be named; the first one shows.
|
||
- `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/ the playground: cumulus on copies of two sample documents (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, clip (nodes taken out, to paste)
|
||
├─ 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, arrange (:tidy, :align), view), outline, note, 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 (the config and data folders), settings (config.toml, `:set`), keys_file
|
||
│ (keys.toml's template), theme_file (themes laid over the built-in one)
|
||
├─ storage/ document_file (RON), write_atomically, names (file names from titles), link_paths
|
||
│ (portal paths: relative, with /), backup, lock, stamp (noticing changes made
|
||
│ outside), memory (memory.ron), markdown (nodes as Markdown, for the clipboard
|
||
│ and :export; text read back as nodes), plain_text (:export txt)
|
||
└─ app/ mod.rs (App), input (keys and the mouse, routed), update (carrying actions out),
|
||
render, terminal (the main loop, setup, teardown, panic hook), startup (what
|
||
`cumulus` opens), documents (the open documents and their files), files
|
||
(saving, reloading, locks, where you left off), configure (settings, keys and
|
||
themes applied; `:config`), action, mode, buffer, commands
|
||
(keys every view shares), calls (`:` commands), text_edit (a node's text being
|
||
edited, rapid entry), registers (text or nodes), clipboard (the system's, for
|
||
"+), yank (nodes into registers and back), visual (selections), repeat (`.`),
|
||
search (`/`, and `<Space>/` across documents), jumps (each pane's jump list),
|
||
marks (capital marks, `:marks`),
|
||
connect (connect mode), portals (what portals show, going through them,
|
||
`:portal`, backlinks, renames), branch_out (`:branchout`), panes (the split
|
||
tree), export (:export, :import), popup (help, menu, pickers, card, questions)
|
||
```
|
||
|
||
The end-to-end tests are in `app/tests.rs` (with `app/tests/files.rs`,
|
||
`app/tests/config.rs`, `app/tests/vim.rs`, `app/tests/connect.rs`,
|
||
`app/tests/portals.rs`, `app/tests/arrange.rs`, `app/tests/branch_out.rs`,
|
||
`app/tests/export.rs`, `app/tests/everywhere.rs` and `app/tests/board.rs`):
|
||
they copy the sample
|
||
documents to a temporary folder, type keys and click on a test terminal, and
|
||
check the screen, the documents and the files.
|
||
|
||
**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
|
||
`Action`s, 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.
|
||
|
||
```rust
|
||
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
|
||
// Added in part 8a, with defaults (only the canvas has a camera):
|
||
fn camera(&self) -> Option<Camera>; // kept with the document
|
||
fn set_camera(&mut self, camera: Camera); // where you left off
|
||
}
|
||
// 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 |
|
||
| 8a | App core: the main loop, terminal setup and panic hook, buffers and panes (from the example into the app), files (load, autosave, reload, backups, locks, where you left off), the startup rules and `cumulus new`, `:set` | `cumulus` |
|
||
| 8b | Settings: config.toml, keys.toml, themes (named styles, styles by tag), `:theme`, `:config` | |
|
||
| 8c | The rest of vim: visual mode, node yank and paste with registers (`"+`: the clipboard), `.` on nodes, `/` `n` `N`, the jump list, capital marks, `gd` | |
|
||
| 8d | Connect mode, portals, backlinks, file names that follow the root's title | 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. Stage 8, as big as stages 5 to 7 together, comes in four parts
|
||
with a commit each, and a pause after each for the user to try it (their
|
||
choice). Stage 9 comes in small parts the same way (their choice too): 9a
|
||
`:tidy` and align, 9b branch out, 9c export and import, 9d the search
|
||
across documents, 9e the Board.
|
||
|
||
## 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.
|