1use std::{fmt, sync::OnceLock};
11
12use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
13
14use crate::types::ExplorerCommand;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum ChordMods {
21 Any,
23 Plain,
26 Exact(KeyModifiers),
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub struct KeyChord {
33 pub code: KeyCode,
35 pub mods: ChordMods,
37}
38
39impl KeyChord {
40 #[must_use]
42 pub const fn any(code: KeyCode) -> Self {
43 Self {
44 code,
45 mods: ChordMods::Any,
46 }
47 }
48
49 #[must_use]
51 pub const fn plain(code: KeyCode) -> Self {
52 Self {
53 code,
54 mods: ChordMods::Plain,
55 }
56 }
57
58 #[must_use]
60 pub const fn exact(code: KeyCode, mods: KeyModifiers) -> Self {
61 Self {
62 code,
63 mods: ChordMods::Exact(mods),
64 }
65 }
66
67 #[must_use]
69 pub fn matches(&self, key: KeyEvent) -> bool {
70 if self.code != key.code {
71 return false;
72 }
73 match self.mods {
74 ChordMods::Any => true,
75 ChordMods::Plain => {
76 key.modifiers.is_empty()
77 || (key.modifiers == KeyModifiers::SHIFT
78 && matches!(key.code, KeyCode::Char(c) if c.is_uppercase()))
79 }
80 ChordMods::Exact(m) => {
81 let strip = |m: KeyModifiers| {
83 if matches!(self.code, KeyCode::Char(c) if c.is_uppercase()) {
84 m - KeyModifiers::SHIFT
85 } else {
86 m
87 }
88 };
89 strip(m) == strip(key.modifiers)
90 }
91 }
92 }
93
94 #[must_use]
96 pub fn to_event(&self) -> KeyEvent {
97 let mods = match self.mods {
98 ChordMods::Exact(m) => m,
99 _ => match self.code {
100 KeyCode::Char(c) if c.is_uppercase() => KeyModifiers::SHIFT,
101 _ => KeyModifiers::NONE,
102 },
103 };
104 KeyEvent::new(self.code, mods)
105 }
106
107 pub fn parse(text: &str) -> Result<Self, KeymapError> {
114 let trimmed = text.trim();
115 if trimmed.is_empty() {
116 return Err(KeymapError::new(text, "empty key"));
117 }
118 let parts: Vec<&str> = if trimmed == "+" {
120 vec!["+"]
121 } else {
122 trimmed.split('+').collect()
123 };
124 let (key_part, mod_parts) = parts.split_last().expect("split yields one part");
125
126 let mut mods = KeyModifiers::NONE;
127 for m in mod_parts {
128 mods |= match m.trim().to_ascii_lowercase().as_str() {
129 "ctrl" | "control" => KeyModifiers::CONTROL,
130 "alt" | "meta" | "opt" | "option" => KeyModifiers::ALT,
131 "shift" => KeyModifiers::SHIFT,
132 other => {
133 return Err(KeymapError::new(
134 text,
135 format!("unknown modifier '{other}'"),
136 ));
137 }
138 };
139 }
140
141 let key_part = key_part.trim();
142 let mut code = parse_key_name(key_part)
143 .ok_or_else(|| KeymapError::new(text, format!("unknown key '{key_part}'")))?;
144 if mods.contains(KeyModifiers::SHIFT)
146 && let KeyCode::Char(c) = code
147 && c.is_alphabetic()
148 {
149 code = KeyCode::Char(c.to_ascii_uppercase());
150 }
151 if mods.is_empty() {
152 Ok(Self::plain(code))
153 } else {
154 Ok(Self::exact(code, mods))
155 }
156 }
157}
158
159fn parse_key_name(name: &str) -> Option<KeyCode> {
160 let lower = name.to_ascii_lowercase();
161 let code = match lower.as_str() {
162 "up" => KeyCode::Up,
163 "down" => KeyCode::Down,
164 "left" => KeyCode::Left,
165 "right" => KeyCode::Right,
166 "enter" | "return" => KeyCode::Enter,
167 "esc" | "escape" => KeyCode::Esc,
168 "tab" => KeyCode::Tab,
169 "backtab" => KeyCode::BackTab,
170 "backspace" | "bs" => KeyCode::Backspace,
171 "delete" | "del" => KeyCode::Delete,
172 "insert" | "ins" => KeyCode::Insert,
173 "home" => KeyCode::Home,
174 "end" => KeyCode::End,
175 "pageup" | "pgup" => KeyCode::PageUp,
176 "pagedown" | "pgdn" | "pgdown" => KeyCode::PageDown,
177 "space" => KeyCode::Char(' '),
178 _ => {
179 if let Some(n) = lower.strip_prefix('f').and_then(|n| n.parse::<u8>().ok()) {
180 return (1..=24).contains(&n).then_some(KeyCode::F(n));
181 }
182 let mut chars = name.chars();
183 return match (chars.next(), chars.next()) {
184 (Some(c), None) => Some(KeyCode::Char(c)),
185 _ => None,
186 };
187 }
188 };
189 Some(code)
190}
191
192impl fmt::Display for KeyChord {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 let mods = match self.mods {
195 ChordMods::Exact(m) => m,
196 _ => KeyModifiers::NONE,
197 };
198 if mods.contains(KeyModifiers::CONTROL) {
199 f.write_str("ctrl+")?;
200 }
201 if mods.contains(KeyModifiers::ALT) {
202 f.write_str("alt+")?;
203 }
204 let upper = matches!(self.code, KeyCode::Char(c) if c.is_uppercase());
206 if mods.contains(KeyModifiers::SHIFT) && !upper {
207 f.write_str("shift+")?;
208 }
209 match self.code {
210 KeyCode::Char(' ') => f.write_str("space"),
211 KeyCode::Char(c) => write!(f, "{c}"),
212 KeyCode::F(n) => write!(f, "f{n}"),
213 KeyCode::Up => f.write_str("up"),
214 KeyCode::Down => f.write_str("down"),
215 KeyCode::Left => f.write_str("left"),
216 KeyCode::Right => f.write_str("right"),
217 KeyCode::Enter => f.write_str("enter"),
218 KeyCode::Esc => f.write_str("esc"),
219 KeyCode::Tab => f.write_str("tab"),
220 KeyCode::BackTab => f.write_str("backtab"),
221 KeyCode::Backspace => f.write_str("backspace"),
222 KeyCode::Delete => f.write_str("delete"),
223 KeyCode::Insert => f.write_str("insert"),
224 KeyCode::Home => f.write_str("home"),
225 KeyCode::End => f.write_str("end"),
226 KeyCode::PageUp => f.write_str("pageup"),
227 KeyCode::PageDown => f.write_str("pagedown"),
228 other => write!(f, "{other:?}"),
229 }
230 }
231}
232
233#[derive(Debug, Clone, PartialEq, Eq)]
235pub struct KeymapError {
236 pub input: String,
238 pub reason: String,
240}
241
242impl KeymapError {
243 fn new(input: &str, reason: impl Into<String>) -> Self {
244 Self {
245 input: input.to_string(),
246 reason: reason.into(),
247 }
248 }
249}
250
251impl fmt::Display for KeymapError {
252 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253 write!(f, "invalid key '{}': {}", self.input, self.reason)
254 }
255}
256
257impl std::error::Error for KeymapError {}
258
259#[derive(Debug, Clone, PartialEq, Eq)]
267pub struct Keymap {
268 bindings: Vec<(KeyChord, ExplorerCommand)>,
269}
270
271impl Default for Keymap {
272 fn default() -> Self {
273 use ExplorerCommand as C;
274 let any = KeyChord::any;
275 let plain = KeyChord::plain;
276 let ch = KeyCode::Char;
277 Self {
278 bindings: vec![
279 (any(KeyCode::Esc), C::Dismiss),
280 (plain(ch('q')), C::Dismiss),
281 (any(KeyCode::Up), C::MoveUp),
282 (any(ch('k')), C::MoveUp),
283 (any(KeyCode::Down), C::MoveDown),
284 (any(ch('j')), C::MoveDown),
285 (any(KeyCode::PageUp), C::PageUp),
286 (any(KeyCode::PageDown), C::PageDown),
287 (any(KeyCode::Home), C::Top),
288 (any(ch('g')), C::Top),
289 (any(KeyCode::End), C::Bottom),
290 (any(ch('G')), C::Bottom),
291 (any(KeyCode::Left), C::Ascend),
293 (any(KeyCode::Backspace), C::Ascend),
294 (any(ch('h')), C::Ascend),
295 (any(KeyCode::Right), C::Navigate),
297 (any(KeyCode::Enter), C::Confirm),
298 (any(ch('l')), C::Confirm),
299 (any(ch('.')), C::ToggleHidden),
300 (plain(ch('z')), C::ToggleSizes),
301 (plain(ch('Z')), C::ToggleDirSizes),
302 (plain(ch('/')), C::Search),
303 (plain(ch('s')), C::CycleSort),
304 (any(ch(' ')), C::ToggleMark),
305 (plain(ch('n')), C::NewDirectory),
306 (plain(ch('N')), C::NewFile),
307 (plain(ch('r')), C::Rename),
308 ],
309 }
310 }
311}
312
313impl Keymap {
314 #[must_use]
316 pub fn builtin() -> &'static Keymap {
317 static DEFAULT: OnceLock<Keymap> = OnceLock::new();
318 DEFAULT.get_or_init(Keymap::default)
319 }
320
321 #[must_use]
323 pub fn empty() -> Self {
324 Self {
325 bindings: Vec::new(),
326 }
327 }
328
329 #[must_use]
331 pub fn resolve(&self, key: KeyEvent) -> Option<ExplorerCommand> {
332 self.bindings
333 .iter()
334 .find(|(chord, _)| chord.matches(key))
335 .map(|&(_, command)| command)
336 }
337
338 pub fn chords_for(&self, command: ExplorerCommand) -> impl Iterator<Item = KeyChord> + '_ {
340 self.bindings
341 .iter()
342 .filter(move |&&(_, c)| c == command)
343 .map(|&(chord, _)| chord)
344 }
345
346 #[must_use]
348 pub fn canonical_key(&self, command: ExplorerCommand) -> KeyEvent {
349 self.chords_for(command).next().map_or_else(
350 || KeyEvent::new(KeyCode::Null, KeyModifiers::NONE),
351 |c| c.to_event(),
352 )
353 }
354
355 #[must_use]
357 pub fn bind(mut self, chord: KeyChord, command: ExplorerCommand) -> Self {
358 self.bindings.retain(|(c, _)| *c != chord);
359 self.bindings.push((chord, command));
360 self
361 }
362
363 #[must_use]
368 pub fn set_command(mut self, command: ExplorerCommand, chords: &[KeyChord]) -> Self {
369 self.bindings
372 .retain(|&(c, cmd)| cmd != command && !chords.contains(&c));
373 self.bindings.extend(chords.iter().map(|&c| (c, command)));
374 self
375 }
376
377 #[must_use]
379 pub fn unbind_command(mut self, command: ExplorerCommand) -> Self {
380 self.bindings.retain(|&(_, c)| c != command);
381 self
382 }
383
384 #[must_use]
386 pub fn bindings(&self) -> &[(KeyChord, ExplorerCommand)] {
387 &self.bindings
388 }
389}
390
391#[cfg(feature = "full-app")]
392mod config;
393#[cfg(feature = "full-app")]
394pub use config::{
395 KeymapConfig, default_keymap_toml, keymap_path, load_keymap_config, parse_keymap_config,
396};
397
398#[cfg(test)]
399mod tests;