use crate::{state::BufferId, terminal::TerminalEvent}; use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use std::path::PathBuf; #[derive(Clone, Debug)] pub enum Dir { Left, Right, Up, Down, } #[derive(Clone, Debug)] pub enum Action { Char(char), // Insert a character Indent(bool), // Indent (indent vs deindent) Move(Dir, bool, bool, bool), // Move the cursor (dir, page, retain_base, word) PaneMove(Dir), // Move panes PaneOpen(Dir), // Create a new pane PaneClose, // Close the current pane Cancel, // Cancels the current action Continue, // Continue past an info-only element (like a help screen) Go, // Search, accept, or select the current option Yes, // A binary confirmation is answered 'yes' No, // A binary confirmation is answered 'no' Quit, // Quit the application OpenPrompt, // Open the command prompt Show(Option, String), // Display an optionally titled informational text box to the user OpenSwitcher, // Open the buffer switcher OpenOpener(PathBuf), // Open the file opener OpenFinder(Option), // Open the finder, with the given default query SwitchBuffer(BufferId), // Switch the current pane to the given buffer OpenFile(PathBuf), // Open the file and switch the current pane to it CommandStart(&'static str), // Start a new command GotoLine(isize), // Go to the specified file line SelectToken, // Fully select the token under the cursor Save, // Save the current buffer } #[derive(Debug)] pub enum Event { // The incoming event is an action generated by some other internal component. Action(Action), // The incoming event is a raw user input. Raw(RawEvent), } impl From for Event { fn from(action: Action) -> Self { Self::Action(action) } } impl Event { pub fn from_raw(e: TerminalEvent) -> Self { Self::Raw(RawEvent(e)) } /// Turn the event into an action (if possible). /// /// The translation function allows elements to translate raw events into their own context-specific actions. pub fn to_action(&self, translate: impl FnOnce(&RawEvent) -> Option) -> Option { match self { Self::Action(a) => Some(a.clone()), Self::Raw(te) => translate(te), } } } const ALT_SHIFT: KeyModifiers = KeyModifiers::ALT.union(KeyModifiers::SHIFT); #[derive(Debug)] pub struct RawEvent(TerminalEvent); impl RawEvent { pub fn to_char(&self) -> Option { match self.0 { TerminalEvent::Key(KeyEvent { code, modifiers, kind: KeyEventKind::Press | KeyEventKind::Repeat, .. }) => match code { KeyCode::Char(c) if matches!(modifiers, KeyModifiers::NONE | KeyModifiers::SHIFT) => { Some(c) } KeyCode::Backspace if modifiers == KeyModifiers::NONE => Some('\x08'), KeyCode::Delete if modifiers == KeyModifiers::NONE => Some('\x7F'), KeyCode::Enter if modifiers == KeyModifiers::NONE => Some('\n'), _ => None, }, _ => None, } } pub fn to_pane_move(&self) -> Option { match &self.0 { TerminalEvent::Key(KeyEvent { code, modifiers: KeyModifiers::ALT, kind: KeyEventKind::Press | KeyEventKind::Repeat, .. }) => match code { KeyCode::Char('a') => Some(Dir::Left), KeyCode::Char('d') => Some(Dir::Right), KeyCode::Char('w') => Some(Dir::Up), KeyCode::Char('s') => Some(Dir::Down), _ => None, }, _ => None, } } pub fn to_pane_open(&self) -> Option { match &self.0 { TerminalEvent::Key(KeyEvent { code, modifiers: ALT_SHIFT, kind: KeyEventKind::Press | KeyEventKind::Repeat, .. }) => match code { KeyCode::Char('A') => Some(Dir::Left), KeyCode::Char('D') => Some(Dir::Right), KeyCode::Char('W') => Some(Dir::Up), KeyCode::Char('S') => Some(Dir::Down), _ => None, }, _ => None, } } pub fn to_pane_close(&self) -> Option { if matches!( &self.0, TerminalEvent::Key(KeyEvent { code: KeyCode::Char('q'), modifiers: KeyModifiers::ALT, kind: KeyEventKind::Press, .. }) ) { Some(Action::PaneClose) } else { None } } pub fn to_move(&self) -> Option { let TerminalEvent::Key(KeyEvent { code, modifiers, kind: KeyEventKind::Press | KeyEventKind::Repeat, .. }) = &self.0 else { return None; }; let retain_base = modifiers.contains(KeyModifiers::SHIFT); let word = modifiers.contains(KeyModifiers::CONTROL); let (dir, page) = match code { KeyCode::PageUp => (Dir::Up, true), KeyCode::PageDown => (Dir::Down, true), KeyCode::Left => (Dir::Left, false), KeyCode::Right => (Dir::Right, false), KeyCode::Up => (Dir::Up, false), KeyCode::Down => (Dir::Down, false), _ => return None, }; Some(Action::Move(dir, page, retain_base, word)) } pub fn to_select_token(&self) -> Option { if matches!( &self.0, TerminalEvent::Key(KeyEvent { code: KeyCode::Char(' '), modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, .. }) ) { Some(Action::SelectToken) } else { None } } pub fn to_indent(&self) -> Option { if let TerminalEvent::Key(KeyEvent { code: c @ (KeyCode::Tab | KeyCode::BackTab), modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, kind: KeyEventKind::Press, .. }) = &self.0 { Some(Action::Indent(*c == KeyCode::Tab)) } else { None } } pub fn to_open_prompt(&self) -> Option { if matches!( &self.0, TerminalEvent::Key(KeyEvent { code: KeyCode::Enter, modifiers: KeyModifiers::ALT, kind: KeyEventKind::Press, .. }) ) { Some(Action::OpenPrompt) } else { None } } pub fn to_open_switcher(&self) -> Option { if matches!( &self.0, TerminalEvent::Key(KeyEvent { code: KeyCode::Char('b'), modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, .. }) ) { Some(Action::OpenSwitcher) } else { None } } pub fn to_open_opener(&self, path: PathBuf) -> Option { if matches!( &self.0, TerminalEvent::Key(KeyEvent { code: KeyCode::Char('o'), modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, .. }) ) { Some(Action::OpenOpener(path)) } else { None } } pub fn to_open_finder(&self, selection: Option) -> Option { if matches!( &self.0, TerminalEvent::Key(KeyEvent { code: KeyCode::Char('f'), modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, .. }) ) { Some(Action::OpenFinder(selection)) } else { None } } pub fn to_command_start(&self) -> Option { if matches!( &self.0, TerminalEvent::Key(KeyEvent { code: KeyCode::Char('l'), modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, .. }) ) { Some(Action::CommandStart("goto_line")) } else { None } } pub fn to_go(&self) -> Option { if matches!( &self.0, TerminalEvent::Key(KeyEvent { code: KeyCode::Enter, modifiers: KeyModifiers::NONE, kind: KeyEventKind::Press, .. }) ) { Some(Action::Go) } else { None } } pub fn to_yes(&self) -> Option { if matches!(self.to_char(), Some('y' | 'Y')) { Some(Action::Yes) } else { None } } pub fn to_cancel(&self) -> Option { if matches!( &self.0, TerminalEvent::Key(KeyEvent { code: KeyCode::Esc, modifiers: KeyModifiers::NONE, kind: KeyEventKind::Press, .. }) ) { Some(Action::Cancel) } else { None } } pub fn to_continue(&self) -> Option { if matches!( &self.0, TerminalEvent::Key(KeyEvent { code: KeyCode::Esc | KeyCode::Enter | KeyCode::Char(' '), modifiers: KeyModifiers::NONE, kind: KeyEventKind::Press, .. }) ) { Some(Action::Continue) } else { None } } pub fn to_no(&self) -> Option { if matches!(self.to_char(), Some('n' | 'N')) { Some(Action::No) } else { None } } pub fn to_save(&self) -> Option { if matches!( &self.0, TerminalEvent::Key(KeyEvent { code: KeyCode::Char('s'), modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, .. }) ) { Some(Action::Save) } else { None } } }