Skip to main content

leader_sequence/
leader_sequence.rs

1//! A leader / prefix-chord keymap driven by a simulated key stream.
2//!
3//! Shows the intended split: `keymap-seq` answers "exact / prefix / miss" for the
4//! keys so far, and the *caller* owns the pending buffer and decides when to fire
5//! or reset. The second demo (`timed`) uses [`TimedPending`] to add the time
6//! dimension a `jj`-style binding needs — the library holds no clock, so `now` is
7//! caller-supplied data.
8//! Run with `cargo run -p keymap-seq --example leader_sequence`.
9
10use std::time::{Duration, Instant};
11
12use keymap_core::{Key, KeyInput, Modifiers};
13use keymap_seq::{Match, SequenceKeymap, Step, TimedPending};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16enum Action {
17    Save,
18    Quit,
19    GotoTop,
20    NormalMode,
21}
22
23fn ctrl(c: char) -> KeyInput {
24    KeyInput::new(Key::Char(c), Modifiers::CTRL)
25}
26
27fn plain(c: char) -> KeyInput {
28    KeyInput::new(Key::Char(c), Modifiers::NONE)
29}
30
31fn main() {
32    untimed();
33    println!();
34    timed();
35}
36
37/// Resolution without a clock: the buffer resets only on `Exact`/`NoMatch`.
38fn untimed() {
39    println!("== untimed ==");
40    let mut map = SequenceKeymap::new();
41    map.bind([ctrl('x'), ctrl('s')], Action::Save).unwrap();
42    map.bind([ctrl('x'), ctrl('c')], Action::Quit).unwrap();
43    map.bind([plain('g'), plain('g')], Action::GotoTop).unwrap();
44
45    // A stream a terminal might deliver: a completed save, an abandoned prefix
46    // (`ctrl+x` then an unrelated key), then `g g`.
47    let stream = [
48        ctrl('x'),
49        ctrl('s'),
50        ctrl('x'),
51        plain('z'),
52        plain('g'),
53        plain('g'),
54    ];
55
56    let mut pending: Vec<KeyInput> = Vec::new();
57    for key in stream {
58        pending.push(key);
59        match map.lookup(&pending) {
60            Match::Exact(action) => {
61                println!("{} -> fire {action:?}", render(&pending));
62                pending.clear();
63            }
64            Match::Prefix => {
65                println!("{} -> prefix, waiting", render(&pending));
66            }
67            Match::NoMatch => {
68                println!("{} -> no binding, passing through", render(&pending));
69                pending.clear();
70            }
71        }
72    }
73}
74
75/// `jj`-style time window using [`TimedPending`].
76///
77/// [`TimedPending`] bundles the expiry check, flush, and new-key processing
78/// into a single `feed` call. The library still holds no clock — `now` is
79/// caller-supplied data (a `base + offset` pair here, so the demo is
80/// deterministic and never flaky).
81///
82/// Note what this demo does *not* do: real vim `jj` also binds `j` on its own
83/// (a literal cursor move). That needs both `j` *and* `[j, j]` bound — which
84/// the prefix-free invariant forbids (`bind` rejects it with `PrefixShadow`).
85/// Here `j` alone is unbound, so the timeout is the *only* caller policy needed.
86fn timed() {
87    const WINDOW: Duration = Duration::from_millis(500);
88
89    println!("== timed (jj, via TimedPending) ==");
90    let mut map = SequenceKeymap::new();
91    map.bind([plain('j'), plain('j')], Action::NormalMode)
92        .unwrap();
93
94    // Deterministic timestamps: base + offset (no real sleep / Instant::now).
95    let base = Instant::now();
96    let stream = [
97        (plain('j'), 0u64),
98        (plain('j'), 120), // 120ms gap: quick, completes `jj`
99        (plain('j'), 900),
100        (plain('j'), 1700), // 800ms gap: too slow, expiry fires
101    ];
102
103    let mut pending = TimedPending::new();
104    for (key, ms) in stream {
105        let now = base + Duration::from_millis(ms);
106        let result = pending.feed(&map, key, now, WINDOW);
107
108        // If a previous prefix expired, report its keys as literals first.
109        if let Some(expired) = result.expired {
110            println!(
111                "{} @ +{ms}ms -> idle timeout, flushed as literals",
112                render(&expired),
113            );
114        }
115
116        match result.step {
117            Step::Fired(action) => println!("{key} @ +{ms}ms -> fire {action:?}"),
118            Step::Pending => println!("{key} @ +{ms}ms -> prefix, waiting (window {WINDOW:?})"),
119            Step::PassThrough(keys) => {
120                println!("{} @ +{ms}ms -> no binding, passing through", render(&keys));
121            }
122        }
123    }
124
125    // The stream ended mid-prefix. Drain as literals (what an idle timer does).
126    let dangling = pending.flush();
127    if !dangling.is_empty() {
128        println!(
129            "{} -> pending at end; flushed as literals",
130            render(&dangling)
131        );
132    }
133}
134
135fn render(keys: &[KeyInput]) -> String {
136    keys.iter()
137        .map(ToString::to_string)
138        .collect::<Vec<_>>()
139        .join(" ")
140}