summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorCanop <cano.petrole@gmail.com>2022-01-30 18:59:30 +0100
committerCanop <cano.petrole@gmail.com>2022-01-30 18:59:30 +0100
commit122dccd34b3a3fbd72e5ffced18e45fe621ccf83 (patch)
tree85552b3bcdce962ad5f67c1d7f06270aa120dedb /src
parent4cfb2d0e8568cb4594211f33a7fd5019fc36d88b (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.rs4
-rw-r--r--src/browser/browser_state.rs2
-rw-r--r--src/command/panel_input.rs10
-rw-r--r--src/conf/verb_conf.rs6
-rw-r--r--src/errors.rs1
-rw-r--r--src/keys.rs200
-rw-r--r--src/verb/builtin.rs2
-rw-r--r--src/verb/verb.rs4
-rw-r--r--src/verb/verb_store.rs6
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