The canvas example did everything itself; now it runs the way the app will. The key engine turns keys into commands, the focused view answers with actions, and the commands every view shares (undo, marks, adding, deleting, folds, tree moves, modes) answer the rest. A view can still give a key its own meaning, since it gets the first say. `:` lines become actions the same way (app::calls). - View trait: kind, contexts (its key tables), layout, render, command and mouse. layout comes before render because a view keeps the frame it drew, to match clicks against, and follows the cursor with its camera; drawing changes nothing. CanvasView owns the camera, the last scene and the mouse drag; it reads the rest from a ViewContext. - Edits travel as data (EditOp), so views never touch the document. Buffer::apply is the one place a document changes: the edit, its undo step, where the cursor goes, marks and `''`. - Mode holds each mode's own state: move mode carries its Grab (whole branch or not), the command line lives in Mode::Command. - Undo groups: a whole move-mode session or mouse drag is one undo step, built up as it goes. After an undo the cursor lands on a node the change added or removed, before the others it touched. - Actions run in order, and a problem stops the rest. Small improvements on the way: zz centers the middle of the node, :find says which match it's on, :unlink with no links says so, folding a node without children says why nothing happened, and a captured idea is placed next to a new Inbox even on the first :in. The example has end-to-end tests now: they type key sequences and mouse gestures into it on a test terminal, and `cargo test` runs them. Co-Authored-By: Claude Opus 5.5 <[email protected]>
493 lines
25 KiB
Markdown
493 lines
25 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 · `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 it, Enter on an empty line stops.
|
||
Esc switches to vim motions inside the text; a second Esc leaves.
|
||
- `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.
|
||
|
||
**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)
|
||
|
||
```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) | Sorting: columns by tag or color. Moving a card to another column changes its tag. |
|
||
|
||
- **Pickers:** Documents 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).
|
||
|
||
**Mouse** (on the canvas; other views get click, double-click and scroll):
|
||
click selects, double-click edits the title, 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
|
||
|
||
```rust
|
||
enum Mode {
|
||
Normal,
|
||
Insert(TextEdit), // typing a title or body; rapid entry lives here
|
||
TextNormal(TextEdit), // vim motions inside that text
|
||
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
|
||
}
|
||
```
|
||
|
||
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 c` close · `Ctrl-w o` only · `Ctrl-w =` even sizes · `gt` `gT` `]b` `[b` next / previous document · `ZZ` close pane |
|
||
| **Typing** | `Ctrl-w` delete word · `Ctrl-u` delete line · arrows, Home, End · Enter (title: done or next sibling; body: new line) · Tab Shift-Tab (new node: child / back out) · Esc to vim motions |
|
||
| **Inside text** | `h j k l w b e 0 ^ $ f t x r J o O p P u Ctrl-r .` · `d` `c` `y` with motions and `iw aw i" a" i( a(` · `i a I A` back to typing |
|
||
| **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.
|
||
|
||
```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/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.
|
||
|
||
```toml
|
||
# 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; 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, board
|
||
├─ ui/ text_editor (+ vim normal), which_key, command_line, line_input, status_bar,
|
||
│ document_bar, card, menu, picker, help, 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);
|
||
later App, panes (split tree), jumps, registers, 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
|
||
`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.
|
||
|
||
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.
|
||
|
||
```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>;
|
||
}
|
||
// 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; status bar, document bar, help, `K` card, right-click menu, Documents picker | the example with splits |
|
||
| 8 | App core: main loop, terminal setup and panic hook, buffers, splits, 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.
|
||
- **Files:** RON round trip, load errors, config parsing, and backlink and
|
||
rename fix-ups, in temporary folders.
|