modal_keymap/modal_keymap.rs
1//! Context-dependent bindings via layered resolution.
2//!
3//! The same `ctrl+s` resolves to different actions depending on the app's
4//! context — but the library never learns what a "context" is. The app holds
5//! its own mode/focus state, decides which keymap layers are active and in what
6//! priority order, and passes them to `resolve_layered`. Earlier layers win;
7//! anything they don't bind falls through to the base layer.
8//!
9//! This *is* a lexical scope chain: `block → panel → global` resolved
10//! innermost-first, first hit wins, a miss falls outward — the same shape as
11//! JavaScript variable resolution. A miss in every layer (`None`) is the
12//! "pass it through" signal; in a terminal multiplexer that is where a key flows
13//! to the PTY, which sits *past the end of the chain* (a sink), never as a layer
14//! inside it. See `keymap-tui` for a live, layer-by-layer view of this.
15//!
16//! Run with: `cargo run -p keymap-core --example modal_keymap`
17
18use keymap_core::{Key, KeyInput, Keymap, Modifiers, resolve_layered};
19
20#[derive(Clone, Debug, PartialEq)]
21enum Action {
22 Save,
23 SplitPanel,
24 Quit,
25}
26
27/// The app's own context — the library knows nothing about this type.
28enum Context {
29 Editor,
30 Panel,
31}
32
33fn ctrl(c: char) -> KeyInput {
34 KeyInput::new(Key::Char(c), Modifiers::CTRL)
35}
36
37fn main() {
38 // Base layer: shared bindings live here once.
39 let mut base = Keymap::new();
40 base.bind(ctrl('s'), Action::Save);
41 base.bind(ctrl('q'), Action::Quit);
42
43 // Panel layer: only the overrides for panel context.
44 let mut panel = Keymap::new();
45 panel.bind(ctrl('s'), Action::SplitPanel);
46
47 // The app maps its context to an ordered layer stack. This `match` is the
48 // only place "context" exists — entirely on the application side.
49 let layers_for = |ctx: &Context| -> Vec<&Keymap<Action>> {
50 match ctx {
51 Context::Editor => vec![&base],
52 Context::Panel => vec![&panel, &base],
53 }
54 };
55
56 for ctx in [Context::Editor, Context::Panel] {
57 let name = match ctx {
58 Context::Editor => "editor",
59 Context::Panel => "panel",
60 };
61 let layers = layers_for(&ctx);
62 for key in [ctrl('s'), ctrl('q')] {
63 match resolve_layered(layers.iter().copied(), &key) {
64 Some(action) => println!("[{name:>6}] {key:>6} -> {action:?}"),
65 None => println!("[{name:>6}] {key:>6} -> pass through"),
66 }
67 }
68 }
69}