diff options
author | Canop <cano.petrole@gmail.com> | 2022-01-30 18:59:30 +0100 |
---|---|---|
committer | Canop <cano.petrole@gmail.com> | 2022-01-30 18:59:30 +0100 |
commit | 122dccd34b3a3fbd72e5ffced18e45fe621ccf83 (patch) | |
tree | 85552b3bcdce962ad5f67c1d7f06270aa120dedb /src | |
parent | 4cfb2d0e8568cb4594211f33a7fd5019fc36d88b (diff) |
export key event parsing and formatting in crokey
This keys.rs file was copied into so many of my TUI applications,
I had to make it a distinct crate.
Diffstat (limited to 'src')
-rw-r--r-- | src/app/panel.rs | 4 | ||||
-rw-r--r-- | src/browser/browser_state.rs | 2 | ||||
-rw-r--r-- | src/command/panel_input.rs | 10 | ||||
-rw-r--r-- | src/conf/verb_conf.rs | 6 | ||||
-rw-r--r-- | src/errors.rs | 1 | ||||
-rw-r--r-- | src/keys.rs | 200 | ||||
-rw-r--r-- | src/verb/builtin.rs | 2 | ||||
-rw-r--r-- | src/verb/verb.rs | 4 | ||||
-rw-r--r-- | src/verb/verb_store.rs | 6 |
9 files changed, 24 insertions, 211 deletions
diff --git a/src/app/panel.rs b/src/app/panel.rs index 24ee154..0411e49 100644 --- a/src/app/panel.rs +++ b/src/app/panel.rs @@ -11,7 +11,7 @@ use { flags_display, }, errors::ProgramError, - keys, + keys::KEY_FORMAT, skin::PanelSkin, stage::*, task_sync::Dam, @@ -255,7 +255,7 @@ impl Panel { _ => false, }) .filter_map(|v| v.keys.first()) - .map(|&k| keys::key_event_desc(k)) + .map(|&k| KEY_FORMAT.to_string(k)) .next() .unwrap_or_else(|| ":start_end_panel".to_string()); diff --git a/src/browser/browser_state.rs b/src/browser/browser_state.rs index 6112e46..956e91e 100644 --- a/src/browser/browser_state.rs +++ b/src/browser/browser_state.rs @@ -500,7 +500,7 @@ impl PanelState for BrowserState { Internal::total_search => { if let Some(tree) = &self.filtered_tree { if tree.total_search { - CmdResult::error("search was already total - all children have been rated") + CmdResult::error("search was already total: all possible matches have been ranked") } else { self.pending_pattern = tree.options.pattern.clone(); self.total_search_required = true; diff --git a/src/command/panel_input.rs b/src/command/panel_input.rs index 879b7fb..7f097c0 100644 --- a/src/command/panel_input.rs +++ b/src/command/panel_input.rs @@ -163,7 +163,7 @@ impl PanelInput { key: KeyEvent, parts: &CommandParts, ) { - if let Some(c) = keys::as_letter(key) { + if let Some(c) = crokey::as_letter(key) { let add = match c { // '/' if !parts.raw_pattern.is_empty() => true, ' ' if parts.verb_invocation.is_none() => true, @@ -224,7 +224,7 @@ impl PanelInput { // we first handle the cases that MUST absolutely // not be overriden by configuration - if key == keys::ESC { + if key == crokey::ESC { // tab cycling self.tab_cycle_count = 0; if let Some(raw) = self.input_before_cycle.take() { @@ -250,7 +250,7 @@ impl PanelInput { } // tab completion - if key == keys::TAB { + if key == crokey::TAB { if parts.verb_invocation.is_some() { let parts_before_cycle; let completable_parts = if let Some(s) = &self.input_before_cycle { @@ -298,11 +298,11 @@ impl PanelInput { self.input_before_cycle = None; } - if key == keys::ENTER && parts.has_not_empty_verb_invocation() { + if key == crokey::ENTER && parts.has_not_empty_verb_invocation() { return Command::from_parts(parts, true); } - if key == keys::QUESTION && (raw.is_empty() || parts.verb_invocation.is_some()) { + if key == crokey::QUESTION && (raw.is_empty() || parts.verb_invocation.is_some()) { // a '?' opens the help when it's the first char // or when it's part of the verb invocation return Command::Internal { diff --git a/src/conf/verb_conf.rs b/src/conf/verb_conf.rs index cf4b1dc..c4c79c5 100644 --- a/src/conf/verb_conf.rs +++ b/src/conf/verb_conf.rs @@ -1,9 +1,9 @@ use { crate::{ app::SelectionType, - keys, command::Sequence, errors::ConfError, + keys, verb::*, }, serde::Deserialize, @@ -132,10 +132,10 @@ impl VerbConf { } let mut checked_keys = Vec::new(); for key in &unchecked_keys { - let key = keys::parse_key(key)?; + let key = crokey::parse(key)?; if keys::is_reserved(key) { return Err(ConfError::ReservedKey { - key: keys::key_event_desc(key), + key: keys::KEY_FORMAT.to_string(key) }); } checked_keys.push(key); diff --git a/src/errors.rs b/src/errors.rs index 749889d..78e2124 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -49,6 +49,7 @@ custom_error! {pub ConfError UnknownInternal {verb: String} = "not a known internal: {}", InvalidSearchMode {details: String} = "invalid search mode: {}", InvalidKey {raw: String} = "not a valid key: {}", + ParseKey {source: crokey::ParseKeyError} = "{}", ReservedKey {key: String} = "reserved key: {}", UnexpectedInternalArg {invocation: String} = "unexpected argument for internal: {}", InvalidCols {details: String} = "invalid cols definition: {}", diff --git a/src/keys.rs b/src/keys.rs index 8868733..4583ac4 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -1,85 +1,19 @@ -//! parsing keys from strings, and describing keys in strings - use { crate::{ app::Mode, - errors::ConfError, }, + crokey::*, crossterm::event::{ - KeyCode::{self, *}, + KeyCode, KeyEvent, KeyModifiers, }, + once_cell::sync::Lazy, }; -macro_rules! const_key { - ($name:ident, $code:expr) => { - pub const $name: KeyEvent = KeyEvent { - code: $code, - modifiers: KeyModifiers::empty(), - }; - }; - ($name:ident, $code:expr, $mod:expr) => { - pub const $name: KeyEvent = KeyEvent { - code: $code, - modifiers: $mod, - }; - }; -} - -// we define a few constants which make it easier to check key events -const_key!(ALT_ENTER, Enter, KeyModifiers::ALT); -const_key!(ENTER, Enter); -const_key!(BACKSPACE, Backspace); -const_key!(BACK_TAB, BackTab, KeyModifiers::SHIFT); // backtab needs shift -const_key!(DELETE, Delete); -const_key!(DOWN, Down); -const_key!(PAGE_DOWN, PageDown); -const_key!(END, End); -const_key!(ESC, Esc); -const_key!(HOME, Home); -const_key!(LEFT, Left); -const_key!(QUESTION, Char('?')); -const_key!(RIGHT, Right); -const_key!(SPACE, Char(' ')); -const_key!(TAB, Tab); -const_key!(UP, Up); -const_key!(PAGE_UP, PageUp); -const_key!(F1, F(1)); -const_key!(F2, F(2)); -const_key!(F3, F(3)); -const_key!(F4, F(4)); -const_key!(F5, F(5)); -const_key!(F6, F(6)); - -/// build a human description of a key event -pub fn key_event_desc(key: KeyEvent) -> String { - let mut s = String::new(); - if key.modifiers.contains(KeyModifiers::CONTROL) { - s.push_str("ctrl-"); - } - if key.modifiers.contains(KeyModifiers::ALT) { - s.push_str("alt-"); - } - if key.modifiers.contains(KeyModifiers::SHIFT) { - s.push_str("shift-"); - } - match key.code { - Char('\r') | Char('\n') | Enter => { - s.push_str("enter"); - } - Char(c) => { - s.push(c); - } - F(u) => { - s.push_str(&format!("F{u}")); - } - _ => { - s.push_str(&format!("{:?}", key.code)); // FIXME check - } - } - s -} +pub static KEY_FORMAT: Lazy<KeyEventFormat> = Lazy::new(|| { + KeyEventFormat::default().with_lowercase_modifiers() +}); pub fn is_reserved(key: KeyEvent) -> bool { key == BACKSPACE || key == DELETE || key == ESC @@ -103,125 +37,3 @@ pub fn is_key_allowed_for_verb( } } -/// return the raw char if the event is a letter event -pub fn as_letter(key: KeyEvent) -> Option<char> { - match key { - KeyEvent { code: KeyCode::Char(l), modifiers: KeyModifiers::NONE } => Some(l), - _ => None, - } -} - -/// parse a string as a keyboard key definition. -/// -/// About the case: -/// The char we receive as code from crossterm is usually lowercase -/// but uppercase when it was typed with shift (i.e. we receive -/// "g" for a lowercase, and "shift-G" for an uppercase) -pub fn parse_key(raw: &str) -> Result<KeyEvent, ConfError> { - fn bad_key(raw: &str) -> Result<KeyEvent, ConfError> { - Err(ConfError::InvalidKey { - raw: raw.to_owned(), - }) - } - let tokens: Vec<&str> = raw.split('-').collect(); - let last = tokens[tokens.len() - 1].to_ascii_lowercase(); - let mut code = match last.as_ref() { - "esc" => Esc, - "enter" => Enter, - "left" => Left, - "right" => Right, - "up" => Up, - "down" => Down, - "home" => Home, - "end" => End, - "pageup" => PageUp, - "pagedown" => PageDown, - "backtab" => BackTab, - "backspace" => Backspace, - "del" => Delete, - "delete" => Delete, - "insert" => Insert, - "ins" => Insert, - "f1" => F(1), - "f2" => F(2), - "f3" => F(3), - "f4" => F(4), - "f5" => F(5), - "f6" => F(6), - "f7" => F(7), - "f8" => F(8), - "f9" => F(9), - "f10" => F(10), - "f11" => F(11), - "f12" => F(12), - "space" => Char(' '), - "tab" => Tab, - c if c.len() == 1 => Char(c.chars().next().unwrap()), - _ => { - return bad_key(raw); - } - }; - let mut modifiers = KeyModifiers::empty(); - if code == BackTab { - // Crossterm always sends the shift key with - // backtab - modifiers.insert(KeyModifiers::SHIFT); - } - for token in tokens.iter().take(tokens.len() - 1) { - match token.to_ascii_lowercase().as_ref() { - "ctrl" => { - modifiers.insert(KeyModifiers::CONTROL); - } - "alt" => { - modifiers.insert(KeyModifiers::ALT); - } - "shift" => { - modifiers.insert(KeyModifiers::SHIFT); - if let Char(c) = code { - if c.is_ascii_lowercase() { - code = Char(c.to_ascii_uppercase()); - } - } - } - _ => { - return bad_key(raw); - } - } - } - Ok(KeyEvent { code, modifiers }) -} -#[cfg(test)] -mod key_parsing_tests { - - use { - crate::keys::*, - crossterm::event::KeyEvent, - }; - - #[test] - fn check_key_description() { - assert_eq!(key_event_desc(ALT_ENTER), "alt-enter"); - } - - fn check_ok(raw: &str, key: KeyEvent) { - let parsed = parse_key(raw); - assert!(parsed.is_ok(), "failed to parse {:?} as key", raw); - assert_eq!(parsed.unwrap(), key); - } - - #[test] - fn check_key_parsing() { - check_ok("left", LEFT); - check_ok("RIGHT", RIGHT); - check_ok("Home", HOME); - check_ok("f1", KeyEvent::from(F(1))); - check_ok("F2", KeyEvent::from(F(2))); - check_ok("Enter", KeyEvent::from(Enter)); - check_ok("alt-enter", KeyEvent::new(Enter, KeyModifiers::ALT)); - check_ok("insert", KeyEvent::from(Insert)); - check_ok("ctrl-q", KeyEvent::new(Char('q'), KeyModifiers::CONTROL)); - check_ok("shift-q", KeyEvent::new(Char('Q'), KeyModifiers::SHIFT)); - check_ok("ctrl-Q", KeyEvent::new(Char('q'), KeyModifiers::CONTROL)); - check_ok("shift-Q", KeyEvent::new(Char('Q'), KeyModifiers::SHIFT)); - } -} diff --git a/src/verb/builtin.rs b/src/verb/builtin.rs index b8da1de..be306a6 100644 --- a/src/verb/builtin.rs +++ b/src/verb/builtin.rs @@ -2,8 +2,8 @@ use { super::*, crate::{ app::SelectionType, - keys::*, }, + crokey::*, crossterm::event::{KeyCode, KeyEvent, KeyModifiers}, }; diff --git a/src/verb/verb.rs b/src/verb/verb.rs index 336de58..f0e4f33 100644 --- a/src/verb/verb.rs +++ b/src/verb/verb.rs @@ -3,7 +3,7 @@ use { crate::{ app::*, errors::ConfError, - keys, + keys::KEY_FORMAT, path::{self, PathAnchor}, }, crossterm::event::{KeyCode, KeyEvent, KeyModifiers}, @@ -111,7 +111,7 @@ impl Verb { self.keys_desc = self .keys .iter() - .map(|&k| keys::key_event_desc(k)) + .map(|&k| KEY_FORMAT.to_string(k)) .collect::<Vec<String>>() // no way to join an iterator today ? .join(", "); } diff --git a/src/verb/verb_store.rs b/src/verb/verb_store.rs index b7db794..534ff53 100644 --- a/src/verb/verb_store.rs +++ b/src/verb/verb_store.rs @@ -8,7 +8,7 @@ use { app::*, conf::Conf, errors::ConfError, - keys, + keys::KEY_FORMAT, }, crossterm::event::KeyEvent, }; @@ -128,7 +128,7 @@ impl VerbStore { ) -> Option<String> { for verb in &self.verbs { if verb.get_internal() == Some(internal) && stype.respects(verb.selection_condition) { - return verb.keys.get(0).map(|&k| keys::key_event_desc(k)); + return verb.keys.get(0).map(|&k| KEY_FORMAT.to_string(k)); } } None @@ -140,7 +140,7 @@ impl VerbStore { ) -> Option<String> { for verb in &self.verbs { if verb.get_internal() == Some(internal) { - return verb.keys.get(0).map(|&k| keys::key_event_desc(k)); + return verb.keys.get(0).map(|&k| KEY_FORMAT.to_string(k)); } } None |