Skip to main content

ex_command/
ex_command.rs

1//! Ex-command (`:wq`) input, 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). It decomposes into three stages, and
7//! the library adds *no new type* for any of 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, deliberately
16//!    not that crate's type, so the command line stays free of a new public API.
17//!
18//! Deliberate non-goals (until demand is proven — see `docs/ROADMAP.md`):
19//! compound splitting (`wq` → `w` + `q`), arguments (`:w file`), ranges
20//! (`:%s/a/b/g`), and `:`-completion.
21//!
22//! Run with: `cargo run -p keymap-core --example ex_command`
23
24use keymap_core::{Key, KeyInput, Keymap, Modifiers};
25
26#[derive(Clone, Debug, PartialEq)]
27enum Action {
28    /// Bound to `:` in the keymap — opens the command line.
29    EnterCommandMode,
30    /// Resolved from the command name on Enter.
31    Write,
32    Quit,
33    WriteQuit,
34}
35
36/// The caller's own command-line state. The library never sees this type; the
37/// line buffer lives *inside* the `Command` variant, so a buffer can't exist
38/// while we're in `Normal` mode (illegal states are unrepresentable).
39enum Mode {
40    Normal,
41    Command(String),
42}
43
44fn plain(c: char) -> KeyInput {
45    KeyInput::new(Key::Char(c), Modifiers::NONE)
46}
47
48fn enter() -> KeyInput {
49    KeyInput::new(Key::Enter, Modifiers::NONE)
50}
51
52fn main() {
53    let mut map = Keymap::new();
54    map.bind(plain(':'), Action::EnterCommandMode);
55
56    // Stage 3's resolver: command name -> action. Same shape as
57    // `keymap_config`'s `FnMut(&str) -> Option<A>`, intentionally a plain
58    // closure rather than that crate's type.
59    let resolve = |name: &str| -> Option<Action> {
60        match name {
61            "w" => Some(Action::Write),
62            "q" => Some(Action::Quit),
63            "wq" => Some(Action::WriteQuit),
64            _ => None,
65        }
66    };
67
68    // A simulated keystroke stream: `:wq⏎`, then `:q⏎`, then an unknown `:zz⏎`.
69    let stream = [
70        plain(':'),
71        plain('w'),
72        plain('q'),
73        enter(),
74        plain(':'),
75        plain('q'),
76        enter(),
77        plain(':'),
78        plain('z'),
79        plain('z'),
80        enter(),
81    ];
82
83    let mut mode = Mode::Normal;
84    for key in stream {
85        match &mut mode {
86            Mode::Normal => {
87                if map.get(&key) == Some(&Action::EnterCommandMode) {
88                    println!(": -> enter command mode");
89                    mode = Mode::Command(String::new());
90                } else {
91                    // Any other normal-mode key would run its own binding; this
92                    // demo only cares about the `:` entry point.
93                }
94            }
95            Mode::Command(buf) => match key.key() {
96                Key::Char(c) => {
97                    buf.push(c);
98                    println!("  :{buf}");
99                }
100                Key::Enter => {
101                    // Take the buffer out and drop back to Normal in one move.
102                    let name = std::mem::take(buf);
103                    match resolve(&name) {
104                        Some(action) => println!("  :{name} -> fire {action:?}"),
105                        None => println!("  :{name} -> unknown command, no-op"),
106                    }
107                    mode = Mode::Normal;
108                }
109                Key::Esc => {
110                    println!("  esc -> abandon command line");
111                    mode = Mode::Normal;
112                }
113                // `Key` is #[non_exhaustive]; other editing keys are ignored here.
114                _ => {}
115            },
116        }
117    }
118}