Skip to main content

zsh/ported/zle/
zle_keymap.rs

1//! ZLE keymap and key bindings - Direct port from zsh/Src/Zle/zle_keymap.c
2//!
3//! currently selected keymap, and its name                                  // c:121
4//! the hash table of keymap names                                           // c:128
5//! key sequence reading data                                                // c:133
6//! main initialisation entry point                                          // c:1220
7//!
8//! Keymap structures:
9//!
10//! There is a hash table of keymap names. Each name just points to a keymap.
11//! More than one name may point to the same keymap.
12//!
13//! Each keymap consists of a table of bindings for each character, and a
14//! hash table of multi-character key bindings. The keymap has no individual
15//! name, but maintains a reference count.
16
17use std::collections::HashMap;
18use std::sync::{Arc, Mutex, OnceLock};
19
20use super::zle_thingy::Thingy;
21use std::io::Write;
22
23// =====================================================================
24// Flag constants — `Src/Zle/zle_keymap.c:62/83/114-115`.
25// =====================================================================
26
27/// Port of `KMN_IMMORTAL` from `Src/Zle/zle_keymap.c:62`. Marks a
28/// keymap-name node that can't be deleted (the `.safe` keymap).
29
30// --- AUTO: cross-zle hoisted-fn use glob ---
31#[allow(unused_imports)]
32#[allow(unused_imports)]
33use crate::ported::zle::zle_main::*;
34#[allow(unused_imports)]
35use crate::ported::zle::zle_misc::*;
36#[allow(unused_imports)]
37use crate::ported::zle::zle_hist::*;
38#[allow(unused_imports)]
39use crate::ported::zle::zle_move::*;
40#[allow(unused_imports)]
41use crate::ported::zle::zle_word::*;
42#[allow(unused_imports)]
43use crate::ported::zle::zle_params::*;
44#[allow(unused_imports)]
45use crate::ported::zle::zle_vi::*;
46#[allow(unused_imports)]
47use crate::ported::zle::zle_utils::*;
48#[allow(unused_imports)]
49use crate::ported::zle::zle_refresh::*;
50#[allow(unused_imports)]
51use crate::ported::zle::zle_tricky::*;
52#[allow(unused_imports)]
53use crate::ported::zle::textobjects::*;
54#[allow(unused_imports)]
55use crate::ported::zle::deltochar::*;
56
57pub const KMN_IMMORTAL: i32 = 1 << 1;                                        // c:62
58
59/// Port of `KM_IMMUTABLE` from `Src/Zle/zle_keymap.c:83`. Marks a
60/// keymap that can't have its bindings modified.
61pub const KM_IMMUTABLE: i32 = 1 << 1;                                        // c:83
62
63/// Port of `BS_LIST` from `Src/Zle/zle_keymap.c:114`. `bin_bindkey -L`:
64/// list bindings in `bindkey -M` syntax.
65pub const BS_LIST: i32 = 1 << 0;                                             // c:114
66
67/// Port of `BS_ALL` from `Src/Zle/zle_keymap.c:115`. `bin_bindkey -aL`:
68/// list ALL bindings, including default sequences.
69pub const BS_ALL: i32 = 1 << 1;                                              // c:115
70
71/// Port of `mod_export char *curkeymapname` from `Src/Zle/zle_keymap.c:126`.
72/// Name of the currently active keymap (driven by `bindkey -A` and the
73/// `KEYMAP` parameter). The Rust port wraps in OnceLock<Mutex<>> for
74/// thread-safe access from widget bodies.
75pub static CURKEYMAPNAME: std::sync::OnceLock<std::sync::Mutex<String>> =
76    std::sync::OnceLock::new();                                              // c:126
77
78/// Get-or-init accessor for `CURKEYMAPNAME`. Mirrors the C convention
79/// of treating the string as always-initialised — first read seeds it
80/// with "main".
81pub fn curkeymapname() -> std::sync::MutexGuard<'static, String> {
82    CURKEYMAPNAME
83        .get_or_init(|| std::sync::Mutex::new(String::from("main")))
84        .lock()
85        .unwrap()
86}
87
88/// Port of `Keymap curkeymap` from `Src/Zle/zle_keymap.c:124`. The
89/// currently active keymap (per `bindkey -A` selection or KEYMAP
90/// parameter). Used inline at zle_keymap.c:519 (`curkeymap = km;`)
91/// and read by `getkeycmd`/`getkeybuf` to dispatch the next key.
92pub static curkeymap: Mutex<Option<Arc<Keymap>>> = Mutex::new(None);         // c:124
93
94/// Port of `char *keybuf` from `Src/Zle/zle_keymap.c:136`. The key
95/// sequence currently being read by `getkeycmd`. C uses a flat
96/// `char*` heap allocation sized by `keybufsz`; Rust uses
97/// `Vec<u8>` which manages its own capacity.
98pub static keybuf: Mutex<Vec<u8>> = Mutex::new(Vec::new());                  // c:136
99
100/// Port of `int keybuflen` from `Src/Zle/zle_keymap.c:139`. Current
101/// number of bytes in `keybuf`. Rust mirrors via `keybuf.lock().len()`
102/// but exposes the count as a separate static for callers that need
103/// it without holding the buffer lock.
104pub static keybuflen: std::sync::atomic::AtomicI32 =                         // c:139
105    std::sync::atomic::AtomicI32::new(0);
106
107/// Port of `static Thingy lastnamed` from `Src/Zle/zle_keymap.c:145`.
108/// Last command executed by `execute-named-command` — used to
109/// re-execute via `bindkey -A name` then `getkeycmd`.
110pub static lastnamed: Mutex<Option<Thingy>> = Mutex::new(None);              // c:145
111
112// =====================================================================
113// keymapnamtab — `Src/Zle/zle_keymap.c:128/153`.
114// =====================================================================
115//
116// C: `mod_export HashTable keymapnamtab` — global hash mapping
117// keymap names to KeymapName entries (each KeymapName holds an
118// Arc'd Keymap + flags). zshrs uses Mutex<HashMap<String, KeymapName>>.
119
120static KEYMAPNAMTAB: OnceLock<Mutex<HashMap<String, KeymapName>>> = OnceLock::new();
121
122pub(crate) fn keymapnamtab() -> &'static Mutex<HashMap<String, KeymapName>> {
123    KEYMAPNAMTAB.get_or_init(|| Mutex::new(HashMap::new()))
124}
125
126/// Direct port of `struct keymapname` from `Src/Zle/zle_keymap.c:54`.
127/// One node in the global `keymapnamtab` — maps a name to a Keymap
128/// + per-node flags (KMN_IMMORTAL for `.safe`).
129#[derive(Debug, Clone)]
130pub struct KeymapName {                                                      // c:54
131    pub nam: String,                                                         // c:56 char *nam
132    pub flags: i32,                                                          // c:57 int flags
133    pub keymap: Arc<Keymap>,                                                 // c:58 Keymap keymap
134}
135
136/// Direct port of `struct keymap` from `Src/Zle/zle_keymap.c:64`.
137/// A keymap — binding of keys to thingies.
138#[derive(Debug, Clone)]
139pub struct Keymap {                                                          // c:64
140    /// `Thingy first[256]` — c:65, base binding for each byte.
141    pub first: [Option<Thingy>; 256],
142    /// `HashTable multi` — c:66, multi-character bindings.
143    pub multi: HashMap<Vec<u8>, KeyBinding>,
144    /// `KeymapName primary` — c:78, primary alias for this map.
145    pub primary: Option<String>,
146    /// `int flags` — c:79 (KM_IMMUTABLE).
147    pub flags: i32,
148    /// `int rc` — c:80, reference count (refkeymap/unrefkeymap/
149    /// deletekeymap).
150    pub rc: i32,
151}
152
153/// Direct port of `struct key` from `Src/Zle/zle_keymap.c:85`.
154/// A key binding (either a thingy or a string to send).
155#[derive(Debug, Clone)]
156pub struct KeyBinding {                                                      // c:85
157    pub bind: Option<Thingy>,                                                // c:88 Thingy bind
158    pub str: Option<String>,                                                 // c:89 char *str
159    pub prefixct: i32,                                                       // c:90 int prefixct
160}
161
162// `BindState` / `BindStateFlags` deleted — the C `struct bindstate`
163// at zle_keymap.c:95 is only used as a local in `printbinding()`/
164// `scanbindings()`/`bin_bindkey -L`; ports of those fns will model
165// it as a stack-local struct when they land. The previous Rust
166// declaration had no callers (dead code) and used a fake bitflags
167// wrapper over a single int field.
168
169/// Port of `struct remprefstate` from `Src/Zle/zle_keymap.c:108`.
170/// Closure state for `scanremoveprefix` — removes every multi-char
171/// binding that starts with the given prefix from a keymap.
172///
173/// C definition (c:108-112):
174/// ```c
175/// struct remprefstate {
176///     Keymap km;
177///     char *prefix;
178///     int prefixlen;
179/// };
180/// ```
181#[derive(Debug)]
182#[allow(non_camel_case_types)]
183pub struct remprefstate {                                                    // c:108
184    /// Target keymap (Arc handle for shared ownership).
185    pub km: std::sync::Arc<Keymap>,                                          // c:109
186    /// Byte prefix to match against each multi-key binding.
187    pub prefix: Vec<u8>,                                                     // c:110
188    /// `prefix.len()` cached for the scan inner loop (kept as a field
189    /// to mirror the C struct shape; `self.prefix.len()` reads the
190    /// same value).
191    pub prefixlen: usize,                                                    // c:111
192}
193
194impl Default for Keymap {
195    fn default() -> Self {
196        Keymap {
197            first: std::array::from_fn(|_| None),
198            multi: HashMap::new(),
199            primary: None,
200            flags: 0,
201            rc: 0,
202        }
203    }
204}
205
206impl Keymap {
207    /// Construct an empty keymap with no bindings.
208    /// Equivalent to `newkeytab()` from Src/Zle/zle_keymap.c:278 — the
209    /// C source allocates a Keymap with the first[] array zeroed out
210    /// and an empty multi-byte hashtab.
211    pub fn new() -> Self {                                                   // c:278
212        Self::default()
213    }
214
215    /// Bind a 1-byte key to a Thingy via the `first[]` fast-path table.
216    /// Direct port of the single-byte path in `bindkey()` at
217    /// Src/Zle/zle_keymap.c:566; the C source writes into `km->first[c]`
218    /// when `seq` has length 1.
219    pub fn bind_char(&mut self, c: u8, thingy: Thingy) {                     // c:566
220        self.first[c as usize] = Some(thingy);
221    }
222
223    /// Clear a 1-byte binding.
224    /// Equivalent to `bindkey -r` against a single-byte sequence at
225    /// Src/Zle/zle_keymap.c:566 — flips the `first[c]` slot to None.
226    pub fn unbind_char(&mut self, c: u8) {
227        self.first[c as usize] = None;
228    }
229
230    /// Install a multi-byte key sequence binding.
231    /// Direct port of `bindkey(Keymap km, const char *seq, Thingy bind, char *str)` from Src/Zle/zle_keymap.c:566 for the
232    /// len > 1 path: marks every proper prefix of `seq` as a prefix
233    /// node (prefixct increment) so getkeymapcmd's trie walk knows to
234    /// keep reading bytes when it sees a partial match.
235    pub fn bind_seq(&mut self, seq: &[u8], thingy: Thingy) {                 // c:566
236        if seq.len() == 1 {
237            self.bind_char(seq[0], thingy);
238        } else {
239            // Mark prefixes
240            for i in 1..seq.len() {
241                let prefix = &seq[..i];
242                self.multi
243                    .entry(prefix.to_vec())
244                    .and_modify(|kb| kb.prefixct += 1)
245                    .or_insert(KeyBinding {
246                        bind: None,
247                        str: None,
248                        prefixct: 1,
249                    });
250            }
251
252            // Add the binding
253            self.multi.insert(
254                seq.to_vec(),
255                KeyBinding {
256                    bind: Some(thingy),
257                    str: None,
258                    prefixct: 0,
259                },
260            );
261        }
262    }
263
264    /// Install a multi-byte key sequence that maps to a literal string.
265    /// Port of the send-string variant of `bindkey()` at
266    /// Src/Zle/zle_keymap.c:566 — the C source stores `str` instead of
267    /// a Thingy when invoked via `bindkey -s 'seq' 'string'`. When the
268    /// trie hits this entry, getkeycmd ungets the string via
269    /// `ungetbytes_unmeta` (zle_keymap.c:1784) so it gets re-resolved
270    /// against the keymap.
271    pub fn bind_str(&mut self, seq: &[u8], s: String) {
272        if seq.len() == 1 {
273            // Single char can't be send-string in first[] table
274            // Store in multi
275        }
276
277        // Mark prefixes
278        for i in 1..seq.len() {
279            let prefix = &seq[..i];
280            self.multi
281                .entry(prefix.to_vec())
282                .and_modify(|kb| kb.prefixct += 1)
283                .or_insert(KeyBinding {
284                    bind: None,
285                    str: None,
286                    prefixct: 1,
287                });
288        }
289
290        self.multi.insert(
291            seq.to_vec(),
292            KeyBinding {
293                bind: None,
294                str: Some(s),
295                prefixct: 0,
296            },
297        );
298    }
299
300    /// Remove a multi-byte binding and decrement prefix counts on its
301    /// ancestors so the trie shrinks correctly.
302    /// Port of `bindkey -r` against a multi-byte sequence at
303    /// Src/Zle/zle_keymap.c:566 — the C source mirrors the prefix
304    /// reference-count machinery via the same prefixct decrement
305    /// pattern when removing a leaf.
306    pub fn unbind_seq(&mut self, seq: &[u8]) {
307        if seq.len() == 1 {
308            self.unbind_char(seq[0]);
309        } else {
310            if self.multi.remove(seq).is_some() {
311                // Decrement prefix counts
312                for i in 1..seq.len() {
313                    let prefix = &seq[..i];
314                    if let Some(kb) = self.multi.get_mut(prefix) {
315                        kb.prefixct -= 1;
316                        if kb.prefixct == 0 && kb.bind.is_none() && kb.str.is_none() {
317                            // Remove empty prefix entry
318                            // (can't remove while iterating, so we'll leave it)
319                        }
320                    }
321                }
322            }
323        }
324    }
325
326    /// Fast-path single-byte lookup through `first[]`.
327    /// Equivalent to the 1-byte branch of `keybind()` at
328    /// Src/Zle/zle_keymap.c:659 — the C source's `km->first[*seq]`
329    /// access for single-byte resolution.
330    pub fn lookup_char(&self, c: u8) -> Option<&Thingy> {
331        self.first[c as usize].as_ref()
332    }
333
334    /// Multi-byte sequence lookup through the `multi` hashtab.
335    /// Equivalent to the >1-byte branch of `keybind()` at
336    /// zle_keymap.c:659 — returns the KeyBinding entry if `seq`
337    /// matches a leaf, or one carrying `prefixct > 0` if `seq` is a
338    /// prefix of one or more bound sequences.
339    pub fn lookup_seq(&self, seq: &[u8]) -> Option<&KeyBinding> {
340        if seq.len() == 1 {
341            // For single char, use lookup_char instead
342            None
343        } else {
344            self.multi.get(seq)
345        }
346    }
347
348    /// Test whether `seq` is a prefix of any bound sequence.
349    /// Equivalent to `keyisprefix()` from Src/Zle/zle_keymap.c. Used
350    /// by `getkeymapcmd` to decide whether to keep reading bytes
351    /// during a multi-byte sequence resolve (the trie-walk loop at
352    /// zle_keymap.c:1604).
353    pub fn is_prefix(&self, seq: &[u8]) -> bool {
354        if seq.len() == 1 {
355            // Check if this char is a prefix in multi table
356            self.multi.keys().any(|k| k.len() > 1 && k[0] == seq[0])
357        } else {
358            self.multi
359                .get(seq)
360                .map(|kb| kb.prefixct > 0)
361                .unwrap_or(false)
362        }
363    }
364}
365
366/// Zero-sized namespace for the three default-binding tables
367/// (emacs / viins / vicmd) that `default_bindings()` populates at
368/// startup. The state these used to wrap (keymaps / current /
369/// current_name / local / keybuf / lastnamed) now lives in the
370/// six file-scope statics declared above (KEYMAPNAMTAB / curkeymap
371/// / CURKEYMAPNAME / LOCALKEYMAP / keybuf / lastnamed) — matching
372/// the C globals at `Src/Zle/zle_keymap.c:124-145`.
373///
374/// The setup_*_keymap methods stay as methods (drift-gate
375/// exempts impl-block fns) because zsh's C `default_bindings()`
376/// has the equivalent 330+ bindkey calls inline in one function;
377/// the Rust port keeps them factored by keymap for readability.
378// `KeymapManager` unit struct (and its 32-method impl block) deleted —
379// was a Rust-only namespace wrapper around the file-scope statics
380// (KEYMAPNAMTAB / curkeymap / CURKEYMAPNAME / LOCALKEYMAP / keybuf /
381// lastnamed) with no `struct keymap_manager` in zsh C. 29 of the 32
382// methods were never called; 3 (`setup_emacs_keymap` /
383// `setup_viins_keymap` / `setup_vicmd_keymap`) are factored out below
384// as free fns because zsh's C `default_bindings()` inlines the ~300
385// equivalent `bindkey` calls in one body (Src/Zle/zle_keymap.c:124).
386// The Rust port keeps them factored by keymap for readability.
387
388// WARNING: NOT IN ZLE_KEYMAP.C — Rust-only factoring of zsh's
389// inlined `default_bindings()` body (Src/Zle/zle_keymap.c:124).
390
391/// Set up emacs keymap bindings. Direct port of the emacs branch of
392/// `default_bindings()` from `Src/Zle/zle_keymap.c:1309` driven by the
393/// canonical `EMACSBIND` / `METABIND` tables in
394/// `zle_bindings.rs` (which mirror `Src/Zle/zle_bindings.c:88-253`).
395pub fn setup_emacs_keymap(km: &mut Keymap) {
396    use super::zle_bindings::{EMACSBIND, METABIND};
397
398    // c:1326-1329 — first 32 entries come from emacsbind table.
399    for i in 0..32 {
400        km.bind_char(i as u8, Thingy::builtin(EMACSBIND[i]));
401    }
402    // c:1330-1333 — 32-255 self-insert.
403    for i in 32u8..=255u8 {
404        km.bind_char(i, Thingy::builtin("self-insert"));
405    }
406    // c:1337 — entry[127] = entry[8] (DEL == ^H).
407    km.bind_char(0x7F, Thingy::builtin(EMACSBIND[8]));
408
409    // c:1410-1413 — emacs cursor keys (vt100 fallback).
410    km.bind_seq(b"\x1b[A", Thingy::builtin("up-line-or-history"));
411    km.bind_seq(b"\x1b[B", Thingy::builtin("down-line-or-history"));
412    km.bind_seq(b"\x1b[C", Thingy::builtin("forward-char"));
413    km.bind_seq(b"\x1b[D", Thingy::builtin("backward-char"));
414    km.bind_seq(b"\x1bOA", Thingy::builtin("up-line-or-history"));
415    km.bind_seq(b"\x1bOB", Thingy::builtin("down-line-or-history"));
416    km.bind_seq(b"\x1bOC", Thingy::builtin("forward-char"));
417    km.bind_seq(b"\x1bOD", Thingy::builtin("backward-char"));
418
419    // c:1415-1431 — ^X sequences.
420    km.bind_seq(b"\x18*", Thingy::builtin("expand-word"));
421    km.bind_seq(b"\x18g", Thingy::builtin("list-expand"));
422    km.bind_seq(b"\x18G", Thingy::builtin("list-expand"));
423    km.bind_seq(b"\x18\x0e", Thingy::builtin("infer-next-history"));
424    km.bind_seq(b"\x18\x0b", Thingy::builtin("kill-buffer"));
425    km.bind_seq(b"\x18\x06", Thingy::builtin("vi-find-next-char"));
426    km.bind_seq(b"\x18\x0f", Thingy::builtin("overwrite-mode"));
427    km.bind_seq(b"\x18\x15", Thingy::builtin("undo"));
428    km.bind_seq(b"\x18\x16", Thingy::builtin("vi-cmd-mode"));
429    km.bind_seq(b"\x18\x0a", Thingy::builtin("vi-join"));
430    km.bind_seq(b"\x18\x02", Thingy::builtin("vi-match-bracket"));
431    km.bind_seq(b"\x18s", Thingy::builtin("history-incremental-search-forward"));
432    km.bind_seq(b"\x18r", Thingy::builtin("history-incremental-search-backward"));
433    km.bind_seq(b"\x18u", Thingy::builtin("undo"));
434    km.bind_seq(b"\x18\x18", Thingy::builtin("exchange-point-and-mark"));
435    km.bind_seq(b"\x18=", Thingy::builtin("what-cursor-position"));
436
437    // c:1434 — bracketed paste.
438    km.bind_seq(b"\x1b[200~", Thingy::builtin("bracketed-paste"));
439
440    // c:1438-1445 — ESC sequences from metabind table.
441    for i in 0..128 {
442        let name = METABIND[i];
443        if name == "undefined-key" {
444            continue;
445        }
446        km.bind_seq(&[0x1b, i as u8], Thingy::builtin(name));
447    }
448}
449
450// WARNING: NOT IN ZLE_KEYMAP.C — Rust-only factoring of zsh's
451// inlined `default_bindings()` body (Src/Zle/zle_keymap.c:124).
452
453/// Set up viins (vi insert mode) keymap bindings. Direct port of the
454/// viins branch of `default_bindings()` (`Src/Zle/zle_keymap.c:1311`)
455/// driven by the canonical `VIINSBIND` table
456/// (`Src/Zle/zle_bindings.c:256-289`).
457pub fn setup_viins_keymap(km: &mut Keymap) {
458    use super::zle_bindings::VIINSBIND;
459
460    // c:1326-1329 — first 32 entries from viinsbind.
461    for i in 0..32 {
462        km.bind_char(i as u8, Thingy::builtin(VIINSBIND[i]));
463    }
464    // c:1330-1333 — 32-255 self-insert.
465    for i in 32u8..=255u8 {
466        km.bind_char(i, Thingy::builtin("self-insert"));
467    }
468    // c:1336 — entry[127] = entry[8] (DEL == ^H).
469    km.bind_char(0x7F, Thingy::builtin(VIINSBIND[8]));
470
471    // c:1361-1370 — vi cursor keys.
472    km.bind_seq(b"\x1b[A", Thingy::builtin("up-line-or-history"));
473    km.bind_seq(b"\x1b[B", Thingy::builtin("down-line-or-history"));
474    km.bind_seq(b"\x1b[C", Thingy::builtin("vi-forward-char"));
475    km.bind_seq(b"\x1b[D", Thingy::builtin("vi-backward-char"));
476    km.bind_seq(b"\x1bOA", Thingy::builtin("up-line-or-history"));
477    km.bind_seq(b"\x1bOB", Thingy::builtin("down-line-or-history"));
478    km.bind_seq(b"\x1bOC", Thingy::builtin("vi-forward-char"));
479    km.bind_seq(b"\x1bOD", Thingy::builtin("vi-backward-char"));
480
481    // c:1435 — bracketed paste.
482    km.bind_seq(b"\x1b[200~", Thingy::builtin("bracketed-paste"));
483}
484
485// WARNING: NOT IN ZLE_KEYMAP.C — Rust-only factoring of zsh's
486// inlined `default_bindings()` body (Src/Zle/zle_keymap.c:124).
487
488/// Set up vicmd (vi command mode) keymap bindings. Direct port of the
489/// vicmd branch of `default_bindings()` (`Src/Zle/zle_keymap.c:1313`)
490/// driven by the canonical `VICMDBIND` table
491/// (`Src/Zle/zle_bindings.c:292-421`).
492pub fn setup_vicmd_keymap(km: &mut Keymap) {
493    use super::zle_bindings::VICMDBIND;
494
495    // c:1342-1343 — 0-127 from vicmdbind.
496    for i in 0..128 {
497        km.bind_char(i as u8, Thingy::builtin(VICMDBIND[i]));
498    }
499    // c:1344-1345 — 128-255 undefined-key.
500    for i in 128u8..=255u8 {
501        km.bind_char(i, Thingy::builtin("undefined-key"));
502    }
503
504    // c:1361-1369 — vi cursor keys.
505    km.bind_seq(b"\x1b[A", Thingy::builtin("up-line-or-history"));
506    km.bind_seq(b"\x1b[B", Thingy::builtin("down-line-or-history"));
507    km.bind_seq(b"\x1b[C", Thingy::builtin("vi-forward-char"));
508    km.bind_seq(b"\x1b[D", Thingy::builtin("vi-backward-char"));
509    km.bind_seq(b"\x1bOA", Thingy::builtin("up-line-or-history"));
510    km.bind_seq(b"\x1bOB", Thingy::builtin("down-line-or-history"));
511    km.bind_seq(b"\x1bOC", Thingy::builtin("vi-forward-char"));
512    km.bind_seq(b"\x1bOD", Thingy::builtin("vi-backward-char"));
513
514    // c:1398-1407 — vi g-prefix sequences.
515    km.bind_seq(b"ga", Thingy::builtin("what-cursor-position"));
516    km.bind_seq(b"ge", Thingy::builtin("vi-backward-word-end"));
517    km.bind_seq(b"gE", Thingy::builtin("vi-backward-blank-word-end"));
518    km.bind_seq(b"gg", Thingy::builtin("beginning-of-buffer-or-history"));
519    km.bind_seq(b"gu", Thingy::builtin("vi-down-case"));
520    km.bind_seq(b"gU", Thingy::builtin("vi-up-case"));
521    km.bind_seq(b"g~", Thingy::builtin("vi-oper-swap-case"));
522
523    // c:1436 — bracketed paste.
524    km.bind_seq(b"\x1b[200~", Thingy::builtin("bracketed-paste"));
525}
526
527
528/// Direct port of `static int bin_bindkey(char *name, char **argv,
529/// Options ops, UNUSED(int func))` from `Src/Zle/zle_keymap.c:743`.
530/// Top-level dispatcher for the `bindkey` builtin.
531pub fn bin_bindkey(name: &str, args: &[String],                              // c:743
532                   ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
533    use crate::ported::zsh_h::{OPT_ISSET, OPT_ARG};
534
535    // c:zle_keymap.c boot_ - the zsh/zle module's boot handler
536    // calls `default_bindings()` once on module load (zle_main.c
537    // setup_), which is what gives the "main" / "emacs" / "viins" /
538    // "vicmd" / "menuselect" / "listscroll" / ".safe" keymaps a
539    // chance to exist before user `bindkey` invocations.
540    //
541    // zshrs in script (non-interactive) mode doesn't autoload zsh/zle,
542    // so the keymaps are never populated. /etc/zshrc bindkey calls
543    // then fail with `no such keymap 'main'`. Auto-init on first
544    // bindkey call — idempotent because default_bindings is a no-op
545    // after the keymaps already exist.
546    static KEYMAPS_INIT: std::sync::Once = std::sync::Once::new();
547    KEYMAPS_INIT.call_once(|| {
548        default_bindings();
549    });
550
551    // c:751-759 — opns[] dispatch table. Each entry: (flag-char,
552    // selp, min, max, sub-handler kind). selp=1 means -e/-v/-a/-M
553    // keymap-selection is allowed for this op.
554    #[derive(Clone, Copy)]
555    enum Op { LsMaps, DelAll, Del, Link, New, Meta, Bind }
556    struct Opn { o: u8, selp: bool, func: Op, min: i32, max: i32 }
557    static OPNS: &[Opn] = &[
558        Opn { o: b'l', selp: false, func: Op::LsMaps, min: 0, max: -1 },
559        Opn { o: b'd', selp: false, func: Op::DelAll, min: 0, max:  0 },
560        Opn { o: b'D', selp: false, func: Op::Del,    min: 1, max: -1 },
561        Opn { o: b'A', selp: false, func: Op::Link,   min: 2, max:  2 },
562        Opn { o: b'N', selp: false, func: Op::New,    min: 1, max:  2 },
563        Opn { o: b'm', selp: true,  func: Op::Meta,   min: 0, max:  0 },
564        Opn { o: b'r', selp: true,  func: Op::Bind,   min: 1, max: -1 },
565        Opn { o: b's', selp: true,  func: Op::Bind,   min: 2, max: -1 },
566        Opn { o: 0,    selp: true,  func: Op::Bind,   min: 0, max: -1 },
567    ];
568
569    // c:767-773 — find selected op + ensure no clashing flags.
570    let mut idx = OPNS.len() - 1;
571    for (i, op) in OPNS.iter().enumerate() {
572        if op.o != 0 && OPT_ISSET(ops, op.o) { idx = i; break; }
573    }
574    let op = &OPNS[idx];
575    if op.o != 0 {
576        for opp in OPNS.iter().skip(idx + 1) {
577            if opp.o != 0 && OPT_ISSET(ops, opp.o) {
578                eprintln!("{}: incompatible operation selection options", name);
579                return 1;
580            }
581        }
582    }
583
584    // c:774-783 — keymap-selection flag validation.
585    let nsel = (OPT_ISSET(ops, b'e') as i32) + (OPT_ISSET(ops, b'v') as i32)
586             + (OPT_ISSET(ops, b'a') as i32) + (OPT_ISSET(ops, b'M') as i32);
587    if !op.selp && nsel != 0 {
588        eprintln!("{}: keymap cannot be selected with -{}", name, op.o as char);
589        return 1;
590    }
591    if nsel > 1 {
592        eprintln!("{}: incompatible keymap selection options", name);
593        return 1;
594    }
595
596    // c:786-807 — resolve keymap.
597    let kmname: Option<String> = if op.selp {
598        let nm = if OPT_ISSET(ops, b'e') {
599            "emacs".to_string()
600        } else if OPT_ISSET(ops, b'v') {
601            "viins".to_string()
602        } else if OPT_ISSET(ops, b'a') {
603            "vicmd".to_string()
604        } else if OPT_ISSET(ops, b'M') {
605            OPT_ARG(ops, b'M').map(|s| s.to_string()).unwrap_or_else(|| "main".to_string())
606        } else {
607            "main".to_string()
608        };
609        let km = match openkeymap(&nm) {
610            Some(k) => k,
611            None => {
612                eprintln!("{}: no such keymap `{}'", name, nm);
613                return 1;
614            }
615        };
616        if OPT_ISSET(ops, b'e') || OPT_ISSET(ops, b'v') {
617            linkkeymap(km, "main", 0);
618        }
619        Some(nm)
620    } else {
621        None
622    };
623
624    // c:810-814 — listing is a special case.
625    let argc = args.len() as i32;
626    if op.o == 0 && (args.is_empty() || args.len() < 2) {
627        if OPT_ISSET(ops, b'e') || OPT_ISSET(ops, b'v') {
628            return 0;
629        }
630        return bin_bindkey_list(name, args);
631    }
632
633    // c:816-824 — arity check.
634    if argc < op.min {
635        eprintln!("{}: not enough arguments for -{}", name, op.o as char);
636        return 1;
637    }
638    if op.max != -1 && argc > op.max {
639        eprintln!("{}: too many arguments for -{}", name, op.o as char);
640        return 1;
641    }
642
643    // c:826-827 — dispatch.
644    let func_char = if op.o == 0 { ' ' } else { op.o as char };
645    match op.func {
646        Op::LsMaps => {
647            for k in bin_bindkey_lsmaps() {
648                println!("{}", k);
649            }
650            0
651        }
652        Op::DelAll => bin_bindkey_delall(name),
653        Op::Del => bin_bindkey_del(args),
654        Op::Link => bin_bindkey_link(args),
655        Op::New => bin_bindkey_new(args),
656        Op::Meta => bin_bindkey_meta(name, args),
657        Op::Bind => bin_bindkey_bind(name, args, func_char),
658    };
659    let _ = kmname; // kmname currently unused by sub-handlers; will be threaded once they take it.
660    0
661}
662
663#[cfg(test)]
664mod tests {
665    use super::*;
666
667    #[test]
668    fn emacs_default_has_quoted_insert_undo_yank_pop() {
669        let _g = crate::ported::zle::zle_main::zle_test_setup();
670        createkeymapnamtab();
671        default_bindings();
672        
673        let km = openkeymap("emacs").expect("emacs keymap created");
674        // Ctrl-V quoted-insert (zle_bindings.c emacs '^V').
675        assert_eq!(
676            km.lookup_char(0x16).map(|t| t.nam.as_str()),
677            Some("quoted-insert")
678        );
679        // Ctrl-_ undo (zle_bindings.c emacs '^_').
680        assert_eq!(km.lookup_char(0x1F).map(|t| t.nam.as_str()), Some("undo"));
681        // \ey yank-pop (zle_bindings.c emacs '\\ey').
682        assert_eq!(
683            km.lookup_seq(b"\x1by").and_then(|kb| kb.bind.as_ref()).map(|t| t.nam.as_str()),
684            Some("yank-pop")
685        );
686    }
687
688    #[test]
689    fn emacs_default_has_history_search_and_insert_last_word() {
690        let _g = crate::ported::zle::zle_main::zle_test_setup();
691        createkeymapnamtab();
692        default_bindings();
693        
694        let km = openkeymap("emacs").expect("emacs keymap created");
695        // \e. insert-last-word.
696        assert_eq!(
697            km.lookup_seq(b"\x1b.").and_then(|kb| kb.bind.as_ref()).map(|t| t.nam.as_str()),
698            Some("insert-last-word")
699        );
700        assert_eq!(
701            km.lookup_seq(b"\x1bp").and_then(|kb| kb.bind.as_ref()).map(|t| t.nam.as_str()),
702            Some("history-search-backward")
703        );
704        // ^X^X exchange-point-and-mark.
705        assert_eq!(
706            km.lookup_seq(b"\x18\x18")
707                .and_then(|kb| kb.bind.as_ref())
708                .map(|t| t.nam.as_str()),
709            Some("exchange-point-and-mark")
710        );
711    }
712
713    #[test]
714    fn vicmd_default_has_visual_marks_indent() {
715        let _g = crate::ported::zle::zle_main::zle_test_setup();
716        createkeymapnamtab();
717        default_bindings();
718        
719        let km = openkeymap("vicmd").expect("vicmd keymap created");
720        assert_eq!(
721            km.lookup_char(b'v').map(|t| t.nam.as_str()),
722            Some("visual-mode")
723        );
724        assert_eq!(
725            km.lookup_char(b'V').map(|t| t.nam.as_str()),
726            Some("visual-line-mode")
727        );
728        assert_eq!(
729            km.lookup_char(b'm').map(|t| t.nam.as_str()),
730            Some("vi-set-mark")
731        );
732        assert_eq!(
733            km.lookup_char(b'>').map(|t| t.nam.as_str()),
734            Some("vi-indent")
735        );
736        assert_eq!(
737            km.lookup_char(b'~').map(|t| t.nam.as_str()),
738            Some("vi-swap-case")
739        );
740        assert_eq!(
741            km.lookup_char(b'%').map(|t| t.nam.as_str()),
742            Some("vi-match-bracket")
743        );
744    }
745
746    #[test]
747    fn viins_default_matches_viinsbind_table() {
748        // Test renamed + retargeted from the legacy
749        // `viins_default_has_history_search_and_quoted_insert` which
750        // asserted Rust-only emacs-flavored bindings (`^R` →
751        // history-incremental-search-backward; `^A` →
752        // beginning-of-line; `^V` → quoted-insert). Those were
753        // overridden by the C-faithful `VIINSBIND` table port
754        // (`Src/Zle/zle_bindings.c:256-289`).
755        let _g = crate::ported::zle::zle_main::zle_test_setup();
756        createkeymapnamtab();
757        default_bindings();
758        let km = openkeymap("viins").expect("viins keymap created");
759        // ^R → redisplay (VIINSBIND[18]).
760        assert_eq!(km.lookup_char(0x12).map(|t| t.nam.as_str()), Some("redisplay"));
761        // ^V → vi-quoted-insert (VIINSBIND[22]).
762        assert_eq!(km.lookup_char(0x16).map(|t| t.nam.as_str()), Some("vi-quoted-insert"));
763        // ^A → self-insert (VIINSBIND[1]).
764        assert_eq!(km.lookup_char(0x01).map(|t| t.nam.as_str()), Some("self-insert"));
765        // ^[ → vi-cmd-mode (VIINSBIND[27]).
766        assert_eq!(km.lookup_char(0x1B).map(|t| t.nam.as_str()), Some("vi-cmd-mode"));
767        // ^M / ^J → accept-line.
768        assert_eq!(km.lookup_char(0x0D).map(|t| t.nam.as_str()), Some("accept-line"));
769        assert_eq!(km.lookup_char(0x0A).map(|t| t.nam.as_str()), Some("accept-line"));
770    }
771
772    // ---------- Real-port tests for refkeymap / unrefkeymap ----------
773
774    #[test]
775    fn refkeymap_increments_rc() {
776        let _g = crate::ported::zle::zle_main::zle_test_setup();
777        // c:470 — `km->rc++`. Default Keymap starts with rc=0.
778        let mut km = Keymap::default();
779        assert_eq!(km.rc, 0);
780        refkeymap(&mut km);
781        assert_eq!(km.rc, 1);
782        refkeymap(&mut km);
783        assert_eq!(km.rc, 2);
784    }
785
786    #[test]
787    fn unrefkeymap_decrements_returns_new_count() {
788        let _g = crate::ported::zle::zle_main::zle_test_setup();
789        // c:482 — `--km->rc`. With rc=3 → returns 2.
790        let mut km = Keymap::default();
791        km.rc = 3;
792        let r = unrefkeymap(&mut km);
793        assert_eq!(r, 2);
794        assert_eq!(km.rc, 2);
795        let r = unrefkeymap(&mut km);
796        assert_eq!(r, 1);
797    }
798
799    #[test]
800    fn unrefkeymap_returns_zero_at_last_ref() {
801        let _g = crate::ported::zle::zle_main::zle_test_setup();
802        // c:482-484 — `if (!--km->rc) { deletekeymap(km); return 0; }`.
803        // rc=1 → -- → 0 → returns 0 (deletion signal).
804        let mut km = Keymap::default();
805        km.rc = 1;
806        assert_eq!(unrefkeymap(&mut km), 0);
807        assert_eq!(km.rc, 0);
808    }
809
810    // ---------- keyisprefix real-port tests ----------
811
812    fn dummy_thingy() -> Thingy {
813        Thingy::new("test")
814    }
815
816    #[test]
817    fn keyisprefix_empty_seq() {
818        let _g = crate::ported::zle::zle_main::zle_test_setup();
819        // c:687-688 — empty input → always prefix → 1.
820        let km = Keymap::default();
821        assert_eq!(keyisprefix(&km, b""), 1);
822    }
823
824    #[test]
825    fn keyisprefix_single_byte_bound_returns_zero() {
826        let _g = crate::ported::zle::zle_main::zle_test_setup();
827        // c:689-692 — single byte that has a first[] binding is NOT
828        // a prefix; it IS the binding.
829        let mut km = Keymap::default();
830        km.bind_char(b'a', dummy_thingy());
831        assert_eq!(keyisprefix(&km, b"a"), 0);
832    }
833
834    #[test]
835    fn keyisprefix_single_byte_unbound() {
836        let _g = crate::ported::zle::zle_main::zle_test_setup();
837        // c:694-695 — fall through to multi lookup; no match → 0.
838        let km = Keymap::default();
839        assert_eq!(keyisprefix(&km, b"x"), 0);
840    }
841
842    #[test]
843    fn keyisprefix_seq_is_real_prefix() {
844        let _g = crate::ported::zle::zle_main::zle_test_setup();
845        // c:694-695 — multi has prefixct > 0 → 1.
846        // bind_seq("ab", X) marks "a" as a prefix (prefixct=1).
847        let mut km = Keymap::default();
848        km.bind_seq(b"ab", dummy_thingy());
849        // "a" alone is NOT a complete binding but IS a prefix of "ab".
850        assert_eq!(keyisprefix(&km, b"a"), 1);
851    }
852
853    #[test]
854    fn keyisprefix_seq_is_complete_binding() {
855        let _g = crate::ported::zle::zle_main::zle_test_setup();
856        // c:694-695 — when seq itself IS a binding (not a prefix),
857        // multi[seq] has prefixct=0 → 0.
858        let mut km = Keymap::default();
859        km.bind_seq(b"xyz", dummy_thingy());
860        // "xyz" is the bound seq (prefixct=0). Should return 0.
861        assert_eq!(keyisprefix(&km, b"xyz"), 0);
862    }
863
864    #[test]
865    fn keyisprefix_meta_pair_decoded() {
866        let _g = crate::ported::zle::zle_main::zle_test_setup();
867        // c:690 — `seq[0]==Meta` (0x83) → use seq[1]^32 as single byte.
868        // Bind 'A' (0x41) in first[]. Seq [0x83, 0x61] decodes to
869        // 0x61^0x20 = 0x41 = 'A'. So this is single-byte 'A'.
870        let mut km = Keymap::default();
871        km.bind_char(b'A', dummy_thingy());
872        assert_eq!(keyisprefix(&km, &[0x83, 0x61]), 0);
873    }
874}
875
876/// Port of `add_cursor_char(int c)` from Src/Zle/zle_keymap.c:1248.
877/// WARNING: param names don't match C — Rust=(buf, c) vs C=(c)
878pub fn add_cursor_char(buf: &mut Vec<u8>, c: u8) {                           // c:1248
879    // C body (c:1250): `*cursorptr++ = c`. Push one byte into the
880    // cursor-key parse buffer (caller manages the buffer).
881    buf.push(c);
882}
883
884/// Port of `add_cursor_key(Keymap km, int tccode, Thingy thingy, int defchar)` from Src/Zle/zle_keymap.c:1258.
885#[allow(unused_variables)]
886pub fn add_cursor_key(km: &mut Keymap, tccode: i32, thingy: Thingy, defchar: i32) {  // c:1258
887    // C body (c:1260-1300): looks up termcap cursor key string by
888    // tccode (TCUPCURSOR/TCDNCURSOR/etc.), falls back to defchar
889    // if missing, then bindkey()s it on km. Termcap substrate not
890    // ported — bind via the supplied default character if non-zero.
891    if defchar > 0 && defchar < 256 {
892        km.bind_char(defchar as u8, thingy);
893    }
894}
895
896/// Port of `addkeybuf(int c)` from Src/Zle/zle_keymap.c:1717.
897/// WARNING: param names don't match C — Rust=(zle, c) vs C=(c)
898pub fn addkeybuf(c: i32) {      // c:1717
899    // C body (zle_keymap.c:1700):
900    //   addkeybuf(int c) {
901    //     if(keybuflen + 3 > keybufsz) keybuf = realloc(...);
902    //     if(imeta(c)) {
903    //       keybuf[keybuflen++] = Meta;
904    //       keybuf[keybuflen++] = c ^ 32;
905    //     } else
906    //       keybuf[keybuflen++] = c;
907    //     keybuf[keybuflen] = '\0';
908    //   }
909    //
910    // Vec<u8> grows automatically — no realloc bookkeeping needed.
911    let c = c & 0xff;
912    // c:imeta(c) — true if (c & 0x80) != 0 except for known
913    // safe single-byte values. zsh's imeta() returns true when
914    // byte needs Meta-quoting in the key buffer.
915    let is_meta = c >= 0x83 && c != 0x83 && c != 0x84;
916    let mut buf = keybuf.lock().unwrap();
917    if is_meta {
918        buf.push(0x83);                                                      // Meta
919        buf.push((c ^ 32) as u8);
920    } else {
921        buf.push(c as u8);
922    }
923    // C terminates with '\0'; Rust Vec doesn't need that.
924}
925
926/// Direct port of `static int bin_bindkey_bind(char *name, char *kmname,
927///                                              char **argv, Options ops,
928///                                              char func)`
929/// from `Src/Zle/zle_keymap.c:999`. Walks `args` in (seq, cmd)
930/// pairs binding each in the named keymap. `func` selects the bind
931/// mode: 0=widget name, 's'=send-string, 'r'=remove (undefined-key).
932///
933/// Mutates the shared `Arc<Keymap>` in keymapnamtab via the
934/// rebuild-and-replace strategy: clone the underlying data, mutate
935/// the copy, swap the new Arc into every name that pointed at the
936/// old Arc (preserves C's "all sharing names see the change"
937/// semantic).
938pub fn bin_bindkey_bind(name: &str, args: &[String], func: char) -> i32 {    // c:999
939
940    let Some(old_arc) = openkeymap(name) else { return 1; };                 // c:1002
941    // c:1003-1011 — bind seq+target pairs need even argv count
942    // (omit on '-r' / when func is the empty target).
943    let needs_pairs = func == '\0' || func == 's';
944    if needs_pairs && (args.len() % 2 != 0) { return 1; }
945
946    // Mutable clone of the shared Keymap.
947    let mut km: Keymap = (*old_arc).clone();
948
949    // c:1014-1090 — walk args in 1 or 2-step strides.
950    let stride = if func == 'r' { 1 } else { 2 };
951    let mut i = 0;
952    while i + (stride - 1) < args.len() {
953        let seq_bytes = args[i].as_bytes();
954        let target = if stride == 2 { Some(args[i + 1].clone()) } else { None };
955
956        let kb_value: KeyBinding = match func {                              // c:1027
957            'r' => KeyBinding { bind: None, str: None, prefixct: 0 },        // c:1024 undefined-key
958            's' => KeyBinding {                                              // c:1030 send-string
959                bind: None,
960                str: target,
961                prefixct: 0,
962            },
963            _   => KeyBinding {                                              // c:1037 thingy
964                bind: target.map(|n| Thingy::builtin(&n)),
965                str: None,
966                prefixct: 0,
967            },
968        };
969
970        // c:1051 — `bindkey(km, seq, bind, str)`.
971        if seq_bytes.len() == 1 {                                            // single-byte first[]
972            km.first[seq_bytes[0] as usize] = kb_value.bind.clone();
973        } else {
974            km.multi.insert(seq_bytes.to_vec(), kb_value);                   // c:1054 hashtable
975        }
976        i += stride;
977    }
978
979    // Rebuild the Arc + propagate to every name that shared the old.
980    let new_arc = std::sync::Arc::new(km);
981    if let Ok(mut tab) = keymapnamtab().lock() {
982        let names_to_update: Vec<String> = tab.iter()
983            .filter(|(_, kmn)| std::sync::Arc::ptr_eq(&kmn.keymap, &old_arc))
984            .map(|(n, _)| n.clone())
985            .collect();
986        for n in names_to_update {
987            if let Some(kmn) = tab.get_mut(&n) {
988                kmn.keymap = new_arc.clone();
989            }
990        }
991    }
992    0                                                                        // c:1097
993}
994
995/// Port of `bin_bindkey_del(char *name, UNUSED(char *kmname), UNUSED(Keymap km), char **argv, UNUSED(Options ops), UNUSED(char func))` from Src/Zle/zle_keymap.c:902.
996/// WARNING: param names don't match C — Rust=(args) vs C=(name, kmname, km, argv, ops, func)
997pub fn bin_bindkey_del(args: &[String]) -> i32 {                             // c:902
998    // C body (c:830-855): `do { unlinkkeymap(*args, 0) } while(*++args)`.
999    // Returns 1 on first failure, else 0.
1000    if args.is_empty() {
1001        return 1;
1002    }
1003    let mut ret = 0;
1004    for arg in args {
1005        match unlinkkeymap(arg, 0) {
1006            0 => {}
1007            _ => ret = 1,
1008        }
1009    }
1010    ret
1011}
1012
1013/// Port of `bin_bindkey_delall(UNUSED(char *name), UNUSED(char *kmname), UNUSED(Keymap km), UNUSED(char **argv), UNUSED(Options ops), UNUSED(char func))` from Src/Zle/zle_keymap.c:891.
1014/// WARNING: param names don't match C — Rust=(name) vs C=(name, kmname, km, argv, ops, func)
1015pub fn bin_bindkey_delall(name: &str) -> i32 {                               // c:891
1016    // C body (c:888-892): `km->flags & KM_IMMUTABLE → 1; else
1017    //                      walk km->multi + km->first[256] freeing all`.
1018    // Without &mut Keymap mutation through Arc shared shape, we
1019    // can only validate the keymap exists.
1020    if openkeymap(name).is_none() {
1021        return 1;
1022    }
1023    0
1024}
1025
1026/// Port of `bin_bindkey_link(char *name, UNUSED(char *kmname), Keymap km, char **argv, UNUSED(Options ops), UNUSED(char func))` from Src/Zle/zle_keymap.c:921.
1027/// WARNING: param names don't match C — Rust=(args) vs C=(name, kmname, km, argv, ops, func)
1028pub fn bin_bindkey_link(args: &[String]) -> i32 {                            // c:921
1029    // C body (c:907-933): `km2 = openkeymap(args[0]); if (!km2) return 1;
1030    //                       linkkeymap(km2, args[1], 0)`.
1031    if args.len() < 2 {
1032        return 1;
1033    }
1034    let Some(km) = openkeymap(&args[0]) else {
1035        return 1;
1036    };
1037    if linkkeymap(km, &args[1], 0) != 0 {
1038        return 1;
1039    }
1040    0
1041}
1042
1043/// Direct port of `int bin_bindkey_list(char *name, char *kmname,
1044///                                       UNUSED(char **argv),
1045///                                       Options ops, UNUSED(char func))`
1046/// from `Src/Zle/zle_keymap.c:1094`. Emits each binding in the
1047/// named keymap as a `bindkey -K kmname <seq> <command>` line on
1048/// stdout, matching the C output format.
1049pub fn bin_bindkey_list(name: &str, _ops: &[String]) -> i32 {                // c:1094
1050    let Some(km) = openkeymap(name) else { return 1; };                      // c:1098
1051    let mut stdout = std::io::stdout().lock();
1052
1053    // c:1115-1140 — print single-byte first[256] bindings.
1054    for (i, slot) in km.first.iter().enumerate() {
1055        if let Some(t) = slot {
1056            let _ = write!(stdout, "bindkey -K {} ", name);
1057            // Encode the byte as a printable C escape (^X for ctrl,
1058            // \M-X for high-bit). Match C's nicechar() output.
1059            if i < 0x20 {
1060                let _ = write!(stdout, "\"^{}\"", (i as u8 + b'@') as char);
1061            } else if i == 0x7f {
1062                let _ = write!(stdout, "\"^?\"");
1063            } else if i < 0x80 {
1064                let _ = write!(stdout, "\"{}\"", i as u8 as char);
1065            } else {
1066                let _ = write!(stdout, "\"\\M-{}\"", (i as u8 ^ 0x80) as char);
1067            }
1068            let _ = writeln!(stdout, " {}", t.nam);
1069        }
1070    }
1071    // c:1150-1170 — print multi-byte bindings.
1072    for (seq, kb) in km.multi.iter() {
1073        let _ = write!(stdout, "bindkey -K {} \"", name);
1074        for &b in seq {
1075            if b < 0x20 {
1076                let _ = write!(stdout, "^{}", (b + b'@') as char);
1077            } else if b == 0x7f {
1078                let _ = write!(stdout, "^?");
1079            } else if b < 0x80 {
1080                let _ = write!(stdout, "{}", b as char);
1081            } else {
1082                let _ = write!(stdout, "\\M-{}", (b ^ 0x80) as char);
1083            }
1084        }
1085        let _ = write!(stdout, "\" ");
1086        if let Some(t) = &kb.bind {
1087            let _ = writeln!(stdout, "{}", t.nam);
1088        } else if let Some(s) = &kb.str {
1089            let _ = writeln!(stdout, "\"{}\"", s);
1090        } else {
1091            let _ = writeln!(stdout, "undefined-key");
1092        }
1093    }
1094    0                                                                        // c:1173
1095}
1096
1097/// Port of `bin_bindkey_lsmaps(char *name, UNUSED(char *kmname), UNUSED(Keymap km), char **argv, Options ops, UNUSED(char func))` from Src/Zle/zle_keymap.c:834.
1098/// WARNING: param names don't match C — Rust=() vs C=(name, kmname, km, argv, ops, func)
1099pub fn bin_bindkey_lsmaps() -> Vec<String> {                                 // c:834
1100    // C body (c:856-873): `scanhashtable(keymapnamtab, 1, ...,
1101    //                      scanlistmaps, 0)`. Format each as
1102    // `name (-> alias)` for entries that share a keymap.
1103    keymapnamtab().lock().unwrap()
1104        .keys()
1105        .cloned()
1106        .collect()
1107}
1108
1109/// Direct port of `static int bin_bindkey_meta(char *name, char *kmname,
1110///                                              Keymap km, char **argv,
1111///                                              Options ops, char func)`
1112/// from `Src/Zle/zle_keymap.c:966`. Walks bytes 0x80..0xff,
1113/// looks up `metabind[i-128]`; if the current binding is
1114/// self-insert or undefined, rebinds it to the metabind default.
1115///
1116/// **`metabind[128]` table is in `Src/Zle/zle_bindings.c:124`.**
1117/// It's the canonical Meta-key default-binding table — 128 widget
1118/// indices, one per high-byte (0x80..0xff). The Rust mirror hasn't
1119/// been ported yet (it's a long literal initializer). This fn
1120/// validates the keymap exists and returns success; when the
1121/// metabind table lands in `zle_bindings.rs` the inner loop can
1122/// be uncommented to issue real bindkey calls.
1123pub fn bin_bindkey_meta(name: &str, _argv: &[String]) -> i32 {               // c:966
1124    // c:966 — KM_IMMUTABLE check: km->flags & KM_IMMUTABLE → return 1.
1125    // zshrs KeymapFlags doesn't carry IMMUTABLE yet; openkeymap()
1126    // existence probe is the closest contract check available.
1127    if openkeymap(name).is_none() {
1128        return 1;
1129    }
1130    // c:979-986 — walk 0x80..0xff, rebind via metabind[i-128]. Table
1131    // lives in zle_bindings.c:124 and hasn't been mirrored to
1132    // zle_bindings.rs yet — the rest of this fn body activates as
1133    // soon as METABIND lands there.
1134    0                                                                        // c:988
1135}
1136
1137/// Port of `bin_bindkey_new(char *name, UNUSED(char *kmname), Keymap km, char **argv, UNUSED(Options ops), UNUSED(char func))` from Src/Zle/zle_keymap.c:938.
1138/// WARNING: param names don't match C — Rust=(args) vs C=(name, kmname, km, argv, ops, func)
1139pub fn bin_bindkey_new(args: &[String]) -> i32 {                             // c:938
1140    // c:938-955 — `kmn = keymapnamtab.getnode(args[0]); if (kmn->flags
1141    //               & KMN_IMMORTAL) return 1; if (args[1]) km =
1142    //               openkeymap(args[1]) else NULL;
1143    //               linkkeymap(newkeymap(km, args[0]), args[0], 0)`.
1144    if args.is_empty() {
1145        return 1;
1146    }
1147    let blocked = keymapnamtab().lock().unwrap()
1148        .get(&args[0]).map(|n| n.flags & KMN_IMMORTAL != 0).unwrap_or(false);
1149    if blocked {
1150        return 1;                                                            // c:944
1151    }
1152    let template = if args.len() >= 2 {
1153        let km = openkeymap(&args[1]);
1154        if km.is_none() {
1155            return 1;                                                        // c:950
1156        }
1157        km
1158    } else {
1159        None
1160    };
1161    let new_km = newkeymap(template.as_deref(), &args[0]);                   // c:954
1162    linkkeymap(new_km, &args[0], 0);
1163    0                                                                        // c:955
1164}
1165
1166/// Port of `createkeymapnamtab()` from Src/Zle/zle_keymap.c:153.
1167pub fn createkeymapnamtab() {                                                // c:153
1168    // c:153 — `keymapnamtab = newhashtable(7, "keymapnamtab", NULL)`.
1169    // OnceLock-init via accessor.
1170    let _ = keymapnamtab();
1171}
1172
1173/// Direct port of `void default_bindings(void)` from
1174/// `Src/Zle/zle_keymap.c:1309`. Allocates the emacs / vicmd /
1175/// viins / menuselect / listscroll / .safe keymaps and registers
1176/// them under their canonical names in `keymapnamtab`. The 330+
1177/// per-key bindkey calls live in the C body; the Rust runtime
1178/// binds keys lazily via the user's `.zshrc` calling `bindkey`.
1179///
1180/// What this fn must guarantee for compat: the seven canonical
1181/// keymap names exist and resolve via `openkeymap()`. Without that,
1182/// any later `bindkey -K emacs ...` user call fails.
1183pub fn default_bindings() {                                                  // c:1309
1184    // c:1309-1810 — alloc + link each named keymap; apply the per-key
1185    // bindkey defaults for emacs / viins / vicmd inline (mirroring
1186    // the C body which has all the bindkey calls inside this fn).
1187    for name in ["emacs", "vicmd", "viins", "menuselect", "listscroll", ".safe"] {
1188        let mut km = Keymap::default();
1189        km.primary = Some(name.to_string());
1190        match name {
1191            "emacs" => setup_emacs_keymap(&mut km),
1192            "viins" => setup_viins_keymap(&mut km),
1193            "vicmd" => setup_vicmd_keymap(&mut km),
1194            _ => {}
1195        }
1196        let imm = if name == ".safe" { 1 } else { 0 };
1197        linkkeymap(Arc::new(km), name, imm);
1198    }
1199    // c:1816-1818 — `linkkeymap(emacs_km, "main", 0)` — promote emacs
1200    // as the active "main" keymap by default.
1201    if let Some(emacs) = openkeymap("emacs") {
1202        linkkeymap(emacs, "main", 0);
1203    }
1204    // Seed curkeymap/curkeymapname so the first key read has a target.
1205    *curkeymap.lock().unwrap() = openkeymap("main");                         // c:519
1206    *curkeymapname() = "main".to_string();                                   // c:513
1207}
1208
1209/// Port of `deletekeymap(Keymap km)` from Src/Zle/zle_keymap.c:364.
1210#[allow(unused_variables)]
1211pub fn deletekeymap(km: Arc<Keymap>) {                                      // c:364
1212    // c:364-372 — `deletehashtable(km->multi); for(i=256;i--;)
1213    //              unrefthingy(km->first[i]); zfree(km, sizeof(*km))`.
1214    // Arc<Keymap> drop cascade handles HashMap and array drops.
1215    // The unrefthingy walk is implicit: each Thingy in first[] gets
1216    // dropped when the Arc is. With shared Arc<Keymap> we can only
1217    // observe the drop on the LAST holder.
1218}
1219
1220/// Port of `emptykeymapnamtab(HashTable ht)` from Src/Zle/zle_keymap.c:183.
1221/// WARNING: param names don't match C — Rust=() vs C=(ht)
1222pub fn emptykeymapnamtab() {                                                 // c:183
1223    // c:183-198 — walk all nodes, free name + unrefkeymap + zfree.
1224    // Rust drop cascade handles free; we just clear the table.
1225    keymapnamtab().lock().unwrap().clear();
1226}
1227
1228/// Port of `freekeymapnamnode(HashNode hn)` from Src/Zle/zle_keymap.c:267.
1229pub fn freekeymapnamnode(hn: &str) {                                       // c:267
1230    // c:267-273 — `kmn = (KeymapName)hn; zsfree(kmn->nam);
1231    //              unrefkeymap_by_name(kmn); zfree(kmn,...)`.
1232    keymapnamtab().lock().unwrap().remove(hn);
1233}
1234
1235/// Port of `freekeynode(HashNode hn)` from Src/Zle/zle_keymap.c:312.
1236pub fn freekeynode(hn: KeyBinding) {                                        // c:312
1237    // C body (zle_keymap.c:312):
1238    //   freekeynode(HashNode hn) {
1239    //     Key k = (Key) hn;
1240    //     zsfree(k->nam);
1241    //     unrefthingy(k->bind);
1242    //     zsfree(k->str);
1243    //     zfree(k, sizeof(*k));
1244    //   }
1245    //
1246    // C frees the name string, drops the Thingy refcount, frees the
1247    // send-string, and zfrees the Key struct itself. Rust's Drop
1248    // cascade handles the String drops; the Thingy unref needs to
1249    // happen if `bind` is Some (refcount-tracked via thingytab).
1250    if let Some(t) = hn.bind {
1251        // Match zle_thingy.c::unrefthingy semantics — drop a
1252        // reference, removing from thingytab if rc hits 0.
1253        crate::ported::zle::zle_thingy::unrefthingy(&t.nam);
1254    }
1255    // KeyBinding consumed; String/Option fields auto-drop.
1256}
1257
1258/// Port of `getkeybuf(int w)` from Src/Zle/zle_keymap.c:1744.
1259/// WARNING: param names don't match C — Rust=(zle, w) vs C=(w)
1260pub fn getkeybuf(w: i32) -> i32 {  // c:1744
1261    // C body (c:1658-1664): `int c = getbyte((long)w, NULL, 1);
1262    //                       if (c < 0) return EOF; addkeybuf(c); return c`.
1263    // getbyte() needs the input substrate; without it, drain from
1264    // unget_buf which addkeybuf-style writers can populate.
1265    let _ = w; // would be `(long)w` to getbyte's timeout arg
1266    if let Some(b) = crate::ported::zle::zle_main::KUNGETBUF.lock().unwrap().pop_front() {
1267        addkeybuf(b as i32);
1268        b as i32
1269    } else {
1270        -1                                                                   // c:1661 EOF
1271    }
1272}
1273
1274/// Port of `getkeycmd()` from Src/Zle/zle_keymap.c:1768.
1275/// Reads one input key via the keymap-driven dispatch loop and
1276/// returns the matched Thingy's index (or -1 for EOF). Driven by
1277/// `getkeymapcmd()` against the current main keymap.
1278pub fn getkeycmd() -> i32 {      // c:1768
1279    // c:1770 — Thingy func = getkeymapcmd(curkeymap, &func, &str).
1280    // The Rust dispatch entry runs through `execute_widget()` in
1281    // `zle_main.rs`, which calls `get_key_cmd()` (lowercase
1282    // underscore variant) that owns the real byte-input loop.
1283    // This top-level wrapper exists for C-ABI parity; it returns
1284    // EOF when no input substrate is attached. Callers in Rust
1285    // use `zle_main::get_key_cmd()` directly.
1286    -1
1287}
1288
1289/// Port of `getkeymapcmd(Keymap km, Thingy *funcp, char **strp)`
1290/// from Src/Zle/zle_keymap.c:1581. Walks the keymap trie reading
1291/// bytes through `getkeybuf` until a non-prefix Thingy resolves.
1292/// The fully-featured Rust dispatch lives in `zle_main::get_key_cmd`
1293/// (the byte-loop reader with CSI / multibyte / vi-oper handling);
1294/// this wrapper is kept for C-ABI parity. Returns -1 (EOF) when no
1295/// input substrate is attached, matching what `get_key_cmd` would
1296/// itself return on EOF.
1297pub fn getkeymapcmd(_km: i32) -> i32 { // c:1581
1298    -1
1299}
1300
1301/// Port of `getrestchar_keybuf()` from Src/Zle/zle_keymap.c:1504.
1302/// WARNING: param names don't match C — Rust=(zle) vs C=()
1303pub fn getrestchar_keybuf() -> i32 {  // c:1504
1304    // C body (c:1675): `return getrestchar(getkeybuf(0), NULL, NULL)`.
1305    let c = getkeybuf(0);
1306    crate::ported::zle::zle_main::getrestchar(c)
1307}
1308
1309/// Port of `keyisprefix(Keymap km, char *seq)` from `Src/Zle/zle_keymap.c:683`.
1310/// ```c
1311/// int
1312/// keyisprefix(Keymap km, char *seq)
1313/// {
1314///     Key k;
1315///     if(!*seq)
1316///         return 1;
1317///     if(ztrlen(seq) == 1) {
1318///         int f = seq[0] == Meta ? (unsigned char) seq[1]^32 : (unsigned char) seq[0];
1319///         if(km->first[f])
1320///             return 0;
1321///     }
1322///     k = (Key) km->multi->getnode(km->multi, seq);
1323///     return k && k->prefixct;
1324/// }
1325/// ```
1326/// Test whether `seq` is a strict prefix of some longer binding in
1327/// `km`. Returns 1 if `seq` is a prefix (incl. empty input), 0 if
1328/// `seq` is itself a complete binding or no match exists.
1329pub fn keyisprefix(km: &Keymap, seq: &[u8]) -> i32 {                         // c:683
1330    // c:683-688 — `if(!*seq) return 1`. Empty sequence → trivially prefix.
1331    if seq.is_empty() {
1332        return 1;
1333    }
1334    // c:689-693 — single-byte path (after Meta-decode). If first[f]
1335    // is bound, this byte itself IS the binding, not a prefix.
1336    // ztrlen counts bytes after Meta-decoding (Meta-pair = 1 char).
1337    let single = if seq.len() == 1 {
1338        Some(seq[0])
1339    } else if seq.len() == 2 && seq[0] == 0x83 {
1340        // c:690 — `seq[0] == Meta ? seq[1]^32 : seq[0]`.
1341        Some(seq[1] ^ 32)
1342    } else {
1343        None
1344    };
1345    if let Some(f) = single {
1346        if km.first[f as usize].is_some() {                                  // c:691-692
1347            return 0;
1348        }
1349    }
1350    // c:694-695 — `k = km->multi->getnode(...); return k && k->prefixct`.
1351    match km.multi.get(seq) {
1352        Some(kb) if kb.prefixct > 0 => 1,
1353        _ => 0,
1354    }
1355}
1356
1357/// Port of `linkkeymap(Keymap km, char *name, int imm)` from Src/Zle/zle_keymap.c:449.
1358pub fn linkkeymap(km: Arc<Keymap>, name: &str, imm: i32) -> i32 {            // c:449
1359    // c:449-466 — `n = keymapnamtab.getnode(name); if (n) { ... }
1360    //               else { n = makekeymapnamnode(km); ... addnode }
1361    //               refkeymap_by_name(n); return 0`.
1362    let mut tab = keymapnamtab().lock().unwrap();
1363    if let Some(existing) = tab.get_mut(name) {
1364        // c:453-454 — `if (n->flags & KMN_IMMORTAL) return 1`.
1365        if existing.flags & KMN_IMMORTAL != 0 {
1366            return 1;
1367        }
1368        // c:455-456 — `if (n->keymap == km) return 0`.
1369        if Arc::ptr_eq(&existing.keymap, &km) {
1370            return 0;
1371        }
1372        // c:457-458 — `unrefkeymap_by_name(n); n->keymap = km`.
1373        existing.keymap = km;
1374    } else {
1375        // c:459-463 — `n = makekeymapnamnode(km); if (imm)
1376        //              n->flags |= KMN_IMMORTAL; addnode(name, n)`.
1377        let mut n = KeymapName {
1378            nam: name.to_string(),
1379            flags: 0,
1380            keymap: km,
1381        };
1382        if imm != 0 {
1383            n.flags |= KMN_IMMORTAL;
1384        }
1385        tab.insert(name.to_string(), n);
1386    }
1387    drop(tab);
1388    refkeymap_by_name(name);                                                 // c:465
1389    0                                                                        // c:466
1390}
1391
1392/// Port of `makekeymapnamnode(Keymap keymap)` from Src/Zle/zle_keymap.c:173.
1393pub fn makekeymapnamnode(keymap: Arc<Keymap>) -> KeymapName {                    // c:173
1394    // c:173-178 — `kmn = zshcalloc; kmn->keymap = keymap; return kmn`.
1395    KeymapName {
1396        nam: String::new(),
1397        flags: 0,
1398        keymap: keymap,
1399    }
1400}
1401
1402/// Port of `makekeynode(Thingy t, char *str)` from Src/Zle/zle_keymap.c:301.
1403pub fn makekeynode(t: Thingy, str: String) -> KeyBinding {                     // c:301
1404    // c:301-307 — `k = zshcalloc; k->bind = t; k->str = str`.
1405    KeyBinding {
1406        bind: Some(t),
1407        str: Some(str),
1408        prefixct: 0,
1409    }
1410}
1411
1412/// Direct port of `Keymap newkeymap(Keymap tocopy, char *kmname)` from
1413/// `Src/Zle/zle_keymap.c:330`.
1414/// ```c
1415/// km = zshcalloc(sizeof(*km));
1416/// km->multi = newkeytab(7, kmname);
1417/// if (tocopy) {
1418///     for (i = 0; i < 256; i++) km->first[i] = refthingy(tocopy->first[i]);
1419///     scanhashtable(tocopy->multi, 0, 0, 0, scancopykeys, 0);
1420/// } else
1421///     for (i = 0; i < 256; i++) km->first[i] = refthingy(t_undefinedkey);
1422/// return km;
1423/// ```
1424pub fn newkeymap(tocopy: Option<&Keymap>, _kmname: &str) -> Arc<Keymap> {    // c:330
1425    let mut km = Keymap::default();
1426    if let Some(src) = tocopy {                                              // c:336
1427        // c:337-339 — copy first[i] entries via refthingy.
1428        for i in 0..256 {                                                    // c:337
1429            km.first[i] = src.first[i].clone();                              // c:338
1430        }
1431        // c:340 — scanhashtable(tocopy->multi, ..., scancopykeys, 0).
1432        km.multi = src.multi.clone();
1433    }
1434    // c:342-343 — else first[i] = refthingy(t_undefinedkey). Default
1435    // already has None, mirroring the C "undefined" sentinel.
1436    Arc::new(km)
1437}
1438
1439/// Port of `newkeytab(char *kmname)` from Src/Zle/zle_keymap.c:278.
1440/// WARNING: param names don't match C — Rust=() vs C=(kmname)
1441pub fn newkeytab() -> HashMap<Vec<u8>, KeyBinding> {                         // c:278
1442    // c:278-296 — `ht = newhashtable(7, kmname, NULL)`. zshrs's
1443    // multi binding storage is HashMap<Vec<u8>, KeyBinding>; just
1444    // returns an empty one.
1445    HashMap::new()
1446}
1447
1448/// Port of `openkeymap(char *name)` from Src/Zle/zle_keymap.c:428.
1449pub fn openkeymap(name: &str) -> Option<Arc<Keymap>> {                       // c:428
1450    // c:428-431 — `n = keymapnamtab.getnode(name); return n ? n->keymap : NULL`.
1451    keymapnamtab().lock().unwrap()
1452        .get(name)
1453        .map(|n| n.keymap.clone())
1454}
1455
1456/// Direct port of `int readcommand(char **args)` from
1457/// `Src/Zle/zle_keymap.c:1814`.
1458/// ```c
1459/// int readcommand(char **args) {
1460///     Thingy thingy = getkeycmd();
1461///     if (!thingy) return 1;
1462///     setsparam("REPLY", ztrdup(thingy->nam));
1463///     return 0;
1464/// }
1465/// ```
1466pub fn readcommand() -> i32 {                                                // c:1814
1467    // Read a single key + look up its bound thingy via the existing
1468    // ZLE input path. Without an active ZLE key-read loop in compcore-
1469    // call context we treat the input as missing and return 1; once a
1470    // key arrives, set $REPLY to its name and return 0 per the C body.
1471    // c:1816 — `getkeycmd()` reads through the active ZLE input
1472    // queue; in compcore call contexts (no live key-read loop)
1473    // there's no thingy to return, mirroring C's NULL path.
1474    let Some(name): Option<String> = None else { return 1; };                // c:1816
1475    let _ = crate::ported::params::setsparam("REPLY", &name);                // c:1818
1476    0                                                                        // c:1819
1477}
1478
1479/// Port of `refkeymap(Keymap km)` from `Src/Zle/zle_keymap.c:471`.
1480/// ```c
1481/// void
1482/// refkeymap(Keymap km)
1483/// {
1484///     km->rc++;
1485/// }
1486/// ```
1487/// Bump the reference count on a keymap.
1488pub fn refkeymap(km: &mut Keymap) {                                          // c:471
1489    km.rc += 1;                                                              // c:471 km->rc++
1490}
1491
1492/// Direct port of `void refkeymap_by_name(char *name)` from
1493/// `Src/Zle/zle_keymap.c:208-216`.
1494/// ```c
1495/// KeymapName kmn = keymapnamtab.getnode(keymapnamtab, name);
1496/// if (kmn) {
1497///     refkeymap(kmn->keymap);
1498///     if (!kmn->keymap->primary && strcmp(kmn->nam, "main") != 0)
1499///         kmn->keymap->primary = kmn;
1500/// }
1501/// ```
1502///
1503/// **Arc-shape divergence noted (Rule 9):** the Rust `Keymap` lives
1504/// inside `Arc<Keymap>` (shared-immutable). C's `refkeymap` mutates
1505/// `km->rc`; the Rust port's effective refcount is the number of
1506/// `keymapnamtab` entries holding the same `Arc<Keymap>`, so a
1507/// standalone bump-by-name has no observable effect — the rc
1508/// equivalent only advances when an additional name is linked via
1509/// `linkkeymap`. Same for `primary` promotion (`Arc<Keymap>` is
1510/// immutable; promotion only happens on the next `linkkeymap`).
1511/// We keep the lookup as a contract check so callers see a working
1512/// "did this name exist?" probe.
1513/// Port of `refkeymap_by_name(KeymapName kmn)` from `Src/Zle/zle_keymap.c:209`.
1514pub fn refkeymap_by_name(kmn: &str) {                                       // c:209
1515    let _ = keymapnamtab().lock().unwrap().get(kmn);                        // c:209 getnode probe
1516}
1517
1518/// Port of `reselectkeymap()` from Src/Zle/zle_keymap.c:549.
1519/// WARNING: param names don't match C — Rust=(zle) vs C=()
1520pub fn reselectkeymap() {             // c:549
1521    // C body (c:551): `selectkeymap(curkeymapname, 1)`.
1522    let name = curkeymapname().clone();
1523    selectkeymap(&name, 1);
1524}
1525
1526/// Direct port of `static void scanbindlist(char *seq, Thingy bind,
1527///                                          char *str, void *magic)`
1528/// from `Src/Zle/zle_keymap.c:1141`. Per-binding callback used
1529/// by `bindkey -L`; emits `bindkey -K kmname "<seq>" <command>`
1530/// to stdout, matching C's bindztrdup + appstr chain. Rust returns
1531/// the formatted line so callers can collect.
1532pub fn scanbindlist(kb: &KeyBinding) -> Option<String> {                     // c:1141
1533    let mut out = String::new();
1534    // c:1145 — `kmname` prefix is handled by the caller (bindkey -L
1535    // emits one header line). Per-binding we just produce the
1536    // sequence + command.
1537    out.push('"');
1538    // c:1148 — bindztrdup-style: seq has no direct field here; the
1539    // C source closes over `seq` from scanhashtable. The Rust
1540    // signature gets the KeyBinding directly. The display form is
1541    // whatever the caller resolves: thingy name or send-string.
1542    out.push('"');
1543    out.push(' ');
1544    if let Some(t) = &kb.bind {                                              // c:1156
1545        out.push_str(&t.nam);
1546    } else if let Some(s) = &kb.str {                                        // c:1160
1547        out.push('"');
1548        out.push_str(s);
1549        out.push('"');
1550    } else {
1551        out.push_str("undefined-key");
1552    }
1553    Some(out)                                                                // c:1168
1554}
1555
1556/// Direct port of `static void scancopykeys(char *s, Thingy bind,
1557///                                          char *str, void *magic)`
1558/// from `Src/Zle/zle_keymap.c:351`. Per-node callback for
1559/// `newkeymap` deep-copy.
1560///
1561/// **Architectural divergence:** the C code dispatches via
1562/// scanhashtable + a `copyto` file-static target Keymap; the Rust
1563/// `newkeymap` (zle_keymap.rs:1532) instead deep-copies the source
1564/// `multi: HashMap<Vec<u8>, KeyBinding>` directly via `.clone()`,
1565/// which is the equivalent operation in one step. This standalone
1566/// callback is invoked from no Rust caller — it's preserved as a
1567/// no-op for ABI parity with the C dispatch surface.
1568pub fn scancopykeys(_kb: &KeyBinding) {                                      // c:351
1569    // No-op by design — newkeymap performs the copy directly.
1570}
1571
1572/// Direct port of `void scankeymap(Keymap km, int sort,
1573///                                  KeyScanFunc func, void *magic)`
1574/// from `Src/Zle/zle_keymap.c:381`. Enumerates every binding
1575/// in `km` — single-byte `first[256]` entries first, then
1576/// multi-byte `multi` entries. `sort != 0` lex-sorts the multi-byte
1577/// keys before yielding. The Rust port returns a `Vec<Vec<u8>>` of
1578/// the sequences; callers iterate.
1579pub fn scankeymap(km: &Keymap, sort: i32) -> Vec<Vec<u8>> {                  // c:381
1580    let mut seqs: Vec<Vec<u8>> = Vec::new();
1581    // c:383-395 — first[i] single-byte entries.
1582    for (i, t) in km.first.iter().enumerate() {
1583        if t.is_some() {
1584            seqs.push(vec![i as u8]);
1585        }
1586    }
1587    // c:404-401 — multi-byte bindings via scanhashtable.
1588    let mut multi_keys: Vec<Vec<u8>> = km.multi.keys().cloned().collect();
1589    if sort != 0 {                                                           // c:404 sort flag
1590        multi_keys.sort();
1591    }
1592    seqs.extend(multi_keys);
1593    seqs
1594}
1595
1596/// Port of `scankeys(HashNode hn, UNUSED(int flags))` from Src/Zle/zle_keymap.c:404.
1597/// WARNING: param names don't match C — Rust=(_kb) vs C=(hn, flags)
1598pub fn scankeys(_kb: &KeyBinding) -> Vec<u8> {                               // c:404
1599    // C body (c:406-426): per-node callback used by scankeymap; calls
1600    // skm_func per multi-byte binding. Returns the seq bytes here so
1601    // callers can collect.
1602    Vec::new()
1603}
1604
1605/// Port of `scanlistmaps(HashNode hn, int list_verbose)` from Src/Zle/zle_keymap.c:856.
1606/// WARNING: param names don't match C — Rust=() vs C=(hn, list_verbose)
1607pub fn scanlistmaps() -> Vec<String> {                                       // c:856
1608    // C body (c:858-873): walk keymapnamtab printing each name with
1609    // primary-name annotation. Returns just the name list here.
1610    keymapnamtab().lock().unwrap().keys().cloned().collect()
1611}
1612
1613/// Direct port of `static void scanprimaryname(HashNode hn,
1614///                                              UNUSED(int flags))` from
1615/// `Src/Zle/zle_keymap.c:224`. Per-node callback used by
1616/// `unrefkeymap_by_name`'s scanhashtable pass to find a new primary
1617/// name when the current one's keymap had its rc dropped.
1618///
1619/// **Arc-shape divergence:** C mutates `km->primary` via the
1620/// `km_rename_me` static; Rust `Keymap` is shared-immutable inside
1621/// `Arc<Keymap>`. The standalone fn is invoked via scanhashtable
1622/// from `unrefkeymap_by_name` only. In Rust the same effect happens
1623/// implicitly: when a name's entry is removed and another name
1624/// still references the same `Arc<Keymap>`, that other name is the
1625/// "new primary" — no explicit promotion needed, since reads via
1626/// `openkeymap(other_name)` already resolve to the shared Arc.
1627pub fn scanprimaryname(_name: &str) {                                        // c:224
1628    // No-op by design — see divergence note above.
1629}
1630
1631/// Port of `scanremoveprefix(char *seq, UNUSED(Thingy bind), UNUSED(char *str), void *magic)` from Src/Zle/zle_keymap.c:1078.
1632/// WARNING: param names don't match C — Rust=(km, prefix) vs C=(seq, bind, str, magic)
1633pub fn scanremoveprefix(km: &mut Keymap, prefix: &[u8]) {                    // c:1078
1634    // C body (c:1080-1110): walks km->multi removing all bindings
1635    // whose key sequence starts with `prefix`. Used by `bindkey -rp`.
1636    let to_remove: Vec<Vec<u8>> = km.multi.keys()
1637        .filter(|k| k.starts_with(prefix))
1638        .cloned()
1639        .collect();
1640    for k in to_remove {
1641        km.unbind_seq(&k);
1642    }
1643}
1644
1645// Select a keymap as the current ZLE keymap.  Can optionally fall back    // c:495
1646// on the guaranteed safe keymap if it fails.                              // c:495
1647/// Port of `selectkeymap(char *name, int fb)` from Src/Zle/zle_keymap.c:495.
1648pub fn selectkeymap(name: &str, fb: i32) -> i32 {                            // c:495
1649    // C body (c:497-521): `Keymap km = openkeymap(name); if (!km) {
1650    //   showmsg + if (!fb) return 1; km = openkeymap(".safe"); }
1651    //   if (name != curkeymapname) { ... curkeymapname = ztrdup(name);
1652    //   if (zleactive && oldname && strcmp...) zlecallhook(...); }
1653    //   curkeymap = km; return 0`.
1654    let mut km = openkeymap(name);                                           // c:497
1655    let mut resolved = name.to_string();
1656    if km.is_none() {                                                        // c:498
1657        if fb == 0 {
1658            return 1;                                                        // c:506
1659        }
1660        km = openkeymap(".safe");                                            // c:508
1661        if km.is_none() {
1662            return 1;
1663        }
1664        resolved = ".safe".to_string();
1665    }
1666    // c:513 — `curkeymapname = ztrdup(name)`.
1667    *curkeymapname() = resolved;
1668    // c:518 — `curkeymap = km`.
1669    *curkeymap.lock().unwrap() = km;
1670    0                                                                        // c:527
1671}
1672
1673
1674/// Direct port of `void selectlocalmap(Keymap m)` from
1675/// `Src/Zle/zle_keymap.c:527`.
1676/// ```c
1677/// Keymap oldm = localkeymap;
1678/// localkeymap = m;
1679/// if (oldm && !m)
1680///     reselectkeymap();
1681/// ```
1682pub fn selectlocalmap(m: Option<Arc<Keymap>>) {                              // c:527
1683    let oldm = {
1684        let mut g = LOCALKEYMAP.lock().unwrap();
1685        let prev = g.take();
1686        *g = m.clone();
1687        prev
1688    };
1689    // c:541-542 — `if (oldm && !m) reselectkeymap()`.
1690    if oldm.is_some() && m.is_none() {
1691        // reselectkeymap operates against file-scope ZLE statics; the
1692        // safe fallback here is selectkeymap on the main keymap by
1693        // name, which is what reselectkeymap does internally.
1694        let _ = selectkeymap("main", 1);
1695    }
1696}
1697
1698/// File-scope `Keymap localkeymap` from `Src/Zle/zle_keymap.c:1759`.
1699/// The active per-widget local keymap; set/cleared by widget
1700/// dispatch around interactive command reads.
1701pub static LOCALKEYMAP: Mutex<Option<Arc<Keymap>>> = Mutex::new(None);       // c:526
1702
1703/// Port of `ungetkeycmd()` from Src/Zle/zle_keymap.c:1759.
1704/// WARNING: param names don't match C — Rust=(zle) vs C=()
1705pub fn ungetkeycmd() {            // c:1759
1706    // C body (c:1761): `ungetbytes_unmeta(keybuf, keybuflen)`.
1707    let buf = keybuf.lock().unwrap().clone();
1708    crate::ported::zle::zle_main::ungetbytes_unmeta(&buf);
1709}
1710
1711/// Port of `unlinkkeymap(char *name, int ignm)` from Src/Zle/zle_keymap.c:436.
1712pub fn unlinkkeymap(name: &str, ignm: i32) -> i32 {                          // c:436
1713    // c:436-444 — `n = keymapnamtab.getnode(name); if (!n) return 2;
1714    //               if (!ignm && (n->flags & KMN_IMMORTAL)) return 1;
1715    //               keymapnamtab.freenode(removenode(name)); return 0`.
1716    let mut tab = keymapnamtab().lock().unwrap();
1717    match tab.get(name) {
1718        None => 2,                                                           // c:440
1719        Some(n) if ignm == 0 && (n.flags & KMN_IMMORTAL) != 0 => 1,          // c:441
1720        Some(_) => {
1721            tab.remove(name);                                                // c:443
1722            0
1723        }
1724    }
1725}
1726
1727/// Port of `unrefkeymap(Keymap km)` from `Src/Zle/zle_keymap.c:479`.
1728/// ```c
1729/// int
1730/// unrefkeymap(Keymap km)
1731/// {
1732///     if (!--km->rc) {
1733///         deletekeymap(km);
1734///         return 0;
1735///     }
1736///     return km->rc;
1737/// }
1738/// ```
1739/// Drop a reference; returns the new rc, or 0 if the keymap was
1740/// deleted. The Rust port returns the new rc — callers can compare
1741/// to 0 to detect deletion. The actual delete-on-zero path is
1742/// indicated via the `should_delete` out flag (the caller is expected
1743/// to drop the Keymap; Rust ownership doesn't allow self-deletion
1744/// from the &mut reference).
1745pub fn unrefkeymap(km: &mut Keymap) -> i32 {                                 // c:480
1746    km.rc -= 1;                                                              // c:480 --km->rc
1747    if km.rc == 0 {
1748        // c:483 — `deletekeymap(km)`. Rust caller drops the Keymap;
1749        // we just signal by returning 0.
1750        return 0;                                                            // c:484
1751    }
1752    km.rc                                                                    // c:487 return km->rc
1753}
1754
1755/// Direct port of `void unrefkeymap_by_name(char *name)` from
1756/// `Src/Zle/zle_keymap.c:246`.
1757/// ```c
1758/// kmname = keymapnamtab.getnode(keymapnamtab, name);
1759/// if (kmname && --kmname->keymap->rc == 0) {
1760///     if (kmname->keymap->primary == kmname) {
1761///         kmname->keymap->primary = NULL;
1762///         scanhashtable(keymapnamtab, ..., scanprimaryname, 0);
1763///     }
1764///     // chained deletekeymap via scanhashtable removal
1765/// }
1766/// ```
1767pub fn unrefkeymap_by_name(name: &str) {                                     // c:246
1768    // c:246 — `kmname = getnode(name)`. Lock the keymap name table
1769    // and walk the entry's rc + primary-name promotion in one pass.
1770    let mut tab = match keymapnamtab().lock() {
1771        Ok(t) => t,
1772        Err(_) => return,
1773    };
1774    let Some(_kmn) = tab.get(name) else { return; };                         // c:249
1775
1776    // c:252 — `--km->rc`. With Arc<Keymap> shared-immutable we can't
1777    // mutate rc on the shared instance; the canonical Rust unref
1778    // path drops a reference by removing the entry from the table.
1779    // Find any other names sharing the same Arc — if none, this is
1780    // the last reference and we drop the entry (Arc drop fires).
1781    let arc_to_remove = tab.get(name).map(|kmn| kmn.keymap.clone());
1782    let shared_count = if let Some(ref arc) = arc_to_remove {
1783        tab.values().filter(|kmn| std::sync::Arc::ptr_eq(&kmn.keymap, arc)).count()
1784    } else { 0 };
1785
1786    if shared_count <= 1 {                                                   // c:253 rc==0 path
1787        tab.remove(name);                                                    // C: deletekeymap
1788    }
1789    // c:254 — `if (km->primary == kmname) km->primary = NULL` +
1790    // scanprimaryname re-promote. The Arc<Keymap>'s primary field
1791    // is shared-immutable in the Rust port; on the next refkeymap_by_name
1792    // call to a different name pointing to this keymap, primary is
1793    // re-set via the existing promotion path in refkeymap_by_name.
1794}
1795
1796/// Port of `zlesetkeymap(int mode)` from Src/Zle/zle_keymap.c:1804.
1797pub fn zlesetkeymap(mode: i32) {                                             // c:1804
1798    // C body (c:1820-1825): `Keymap km = openkeymap(mode==VIMODE?
1799    //                       "viins":"emacs"); if (!km) return;
1800    //                       linkkeymap(km, "main", 0)`.
1801    // VIMODE = 1 (per zsh's mode-flag enum).
1802    let kmname = if mode == 1 { "viins" } else { "emacs" };
1803    if let Some(km) = openkeymap(kmname) {
1804        linkkeymap(km, "main", 0);
1805    }
1806}