use std::{fs, io, path::PathBuf};
use super::{KeyChord, Keymap};
use crate::{
app::{AppCommand, KeyBindings},
types::ExplorerCommand,
};
#[derive(Debug, Clone, Default)]
pub struct KeymapConfig {
pub explorer: Keymap,
pub app: KeyBindings,
pub warnings: Vec<String>,
}
#[must_use]
pub fn keymap_path() -> Option<PathBuf> {
crate::persistence::config_dir().map(|d| d.join("keymap.toml"))
}
fn chords(value: &toml::Value, command: &str, warnings: &mut Vec<String>) -> Option<Vec<KeyChord>> {
let texts: Vec<&str> = match value {
toml::Value::String(s) => vec![s.as_str()],
toml::Value::Array(items) => {
let mut v = Vec::new();
for item in items {
if let Some(s) = item.as_str() {
v.push(s);
} else {
warnings.push(format!("{command}: keys must be strings"));
return None;
}
}
v
}
_ => {
warnings.push(format!("{command}: expected a key or an array of keys"));
return None;
}
};
let mut out = Vec::new();
for t in texts {
match KeyChord::parse(t) {
Ok(c) => out.push(c),
Err(e) => warnings.push(format!("{command}: {e}")),
}
}
(!out.is_empty() || matches!(value, toml::Value::Array(a) if a.is_empty())).then_some(out)
}
pub fn parse_keymap_config(text: &str) -> Result<KeymapConfig, String> {
let root: toml::Table = text.parse().map_err(|e: toml::de::Error| e.to_string())?;
let mut cfg = KeymapConfig {
explorer: Keymap::default(),
..KeymapConfig::default()
};
for (section, value) in &root {
let Some(table) = value.as_table() else {
cfg.warnings.push(format!("'{section}' should be a table"));
continue;
};
match section.as_str() {
"explorer" => {
for (name, v) in table {
let Some(command) = ExplorerCommand::from_name(name) else {
cfg.warnings
.push(format!("[explorer] unknown command '{name}'"));
continue;
};
if let Some(keys) = chords(v, &format!("[explorer] {name}"), &mut cfg.warnings)
{
cfg.explorer =
std::mem::take(&mut cfg.explorer).set_command(command, &keys);
}
}
}
"app" => {
for (name, v) in table {
let Some(command) = AppCommand::from_name(name) else {
cfg.warnings.push(format!("[app] unknown command '{name}'"));
continue;
};
if let Some(keys) = chords(v, &format!("[app] {name}"), &mut cfg.warnings) {
cfg.app = std::mem::take(&mut cfg.app).remap(command, &keys);
}
}
}
other => cfg.warnings.push(format!("unknown section [{other}]")),
}
}
Ok(cfg)
}
pub fn load_keymap_config() -> io::Result<Option<KeymapConfig>> {
let Some(path) = keymap_path() else {
return Ok(None);
};
let text = match fs::read_to_string(&path) {
Ok(t) => t,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(e),
};
parse_keymap_config(&text).map(Some).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("{}: {e}", path.display()),
)
})
}
#[must_use]
pub fn default_keymap_toml() -> String {
let mut out = String::from(
"# tfe key bindings — save as keymap.toml in the config directory.\n\
# Each command takes one key or an array of keys; the FIRST is canonical.\n\
# Keys: single chars (k, G, /), esc enter tab backtab backspace delete\n\
# up down left right home end pageup pagedown space f1..f24,\n\
# with modifiers: ctrl+t, alt+enter, shift+tab.\n\
# A command listed here REPLACES all its default keys.\n\n\
[explorer]\n",
);
let map = Keymap::default();
for &command in ExplorerCommand::ALL {
let keys: Vec<String> = map
.chords_for(command)
.map(|c| format!("\"{c}\""))
.collect();
out.push_str(&format!("{} = [{}]\n", command.name(), keys.join(", ")));
}
out.push_str("\n[app]\n");
for &command in AppCommand::ALL {
let ev = command.key_event();
let chord =
if ev.modifiers.is_empty() || ev.modifiers == crossterm::event::KeyModifiers::SHIFT {
KeyChord::plain(ev.code)
} else {
KeyChord::exact(ev.code, ev.modifiers)
};
out.push_str(&format!("{} = [\"{chord}\"]\n", command.name()));
}
out
}
#[cfg(test)]
mod tests;