use std::collections::{HashMap, HashSet};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use serde::{Deserialize, Serialize};
use crate::palette::Action;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct KeyChord {
pub modifiers: KeyModifiers,
pub code: KeyCode,
}
pub fn chord_of(key: &KeyEvent) -> KeyChord {
let mut m = KeyModifiers::empty();
if key.modifiers.contains(KeyModifiers::CONTROL) { m |= KeyModifiers::CONTROL; }
if key.modifiers.contains(KeyModifiers::ALT) { m |= KeyModifiers::ALT; }
if key.modifiers.contains(KeyModifiers::SHIFT) { m |= KeyModifiers::SHIFT; }
if key.modifiers.contains(KeyModifiers::SUPER) { m |= KeyModifiers::SUPER; }
if let KeyCode::Char(c) = key.code {
if !c.is_alphabetic() {
m -= KeyModifiers::SHIFT;
}
}
KeyChord { modifiers: m, code: key.code }
}
pub fn parse_key(s: &str) -> Option<KeyChord> {
let s = s.trim();
for (pfx, m) in [
("ctrl-", KeyModifiers::CONTROL), ("c-", KeyModifiers::CONTROL),
("alt-", KeyModifiers::ALT), ("m-", KeyModifiers::ALT),
("cmd-", KeyModifiers::SUPER), ("super-", KeyModifiers::SUPER),
("shift-", KeyModifiers::SHIFT), ("s-", KeyModifiers::SHIFT),
] {
if s.len() > pfx.len() && s[..pfx.len()].eq_ignore_ascii_case(pfx) {
let inner = parse_key(&s[pfx.len()..])?;
return Some(KeyChord { modifiers: inner.modifiers | m, code: inner.code });
}
}
let code = match s.to_lowercase().as_str() {
"esc" | "escape" => KeyCode::Esc,
"space" | "spc" => KeyCode::Char(' '),
"enter" | "return" | "ret" => KeyCode::Enter,
"tab" => KeyCode::Tab,
"backspace" | "del" => KeyCode::Backspace,
"up" => KeyCode::Up, "down" => KeyCode::Down,
"left" => KeyCode::Left, "right" => KeyCode::Right,
"home" => KeyCode::Home, "end" => KeyCode::End,
"pageup" => KeyCode::PageUp,
"pagedown" => KeyCode::PageDown,
"delete" => KeyCode::Delete,
_ => {
let mut chars = s.chars();
let c = chars.next()?;
if chars.next().is_some() {
return None; }
let modifiers = if c.is_ascii_uppercase() { KeyModifiers::SHIFT } else { KeyModifiers::NONE };
return Some(KeyChord { modifiers, code: KeyCode::Char(c) });
}
};
Some(KeyChord { modifiers: KeyModifiers::NONE, code })
}
pub fn parse_sequence(s: &str) -> Option<Vec<KeyChord>> {
let seq: Vec<KeyChord> = s.split_whitespace().filter_map(parse_key).collect();
if seq.is_empty() { None } else { Some(seq) }
}
pub struct KeyBindings {
pub edit: HashMap<Vec<KeyChord>, Action>,
pub prefixes: HashSet<KeyChord>,
pub bar_open: Vec<KeyChord>,
}
impl KeyBindings {
pub fn is_prefix(&self, c: &KeyChord) -> bool {
self.prefixes.contains(c)
}
pub fn lookup(&self, seq: &[KeyChord]) -> Option<Action> {
self.edit.get(seq).cloned()
}
pub fn binding_for(&self, action: &Action) -> Option<String> {
self.edit
.iter()
.filter(|(_, a)| *a == action)
.map(|(seq, _)| (seq.len(), render_chords(seq)))
.min() .map(|(_, s)| s)
}
pub fn continuations(&self, prefix: &[KeyChord]) -> Vec<(String, Action)> {
let mut out: Vec<(String, Action)> = self
.edit
.iter()
.filter(|(seq, _)| seq.len() > prefix.len() && seq.starts_with(prefix))
.map(|(seq, a)| (render_chords(&seq[prefix.len()..]), a.clone()))
.collect();
out.sort_by(|a, b| a.0.cmp(&b.0));
out
}
}
pub fn render_chords(seq: &[KeyChord]) -> String {
seq.iter()
.map(|c| {
let mut s = String::new();
if c.modifiers.contains(KeyModifiers::SUPER) { s.push_str("⌘-"); }
if c.modifiers.contains(KeyModifiers::CONTROL) { s.push_str("C-"); }
if c.modifiers.contains(KeyModifiers::ALT) { s.push_str("M-"); }
match c.code {
KeyCode::Char(' ') => s.push_str("Spc"),
KeyCode::Char(ch) => s.push(ch),
KeyCode::Enter => s.push_str("RET"),
KeyCode::Tab => s.push_str("TAB"),
KeyCode::Backspace => s.push_str("DEL"),
other => s.push_str(&format!("{:?}", other)),
}
s
})
.collect::<Vec<_>>()
.join(" ")
}
#[derive(Serialize, Deserialize)]
struct RawBindings {
edit: HashMap<String, Action>,
bar_open: Vec<String>,
}
impl RawBindings {
fn defaults() -> Self {
let edit = [
("C-x C-s", Action::Save),
("C-x C-c", Action::Quit),
("C-x C-f", Action::FindFile),
("C-x p", Action::QuickOpen),
("C-x d", Action::ToggleFileTree),
("C-x b", Action::SwitchBuffer),
("C-x k", Action::KillBuffer),
("C-x 2", Action::SplitHorizontal),
("C-x 3", Action::SplitVertical),
("C-\\", Action::SplitVertical), ("C-|", Action::SplitVertical), ("C--", Action::SplitHorizontal), ("M--", Action::SplitHorizontal), ("C-x o", Action::NextPane),
("C-o", Action::NextPane), ("M-o", Action::NextPane),
("C-x x", Action::SwapPane),
("C-x 1", Action::DeleteOtherWindows),
("C-x 0", Action::ClosePane),
("C-t", Action::TabMode),
("C-x t", Action::TabMode),
("M-{", Action::PrevTab), ("M-}", Action::NextTab), ("C-{", Action::PrevTab), ("C-}", Action::NextTab),
("C-pageup", Action::PrevTab), ("C-pagedown", Action::NextTab),
("M-`", Action::OpenTerminal),
("C-x C-t", Action::OpenTerminal),
("C-x C-d", Action::Detach), ("C-k", Action::KillLine),
("C-w", Action::KillRegion),
("M-w", Action::CopyRegion),
("C-c", Action::CopyRegion), ("cmd-c", Action::CopyRegion),
("cmd-v", Action::Paste),
("cmd-s", Action::Save),
("cmd-a", Action::SelectAll),
("C-y", Action::Yank),
("M-y", Action::YankPop),
("C-v", Action::Paste), ("M-d", Action::KillWordForward),
("M-backspace", Action::KillWordBackward),
("C-/", Action::Undo),
("C-_", Action::Undo), ("C-x u", Action::Undo),
("M-/", Action::Redo), ("C-x C-u", Action::Redo),
("cmd-z", Action::Undo), ("cmd-Z", Action::Redo), ("C-u", Action::UndoMode), ("M-%", Action::QueryReplace),
("M-<", Action::GoTop),
("M->", Action::GoBottom),
("M-g", Action::GotoLine),
("C-x [", Action::JumpBlockPrev),
("C-x ]", Action::JumpBlockNext),
("C-x {", Action::JumpSymbolPrev),
("C-x }", Action::JumpSymbolNext),
("C-x m", Action::MatchBracket),
("C-l", Action::Recenter),
("C-x e", Action::ExplainThis),
("C-x ?", Action::ExplainFailure),
("C-x h", Action::SelectAll),
("C-s", Action::Search),
("C-r", Action::SearchBackward),
]
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect();
RawBindings {
edit,
bar_open: vec!["ctrl-space".into(), "M-x".into()],
}
}
fn into_bindings(self) -> KeyBindings {
let mut edit: HashMap<Vec<KeyChord>, Action> = HashMap::new();
for (k, v) in RawBindings::defaults().edit.into_iter().chain(self.edit) {
if let Some(seq) = parse_sequence(&k) {
edit.insert(seq, v);
}
}
let mut prefixes = HashSet::new();
for seq in edit.keys() {
if seq.len() > 1 {
prefixes.insert(seq[0].clone());
}
}
let mut bar_open: Vec<KeyChord> = self.bar_open.iter().filter_map(|s| parse_key(s)).collect();
if bar_open.is_empty() {
bar_open = RawBindings::defaults().bar_open.iter().filter_map(|s| parse_key(s)).collect();
}
KeyBindings { edit, prefixes, bar_open }
}
}
pub fn load() -> KeyBindings {
let config_path = app_config_dir().map(|d| d.join("keys.json"));
let local_path = std::path::PathBuf::from(".mars/keys.json");
let raw: Option<RawBindings> = config_path
.as_ref()
.and_then(|p| try_read(p))
.or_else(|| try_read(&local_path));
match raw {
Some(r) => r.into_bindings(),
None => {
let defaults = RawBindings::defaults();
if let Some(path) = &config_path {
let _ = write_defaults(path, &defaults);
}
defaults.into_bindings()
}
}
}
pub fn state_path() -> Option<std::path::PathBuf> {
app_config_dir().map(|d| d.join("state.json"))
}
fn app_config_dir() -> Option<std::path::PathBuf> {
let base = config_dir()?;
let mars = base.join("mars");
let ares = base.join("ares");
if !mars.join("keys.json").exists() && ares.is_dir() {
let _ = std::fs::create_dir_all(&mars);
for f in ["keys.json", "tuning.json", "state.json"] {
let src = ares.join(f);
if src.exists() {
let _ = std::fs::copy(&src, mars.join(f));
}
}
}
Some(mars)
}
fn config_dir() -> Option<std::path::PathBuf> {
if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
return Some(std::path::PathBuf::from(xdg));
}
std::env::var("HOME").ok().map(|h| std::path::PathBuf::from(h).join(".config"))
}
fn try_read(path: &std::path::Path) -> Option<RawBindings> {
let text = std::fs::read_to_string(path).ok()?;
serde_json::from_str(&text).ok()
}
pub fn reset_keys() -> anyhow::Result<std::path::PathBuf> {
let path = app_config_dir()
.map(|d| d.join("keys.json"))
.ok_or_else(|| anyhow::anyhow!("no config directory"))?;
if path.exists() {
let _ = std::fs::rename(&path, path.with_extension("json.bak"));
}
write_defaults(&path, &RawBindings::defaults())?;
Ok(path)
}
fn write_defaults(path: &std::path::Path, raw: &RawBindings) -> anyhow::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, serde_json::to_string_pretty(raw)?)?;
Ok(())
}