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

+67 -4
View File
@@ -5,13 +5,14 @@
//!
//! [`Buffer::apply`]: super::buffer::Buffer::apply
use super::documents::BufferId;
use super::panes::Arrange;
use super::popup::{MenuFor, PickKind};
use super::text_edit::StartEdit;
use crate::command::parse::SetArg;
use crate::input::command::Command;
use crate::input::key::Key;
use crate::model::{Direction, EditOp, Group, NodeId};
use crate::model::{Direction, EditOp, Group, NodeId, Title};
use crate::view::ViewKind;
/// How a status-bar message is meant.
@@ -44,6 +45,15 @@ impl Message {
pub fn problem(text: impl Into<String>) -> Message {
Message { text: text.into(), tone: Tone::Problem }
}
/// Several messages in one, with ` · ` between them: a problem if one of
/// them is. `None` when there are none.
pub fn joined(messages: Vec<Message>) -> Option<Message> {
let problem = messages.iter().any(|message| message.tone == Tone::Problem);
let tone = if problem { Tone::Problem } else { messages.first()?.tone };
let texts: Vec<&str> = messages.iter().map(|message| message.text.as_str()).collect();
Some(Message { text: texts.join(" · "), tone })
}
}
/// Where the cursor goes after an edit.
@@ -135,18 +145,71 @@ pub enum AppAction {
},
FocusPane(Direction),
NextPane,
/// Close the focused pane (the last one quits).
ClosePane,
/// Close the focused pane (`:q`, `ZZ`, `Ctrl-w c`). The last one quits,
/// unless a document has unsaved changes; `force` (`:q!`) quits anyway.
ClosePane {
force: bool,
},
OnlyPane,
EqualizePanes,
/// The focused pane shows another view.
ShowView(ViewKind),
SwitchDocument(DocumentChoice),
Quit,
// Files.
/// Save (`:w`, `:x`, `:wa`).
Save(SaveWhich),
/// Open a document in the focused pane (`:e`), from a path as typed:
/// relative to the focused document's folder, `~` for the home folder.
/// No path reads the focused document again from its file; `force`
/// (`:e!`) drops unsaved changes to do it.
Edit {
path: Option<String>,
force: bool,
},
/// Make a new document next to the focused one, and show it (`:new`).
NewDocument(Option<Title>),
/// Close the focused document (`:bd`); `force` drops unsaved changes.
CloseDocument {
force: bool,
},
/// A file changed outside cumulus while its document had unsaved
/// changes here: which version to keep.
Resolve {
buffer: BufferId,
keep: Keep,
},
/// Quit cumulus (`:qa`): not while a document has unsaved changes,
/// unless `force` (`:qa!`).
Quit {
force: bool,
},
Say(Message),
}
/// What `:w` and its kin save.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SaveWhich {
/// The focused document, changed or not (`:w`).
This,
/// The focused document, if it has unsaved changes (`:x`, `ZZ`).
IfChanged,
/// Every document with unsaved changes (`:wa`).
All,
}
/// Which version of a document to keep when its file changed outside
/// cumulus while it had unsaved changes here.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Keep {
/// The one on screen: it's saved over the file.
Mine,
/// The file's: it takes the place of the one on screen (`u` brings that
/// back).
Theirs,
}
/// Which document the focused pane switches to.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DocumentChoice {