Files
cumulus/src/app/mod.rs
T
skkeyeandClaude Opus 5.5 3716919db3 Add the board, and finish v1.1 (stage 9e)
The board is the user's design: the document as lists and blocks, the
way Scratch nests blocks inside a loop. Each node without a parent (the
root, then each loose node) is a column with its title in the top border;
its children are cards in it, one under the other; a card with children
on show wraps them in a C: a joint at its bottom left, a bracket down the
left two cells further in, and a foot closing it. Blocks nest as deep as
the tree does. The columns by tag or color the first design had are
dropped (the user's pick), and :board takes no arguments any more.

j and k go down and up a column in reading order, into the blocks; h and
l go to the column beside, onto the card nearest the cursor's height.
Titles are typed right in their cards, and visual mode selects in
reading order as the outline does. <Space>vb or :board shows it.

Move mode (M, the user's pick) moves the card through the tree: h and l
to the end of the column beside, j and k past the cards around it, into
an open block and out of one at its end. A column's own node trades
places with the loose column beside it; the root's column stays first.
A drag does the same with the mouse: dropped on a card it goes after it,
on a block's card into it, on a column at its end.

The fallback message for a key a view ignores now names the view ("does
nothing in the board view"). The version goes to 1.1.0: stage 9 is done.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-02 13:05:05 -04:00

303 lines
10 KiB
Rust

//! The app: everything around the views.
//!
//! How input becomes a change:
//! 1. A key goes through the key engine and comes out as a command; the
//! 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`]
//! (`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 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;
mod branch_out;
pub mod buffer;
pub mod calls;
mod clipboard;
pub mod commands;
mod configure;
pub mod connect;
pub mod documents;
mod export;
mod files;
mod input;
pub mod jumps;
mod marks;
pub mod mode;
pub mod panes;
pub mod popup;
pub mod portals;
pub mod registers;
mod render;
mod repeat;
pub mod search;
pub mod startup;
mod terminal;
pub mod text_edit;
mod update;
pub mod visual;
mod yank;
use std::path::{Path, PathBuf};
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::Config;
use crate::config::paths::Paths;
use crate::config::settings::{ClipboardUse, Settings};
use crate::input::bindings::Context;
use crate::input::engine::KeyEngine;
use crate::input::keymap::Keymap;
use crate::input::mouse::MouseReader;
use crate::model::DocUid;
use crate::storage::memory::Memory;
use crate::ui::theme::Theme;
use crate::view::board::BoardView;
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 jumps::Jumps;
use mode::Mode;
use panes::{PaneId, Panes};
use portals::Portals;
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>,
/// The last change, for `.`.
last_change: Option<repeat::Repeat>,
/// A text edit started by a key command, while it goes on.
recording: Option<repeat::Recording>,
/// `.` is making a change again: it isn't recorded again.
repeating: bool,
/// The last search, for `n` and `N`.
last_search: Option<search::LastSearch>,
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,
/// The config files as they were last read, to apply them again when
/// they change.
config_stamps: configure::ConfigStamps,
/// A file `:config` wants opened in the editor: the main loop does it.
edit_request: Option<PathBuf>,
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,
/// What the documents portals lead to look like.
portals: Portals,
}
impl Shared {
/// What a view showing document `buffer` looks at.
fn ctx(&self, buffer: BufferId) -> ViewContext<'_> {
self.ctx_in(buffer, &self.mode)
}
/// What a view showing document `buffer` looks at, in `mode` (the panes
/// without the focus are drawn as in normal mode).
fn ctx_in<'a>(&'a self, buffer: BufferId, mode: &'a Mode) -> ViewContext<'a> {
let open = self.docs.get(buffer);
ViewContext {
doc: open.buffer.doc(),
cursor: open.buffer.cursor(),
mode,
theme: &self.theme,
settings: &self.view,
portals: &self.portals,
folder: open.file.path.parent().unwrap_or(Path::new(".")),
}
}
/// 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, where it jumped from, and
/// the portals it came through.
struct Pane {
buffer: BufferId,
view: Box<dyn View>,
jumps: Jumps,
/// The documents the pane came to this one through, by portals, the
/// first one first: the breadcrumb starts with them.
trail: Vec<Crumb>,
}
/// A document a pane came through, by a portal.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Crumb {
pub uid: DocUid,
/// Its name then.
pub title: String,
}
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::Board => Box::new(BoardView::new()),
ViewKind::Canvas => Box::new(CanvasView::new()),
};
if let Some(camera) = camera {
view.set_camera(camera);
}
Pane { buffer, view, jumps: Jumps::default(), trail: Vec::new() }
}
/// The pane shows `buffer` in a new view (a canvas starting at `camera`);
/// its jump list and its trail stay.
fn show(&mut self, buffer: BufferId, kind: ViewKind, camera: Option<Camera>) {
let (jumps, trail) = (std::mem::take(&mut self.jumps), std::mem::take(&mut self.trail));
*self = Pane { jumps, trail, ..Pane::new(buffer, kind, camera) };
}
/// The pane got to document `uid` another way than a portal: if it came
/// through that one, the trail goes back to it; otherwise it starts
/// again.
fn arrive(&mut self, uid: DocUid) {
match self.trail.iter().position(|crumb| crumb.uid == uid) {
Some(at) => self.trail.truncate(at),
None => self.trail.clear(),
}
}
}
/// Where things were drawn last, for the mouse.
#[derive(Debug, Clone, Copy, Default)]
struct Screen {
bar: Rect,
panes: Rect,
}
impl App {
/// cumulus with its config, and the documents of `opening` open, the
/// first one shown.
pub fn new(paths: Paths, config: Config, opening: Opening) -> App {
let memory = Memory::load(&paths.memory());
let Opening { first, others, message } = opening;
let Config { settings, keymap, theme, problems } = config;
let config_stamps = configure::ConfigStamps::read(&paths, &settings.theme);
let view = ViewSettings { max_node_width: settings.max_node_width, ..ViewSettings::default() };
let mut registers = Registers::default();
registers.unnamed_plus = settings.clipboard == ClipboardUse::UnnamedPlus;
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, portals: Portals::default() },
command_history: CommandHistory::from_lines(memory.history.clone()),
paths,
settings,
keymap,
keys: KeyEngine::default(),
registers,
mouse: MouseReader::default(),
memory,
message: None,
last_change: None,
recording: None,
repeating: false,
last_search: None,
screen: Screen::default(),
dragging: None,
watched: Instant::now(),
config_stamps,
edit_request: None,
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 config_said = problems.into_iter().map(Message::problem);
let mut said: Vec<Message> = config_said.chain(message).chain(first_said).collect();
for loaded in others {
said.extend(app.add_document(loaded).1);
}
// Once every document named is open, so one that's about to open
// isn't written underneath it.
app.sync_backlinks(first);
app.refresh_portals(true);
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;