use keymap_config::to_toml;
use keymap_core::{Key, KeyInput, Keymap, LegacyForm, Modifiers, resolve_layered};
use keymap_seq::SequenceKeymap;
use keymap_term::{DecodeMode, Decoded, decode};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Action {
CursorDown,
Save,
Split,
}
impl Action {
fn name(self) -> &'static str {
match self {
Action::CursorDown => "cursor_down",
Action::Save => "save",
Action::Split => "split",
}
}
}
fn main() {
let reserved = [
KeyInput::new(Key::Esc, Modifiers::NONE),
KeyInput::new(Key::Char('c'), Modifiers::CTRL),
];
let mut global = Keymap::new();
global.bind(
KeyInput::new(Key::Char('j'), Modifiers::NONE),
Action::CursorDown,
);
global.bind(
KeyInput::new(Key::Char('x'), Modifiers::CTRL),
Action::Split,
);
println!("== capture: what the terminal actually delivers ==");
capture_demo();
println!("\n== validate before mutate: the escape hatch must survive ==");
let cs = capture(&[0x13], DecodeMode::Baseline).expect("ctrl+s decodes to a key");
let layers = [&global];
report(
"rebind Save ->",
cs,
validate_rebind(&layers, 0, cs, Action::Save, &reserved),
);
let esc = capture(&[0x1b], DecodeMode::Baseline).expect("esc decodes to a key");
report(
"rebind Save ->",
esc,
validate_rebind(&layers, 0, esc, Action::Save, &reserved),
);
let j = capture(&[0x6a], DecodeMode::Baseline).expect("'j' decodes to a key");
report(
"rebind Split ->",
j,
validate_rebind(&layers, 0, j, Action::Split, &reserved),
);
println!("\n== composed resolution: an upper layer cannot shadow reserved ==");
let overlay: Keymap<Action> = Keymap::new();
let two = [&overlay, &global];
report(
"rebind Save into overlay ->",
esc,
validate_rebind(&two, 0, esc, Action::Save, &reserved),
);
println!("\n== legacy collapse: ctrl+i ≡ tab on a baseline terminal ==");
let tab_reserved = [KeyInput::new(Key::Tab, Modifiers::NONE)];
let ctrl_i = KeyInput::new(Key::Char('i'), Modifiers::CTRL);
report(
"rebind Save ->",
ctrl_i,
validate_rebind(&layers, 0, ctrl_i, Action::Save, &tab_reserved),
);
println!("\n== persist: serialize for the caller to write ==");
if let RebindVerdict::Allowed { .. } = validate_rebind(&layers, 0, cs, Action::Save, &reserved)
{
global.bind(cs, Action::Save);
}
let toml = to_toml(&global, &SequenceKeymap::<Action>::new(), |a| {
Some(a.name())
});
print!("{toml}");
println!(
"// Write this to the user's *config directory* (a trusted path), where the\n\
// reserved guard above is advisory. Pointing a rebind at a PTY-writable\n\
// per-project file instead crosses a trust boundary: there the guard must\n\
// be a hard error (see docs/ROADMAP.md). Round-trip is semantic, not\n\
// byte-exact — comments, ordering, and `shift+a`-style spellings are lost."
);
}
fn capture(bytes: &[u8], mode: DecodeMode) -> Option<KeyInput> {
match decode(bytes, mode) {
Decoded::Key { input, .. } => Some(input),
_ => None,
}
}
fn capture_demo() {
show("alt+a (bytes c3 a5)", &[0xc3, 0xa5]);
show("ctrl+i (byte 09)", &[0x09]);
show("truncated CSI (bytes 1b 5b)", &[0x1b, 0x5b]);
show("invalid byte (ff)", &[0xff]);
}
fn show(label: &str, bytes: &[u8]) {
match capture(bytes, DecodeMode::Baseline) {
Some(input) => println!(" {label} -> captured {input}"),
None => match decode(bytes, DecodeMode::Baseline) {
Decoded::Incomplete => println!(" {label} -> incomplete, keep reading"),
_ => println!(" {label} -> not a key, rejected"),
},
}
}
#[derive(Clone, Copy)]
enum RebindVerdict<'a> {
BreaksEscape {
reserved: KeyInput,
why: &'static str,
},
Allowed {
shadows: Option<&'a Action>,
legacy: LegacyForm,
},
}
fn validate_rebind<'a>(
layers: &[&'a Keymap<Action>],
target: usize,
proposed: KeyInput,
action: Action,
reserved: &[KeyInput],
) -> RebindVerdict<'a> {
if let LegacyForm::CollapsesTo(byte_twin) = proposed.legacy_form() {
if reserved.contains(&byte_twin) {
return RebindVerdict::BreaksEscape {
reserved: byte_twin,
why: "collapses onto a reserved key on legacy terminals",
};
}
}
let mut simulated: Vec<Keymap<Action>> = layers.iter().map(|l| (*l).clone()).collect();
simulated[target].bind(proposed, action);
for &r in reserved {
let before = resolve_layered(layers.iter().copied(), &r);
let after = resolve_layered(simulated.iter(), &r);
if before != after {
return RebindVerdict::BreaksEscape {
reserved: r,
why: "would steal or shadow this reserved key",
};
}
}
RebindVerdict::Allowed {
shadows: resolve_layered(layers.iter().copied(), &proposed),
legacy: proposed.legacy_form(),
}
}
fn report(what: &str, proposed: KeyInput, verdict: RebindVerdict<'_>) {
match verdict {
RebindVerdict::BreaksEscape { reserved, why } => {
println!(" {what} {proposed}: REFUSED — {why} ({reserved})");
}
RebindVerdict::Allowed { shadows, legacy } => {
let mut notes = Vec::new();
if let Some(prev) = shadows {
notes.push(format!("overrides {}", prev.name()));
}
match legacy {
LegacyForm::Representable => {}
LegacyForm::CollapsesTo(t) => notes.push(format!("legacy: indistinct from {t}")),
LegacyForm::Unrepresentable => {
notes.push("legacy: not deliverable on a C0 terminal".to_string());
}
}
let suffix = if notes.is_empty() {
String::new()
} else {
format!(" ({})", notes.join("; "))
};
println!(" {what} {proposed}: allowed{suffix}");
}
}
}