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

+52 -19
View File
@@ -3,35 +3,68 @@
//! This file only reads the command line. Everything else lives in the
//! library: `src/lib.rs` lists the modules.
use std::path::PathBuf;
use std::process::ExitCode;
use cumulus::app::startup::{self, Startup};
use cumulus::app::{App, run};
use cumulus::config::paths::Paths;
use cumulus::config::settings::Settings;
use cumulus::input::keymap::{DEFAULT_LEADER, Keymap};
const USAGE: &str = "\
cumulus: brainstorm in the terminal
Usage: cumulus
Usage:
cumulus open ./ideas.cum, or ~/ideas.cum
cumulus <file|folder>... open those documents (a folder: its ideas.cum)
cumulus new [title] make a new document here: ./ideas.cum, or a
file named after the title
Options:
-h, --help show this help
-V, --version show the version";
--paths show where cumulus keeps its settings and data
-h, --help show this help
-V, --version show the version
Inside cumulus, ? shows every key.";
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}");
match start(std::env::args().skip(1).collect()) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("cumulus: {error}");
ExitCode::FAILURE
}
}
}
fn start(args: Vec<String>) -> Result<(), String> {
let startup = match args.first().map(String::as_str) {
None => Startup::Default,
Some("-h" | "--help") => {
println!("{USAGE}");
return Ok(());
}
Some("-V" | "--version") => {
println!("{}", cumulus::version());
return Ok(());
}
Some("--paths") => {
let paths = Paths::locate().map_err(|error| error.to_string())?;
println!("config {} (config.toml, keys.toml, themes)", paths.config.display());
println!("data {} (where you left off, history, backups, locks)", paths.data.display());
return Ok(());
}
Some(option) if option.starts_with('-') => return Err(format!("unknown option {option}\n\n{USAGE}")),
// The title's words, quoted or not: `cumulus new Garden plans`.
Some("new") => Startup::New((args.len() > 1).then(|| args[1..].join(" "))),
Some(_) => Startup::Open(args.iter().map(PathBuf::from).collect()),
};
let paths = Paths::locate().map_err(|error| error.to_string())?;
// config.toml and keys.toml come in part 8b of the build.
let settings = Settings::default();
let here =
std::env::current_dir().map_err(|error| format!("can't tell which folder this is: {error}"))?;
let opening = startup::open(&startup, &paths, &here, &settings).map_err(|error| error.to_string())?;
run(App::new(paths, settings, Keymap::new(DEFAULT_LEADER), opening)).map_err(|error| error.to_string())
}