use crate::{ state::BufferId, terminal::{Area, TerminalEvent}, }; use crossterm::event::{ KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEventKind, }; 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, Dist, bool, bool), // Move the cursor (dir, dist, retain_base, word) Pan(Dir, Dist), // Pan the view window 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, usize), // Open the file (on the given line) and switch the current pane to it CreateFile(PathBuf), // Create a new file and switch the current pane to it CommandStart(&'static str), // Start a new command GotoLine(isize), // Go to the specified file line BeginSearch(String), // Request to begin a search with the given needle OpenSearcher(PathBuf, String), // Start a project-wide search with the given location and needle SelectToken, // Fully select the token under the cursor SelectAll, // Fully select the entire input Save, // Save the current buffer Mouse(MouseAction, [isize; 2], bool, usize), // (action, pos, is_ctrl, drag_id) Undo, Redo, Copy, Cut, Paste, Duplicate, Comment, } /// How far should movement go? #[derive(Clone, Debug)] pub enum Dist { Char, Page, Doc, } #[derive(Clone, Debug)] pub enum MouseAction { Click, Drag, ScrollDown, ScrollUp, } #[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), // A terminal bell ring Bell, } 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), Self::Bell => None, } } } 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 (dir, dist, retain_base, word) = match &self.0 { // TerminalEvent::Mouse(ev) => match ev.kind { // MouseEventKind::ScrollUp => (Dir::Up, Dist::Char, false, false), // MouseEventKind::ScrollDown => (Dir::Down, Dist::Char, false, false), // _ => return None, // }, TerminalEvent::Key(KeyEvent { code, modifiers, kind: KeyEventKind::Press | KeyEventKind::Repeat, .. }) => { let retain_base = modifiers.contains(KeyModifiers::SHIFT); let word = modifiers.contains(KeyModifiers::CONTROL); match code { KeyCode::Home => (Dir::Up, Dist::Doc, retain_base, word), KeyCode::End => (Dir::Down, Dist::Doc, retain_base, word), KeyCode::PageUp => (Dir::Up, Dist::Page, retain_base, word), KeyCode::PageDown => (Dir::Down, Dist::Page, retain_base, word), KeyCode::Left => (Dir::Left, Dist::Char, retain_base, word), KeyCode::Right => (Dir::Right, Dist::Char, retain_base, word), KeyCode::Up => (Dir::Up, Dist::Char, retain_base, word), KeyCode::Down => (Dir::Down, Dist::Char, retain_base, word), _ => return None, } } _ => return None, }; Some(Action::Move(dir, dist, retain_base, word)) } pub fn to_pan(&self) -> Option { let (dir, dist) = match &self.0 { TerminalEvent::Mouse(ev) => match ev.kind { MouseEventKind::ScrollUp => (Dir::Up, Dist::Char), MouseEventKind::ScrollDown => (Dir::Down, Dist::Char), _ => return None, }, _ => return None, }; Some(Action::Pan(dir, dist)) } 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_select_all(&self) -> Option { if matches!( &self.0, TerminalEvent::Key(KeyEvent { code: KeyCode::Char('a'), modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, .. }) ) { Some(Action::SelectAll) } 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.clone())) } else { None } } pub fn to_open_finder(&self, query: Option) -> Option { if matches!( &self.0, TerminalEvent::Key(KeyEvent { code: KeyCode::Char('f'), modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, .. }) ) { Some(Action::OpenFinder(query)) } 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 if matches!( &self.0, TerminalEvent::Key(KeyEvent { code: KeyCode::Char('f'), modifiers, kind: KeyEventKind::Press, .. }) if *modifiers == KeyModifiers::CONTROL | KeyModifiers::SHIFT ) { Some(Action::CommandStart("search")) } 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 } } pub fn to_edit(&self) -> Option { match &self.0 { TerminalEvent::Key(KeyEvent { code: KeyCode::Char('z'), modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, .. }) => Some(Action::Undo), TerminalEvent::Key(KeyEvent { code: KeyCode::Char('y'), modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, .. }) => Some(Action::Redo), TerminalEvent::Key(KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, .. }) => Some(Action::Copy), TerminalEvent::Key(KeyEvent { code: KeyCode::Char('x'), modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, .. }) => Some(Action::Cut), TerminalEvent::Key(KeyEvent { code: KeyCode::Char('v'), modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, .. }) => Some(Action::Paste), TerminalEvent::Key(KeyEvent { code: KeyCode::Char('d'), modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, .. }) => Some(Action::Duplicate), TerminalEvent::Key(KeyEvent { code: KeyCode::Char('7'), // ????? modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, .. }) => Some(Action::Comment), _ => None, } } pub fn to_mouse(&self, area: Area, drag_id_counter: &mut usize) -> Option { let TerminalEvent::Mouse(ev) = self.0 else { return None; }; if let Some(pos) = area.contains([ev.column as isize, ev.row as isize]) { let action = match ev.kind { MouseEventKind::ScrollUp => MouseAction::ScrollUp, MouseEventKind::ScrollDown => MouseAction::ScrollDown, MouseEventKind::Down(MouseButton::Left) => { *drag_id_counter += 1; MouseAction::Click } MouseEventKind::Drag(MouseButton::Left) => MouseAction::Drag, _ => return None, }; let is_ctrl = ev.modifiers == KeyModifiers::CONTROL; Some(Action::Mouse(action, pos, is_ctrl, *drag_id_counter)) } else { None } } }