Add the project skeleton and the approved design
Cargo.toml with the release profile and lints, the toolchain pinned to Rust 1.90.0, a build script that records the git commit for `cumulus --version`, and rustfmt settings and cargo aliases (`cargo lint`, `cargo linux`) as in skyshift. DESIGN.md holds the design approved after the question rounds; README.md has build instructions and the planned keys. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
0e03c396bf
12 files changed
+659
No files matched your search
@@ -0,0 +1,15 @@
|
|||||||
|
# Windows (MSVC): link the C runtime statically, so cumulus.exe runs on any
|
||||||
|
# Windows 10/11 machine without installing the "Visual C++ Redistributable".
|
||||||
|
[target.'cfg(all(windows, target_env = "msvc"))']
|
||||||
|
rustflags = ["-C", "target-feature=+crt-static"]
|
||||||
|
|
||||||
|
[alias]
|
||||||
|
# `cargo lint`: the strict check to run before committing.
|
||||||
|
lint = "clippy --all-targets -- -D warnings"
|
||||||
|
# `cargo linux`: build a Linux binary from any OS. It's fully static, so the
|
||||||
|
# same file runs on Void (glibc or musl), Fedora and other distributions:
|
||||||
|
# target/x86_64-unknown-linux-musl/release/cumulus
|
||||||
|
linux = [
|
||||||
|
"build", "--release", "--target", "x86_64-unknown-linux-musl",
|
||||||
|
"--config", "target.x86_64-unknown-linux-musl.linker = 'rust-lld'",
|
||||||
|
]
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Keep LF line endings everywhere (Windows and Linux share this repo).
|
||||||
|
* text=auto eol=lf
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+7
@@ -0,0 +1,7 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cumulus"
|
||||||
|
version = "0.1.0"
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "cumulus"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
# The version cumulus is built and tested with (pinned in rust-toolchain.toml).
|
||||||
|
rust-version = "1.90"
|
||||||
|
description = "Brainstorm in the terminal: idea graphs on a 2D canvas, with vim keys and the mouse."
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
unsafe_code = "forbid"
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
all = { level = "warn", priority = -1 }
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
# A smaller, faster program: optimize across all crates at once (lto, in one
|
||||||
|
# piece with codegen-units = 1), and leave out the debug symbols (strip).
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
strip = true
|
||||||
@@ -0,0 +1,465 @@
|
|||||||
|
# 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, half the spacing),
|
||||||
|
Overview (colored dots). 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,
|
||||||
|
cross-links as 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.
|
||||||
|
- 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
|
||||||
|
|
||||||
|
# keys.toml
|
||||||
|
leader = "<Space>"
|
||||||
|
[normal]
|
||||||
|
"<Space>x" = "delete-branch"
|
||||||
|
[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, history, fold
|
||||||
|
├─ input/ key (vim notation), command (Command enum + names), bindings (default tables),
|
||||||
|
│ keymap (+ keys.toml), engine (counts, registers, operators), which_key, mouse
|
||||||
|
├─ command/ registry, parse, complete, history
|
||||||
|
├─ view/ mod.rs (the View trait), canvas/ (camera, boxes, lines, route, render, hit,
|
||||||
|
│ navigate, placement), outline, note, board
|
||||||
|
├─ ui/ text_editor (+ vim normal), which_key, command_line, 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/ App, mode, action, buffers, panes (split tree), jumps, marks, registers,
|
||||||
|
portals (cache, backlinks, renames), update, render,
|
||||||
|
terminal (setup, teardown, panic hook)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Flow:** a terminal event comes in → `input` turns the key or mouse event into
|
||||||
|
a `Command` → the focused view turns that command into `Action`s →
|
||||||
|
`App::update` applies them (the only place data changes) → the screen is drawn
|
||||||
|
from the state.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
trait View { // expected shape; stage 5 settles it from what stages 2–4 needed
|
||||||
|
fn kind(&self) -> ViewKind;
|
||||||
|
fn keys(&self, mode: &Mode) -> &[KeyTable]; // its tables, most specific first
|
||||||
|
fn command(&mut self, command: Command, ctx: &Ctx) -> Vec<Action>;
|
||||||
|
fn mouse(&mut self, gesture: Gesture, ctx: &Ctx) -> Vec<Action>;
|
||||||
|
fn render(&self, frame: &mut Frame, area: Rect, ctx: &Ctx); // reads state, draws, changes nothing
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# cumulus
|
||||||
|
|
||||||
|
Brainstorm in the terminal: ideas as nodes on a 2D canvas, connected, folded
|
||||||
|
and color-coded, across as many documents as you like. Vim keys first; the
|
||||||
|
mouse works too. Runs on Windows and Linux.
|
||||||
|
|
||||||
|
> **Status:** being built, one stage at a time. See [DESIGN.md](DESIGN.md) for
|
||||||
|
> the plan; this README grows with the app.
|
||||||
|
|
||||||
|
## Build and run
|
||||||
|
|
||||||
|
You need Rust ([rustup.rs](https://rustup.rs)). The exact version is pinned in
|
||||||
|
`rust-toolchain.toml`, and rustup installs it by itself the first time.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
The program is `target/release/cumulus` (`cumulus.exe` on Windows): copy it to
|
||||||
|
a folder on your `PATH`.
|
||||||
|
|
||||||
|
- **Windows**: nothing else to install. The `.exe` carries its own C runtime
|
||||||
|
and runs on any Windows 10 or 11.
|
||||||
|
- **Linux** (Void, Fedora…): build the same way. You can also build a static
|
||||||
|
Linux binary from Windows with `cargo linux`: it lands in
|
||||||
|
`target/x86_64-unknown-linux-musl/release/cumulus` and runs on any
|
||||||
|
distribution.
|
||||||
|
|
||||||
|
## Keys
|
||||||
|
|
||||||
|
The plan for the everyday keys (all of them are in DESIGN.md):
|
||||||
|
|
||||||
|
| Keys | What they do |
|
||||||
|
|-------------------|----------------------------------------------------|
|
||||||
|
| `hjkl` | move to the nearest node that way |
|
||||||
|
| `gh gj gk gl` | parent, next sibling, previous sibling, first child |
|
||||||
|
| `o` `O` `a` | new sibling below / above, new child |
|
||||||
|
| `i` or Enter | edit the title |
|
||||||
|
| `dd` `x` | delete the branch / just the node |
|
||||||
|
| `u` `Ctrl-r` | undo / redo |
|
||||||
|
| `za` `zf` `zi` | fold, auto-fold, focus |
|
||||||
|
| `+` `-` | zoom in / out |
|
||||||
|
| `C` `M` `v` | connect, move, select |
|
||||||
|
| `<Space>` | the action menu |
|
||||||
|
| `:` `?` | command line, help |
|
||||||
|
|
||||||
|
After the first key of a sequence (`g`, `z`, `d`, `<Space>`…), a popup lists
|
||||||
|
what can follow.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo test # run the tests
|
||||||
|
cargo lint # clippy, treating warnings as errors
|
||||||
|
cargo fmt # format the code
|
||||||
|
```
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
//! Build script: records the git commit cumulus is built from, so
|
||||||
|
//! `cumulus --version` can tell exactly which build you're running.
|
||||||
|
//!
|
||||||
|
//! Outside a git checkout, or without git installed, the commit is "unknown".
|
||||||
|
//! (No Windows icon or manifest: a terminal program's icon only shows in
|
||||||
|
//! Explorer, and a manifest changes nothing for it.)
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let commit = git_commit().unwrap_or_else(|| "unknown".to_string());
|
||||||
|
println!("cargo::rustc-env=CUMULUS_COMMIT={commit}");
|
||||||
|
|
||||||
|
// Run again when the commit changes: HEAD moves on checkout, and the
|
||||||
|
// branch files change on commit. Only watch files that exist, or cargo
|
||||||
|
// would run this script on every build.
|
||||||
|
println!("cargo::rerun-if-changed=build.rs");
|
||||||
|
for path in [".git/HEAD", ".git/refs/heads", ".git/packed-refs"] {
|
||||||
|
if Path::new(path).exists() {
|
||||||
|
println!("cargo::rerun-if-changed={path}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The short hash of the current commit, if git can tell.
|
||||||
|
fn git_commit() -> Option<String> {
|
||||||
|
let output = Command::new("git").args(["rev-parse", "--short=9", "HEAD"]).output().ok()?;
|
||||||
|
let hash = String::from_utf8(output.stdout).ok()?;
|
||||||
|
let hash = hash.trim();
|
||||||
|
(output.status.success() && !hash.is_empty()).then(|| hash.to_string())
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# The Rust version cumulus is built with. rustup reads this file and installs
|
||||||
|
# this exact version, with these extras, the first time it's needed.
|
||||||
|
[toolchain]
|
||||||
|
channel = "1.90.0"
|
||||||
|
profile = "minimal"
|
||||||
|
components = ["clippy", "rustfmt"]
|
||||||
|
# For `cargo linux` (see .cargo/config.toml).
|
||||||
|
targets = ["x86_64-unknown-linux-musl"]
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# `cargo fmt` formats all the code the same way. Run it after editing.
|
||||||
|
max_width = 110
|
||||||
|
# Keep short expressions on one line (like `if a { b } else { c }`).
|
||||||
|
use_small_heuristics = "Max"
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
//! cumulus: brainstorm in the terminal.
|
||||||
|
//!
|
||||||
|
//! cumulus is a program, not a library for other projects. It's split into a
|
||||||
|
//! library and a small `main.rs` so the examples in `examples/` can run the
|
||||||
|
//! same code while the app is being built (DESIGN.md, "Build order").
|
||||||
|
|
||||||
|
/// The line `cumulus --version` prints: the version, and the git commit it was
|
||||||
|
/// built from (recorded by `build.rs`).
|
||||||
|
pub fn version() -> String {
|
||||||
|
format!("cumulus {} ({})", env!("CARGO_PKG_VERSION"), env!("CUMULUS_COMMIT"))
|
||||||
|
}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
//! cumulus: brainstorm in the terminal.
|
||||||
|
//!
|
||||||
|
//! This file only reads the command line. Everything else lives in the
|
||||||
|
//! library: `src/lib.rs` lists the modules.
|
||||||
|
|
||||||
|
use std::process::ExitCode;
|
||||||
|
|
||||||
|
const USAGE: &str = "\
|
||||||
|
cumulus: brainstorm in the terminal
|
||||||
|
|
||||||
|
Usage: cumulus
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help show this help
|
||||||
|
-V, --version show the version";
|
||||||
|
|
||||||
|
fn main() -> ExitCode {
|
||||||
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||||
|
match args.first().map(String::as_str) {
|
||||||
|
None => {
|
||||||
|
println!("cumulus is being built, one stage at a time: see DESIGN.md.");
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
}
|
||||||
|
Some("-h" | "--help") => {
|
||||||
|
println!("{USAGE}");
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
}
|
||||||
|
Some("-V" | "--version") => {
|
||||||
|
println!("{}", cumulus::version());
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
}
|
||||||
|
Some(other) => {
|
||||||
|
eprintln!("cumulus: unknown option {other}\n\n{USAGE}");
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user