Skip to main content

tui_file_explorer/keymap/
config.rs

1//! `keymap.toml`: user-configurable key bindings.
2//!
3//! ```toml
4//! # ~/.config/tfe/keymap.toml
5//! [explorer]               # per-pane navigation; replaces ALL keys of a command
6//! move_up   = ["up", "k"]
7//! dismiss   = ["esc"]      # drop the default `q`
8//!
9//! [app]                    # app-wide commands; replaces the built-in key
10//! undo      = ["u", "ctrl+z"]
11//! cut       = ["m"]        # `x` stops cutting
12//! ```
13//!
14//! Values are one chord or an array of chords (see [`KeyChord::parse`]).
15//! Unknown commands or keys never abort startup — they are collected as
16//! [`KeymapConfig::warnings`].
17
18use std::{fs, io, path::PathBuf};
19
20use super::{KeyChord, Keymap};
21use crate::{
22    app::{AppCommand, KeyBindings},
23    types::ExplorerCommand,
24};
25
26/// Parsed `keymap.toml`.
27#[derive(Debug, Clone, Default)]
28pub struct KeymapConfig {
29    /// Explorer key bindings (defaults with the user's overrides applied).
30    pub explorer: Keymap,
31    /// App-level overrides, ready for [`App::set_key_bindings`](crate::App::set_key_bindings).
32    pub app: KeyBindings,
33    /// Human-readable problems that were skipped.
34    pub warnings: Vec<String>,
35}
36
37/// Path of the keymap file (`$XDG_CONFIG_HOME/tfe/keymap.toml`).
38#[must_use]
39pub fn keymap_path() -> Option<PathBuf> {
40    crate::persistence::config_dir().map(|d| d.join("keymap.toml"))
41}
42
43fn chords(value: &toml::Value, command: &str, warnings: &mut Vec<String>) -> Option<Vec<KeyChord>> {
44    let texts: Vec<&str> = match value {
45        toml::Value::String(s) => vec![s.as_str()],
46        toml::Value::Array(items) => {
47            let mut v = Vec::new();
48            for item in items {
49                if let Some(s) = item.as_str() {
50                    v.push(s);
51                } else {
52                    warnings.push(format!("{command}: keys must be strings"));
53                    return None;
54                }
55            }
56            v
57        }
58        _ => {
59            warnings.push(format!("{command}: expected a key or an array of keys"));
60            return None;
61        }
62    };
63    let mut out = Vec::new();
64    for t in texts {
65        match KeyChord::parse(t) {
66            Ok(c) => out.push(c),
67            Err(e) => warnings.push(format!("{command}: {e}")),
68        }
69    }
70    // A command whose every key was invalid keeps its defaults.
71    (!out.is_empty() || matches!(value, toml::Value::Array(a) if a.is_empty())).then_some(out)
72}
73
74/// Parse the text of a `keymap.toml`.
75///
76/// # Errors
77///
78/// Returns a message only for TOML *syntax* errors; semantic problems become
79/// [`KeymapConfig::warnings`].
80pub fn parse_keymap_config(text: &str) -> Result<KeymapConfig, String> {
81    let root: toml::Table = text.parse().map_err(|e: toml::de::Error| e.to_string())?;
82    let mut cfg = KeymapConfig {
83        explorer: Keymap::default(),
84        ..KeymapConfig::default()
85    };
86
87    for (section, value) in &root {
88        let Some(table) = value.as_table() else {
89            cfg.warnings.push(format!("'{section}' should be a table"));
90            continue;
91        };
92        match section.as_str() {
93            "explorer" => {
94                for (name, v) in table {
95                    let Some(command) = ExplorerCommand::from_name(name) else {
96                        cfg.warnings
97                            .push(format!("[explorer] unknown command '{name}'"));
98                        continue;
99                    };
100                    if let Some(keys) = chords(v, &format!("[explorer] {name}"), &mut cfg.warnings)
101                    {
102                        cfg.explorer =
103                            std::mem::take(&mut cfg.explorer).set_command(command, &keys);
104                    }
105                }
106            }
107            "app" => {
108                for (name, v) in table {
109                    let Some(command) = AppCommand::from_name(name) else {
110                        cfg.warnings.push(format!("[app] unknown command '{name}'"));
111                        continue;
112                    };
113                    if let Some(keys) = chords(v, &format!("[app] {name}"), &mut cfg.warnings) {
114                        cfg.app = std::mem::take(&mut cfg.app).remap(command, &keys);
115                    }
116                }
117            }
118            other => cfg.warnings.push(format!("unknown section [{other}]")),
119        }
120    }
121    Ok(cfg)
122}
123
124/// Load `keymap.toml` from the config directory.
125///
126/// `Ok(None)` when the file does not exist.
127///
128/// # Errors
129///
130/// An unreadable file or invalid TOML.
131pub fn load_keymap_config() -> io::Result<Option<KeymapConfig>> {
132    let Some(path) = keymap_path() else {
133        return Ok(None);
134    };
135    let text = match fs::read_to_string(&path) {
136        Ok(t) => t,
137        Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
138        Err(e) => return Err(e),
139    };
140    parse_keymap_config(&text).map(Some).map_err(|e| {
141        io::Error::new(
142            io::ErrorKind::InvalidData,
143            format!("{}: {e}", path.display()),
144        )
145    })
146}
147
148/// A complete, commented `keymap.toml` listing the built-in bindings —
149/// a starting point for customisation (`tfe --dump-keymap`).
150#[must_use]
151pub fn default_keymap_toml() -> String {
152    let mut out = String::from(
153        "# tfe key bindings — save as keymap.toml in the config directory.\n\
154         # Each command takes one key or an array of keys; the FIRST is canonical.\n\
155         # Keys: single chars (k, G, /), esc enter tab backtab backspace delete\n\
156         # up down left right home end pageup pagedown space f1..f24,\n\
157         # with modifiers: ctrl+t, alt+enter, shift+tab.\n\
158         # A command listed here REPLACES all its default keys.\n\n\
159         [explorer]\n",
160    );
161    let map = Keymap::default();
162    for &command in ExplorerCommand::ALL {
163        let keys: Vec<String> = map
164            .chords_for(command)
165            .map(|c| format!("\"{c}\""))
166            .collect();
167        out.push_str(&format!("{} = [{}]\n", command.name(), keys.join(", ")));
168    }
169    out.push_str("\n[app]\n");
170    for &command in AppCommand::ALL {
171        let ev = command.key_event();
172        let chord =
173            if ev.modifiers.is_empty() || ev.modifiers == crossterm::event::KeyModifiers::SHIFT {
174                KeyChord::plain(ev.code)
175            } else {
176                KeyChord::exact(ev.code, ev.modifiers)
177            };
178        out.push_str(&format!("{} = [\"{chord}\"]\n", command.name()));
179    }
180    out
181}
182
183#[cfg(test)]
184mod tests;