571 lines
17 KiB
Rust
571 lines
17 KiB
Rust
use crate::{
|
|
state::BufferId,
|
|
terminal::{Area, TerminalEvent},
|
|
};
|
|
use crossterm::event::{
|
|
KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEventKind,
|
|
};
|
|
use std::path::PathBuf;
|
|
|
|
#[derive(Copy, Clone, Debug)]
|
|
pub enum Dir {
|
|
Left,
|
|
Right,
|
|
Up,
|
|
Down,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub enum Action {
|
|
// Insert a character
|
|
Char(char),
|
|
// Indent (indent vs deindent)
|
|
Indent(bool),
|
|
// Move the cursor (dir, dist, retain_base, word)
|
|
Move(Dir, Dist, bool, bool),
|
|
// Move panes
|
|
PaneMove(Dir),
|
|
// Create a new pane
|
|
PaneOpen(Dir),
|
|
// Close the current pane
|
|
PaneClose,
|
|
// Cancels the current action
|
|
Cancel,
|
|
// Continue past an info-only element (like a help screen)
|
|
Continue,
|
|
// Search, accept, or select the current option
|
|
Go,
|
|
// A binary confirmation is answered 'yes'
|
|
Yes,
|
|
// A binary confirmation is answered 'no'
|
|
No,
|
|
// Quit the application
|
|
Quit,
|
|
// Open the command prompt
|
|
OpenPrompt,
|
|
// Display an optionally titled informational text box to the user
|
|
Show(Option<String>, String),
|
|
// Open the buffer switcher
|
|
OpenSwitcher,
|
|
// Open the file opener
|
|
OpenOpener(PathBuf),
|
|
// Open the finder, with the given default query
|
|
OpenFinder(Option<String>),
|
|
// Switch the current pane to the given buffer
|
|
SwitchBuffer(BufferId),
|
|
// Open the file (on the given line) and switch the current pane to it
|
|
OpenFile(PathBuf, Option<usize>),
|
|
// Create a new file and switch the current pane to it
|
|
CreateFile(PathBuf),
|
|
// Start a new command
|
|
CommandStart(&'static str),
|
|
// Go to the specified file line
|
|
GotoLine(isize),
|
|
// Request to begin a search with the given needle. `None` implies file path search.
|
|
BeginSearch(Option<String>),
|
|
// Start a project-wide search with the given location and needle. `None` implies file path search.
|
|
OpenSearcher(PathBuf, Option<String>),
|
|
// Fully select the token under the cursor
|
|
SelectToken,
|
|
// Fully select the entire input
|
|
SelectAll,
|
|
// Save the current buffer
|
|
Save,
|
|
// Save the current buffer, forcefully
|
|
Overwrite,
|
|
// Reload the current file from disk, losing unsaved changes
|
|
Reload,
|
|
// (action, pos, is_ctrl, drag_id)
|
|
Mouse(MouseAction, [isize; 2], bool, usize),
|
|
Confirm(String, Box<Self>),
|
|
Undo,
|
|
Redo,
|
|
Copy,
|
|
Cut,
|
|
Paste,
|
|
Duplicate,
|
|
Comment,
|
|
// Resize the current pane
|
|
PaneResize(i32),
|
|
}
|
|
|
|
/// How far should movement go?
|
|
#[derive(Clone, Debug)]
|
|
pub enum Dist {
|
|
Char,
|
|
Page,
|
|
Doc,
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug)]
|
|
pub enum MouseAction {
|
|
Click,
|
|
Drag,
|
|
Scroll(Dir),
|
|
}
|
|
|
|
#[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<Action> 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<Action>) -> Option<Action> {
|
|
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<char> {
|
|
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<Dir> {
|
|
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<Dir> {
|
|
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<Action> {
|
|
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<Action> {
|
|
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_select_token(&self) -> Option<Action> {
|
|
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<Action> {
|
|
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<Action> {
|
|
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<Action> {
|
|
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<Action> {
|
|
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<Action> {
|
|
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<String>) -> Option<Action> {
|
|
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<Action> {
|
|
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_path_search(&self) -> Option<Action> {
|
|
if matches!(
|
|
&self.0,
|
|
TerminalEvent::Key(KeyEvent {
|
|
code: KeyCode::Char('o'),
|
|
modifiers,
|
|
kind: KeyEventKind::Press,
|
|
..
|
|
}) if *modifiers == KeyModifiers::CONTROL | KeyModifiers::SHIFT
|
|
) {
|
|
Some(Action::BeginSearch(None))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
pub fn to_go(&self) -> Option<Action> {
|
|
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<Action> {
|
|
if matches!(self.to_char(), Some('y' | 'Y')) {
|
|
Some(Action::Yes)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
pub fn to_cancel(&self) -> Option<Action> {
|
|
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<Action> {
|
|
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<Action> {
|
|
if matches!(self.to_char(), Some('n' | 'N')) {
|
|
Some(Action::No)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
pub fn to_save(&self) -> Option<Action> {
|
|
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_pane_resize(&self) -> Option<Action> {
|
|
match &self.0 {
|
|
TerminalEvent::Key(KeyEvent {
|
|
code: KeyCode::Char('='),
|
|
modifiers: KeyModifiers::ALT,
|
|
kind: KeyEventKind::Press,
|
|
..
|
|
}) => Some(Action::PaneResize(1)),
|
|
TerminalEvent::Key(KeyEvent {
|
|
code: KeyCode::Char('-'),
|
|
modifiers: KeyModifiers::ALT,
|
|
kind: KeyEventKind::Press,
|
|
..
|
|
}) => Some(Action::PaneResize(-1)),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn to_edit(&self) -> Option<Action> {
|
|
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, drag_id_counter: &mut usize) -> Option<Action> {
|
|
let TerminalEvent::Mouse(ev) = self.0 else {
|
|
return None;
|
|
};
|
|
|
|
let pos = [ev.column as isize, ev.row as isize];
|
|
let action = match ev.kind {
|
|
MouseEventKind::ScrollUp => MouseAction::Scroll(Dir::Up),
|
|
MouseEventKind::ScrollDown => MouseAction::Scroll(Dir::Down),
|
|
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))
|
|
}
|
|
}
|