//! 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, 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, /// The last change, for `.`. last_change: Option, /// A text edit started by a key command, while it goes on. recording: Option, /// `.` is making a change again: it isn't recorded again. repeating: bool, /// The last search, for `n` and `N`. last_search: Option, screen: Screen, /// The pane a mouse drag started in: it gets the whole drag. dragging: Option, /// 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, 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 { 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, jumps: Jumps, /// The documents the pane came to this one through, by portals, the /// first one first: the breadcrumb starts with them. trail: Vec, } /// 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) -> Pane { let mut view: Box = 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) { 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 = 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;