Run cumulus for real: the app core and its files (stage 8a)

The playground becomes app::App, and `cumulus` runs it: the main loop
with terminal setup and a panic hook, and the startup rules (./ideas.cum,
else ~/ideas.cum, made the first time; files and folders by name;
`cumulus new`; --paths).

The documents' files:
- autosave a second after the latest change (`:set noautosave` for
  vim-style :w), [+] in the document bar for unsaved changes; quitting
  saves first; without autosave :q refuses, :q! drops, ZZ saves first;
- a file changed outside cumulus is read again as an undo step; with
  unsaved changes here too, cumulus asks which to keep; a file with a
  mistake in it holds autosave until it's fixed or :w saves over it; a
  deleted one waits for :w; nothing is reloaded under an edit;
- the day's first save backs the file up to the data folder (30 days);
- a document open in another cumulus is read-only ([RO]) until that one
  lets go: OS file locks in the data folder, so a crash leaves none;
- where you left off (cursor, zoom, scroll), the recent documents (now
  in the documents picker) and the command history, in memory.ron;
- :e (alone: read it again; ! drops changes), :new, :bd, :w, :wa, :x,
  :qa, <Space>i for :in, and :set for every setting. glyphs=ascii now
  reaches the frames and bars too, not just the canvas and outline.

Stage 8 comes in four parts with a commit each (the user's choice); 8b
is the settings files. The example is now a playground running the app
on copies of the sample documents in a temporary folder, and the
end-to-end tests moved to src/app/tests.rs, with tests for the files.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
skkeyeandClaude Opus 5.5 committed 2026-10-01 23:18:21 -04:00
1 parent e6f3c06ecc
commit f6dc4564aa
47 files changed
+3985 -1024

No files matched your search

+17 -4
View File
@@ -105,7 +105,10 @@ fn pickers_set_what_is_picked() {
);
let no_styles = Popup::picker(PickKind::Style, &ctx, &[], 0).unwrap_err();
assert_eq!(no_styles.text, "this theme has no named styles yet: theme files come in stage 8");
assert_eq!(
no_styles.text,
"this theme has no named styles yet: theme files come in part 8b of the build"
);
// Tags go on and off, and the picker stays open; a new one is added.
let mut tags = pick(PickKind::Tags);
@@ -122,14 +125,24 @@ fn pickers_set_what_is_picked() {
[Action::edit(EditOp::Tag { id: g.soil, add: vec![spring], remove: Vec::new() })]
);
// Documents: it starts on the one shown; the next one is a j away.
let documents = ["Garden plans".to_string(), "Kitchen".to_string()];
// Documents: it starts on the one shown; the next one is a j away, and
// one opened lately two more (past its heading).
let row =
|name: &str, open| DocumentRow { name: name.into(), path: format!("/notes/{name}.cum").into(), open };
let documents = [row("Garden plans", true), row("Kitchen", true), row("Reading", false)];
let mut picker = Popup::picker(PickKind::Document, &ctx, &documents, 0).unwrap();
picker.command(&typed(Command::Move(crate::input::command::Motion::Down)), &mut registers, &ctx);
let down = typed(Command::Move(crate::input::command::Motion::Down));
picker.command(&down, &mut registers, &ctx);
assert_eq!(
picker.command(&typed(Command::Confirm), &mut registers, &ctx),
[app(AppAction::LeaveMode), app(AppAction::SwitchDocument(DocumentChoice::At(1)))]
);
picker.command(&down, &mut registers, &ctx);
let path = Some(std::path::Path::new("/notes/Reading.cum").display().to_string());
assert_eq!(
picker.command(&typed(Command::Confirm), &mut registers, &ctx),
[app(AppAction::LeaveMode), app(AppAction::Edit { path, force: false })]
);
}
#[test]