Skip to main content

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}