zte/src/action.rs

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
}
}
}