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

+197 -8
View File
@@ -5,22 +5,211 @@
//! focused view answers it with [`action`]s, and [`commands::shared`]
//! answers what the view leaves. A `:` command line is answered by
//! [`calls::actions_for`] instead, keys while a text is edited by its
//! [`text_edit`], and keys while a popup is open by the [`popup`].
//! 2. Document actions go to the [`buffer`] holding the document: the one
//! place a document changes. The rest change the [`mode`] or the app.
//! [`text_edit`], and keys while a popup is open by the [`popup`]
//! (`input.rs`).
//! 2. `App::update` carries the actions out: document actions go to the
//! [`buffer`] holding the document, the one place a document changes;
//! the rest change the [`mode`] or the app (`update.rs`).
//! 3. The screen is drawn from the state (`render.rs`).
//!
//! The screen is split into [`panes`], each showing a document through a
//! view.
//!
//! So far this is what stages 2 to 7 needed; stage 8 adds the main loop,
//! files and settings.
//! The screen is split into [`panes`], each showing one of the open
//! [`documents`] through a view. `files.rs` looks after the documents'
//! files: opening, autosave, backups, reloading what changed outside
//! cumulus, read-only documents, where you left off. [`startup`] decides
//! what `cumulus` opens, and [`run`] is the main loop on a real terminal.
pub mod action;
pub mod buffer;
pub mod calls;
pub mod commands;
pub mod documents;
mod files;
mod input;
pub mod mode;
pub mod panes;
pub mod popup;
pub mod registers;
mod render;
pub mod startup;
mod terminal;
pub mod text_edit;
mod update;
use std::time::Instant;
use ratatui::layout::Rect;
pub use files::{Loaded, OpenError, create, load};
pub use terminal::run;
use crate::command::history::History as CommandHistory;
use crate::config::paths::Paths;
use crate::config::settings::Settings;
use crate::input::bindings::Context;
use crate::input::engine::KeyEngine;
use crate::input::keymap::Keymap;
use crate::input::mouse::MouseReader;
use crate::storage::memory::Memory;
use crate::ui::theme::Theme;
use crate::view::canvas::{Camera, CanvasView};
use crate::view::note::NoteView;
use crate::view::outline::OutlineView;
use crate::view::{View, ViewContext, ViewKind, ViewSettings};
use action::Message;
use buffer::Buffer;
use documents::{BufferId, Documents};
use mode::Mode;
use panes::{PaneId, Panes};
use registers::Registers;
use startup::Opening;
/// cumulus, running: the open documents, the panes showing them, the mode,
/// and everything the keys and the mouse go through.
pub struct App {
panes: Panes<Pane>,
shared: Shared,
paths: Paths,
settings: Settings,
keymap: Keymap,
keys: KeyEngine,
/// Yanked and deleted text, kept from one edit to the next.
registers: Registers,
mouse: MouseReader,
command_history: CommandHistory,
/// What cumulus remembers between runs: read at the start, written at
/// the end ([`App::remember`]).
memory: Memory,
/// The status bar's message, until the next key.
message: Option<Message>,
screen: Screen,
/// The pane a mouse drag started in: it gets the whole drag.
dragging: Option<PaneId>,
/// When the files were last checked for changes made outside cumulus.
watched: Instant,
quit: bool,
}
/// What the panes share, and the views look at: the documents and the
/// mode. (Apart from the panes, so a pane's view can answer a key while
/// looking at these.)
struct Shared {
docs: Documents,
mode: Mode,
theme: Theme,
view: ViewSettings,
}
impl Shared {
/// What a view showing document `buffer` looks at.
fn ctx(&self, buffer: BufferId) -> ViewContext<'_> {
let buffer = &self.docs.get(buffer).buffer;
ViewContext {
doc: buffer.doc(),
cursor: buffer.cursor(),
mode: &self.mode,
theme: &self.theme,
settings: &self.view,
}
}
/// The open documents' names, in the bar's order.
fn names(&self) -> Vec<String> {
self.docs.iter().map(documents::Open::name).collect()
}
}
/// A pane: one of the documents, through a view.
struct Pane {
buffer: BufferId,
view: Box<dyn View>,
}
impl Pane {
/// A pane showing `buffer` in a new view; a canvas starts at `camera`.
fn new(buffer: BufferId, kind: ViewKind, camera: Option<Camera>) -> Pane {
let mut view: Box<dyn View> = match kind {
ViewKind::Outline => Box::new(OutlineView::new()),
ViewKind::Note => Box::new(NoteView::new()),
ViewKind::Canvas | ViewKind::Board => Box::new(CanvasView::new()),
};
if let Some(camera) = camera {
view.set_camera(camera);
}
Pane { buffer, view }
}
}
/// Where things were drawn last, for the mouse.
#[derive(Debug, Clone, Copy, Default)]
struct Screen {
bar: Rect,
panes: Rect,
}
impl App {
/// cumulus with the documents of `opening` open, the first one shown.
pub fn new(paths: Paths, settings: Settings, keymap: Keymap, opening: Opening) -> App {
let memory = Memory::load(&paths.memory());
let Opening { first, others, message } = opening;
let theme = Theme { glyphs: settings.glyphs, ..Theme::default() };
let view = ViewSettings { max_node_width: settings.max_node_width, ..ViewSettings::default() };
let (docs, first_said) = files::open_first(first, &paths);
let first = docs.at(0);
let mut app = App {
panes: Panes::new(Pane::new(first, ViewKind::Canvas, None)),
shared: Shared { docs, mode: Mode::Normal, theme, view },
command_history: CommandHistory::from_lines(memory.history.clone()),
paths,
settings,
keymap,
keys: KeyEngine::default(),
registers: Registers::default(),
mouse: MouseReader::default(),
memory,
message: None,
screen: Screen::default(),
dragging: None,
watched: Instant::now(),
quit: false,
};
app.came_back_to(first);
if let Some(camera) = app.shared.docs.get(first).camera {
app.panes.current_mut().view.set_camera(camera);
}
let mut said: Vec<Message> = message.into_iter().chain(first_said).collect();
for loaded in others {
said.extend(app.add_document(loaded).1);
}
app.message = Message::joined(said);
app
}
/// Whether cumulus is done: `:q` on the last pane, or `:qa`.
pub fn is_done(&self) -> bool {
self.quit
}
/// The document the focused pane shows.
fn buffer(&self) -> BufferId {
self.panes.current().buffer
}
/// The buffer the focused pane shows.
fn current(&self) -> &Buffer {
&self.shared.docs.get(self.buffer()).buffer
}
/// The key tables in use now: the popup's, or the focused view's.
fn contexts(&self) -> &'static [Context] {
match &self.shared.mode {
Mode::Popup(popup) => popup.contexts(),
mode => self.panes.current().view.contexts(mode),
}
}
}
/// The app driven by typed keys and mouse gestures on a test terminal, with
/// its documents in a temporary folder: what the stages say works, checked
/// end to end.
#[cfg(test)]
mod tests;