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]>
303 lines
10 KiB
Rust
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;
|