//! Actions: what views, keys and the command line ask for. Views never //! change the document, the cursor or the mode themselves. They answer with //! actions, and the app carries them out: [`Buffer::apply`] for a document //! and its cursor, the app for the rest. //! //! [`Buffer::apply`]: super::buffer::Buffer::apply use super::documents::BufferId; use super::jumps::Place; use super::mode::Connect; use super::panes::Arrange; use super::popup::{MenuFor, PickKind}; use super::repeat::Look; use super::text_edit::StartEdit; use crate::command::parse::SetArg; use crate::config::ConfigFile; use crate::input::command::{Command, Register}; use crate::input::key::Key; use crate::model::{Direction, EditOp, Group, NodeId, Title}; use crate::view::ViewKind; /// How a status-bar message is meant. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Tone { /// Something to know. Info, /// Something got done. Done, /// Something didn't work. Problem, } /// A message for the status bar. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Message { pub text: String, pub tone: Tone, } /// "1 node", "3 nodes". pub fn nodes(count: usize) -> String { if count == 1 { "1 node".to_string() } else { format!("{count} nodes") } } impl Message { pub fn info(text: impl Into) -> Message { Message { text: text.into(), tone: Tone::Info } } pub fn done(text: impl Into) -> Message { Message { text: text.into(), tone: Tone::Done } } pub fn problem(text: impl Into) -> Message { Message { text: text.into(), tone: Tone::Problem } } /// Several messages in one, with ` · ` between them: a problem if one of /// them is. `None` when there are none. pub fn joined(messages: Vec) -> Option { let problem = messages.iter().any(|message| message.tone == Tone::Problem); let tone = if problem { Tone::Problem } else { messages.first()?.tone }; let texts: Vec<&str> = messages.iter().map(|message| message.text.as_str()).collect(); Some(Message { text: texts.join(" · "), tone }) } } /// Where the cursor goes after an edit. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CursorAfter { /// Where it is (or to the root, if its node is gone). Stay, /// To the node the edit added. Added, /// To this node. To(NodeId), } /// What happens to a document and its cursor. #[derive(Debug, Clone, PartialEq, Eq)] pub enum DocAction { /// Move the cursor. A `jump` is one that `''` can come back from. MoveCursor { to: NodeId, jump: bool, }, /// Change the document, as one undo step (or as part of its group's). /// The op is boxed because it can hold whole nodes, which would make /// every action that size. Edit { op: Box, cursor: CursorAfter, group: Option, }, Undo(u32), Redo(u32), SetMark(char), /// Go to a mark; `'` and `` ` `` go back to before the last jump. JumpToMark(char), /// Take back the group's undo step, if it's the newest, leaving nothing /// to redo: Esc in connect mode. Cancel(Group), } /// What happens around the documents. #[derive(Debug, Clone, PartialEq, Eq)] pub enum AppAction { /// Start editing the cursor's title, tags or body. StartEdit(StartEdit), /// The text edit is done: back to normal mode, or to the `K` card the /// body was edited in. EndEdit, /// Put the text cursor here, in the text being edited (a click). TextCursor(usize), /// Visual mode, `V` (`branches`) or `v`; in visual mode, switches /// between the two. Visual { branches: bool, }, /// In visual mode: the cursor goes to the other end (`o`). SwapEnds, /// A node into the selection, or out of it (a Ctrl+click). ToggleSelected(NodeId), /// Nodes into the selection (a Ctrl+drag). SelectAlso(Vec), /// Move mode: hjkl moves the cursor's branch, or these nodes (a /// selection's). EnterMoveMode(Vec), /// In move mode: the whole branch, or just the node. ToggleWhole, /// Connect mode (`C`): from the cursor's node, or from these (a /// selection's). EnterConnectMode(Vec), /// Back to connect mode, from a picker it opened. ResumeConnect(Connect), /// Enter or `gf` on a portal: through it, to its document's root. FollowPortal(NodeId), /// `:portal`: a portal under the cursor's node, to the document at the /// path typed (a new document when nothing's there). NewPortal(String), /// `gu`: up to the portals that lead to this document; `list` /// (`:backlinks`) lists them even when there's just one. GoUp { list: bool, }, /// Back to normal mode. LeaveMode, OpenCommandLine, /// The command line, with this typed already: a `:` command from the /// help that needs more. CommandLineWith(String), /// A command line, run as if typed (without its `:`). RunLine(String), /// A command, run as if its keys were typed: the help and the menu run /// commands this way. Run(Command), /// Keys, as if typed: for a key that waits for more (`m` waits for a /// mark's letter). Type(Vec), /// Keep nodes in a register (`yy`, and before a delete): exactly /// `nodes`, or (`branches`) each with its whole branch. Yank { register: Option, nodes: Vec, branches: bool, }, /// Paste what a register holds (`p`, `P`): its nodes, or its text /// made into nodes, `times` copies. Paste { register: Option, at: PasteAt, times: u32, }, /// A value was picked in a picker: `.` can put it on another node. Picked(Look), /// Focus mode: Open branches act like Auto ones. ToggleFocus, ToggleWhichKey, /// Change a setting. Set(SetArg), // Popups. /// Every key and command, with this already searched for. OpenHelp(String), /// `/`: the search. OpenSearch, /// A search went somewhere: `n` and `N` go on with it. Searched(String), /// `Ctrl-o` (`back`) and `Ctrl-i`: through the focused pane's jump list. Travel { back: bool, times: u32, }, /// `n` and `N`: the next or previous match of the last search. FindAgain { forward: bool, times: u32, }, /// `:find`: straight to the best match. Find(String), /// A capital mark on the cursor's node (`mA`). SetFileMark(char), /// To a capital mark, in any document (`'A`). JumpToFileMark(char), /// To a place from the jump list (`:jumps`). GoToPlace(Place), /// `:marks`, `:jumps` and `:registers`. OpenMarks, OpenJumps, OpenRegisters, /// `gd` on a node with several cross-links: which one to follow. OpenLinks, /// The cursor's body in a card beside it. OpenCard, OpenPicker(PickKind), /// The right-click menu, at a cell on the screen. OpenMenu { what: MenuFor, col: u16, row: u16, }, // Panes and documents. /// Split the focused pane: the new pane shows `view`, or the same view. Split { arrange: Arrange, view: Option, }, FocusPane(Direction), NextPane, /// Close the focused pane (`:q`, `ZZ`, `Ctrl-w c`). The last one quits, /// unless a document has unsaved changes; `force` (`:q!`) quits anyway. ClosePane { force: bool, }, OnlyPane, EqualizePanes, /// The focused pane shows another view. ShowView(ViewKind), SwitchDocument(DocumentChoice), // Files. /// `:config`: the config files, to open one in the editor. OpenConfig, /// Open a config file in the editor (made first if it isn't there). EditConfig(ConfigFile), /// Save (`:w`, `:x`, `:wa`). Save(SaveWhich), /// Open a document in the focused pane (`:e`), from a path as typed: /// relative to the focused document's folder, `~` for the home folder. /// No path reads the focused document again from its file; `force` /// (`:e!`) drops unsaved changes to do it. Edit { path: Option, force: bool, }, /// Make a new document next to the focused one, and show it (`:new`). NewDocument(Option), /// Close the focused document (`:bd`); `force` drops unsaved changes. CloseDocument { force: bool, }, /// A file changed outside cumulus while its document had unsaved /// changes here: which version to keep. Resolve { buffer: BufferId, keep: Keep, }, /// Quit cumulus (`:qa`): not while a document has unsaved changes, /// unless `force` (`:qa!`). Quit { force: bool, }, Say(Message), } /// Where `p` and `P` paste, from the cursor's node (the user's pick after /// trying part 8c: `p` pastes under the node, as `a` adds). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PasteAt { /// Its last children (`p`). Child, /// Its next siblings, below it, where `o` adds one (`P`). Sibling, } /// What `:w` and its kin save. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SaveWhich { /// The focused document, changed or not (`:w`). This, /// The focused document, if it has unsaved changes (`:x`, `ZZ`). IfChanged, /// Every document with unsaved changes (`:wa`). All, } /// Which version of a document to keep when its file changed outside /// cumulus while it had unsaved changes here. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Keep { /// The one on screen: it's saved over the file. Mine, /// The file's: it takes the place of the one on screen (`u` brings that /// back). Theirs, } /// Which document the focused pane switches to. #[derive(Debug, Clone, PartialEq, Eq)] pub enum DocumentChoice { Next, Previous, /// The open document listed at this place (the documents picker). At(usize), /// The open document with this name (`:b`). Named(String), } /// Anything views, keys and the command line ask for. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Action { Doc(DocAction), App(AppAction), } impl Action { /// Move the cursor to a node. pub fn cursor(to: NodeId) -> Action { Action::Doc(DocAction::MoveCursor { to, jump: false }) } /// Jump to a node: `''` comes back. pub fn jump(to: NodeId) -> Action { Action::Doc(DocAction::MoveCursor { to, jump: true }) } /// Change the document, as one undo step; the cursor stays. pub fn edit(op: EditOp) -> Action { Action::Doc(DocAction::Edit { op: Box::new(op), cursor: CursorAfter::Stay, group: None }) } /// Change the document, as one undo step, then move the cursor. pub fn edit_then(op: EditOp, cursor: CursorAfter) -> Action { Action::Doc(DocAction::Edit { op: Box::new(op), cursor, group: None }) } /// Change the document as part of a group's undo step. pub fn edit_in(op: EditOp, group: Group) -> Action { Action::Doc(DocAction::Edit { op: Box::new(op), cursor: CursorAfter::Stay, group: Some(group) }) } /// Show a message. pub fn say(message: Message) -> Action { Action::App(AppAction::Say(message)) } }