520 lines
16 KiB
Rust
520 lines
16 KiB
Rust
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>, String), // Display an optionally titled informational text box to the user
|
|
OpenSwitcher, // Open the buffer switcher
|
|
OpenOpener(PathBuf), // Open the file opener
|
|
OpenFinder(Option<String>), // 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<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_pan(&self) -> Option<Action> {
|
|
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<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_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_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, area: Area, drag_id_counter: &mut usize) -> Option<Action> {
|
|
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
|
|
}
|
|
}
|
|
}
|