Skip to main content

tui_file_explorer/
keymap.rs

1//! Key chords and the explorer keymap.
2//!
3//! A [`Keymap`] maps [`KeyChord`]s to [`ExplorerCommand`]s. The default map
4//! reproduces the built-in bindings; hosts (and the `tfe` binary, via
5//! `keymap.toml`) can replace the keys of any command.
6//!
7//! Chord syntax (used in config files): `k`, `G`, `ctrl+t`, `shift+tab`,
8//! `alt+enter`, `esc`, `space`, `pageup`, `f5`, … — see [`KeyChord::parse`].
9
10use std::{fmt, sync::OnceLock};
11
12use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
13
14use crate::types::ExplorerCommand;
15
16// ── KeyChord ──────────────────────────────────────────────────────────────────
17
18/// How strictly a chord compares the modifier keys of an incoming event.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum ChordMods {
21    /// Modifiers are ignored entirely (`Up`, vim `k`, …).
22    Any,
23    /// No Ctrl/Alt; Shift is tolerated only on uppercase letters (terminals
24    /// speaking the kitty keyboard protocol report it there).
25    Plain,
26    /// Exactly these modifiers.
27    Exact(KeyModifiers),
28}
29
30/// One physical key plus a modifier rule.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub struct KeyChord {
33    /// The key.
34    pub code: KeyCode,
35    /// Which modifiers must be held with it.
36    pub mods: ChordMods,
37}
38
39impl KeyChord {
40    /// Plain key, modifiers ignored.
41    #[must_use]
42    pub const fn any(code: KeyCode) -> Self {
43        Self {
44            code,
45            mods: ChordMods::Any,
46        }
47    }
48
49    /// Plain key without Ctrl/Alt.
50    #[must_use]
51    pub const fn plain(code: KeyCode) -> Self {
52        Self {
53            code,
54            mods: ChordMods::Plain,
55        }
56    }
57
58    /// Key with exactly `mods`.
59    #[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    /// Does `key` trigger this chord?
68    #[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                // Terminals differ on whether uppercase letters carry SHIFT.
82                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    /// The event this chord would be triggered by (for synthesising input).
95    #[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    /// Parse a chord such as `"ctrl+t"` or `"G"`.
108    ///
109    /// # Errors
110    ///
111    /// Returns a [`KeymapError`] for an empty string, an unknown modifier or
112    /// an unknown key name.
113    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        // A lone "+" is the plus key itself.
119        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        // shift+g and G are the same chord.
145        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        // Uppercase letters already imply shift; don't print it twice.
205        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/// A key string that could not be understood.
234#[derive(Debug, Clone, PartialEq, Eq)]
235pub struct KeymapError {
236    /// The key string as written.
237    pub input: String,
238    /// Why it was rejected.
239    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// ── Keymap ────────────────────────────────────────────────────────────────────
260
261/// Key → [`ExplorerCommand`] bindings.
262///
263/// The **first** chord bound to a command is its canonical key — the one
264/// [`FileExplorer::handle_command`](crate::FileExplorer::handle_command)
265/// synthesises. Later chords are aliases.
266#[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                // Left / Backspace / h all ascend to the parent directory.
292                (any(KeyCode::Left), C::Ascend),
293                (any(KeyCode::Backspace), C::Ascend),
294                (any(ch('h')), C::Ascend),
295                // Right is pure navigation (never exits); Enter / l confirm.
296                (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    /// The built-in bindings (shared, allocation-free after first use).
315    #[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    /// A map with no bindings at all.
322    #[must_use]
323    pub fn empty() -> Self {
324        Self {
325            bindings: Vec::new(),
326        }
327    }
328
329    /// The command triggered by `key`, if any. First match wins.
330    #[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    /// Every chord bound to `command`, canonical key first.
339    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    /// The canonical key event for `command` (`KeyCode::Null` if unbound).
347    #[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    /// Bind `chord` to `command`, stealing it from whatever used it before.
356    #[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    /// Replace **all** keys of `command` with `chords` (first = canonical).
364    ///
365    /// Each chord is also removed from any other command it was bound to, so
366    /// the last assignment wins.
367    #[must_use]
368    pub fn set_command(mut self, command: ExplorerCommand, chords: &[KeyChord]) -> Self {
369        // Keep the position of the old canonical binding so the table stays
370        // readable; simply drop old bindings and append the new ones.
371        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    /// Remove every binding of `command`.
378    #[must_use]
379    pub fn unbind_command(mut self, command: ExplorerCommand) -> Self {
380        self.bindings.retain(|&(_, c)| c != command);
381        self
382    }
383
384    /// All bindings in resolution order.
385    #[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;