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:
1 parent
e6f3c06ecc
commit
f6dc4564aa
47 files changed
+3985
-1024
No files matched your search
+197
-8
@@ -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;
|
||||
Reference in new issue
Block a user