//! Keys and the mouse, routed: to the command line while it's open, to the //! text being edited, to an open popup, or through the key engine to the //! focused view and then the shared commands. use ratatui::layout::Position; use super::action::{Action, AppAction, DocumentChoice, Message}; use super::mode::Mode; use super::{App, calls, commands, connect, visual}; use crate::command::parse::{Sources, parse}; use crate::config::settings::Setting; use crate::config::theme_file; use crate::input::command::{Bound, Command, help_for}; use crate::input::engine::{Outcome, Typed}; use crate::input::key::Key; use crate::input::mouse::{Cell, Gesture}; use crate::ui::command_line::LineOutcome; use crate::ui::document_bar::DocumentBar; impl App { /// A key, as typed. pub fn key(&mut self, key: Key) { self.message = None; self.record(key); match self.shared.mode { Mode::Command(_) => self.command_line_key(key), Mode::Text(_) => self.text_key(key), Mode::Popup(_) => self.popup_key(key), Mode::Normal | Mode::Visual(_) | Mode::Move(_) | Mode::Connect(_) => { match self.keys.press(key, &self.keymap.tables(self.contexts())) { Outcome::Run(typed) => self.run_typed(typed), Outcome::Unknown(keys) => { self.message = Some(Message::info(format!("{keys} does nothing here"))); } Outcome::Waiting | Outcome::Cancelled => {} } } } self.end_recording(); // Portals show the documents they lead to as they are now. self.refresh_portals(false); } /// A command: the focused view has the first say, then visual or /// connect mode's commands (in those modes), then the shared commands. pub(super) fn dispatch(&mut self, typed: Typed) { let selected = self.selection().unwrap_or_default(); let pane = self.panes.current_mut(); let kind = pane.view.kind(); let ctx = self.shared.ctx(pane.buffer); let actions = pane .view .command(&typed, &ctx) .or_else(|| match &self.shared.mode { Mode::Visual(visual) => visual::command(&typed, visual, &selected, kind, &ctx), Mode::Connect(connect) => connect::command(&typed, connect, &ctx), _ => None, }) .or_else(|| commands::shared(&typed, &ctx)); let actions = actions.unwrap_or_else(|| { let later = format!("\"{}\" comes in a later part of the build", describe(typed.command)); vec![Action::say(Message::info(later))] }); self.update(actions); } /// Runs a command as if its keys were typed (the help and the menu). pub(super) fn run_command(&mut self, command: Command) { self.dispatch(Typed { command, count: None, register: None }); } /// A key while a text is edited in a view: typed into it, or a vim key /// inside it. fn text_key(&mut self, key: Key) { let tables = self.keymap.tables(self.contexts()); // The edit comes out of the mode while it works, so it can look at // the document; it goes back in before its actions run. let Mode::Text(mut edit) = std::mem::take(&mut self.shared.mode) else { return }; let ctx = self.shared.ctx(self.buffer()); let actions = if edit.editor.is_typing() { // Typing: a key the table doesn't bind types itself. edit.typing(key, tables.bound(&[key]), &ctx) } else { match self.keys.press(key, &tables) { Outcome::Run(typed) => edit.vim(&typed, &mut self.registers, &ctx), Outcome::Unknown(keys) => { vec![Action::say(Message::info(format!("{keys} does nothing here")))] } Outcome::Waiting | Outcome::Cancelled => Vec::new(), } }; self.shared.mode = Mode::Text(edit); self.update(actions); } /// A key while a popup is open: typed into its search (or the card's /// body), or one of its keys. fn popup_key(&mut self, key: Key) { let tables = self.keymap.tables(self.contexts()); let Mode::Popup(mut popup) = std::mem::take(&mut self.shared.mode) else { return }; let ctx = self.shared.ctx(self.buffer()); let actions = if popup.takes_typing() { popup.typing(key, &self.keymap, &ctx) } else { match self.keys.press(key, &tables) { Outcome::Run(typed) => popup.command(&typed, &mut self.registers, &ctx), Outcome::Unknown(keys) => { vec![Action::say(Message::info(format!("{keys} does nothing here")))] } Outcome::Waiting | Outcome::Cancelled => Vec::new(), } }; self.shared.mode = Mode::Popup(popup); self.update(actions); } /// A key while the command line is open. fn command_line_key(&mut self, key: Key) { let sources = self.sources(); let Mode::Command(line) = &mut self.shared.mode else { return }; match line.key(key, &sources, &self.command_history) { LineOutcome::Typing => {} LineOutcome::Close => self.shared.mode = Mode::Normal, LineOutcome::Run(text) => { self.shared.mode = Mode::Normal; if text.trim().is_empty() { return; } self.command_history.add(&text); self.run_line(&text); } } } /// Runs a command line (without its `:`). pub(super) fn run_line(&mut self, text: &str) { let sources = self.sources(); let actions = match parse(text, &sources) { Ok(call) => calls::actions_for(call, &self.shared.ctx(self.buffer())), Err(error) => vec![Action::say(Message::problem(error.to_string()))], }; self.update(actions); } /// What the command line can name: the open documents, the focused /// document's nodes and tags, the palette's colors, the settings. pub(super) fn sources(&self) -> Sources { let doc = self.current().doc(); let nodes = doc.outline().into_iter().filter_map(|id| Some((id, doc.node(id)?.title().to_string()))); let mut tags: Vec = doc .outline() .into_iter() .filter_map(|id| doc.node(id)) .flat_map(|node| node.tags.iter().map(|tag| tag.name().to_string()).collect::>()) .collect(); tags.sort(); tags.dedup(); let theme = &self.shared.theme; Sources { documents: self.shared.names(), nodes: nodes.collect(), colors: theme.palette.keys().map(ToString::to_string).collect(), styles: theme.node_styles.keys().map(ToString::to_string).collect(), themes: theme_file::names(&self.paths), settings: Setting::ALL.iter().map(|setting| setting.name().to_string()).collect(), tags, folder: self.folder(), } } /// A mouse gesture: a popup has the mouse while it's open; otherwise it /// goes to the document bar or to the pane under it (a drag stays with /// the pane it started in), which takes the focus. pub fn gesture(&mut self, gesture: Gesture) { self.route_gesture(gesture); self.refresh_portals(false); } fn route_gesture(&mut self, gesture: Gesture) { let at = |cell: Cell| Position::new(cell.col, cell.row); // In connect mode a click only aims: no editing, no menu, no // selecting. let gesture = match (&self.shared.mode, gesture) { (Mode::Connect(_), Gesture::RightPress { .. }) => return, (Mode::Connect(_), Gesture::Press { at, .. } | Gesture::DoublePress { at }) => { Gesture::Press { at, ctrl: false } } _ => gesture, }; // A click drops a half-typed key sequence: a `d` typed before // clicking somewhere else mustn't wait to delete what comes next. if matches!(gesture, Gesture::Press { .. } | Gesture::DoublePress { .. } | Gesture::RightPress { .. }) { self.keys.reset(); if let Mode::Command(_) = self.shared.mode { self.shared.mode = Mode::Normal; } } // A click without Ctrl ends a selection, as in most apps. if let (Mode::Visual(_), Gesture::Press { ctrl: false, .. } | Gesture::DoublePress { .. }) = (&self.shared.mode, gesture) { self.shared.mode = Mode::Normal; } if let Mode::Popup(_) = self.shared.mode { let Mode::Popup(mut popup) = std::mem::take(&mut self.shared.mode) else { return }; let actions = popup.mouse(gesture, &self.shared.ctx(self.buffer())); self.shared.mode = Mode::Popup(popup); return self.update(actions); } if let Gesture::Press { at: cell, .. } = gesture && cell.row == self.screen.bar.y { let names = self.bar_names(); let current = self.shared.docs.index_of(self.buffer()); let bar = DocumentBar { names: &names, current, theme: &self.shared.theme }; if let Some(index) = bar.at(cell.col.saturating_sub(self.screen.bar.x)) { self.update(vec![Action::App(AppAction::SwitchDocument(DocumentChoice::At(index)))]); } return; } let target = match gesture { Gesture::Press { at: cell, .. } | Gesture::DoublePress { at: cell } | Gesture::RightPress { at: cell } | Gesture::Scroll { at: cell, .. } => self.panes.at(self.screen.panes, at(cell)), Gesture::Drag { .. } | Gesture::Release { .. } => self.dragging, }; let Some(target) = target else { return }; if let Gesture::Press { .. } = gesture { self.dragging = Some(target); } if target != self.panes.focused() { // Another pane: a text edited in this one is done first, move or // visual mode ends, and connect mode is cancelled. let ctx = self.shared.ctx(self.buffer()); let mut actions = self.shared.mode.edit().map(|edit| edit.finish(&ctx)).unwrap_or_default(); match &self.shared.mode { Mode::Move(_) | Mode::Visual(_) => actions.push(Action::App(AppAction::LeaveMode)), Mode::Connect(connecting) => actions.extend(connect::cancel(connecting)), _ => {} } self.update(actions); self.panes.focus(target); } let pane = self.panes.current_mut(); let actions = pane.view.mouse(gesture, &self.shared.ctx(pane.buffer)); if let Gesture::Release { .. } = gesture { self.dragging = None; } self.update(actions); } } /// What a command does, in words: its own help, or its operator's. fn describe(command: Command) -> &'static str { match (help_for(Bound::Command(command)), command) { ("", Command::Operate(operator, _)) => help_for(Bound::Operator(operator)), (help, _) => help, } }