Skip to main content

ex_command/
ex_command.rs

1//! Ex-command (`:wq`) execution, composed entirely caller-side.
2//!
3//! A command line like vim's `:wq` is **not** a chord sequence, so it does not
4//! use `keymap-seq` (that crate is a prefix-free chord trie; a command line is an
5//! open string language — `:w`, `:wq`, `:wqa` coexist, and editing keys mutate a
6//! buffer rather than extend a sequence). The *execution* path decomposes into
7//! three stages, and the library adds no new type for them:
8//!
9//! 1. **Press `:`** — an ordinary single-key binding. `Keymap::get` returns
10//!    `EnterCommandMode`, exactly like any other action.
11//! 2. **Capture `wq`** — the typed text accumulates into a caller-owned line
12//!    buffer. This is caller state, the same way `keymap-seq`'s pending buffer is.
13//! 3. **Dispatch on Enter** — the buffer is resolved to an action by a
14//!    `FnMut(&str) -> Option<Action>` closure. That is the *same shape* as
15//!    `keymap_config::from_str`'s name resolver; we reuse the shape, not that
16//!    crate's type, so the execution path remains a plain closure.
17//!
18//! **Execution vs discovery are orthogonal.** This example shows the execution
19//! path. For the *discovery* layer — front-prefix completion (`:w<Tab>`) and a
20//! full palette listing — see `examples/command_palette.rs` and
21//! [`keymap_core::cmd::CommandIndex`]. The two are independent: you can use one
22//! without the other, and combining them is a matter of wiring in the caller.
23//!
24//! Deliberate non-goals: compound splitting (`wq` → `w` + `q`), arguments
25//! (`:w file`), ranges (`:%s/a/b/g`). See `docs/ROADMAP.md`.
26//!
27//! Run with: `cargo run -p keymap-core --example ex_command`
28
29use keymap_core::{Key, KeyInput, Keymap, Modifiers};
30
31#[derive(Clone, Debug, PartialEq)]
32enum Action {
33    /// Bound to `:` in the keymap — opens the command line.
34    EnterCommandMode,
35    /// Resolved from the command name on Enter.
36    Write,
37    Quit,
38    WriteQuit,
39}
40
41/// The caller's own command-line state. The library never sees this type; the
42/// line buffer lives *inside* the `Command` variant, so a buffer can't exist
43/// while we're in `Normal` mode (illegal states are unrepresentable).
44enum Mode {
45    Normal,
46    Command(String),
47}
48
49fn plain(c: char) -> KeyInput {
50    KeyInput::new(Key::Char(c), Modifiers::NONE)
51}
52
53fn enter() -> KeyInput {
54    KeyInput::new(Key::Enter, Modifiers::NONE)
55}
56
57fn main() {
58    let mut map = Keymap::new();
59    map.bind(plain(':'), Action::EnterCommandMode);
60
61    // Stage 3's resolver: command name -> action. Same shape as
62    // `keymap_config`'s `FnMut(&str) -> Option<A>`, intentionally a plain
63    // closure rather than that crate's type.
64    let resolve = |name: &str| -> Option<Action> {
65        match name {
66            "w" => Some(Action::Write),
67            "q" => Some(Action::Quit),
68            "wq" => Some(Action::WriteQuit),
69            _ => None,
70        }
71    };
72
73    // A simulated keystroke stream: `:wq⏎`, then `:q⏎`, then an unknown `:zz⏎`.
74    let stream = [
75        plain(':'),
76        plain('w'),
77        plain('q'),
78        enter(),
79        plain(':'),
80        plain('q'),
81        enter(),
82        plain(':'),
83        plain('z'),
84        plain('z'),
85        enter(),
86    ];
87
88    let mut mode = Mode::Normal;
89    for key in stream {
90        match &mut mode {
91            Mode::Normal => {
92                if map.get(&key) == Some(&Action::EnterCommandMode) {
93                    println!(": -> enter command mode");
94                    mode = Mode::Command(String::new());
95                } else {
96                    // Any other normal-mode key would run its own binding; this
97                    // demo only cares about the `:` entry point.
98                }
99            }
100            Mode::Command(buf) => match key.key() {
101                Key::Char(c) => {
102                    buf.push(c);
103                    println!("  :{buf}");
104                }
105                Key::Enter => {
106                    // Take the buffer out and drop back to Normal in one move.
107                    let name = std::mem::take(buf);
108                    match resolve(&name) {
109                        Some(action) => println!("  :{name} -> fire {action:?}"),
110                        None => println!("  :{name} -> unknown command, no-op"),
111                    }
112                    mode = Mode::Normal;
113                }
114                Key::Esc => {
115                    println!("  esc -> abandon command line");
116                    mode = Mode::Normal;
117                }
118                // `Key` is #[non_exhaustive]; other editing keys are ignored here.
119                _ => {}
120            },
121        }
122    }
123}