tui_pages/input/
key_sequence.rs1use crate::input::KeyChord;
2use crossterm::event::{KeyCode, KeyModifiers};
3use std::fmt;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum ParseKeyError {
10 Empty,
12 UnknownKey(String),
15}
16
17impl fmt::Display for ParseKeyError {
18 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19 match self {
20 ParseKeyError::Empty => write!(f, "empty key binding"),
21 ParseKeyError::UnknownKey(token) => write!(f, "unrecognised key token: {token:?}"),
22 }
23 }
24}
25
26impl std::error::Error for ParseKeyError {}
27
28pub fn parse_binding(binding: &str) -> Vec<KeyChord> {
34 binding
35 .split_whitespace()
36 .flat_map(parse_binding_token)
37 .collect()
38}
39
40pub fn parse_key(token: &str) -> Option<KeyChord> {
43 try_parse_key(token).ok()
44}
45
46pub fn try_parse_binding(binding: &str) -> Result<Vec<KeyChord>, ParseKeyError> {
52 let chords = binding
53 .split_whitespace()
54 .map(try_parse_binding_token)
55 .collect::<Result<Vec<_>, _>>()?;
56 let chords = chords.into_iter().flatten().collect::<Vec<_>>();
57 if chords.is_empty() {
58 return Err(ParseKeyError::Empty);
59 }
60 Ok(chords)
61}
62
63fn parse_binding_token(token: &str) -> Vec<KeyChord> {
64 try_parse_binding_token(token).unwrap_or_default()
65}
66
67fn try_parse_binding_token(token: &str) -> Result<Vec<KeyChord>, ParseKeyError> {
68 let parts = token.split('+').collect::<Vec<_>>();
69 if parts.len() <= 1 || is_modifier_sequence(&parts) {
70 return try_parse_key(token).map(|key| vec![key]);
71 }
72
73 parts
74 .into_iter()
75 .map(try_parse_key)
76 .collect::<Result<Vec<_>, _>>()
77 .map_err(|_| ParseKeyError::UnknownKey(token.to_string()))
81}
82
83fn is_modifier_sequence(parts: &[&str]) -> bool {
84 parts
85 .iter()
86 .take(parts.len().saturating_sub(1))
87 .all(|part| is_modifier(part))
88}
89
90fn is_modifier(part: &str) -> bool {
91 matches!(
92 part.to_ascii_lowercase().as_str(),
93 "ctrl" | "control" | "c" | "alt" | "meta" | "m" | "shift" | "s"
94 )
95}
96
97pub fn try_parse_key(token: &str) -> Result<KeyChord, ParseKeyError> {
100 let mut modifiers = KeyModifiers::empty();
101 let mut key = token.trim();
102 if key.is_empty() {
103 return Err(ParseKeyError::Empty);
104 }
105
106 loop {
107 let Some((prefix, rest)) = key.split_once('+') else {
108 break;
109 };
110
111 match prefix.to_ascii_lowercase().as_str() {
112 "ctrl" | "control" | "c" => modifiers |= KeyModifiers::CONTROL,
113 "alt" | "meta" | "m" => modifiers |= KeyModifiers::ALT,
114 "shift" | "s" => modifiers |= KeyModifiers::SHIFT,
115 _ => break,
118 }
119 key = rest;
120 }
121
122 let unknown = || ParseKeyError::UnknownKey(token.to_string());
123 let code = match key.to_ascii_lowercase().as_str() {
124 "enter" | "return" => KeyCode::Enter,
125 "tab" => KeyCode::Tab,
126 "backtab" => KeyCode::BackTab,
127 "esc" | "escape" => KeyCode::Esc,
128 "backspace" | "bs" => KeyCode::Backspace,
129 "space" => KeyCode::Char(' '),
130 "up" => KeyCode::Up,
131 "down" => KeyCode::Down,
132 "left" => KeyCode::Left,
133 "right" => KeyCode::Right,
134 "home" => KeyCode::Home,
135 "end" => KeyCode::End,
136 "pageup" | "page_up" => KeyCode::PageUp,
137 "pagedown" | "page_down" => KeyCode::PageDown,
138 "delete" | "del" => KeyCode::Delete,
139 "insert" | "ins" => KeyCode::Insert,
140 text if text.starts_with('f') && text.len() > 1 => {
143 let number = text[1..].parse().map_err(|_| unknown())?;
144 KeyCode::F(number)
145 }
146 text => {
147 let mut chars = text.chars();
148 let first = chars.next().ok_or_else(unknown)?;
149 if chars.next().is_some() {
150 return Err(unknown());
151 }
152 KeyCode::Char(first)
153 }
154 };
155
156 let (code, modifiers) = if code == KeyCode::Tab && modifiers.contains(KeyModifiers::SHIFT) {
157 (KeyCode::BackTab, modifiers - KeyModifiers::SHIFT)
158 } else {
159 (code, modifiers)
160 };
161
162 Ok(KeyChord::new(code, modifiers))
163}