tui_file_explorer/keymap/
config.rs1use std::{fs, io, path::PathBuf};
19
20use super::{KeyChord, Keymap};
21use crate::{
22 app::{AppCommand, KeyBindings},
23 types::ExplorerCommand,
24};
25
26#[derive(Debug, Clone, Default)]
28pub struct KeymapConfig {
29 pub explorer: Keymap,
31 pub app: KeyBindings,
33 pub warnings: Vec<String>,
35}
36
37#[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 (!out.is_empty() || matches!(value, toml::Value::Array(a) if a.is_empty())).then_some(out)
72}
73
74pub 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
124pub 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#[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;