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_bindings::{EMACSBIND, METABIND, VICMDBIND, VIINSBIND};
21use super::zle_main::zle_test_setup;
22use super::zle_thingy::Thingy;
23use crate::ported::utils::inittyptab;
24#[cfg(test)]
25use crate::ported::ztype_h::TYPTAB_TEST_LOCK;
26use std::io::Write;
27
28// =====================================================================
29// Flag constants — `Src/Zle/zle_keymap.c:62/83/114-115`.
30// =====================================================================
31
32#[allow(unused_imports)]
33use crate::ported::zle::{
34    deltochar::*, textobjects::*, zle_hist::*, zle_main::*, zle_misc::*, zle_move::*,
35    zle_params::*, zle_refresh::*, zle_tricky::*, zle_utils::*, zle_vi::*, zle_word::*,
36};
37use crate::ported::zsh_h::{options, OPT_ISSET};
38use crate::ported::ztype_h::imeta;
39
40/// Port of `KMN_IMMORTAL` from `Src/Zle/zle_keymap.c:62`. Marks a
41/// keymap-name node that can't be deleted (the `.safe` keymap).
42
43// --- AUTO: cross-zle hoisted-fn use glob ---
44#[allow(unused_imports)]
45#[allow(unused_imports)]
46
47/// Direct port of `struct keymapname` from `Src/Zle/zle_keymap.c:54`.
48/// One node in the global `keymapnamtab` — maps a name to a Keymap
49/// + per-node flags (KMN_IMMORTAL for `.safe`).
50#[derive(Debug, Clone)]
51pub struct KeymapName {
52    // c:54
53    pub nam: String,         // c:56 char *nam
54    pub flags: i32,          // c:57 int flags
55    pub keymap: Arc<Keymap>, // c:58 Keymap keymap
56}
57/// `KMN_IMMORTAL` constant.
58pub const KMN_IMMORTAL: i32 = 1 << 1; // c:62
59
60/// Port of `KM_IMMUTABLE` from `Src/Zle/zle_keymap.c:83`. Marks a
61/// keymap that can't have its bindings modified.
62pub const KM_IMMUTABLE: i32 = 1 << 1; // c:83
63
64/// Port of `struct bindstate` from `Src/Zle/zle_keymap.c:95-104`.
65/// Closure state for `scanbindlist` / `bindlistout` — threads the
66/// keymap-listing accumulator through `scankeymap`'s per-binding
67/// callback. C definition:
68/// ```c
69/// struct bindstate {
70///     int flags;
71///     char *kmname;
72///     char *firstseq;
73///     char *lastseq;
74///     Thingy bind;
75///     char *str;
76///     char *prefix;
77///     int prefixlen;
78/// };
79/// ```
80#[derive(Debug, Clone)]
81#[allow(non_camel_case_types)]
82pub struct bindstate {
83    // c:95
84    pub flags: i32,              // c:96
85    pub kmname: String,          // c:97
86    pub firstseq: Vec<u8>,       // c:98
87    pub lastseq: Vec<u8>,        // c:99
88    pub bind: Option<Thingy>,    // c:100 — None ≡ C `t_undefinedkey`
89    pub str: Option<String>,     // c:101 — None ≡ C `NULL`
90    pub prefix: Option<Vec<u8>>, // c:102 — None ≡ C `NULL`
91    pub prefixlen: usize,        // c:103
92}
93
94/// Port of `struct remprefstate` from `Src/Zle/zle_keymap.c:108`.
95/// Closure state for `scanremoveprefix` — removes every multi-char
96/// binding that starts with the given prefix from a keymap.
97///
98/// C definition (c:108-112):
99/// ```c
100/// struct remprefstate {
101///     Keymap km;
102///     char *prefix;
103///     int prefixlen;
104/// };
105/// ```
106#[derive(Debug)]
107#[allow(non_camel_case_types)]
108pub struct remprefstate {
109    // c:108
110    /// Target keymap (Arc handle for shared ownership).
111    pub km: Arc<Keymap>, // c:109
112    /// Byte prefix to match against each multi-key binding.
113    pub prefix: Vec<u8>, // c:110
114    /// `prefix.len()` cached for the scan inner loop (kept as a field
115    /// to mirror the C struct shape; `self.prefix.len()` reads the
116    /// same value).
117    pub prefixlen: usize, // c:111
118}
119
120/// Port of `BS_LIST` from `Src/Zle/zle_keymap.c:114`. `bin_bindkey -L`:
121/// list bindings in `bindkey -M` syntax.
122pub const BS_LIST: i32 = 1 << 0; // c:114
123
124/// Port of `BS_ALL` from `Src/Zle/zle_keymap.c:115`. `bin_bindkey -aL`:
125/// list ALL bindings, including default sequences.
126pub const BS_ALL: i32 = 1 << 1; // c:115
127
128/// Port of `static Thingy lastnamed` from `Src/Zle/zle_keymap.c:145`.
129/// Last command executed by `execute-named-command` — used to
130/// re-execute via `bindkey -A name` then `getkeycmd`.
131pub static lastnamed: Mutex<Option<Thingy>> = Mutex::new(None); // c:145
132
133/// Port of `createkeymapnamtab()` from Src/Zle/zle_keymap.c:153.
134pub fn createkeymapnamtab() {
135    // c:153
136    // c:153 — `keymapnamtab = newhashtable(7, "keymapnamtab", NULL)`.
137    // OnceLock-init via accessor.
138    let _ = keymapnamtab();
139}
140
141/// Direct port of `void init_keymaps(void)` from `Src/Zle/zle_keymap.c:1224`.
142/// Module-load entry point — bootstraps the keymap-name table, installs
143/// the default emacs/viins/vicmd bindings, allocates the keybuf, and
144/// seeds `lastnamed` to the undefined-key sentinel (Rust None).
145pub fn init_keymaps() {
146    // c:1224
147    createkeymapnamtab(); // c:1227
148    default_bindings(); // c:1228
149    *keybuf.lock().unwrap() = vec![0u8; 32]; // c:1229 zshcalloc(keybufsz)
150    *lastnamed.lock().unwrap() = None; // c:1230 refthingy(t_undefinedkey)
151}
152
153/// Direct port of `void cleanup_keymaps(void)` from
154/// `Src/Zle/zle_keymap.c:1236`. Module-unload entry point — drops
155/// `lastnamed`, the keymap-name table, and the keybuf.
156pub fn cleanup_keymaps() {
157    // c:1236
158    *lastnamed.lock().unwrap() = None; // c:1239 unrefthingy(lastnamed)
159    keymapnamtab().lock().unwrap().clear(); // c:1240 deletehashtable(keymapnamtab)
160    keybuf.lock().unwrap().clear(); // c:1241 zfree(keybuf, keybufsz)
161}
162
163/// Port of `makekeymapnamnode(Keymap keymap)` from Src/Zle/zle_keymap.c:173.
164pub fn makekeymapnamnode(keymap: Arc<Keymap>) -> KeymapName {
165    // c:173
166    // c:173-178 — `kmn = zshcalloc; kmn->keymap = keymap; return kmn`.
167    KeymapName {
168        nam: String::new(),
169        flags: 0,
170        keymap: keymap,
171    }
172}
173
174/// Port of `emptykeymapnamtab(HashTable ht)` from Src/Zle/zle_keymap.c:183.
175/// WARNING: param names don't match C — Rust=() vs C=(ht)
176pub fn emptykeymapnamtab() {
177    // c:183
178    // c:183-198 — walk all nodes, free name + unrefkeymap + zfree.
179    // Rust drop cascade handles free; we just clear the table.
180    keymapnamtab().lock().unwrap().clear();
181}
182
183/// Direct port of `void refkeymap_by_name(char *name)` from
184/// `Src/Zle/zle_keymap.c:208-216`.
185/// ```c
186/// KeymapName kmn = keymapnamtab.getnode(keymapnamtab, name);
187/// if (kmn) {
188///     refkeymap(kmn->keymap);
189///     if (!kmn->keymap->primary && strcmp(kmn->nam, "main") != 0)
190///         kmn->keymap->primary = kmn;
191/// }
192/// ```
193///
194/// **Arc-shape divergence noted (Rule 9):** the Rust `Keymap` lives
195/// inside `Arc<Keymap>` (shared-immutable). C's `refkeymap` mutates
196/// `km->rc`; the Rust port's effective refcount is the number of
197/// `keymapnamtab` entries holding the same `Arc<Keymap>`, so a
198/// standalone bump-by-name has no observable effect — the rc
199/// equivalent only advances when an additional name is linked via
200/// `linkkeymap`. Same for `primary` promotion (`Arc<Keymap>` is
201/// immutable; promotion only happens on the next `linkkeymap`).
202/// We keep the lookup as a contract check so callers see a working
203/// "did this name exist?" probe.
204/// Port of `refkeymap_by_name(KeymapName kmn)` from `Src/Zle/zle_keymap.c:209`.
205pub fn refkeymap_by_name(kmn: &str) {
206    // c:209
207    let _ = keymapnamtab().lock().unwrap().get(kmn); // c:209 getnode probe
208}
209
210/// Direct port of `static void scanprimaryname(HashNode hn,
211///                                              UNUSED(int flags))` from
212/// `Src/Zle/zle_keymap.c:224`. Per-node callback used by
213/// `unrefkeymap_by_name`'s scanhashtable pass to find a new primary
214/// name when the current one's keymap had its rc dropped.
215///
216/// **Arc-shape divergence:** C mutates `km->primary` via the
217/// `km_rename_me` static; Rust `Keymap` is shared-immutable inside
218/// `Arc<Keymap>`. The standalone fn is invoked via scanhashtable
219/// from `unrefkeymap_by_name` only. In Rust the same effect happens
220/// implicitly: when a name's entry is removed and another name
221/// still references the same `Arc<Keymap>`, that other name is the
222/// "new primary" — no explicit promotion needed, since reads via
223/// `openkeymap(other_name)` already resolve to the shared Arc.
224pub fn scanprimaryname(_name: &str) { // c:224
225                                      // No-op by design — see divergence note above.
226}
227
228/// Direct port of `void unrefkeymap_by_name(char *name)` from
229/// `Src/Zle/zle_keymap.c:246`.
230/// ```c
231/// kmname = keymapnamtab.getnode(keymapnamtab, name);
232/// if (kmname && --kmname->keymap->rc == 0) {
233///     if (kmname->keymap->primary == kmname) {
234///         kmname->keymap->primary = NULL;
235///         scanhashtable(keymapnamtab, ..., scanprimaryname, 0);
236///     }
237///     // chained deletekeymap via scanhashtable removal
238/// }
239/// ```
240pub fn unrefkeymap_by_name(name: &str) {
241    // c:246
242    // c:246 — `kmname = getnode(name)`. Lock the keymap name table
243    // and walk the entry's rc + primary-name promotion in one pass.
244    let mut tab = match keymapnamtab().lock() {
245        Ok(t) => t,
246        Err(_) => return,
247    };
248    let Some(_kmn) = tab.get(name) else {
249        return;
250    }; // c:249
251
252    // c:252 — `--km->rc`. With Arc<Keymap> shared-immutable we can't
253    // mutate rc on the shared instance; the canonical Rust unref
254    // path drops a reference by removing the entry from the table.
255    // Find any other names sharing the same Arc — if none, this is
256    // the last reference and we drop the entry (Arc drop fires).
257    let arc_to_remove = tab.get(name).map(|kmn| kmn.keymap.clone());
258    let shared_count = if let Some(ref arc) = arc_to_remove {
259        tab.values()
260            .filter(|kmn| Arc::ptr_eq(&kmn.keymap, arc))
261            .count()
262    } else {
263        0
264    };
265
266    if shared_count <= 1 {
267        // c:253 rc==0 path
268        tab.remove(name); // C: deletekeymap
269    }
270    // c:254 — `if (km->primary == kmname) km->primary = NULL` +
271    // scanprimaryname re-promote. The Arc<Keymap>'s primary field
272    // is shared-immutable in the Rust port; on the next refkeymap_by_name
273    // call to a different name pointing to this keymap, primary is
274    // re-set via the existing promotion path in refkeymap_by_name.
275}
276
277/// Port of `freekeymapnamnode(HashNode hn)` from Src/Zle/zle_keymap.c:267.
278pub fn freekeymapnamnode(hn: &str) {
279    // c:267
280    // c:267-273 — `kmn = (KeymapName)hn; zsfree(kmn->nam);
281    //              unrefkeymap_by_name(kmn); zfree(kmn,...)`.
282    keymapnamtab().lock().unwrap().remove(hn);
283}
284
285/// Port of `newkeytab(char *kmname)` from Src/Zle/zle_keymap.c:278.
286/// WARNING: param names don't match C — Rust=() vs C=(kmname)
287pub fn newkeytab() -> HashMap<Vec<u8>, KeyBinding> {
288    // c:278
289    // c:278-296 — `ht = newhashtable(7, kmname, NULL)`. zshrs's
290    // multi binding storage is HashMap<Vec<u8>, KeyBinding>; just
291    // returns an empty one.
292    HashMap::new()
293}
294
295/// Port of `makekeynode(Thingy t, char *str)` from Src/Zle/zle_keymap.c:301.
296pub fn makekeynode(t: Thingy, str: String) -> KeyBinding {
297    // c:301
298    // c:301-307 — `k = zshcalloc; k->bind = t; k->str = str`.
299    KeyBinding {
300        bind: Some(t),
301        str: Some(str),
302        prefixct: 0,
303    }
304}
305
306impl Default for Keymap {
307    fn default() -> Self {
308        Keymap {
309            first: std::array::from_fn(|_| None),
310            multi: HashMap::new(),
311            primary: None,
312            flags: 0,
313            rc: 0,
314        }
315    }
316}
317
318impl Keymap {
319    /// Construct an empty keymap with no bindings.
320    /// Equivalent to `newkeytab()` from Src/Zle/zle_keymap.c:278 — the
321    /// C source allocates a Keymap with the first[] array zeroed out
322    /// and an empty multi-byte hashtab.
323    pub fn new() -> Self {
324        // c:278
325        Self::default()
326    }
327
328    /// Bind a 1-byte key to a Thingy via the `first[]` fast-path table.
329    /// Direct port of the single-byte path in `bindkey()` at
330    /// Src/Zle/zle_keymap.c:566; the C source writes into `km->first[c]`
331    /// when `seq` has length 1.
332    pub fn bind_char(&mut self, c: u8, thingy: Thingy) {
333        // c:566
334        self.first[c as usize] = Some(thingy);
335    }
336
337    /// Clear a 1-byte binding.
338    /// Equivalent to `bindkey -r` against a single-byte sequence at
339    /// Src/Zle/zle_keymap.c:566 — flips the `first[c]` slot to None.
340    pub fn unbind_char(&mut self, c: u8) {
341        self.first[c as usize] = None;
342    }
343
344    /// Install a multi-byte key sequence binding.
345    /// Direct port of `bindkey(Keymap km, const char *seq, Thingy bind, char *str)` from Src/Zle/zle_keymap.c:566 for the
346    /// len > 1 path: marks every proper prefix of `seq` as a prefix
347    /// node (prefixct increment) so getkeymapcmd's trie walk knows to
348    /// keep reading bytes when it sees a partial match.
349    pub fn bind_seq(&mut self, seq: &[u8], thingy: Thingy) {
350        // c:566
351        if seq.len() == 1 {
352            self.bind_char(seq[0], thingy);
353        } else {
354            // Mark prefixes
355            for i in 1..seq.len() {
356                let prefix = &seq[..i];
357                self.multi
358                    .entry(prefix.to_vec())
359                    .and_modify(|kb| kb.prefixct += 1)
360                    .or_insert(KeyBinding {
361                        bind: None,
362                        str: None,
363                        prefixct: 1,
364                    });
365            }
366
367            // Add the binding
368            self.multi.insert(
369                seq.to_vec(),
370                KeyBinding {
371                    bind: Some(thingy),
372                    str: None,
373                    prefixct: 0,
374                },
375            );
376        }
377    }
378
379    /// Install a multi-byte key sequence that maps to a literal string.
380    /// Port of the send-string variant of `bindkey()` at
381    /// Src/Zle/zle_keymap.c:566 — the C source stores `str` instead of
382    /// a Thingy when invoked via `bindkey -s 'seq' 'string'`. When the
383    /// trie hits this entry, getkeycmd ungets the string via
384    /// `ungetbytes_unmeta` (zle_keymap.c:1784) so it gets re-resolved
385    /// against the keymap.
386    pub fn bind_str(&mut self, seq: &[u8], s: String) {
387        if seq.len() == 1 {
388            // Single char can't be send-string in first[] table
389            // Store in multi
390        }
391
392        // Mark prefixes
393        for i in 1..seq.len() {
394            let prefix = &seq[..i];
395            self.multi
396                .entry(prefix.to_vec())
397                .and_modify(|kb| kb.prefixct += 1)
398                .or_insert(KeyBinding {
399                    bind: None,
400                    str: None,
401                    prefixct: 1,
402                });
403        }
404
405        self.multi.insert(
406            seq.to_vec(),
407            KeyBinding {
408                bind: None,
409                str: Some(s),
410                prefixct: 0,
411            },
412        );
413    }
414
415    /// Remove a multi-byte binding and decrement prefix counts on its
416    /// ancestors so the trie shrinks correctly.
417    /// Port of `bindkey -r` against a multi-byte sequence at
418    /// Src/Zle/zle_keymap.c:566 — the C source mirrors the prefix
419    /// reference-count machinery via the same prefixct decrement
420    /// pattern when removing a leaf.
421    pub fn unbind_seq(&mut self, seq: &[u8]) {
422        if seq.len() == 1 {
423            self.unbind_char(seq[0]);
424        } else {
425            if self.multi.remove(seq).is_some() {
426                // Decrement prefix counts
427                for i in 1..seq.len() {
428                    let prefix = &seq[..i];
429                    if let Some(kb) = self.multi.get_mut(prefix) {
430                        kb.prefixct -= 1;
431                        if kb.prefixct == 0 && kb.bind.is_none() && kb.str.is_none() {
432                            // Remove empty prefix entry
433                            // (can't remove while iterating, so we'll leave it)
434                        }
435                    }
436                }
437            }
438        }
439    }
440
441    /// Fast-path single-byte lookup through `first[]`.
442    /// Equivalent to the 1-byte branch of `keybind()` at
443    /// Src/Zle/zle_keymap.c:659 — the C source's `km->first[*seq]`
444    /// access for single-byte resolution.
445    pub fn lookup_char(&self, c: u8) -> Option<&Thingy> {
446        self.first[c as usize].as_ref()
447    }
448
449    /// Multi-byte sequence lookup through the `multi` hashtab.
450    /// Equivalent to the >1-byte branch of `keybind()` at
451    /// zle_keymap.c:659 — returns the KeyBinding entry if `seq`
452    /// matches a leaf, or one carrying `prefixct > 0` if `seq` is a
453    /// prefix of one or more bound sequences.
454    pub fn lookup_seq(&self, seq: &[u8]) -> Option<&KeyBinding> {
455        if seq.len() == 1 {
456            // For single char, use lookup_char instead
457            None
458        } else {
459            self.multi.get(seq)
460        }
461    }
462
463    /// Test whether `seq` is a prefix of any bound sequence.
464    /// Equivalent to `keyisprefix()` from Src/Zle/zle_keymap.c. Used
465    /// by `getkeymapcmd` to decide whether to keep reading bytes
466    /// during a multi-byte sequence resolve (the trie-walk loop at
467    /// zle_keymap.c:1604).
468    pub fn is_prefix(&self, seq: &[u8]) -> bool {
469        if seq.len() == 1 {
470            // Check if this char is a prefix in multi table
471            self.multi.keys().any(|k| k.len() > 1 && k[0] == seq[0])
472        } else {
473            self.multi
474                .get(seq)
475                .map(|kb| kb.prefixct > 0)
476                .unwrap_or(false)
477        }
478    }
479}
480
481/// Port of `freekeynode(HashNode hn)` from Src/Zle/zle_keymap.c:312.
482pub fn freekeynode(hn: KeyBinding) {
483    // c:312
484    // C body (zle_keymap.c:312):
485    //   freekeynode(HashNode hn) {
486    //     Key k = (Key) hn;
487    //     zsfree(k->nam);
488    //     unrefthingy(k->bind);
489    //     zsfree(k->str);
490    //     zfree(k, sizeof(*k));
491    //   }
492    //
493    // C frees the name string, drops the Thingy refcount, frees the
494    // send-string, and zfrees the Key struct itself. Rust's Drop
495    // cascade handles the String drops; the Thingy unref needs to
496    // happen if `bind` is Some (refcount-tracked via thingytab).
497    if let Some(t) = hn.bind {
498        // Match zle_thingy.c::unrefthingy semantics — drop a
499        // reference, removing from thingytab if rc hits 0.
500        crate::ported::zle::zle_thingy::unrefthingy(&t.nam);
501    }
502    // KeyBinding consumed; String/Option fields auto-drop.
503}
504
505/// Direct port of `Keymap newkeymap(Keymap tocopy, char *kmname)` from
506/// `Src/Zle/zle_keymap.c:330`.
507/// ```c
508/// km = zshcalloc(sizeof(*km));
509/// km->multi = newkeytab(7, kmname);
510/// if (tocopy) {
511///     for (i = 0; i < 256; i++) km->first[i] = refthingy(tocopy->first[i]);
512///     scanhashtable(tocopy->multi, 0, 0, 0, scancopykeys, 0);
513/// } else
514///     for (i = 0; i < 256; i++) km->first[i] = refthingy(t_undefinedkey);
515/// return km;
516/// ```
517pub fn newkeymap(tocopy: Option<&Keymap>, _kmname: &str) -> Arc<Keymap> {
518    // c:330
519    let mut km = Keymap::default();
520    if let Some(src) = tocopy {
521        // c:336
522        // c:337-339 — copy first[i] entries via refthingy.
523        for i in 0..256 {
524            // c:337
525            km.first[i] = src.first[i].clone(); // c:338
526        }
527        // c:340 — scanhashtable(tocopy->multi, ..., scancopykeys, 0).
528        km.multi = src.multi.clone();
529    }
530    // c:342-343 — else first[i] = refthingy(t_undefinedkey). Default
531    // already has None, mirroring the C "undefined" sentinel.
532    Arc::new(km)
533}
534
535/// Direct port of `static void scancopykeys(char *s, Thingy bind,
536///                                          char *str, void *magic)`
537/// from `Src/Zle/zle_keymap.c:351`. Per-node callback for
538/// `newkeymap` deep-copy.
539///
540/// **Architectural divergence:** the C code dispatches via
541/// scanhashtable + a `copyto` file-static target Keymap; the Rust
542/// `newkeymap` (zle_keymap.rs:1532) instead deep-copies the source
543/// `multi: HashMap<Vec<u8>, KeyBinding>` directly via `.clone()`,
544/// which is the equivalent operation in one step. This standalone
545/// callback is invoked from no Rust caller — it's preserved as a
546/// no-op for ABI parity with the C dispatch surface.
547pub fn scancopykeys(_kb: &KeyBinding) { // c:351
548                                        // No-op by design — newkeymap performs the copy directly.
549}
550
551/// Port of `deletekeymap(Keymap km)` from Src/Zle/zle_keymap.c:364.
552#[allow(unused_variables)]
553pub fn deletekeymap(km: Arc<Keymap>) { // c:364
554                                       // c:364-372 — `deletehashtable(km->multi); for(i=256;i--;)
555                                       //              unrefthingy(km->first[i]); zfree(km, sizeof(*km))`.
556                                       // Arc<Keymap> drop cascade handles HashMap and array drops.
557                                       // The unrefthingy walk is implicit: each Thingy in first[] gets
558                                       // dropped when the Arc is. With shared Arc<Keymap> we can only
559                                       // observe the drop on the LAST holder.
560}
561
562/// Direct port of `void scankeymap(Keymap km, int sort,
563///                                  KeyScanFunc func, void *magic)`
564/// from `Src/Zle/zle_keymap.c:381`. Enumerates every binding
565/// in `km` — single-byte `first[256]` entries first, then
566/// multi-byte `multi` entries. `sort != 0` lex-sorts the multi-byte
567/// keys before yielding. The Rust port returns a `Vec<Vec<u8>>` of
568/// the sequences; callers iterate.
569pub fn scankeymap(
570    km: &Keymap,
571    sort: i32,
572    func: &mut dyn FnMut(&[u8], Option<&Thingy>, Option<&str>),
573) {
574    // c:381
575    // c:386 — `skm_km = km; skm_last = sort ? -1 : 255;
576    //          skm_func = func; skm_magic = magic;`
577    // Rust models the four file-statics as captured state in the
578    // `scankeys` closure call sequence below.
579    let mut skm_last: i32 = if sort != 0 { -1 } else { 255 };
580
581    // c:390 — `scanhashtable(km->multi, sort, 0, 0, scankeys, 0)`.
582    // Walk the multi-byte hash in lex order (when sort != 0),
583    // interleaving any single-byte entries whose byte value is less
584    // than the current multi-key's first byte. The C `scankeys`
585    // callback at c:402 is inlined here as the per-iteration body.
586    let mut multi_keys: Vec<&Vec<u8>> = km.multi.keys().collect();
587    if sort != 0 {
588        // c:381 sort flag
589        multi_keys.sort();
590    }
591    for k_nam in multi_keys {
592        let kb = km.multi.get(k_nam).expect("key from iter");
593        // c:390 — `scanhashtable(km->multi, sort, 0, 0, scankeys, 0)`
594        // calls `scankeys` per multi-byte node; we drive that loop
595        // directly here because the Rust port has no scanhashtable.
596        scankeys(
597            k_nam,
598            kb.bind.as_ref(),
599            kb.str.as_deref(),
600            km,
601            &mut skm_last,
602            func,
603        );
604    }
605
606    // c:392 — `if (!sort) skm_last = -1`. Already sorted-or-not above;
607    // for the unsorted path we reset and walk all 0..255 in order.
608    if sort == 0 {
609        skm_last = -1;
610    }
611    // c:393-401 — flush remaining single-byte slots.
612    while skm_last < 255 {
613        skm_last += 1;
614        if let Some(t) = &km.first[skm_last as usize] {
615            let m = [skm_last as u8];
616            func(&m, Some(t), None);
617        }
618    }
619}
620
621/// Direct port of `static void scankeys(HashNode hn, UNUSED(int flags))`
622/// from `Src/Zle/zle_keymap.c:404`. Per-multi-byte-binding callback
623/// driven by `scankeymap`. Walks `km.first[]` slots whose byte
624/// value is < the current multi-key's first byte, emitting each
625/// non-undefined single-byte binding before the multi-byte binding
626/// itself.
627///
628/// C uses the module-static globals `skm_km`, `skm_last`, `skm_func`,
629/// `skm_magic` to plumb state through `scanhashtable`'s
630/// `(HashNode, int) -> void` callback signature. Rust passes them
631/// in explicitly because Rust closures express the same lifetime
632/// without the global indirection.
633/// WARNING: param names don't match C — Rust=(k_nam, k_bind, k_str,
634/// km, skm_last, func) vs C=(hn, flags).
635fn scankeys(
636    k_nam: &[u8],
637    k_bind: Option<&Thingy>,
638    k_str: Option<&str>,
639    km: &Keymap,
640    skm_last: &mut i32,
641    func: &mut dyn FnMut(&[u8], Option<&Thingy>, Option<&str>),
642) {
643    // c:404
644    // c:407-408 — `f = (k->nam[0] == Meta ? k->nam[1]^32 : k->nam[0])`.
645    // Rust storage is raw bytes, so the Meta-decoded first byte is
646    // just the first byte (high-bit values represent themselves).
647    // Empty key name (C can't produce one — `k->nam` is at least
648    // one byte — but the Rust multi-byte bindkey path can hand an
649    // empty slice for a fully-consumed prefix): nothing to scan.
650    let Some(&f0) = k_nam.first() else {
651        return;
652    };
653    let f = f0 as i32;
654    // c:412-419 — flush every single-byte slot with byte < f.
655    while *skm_last < f {
656        *skm_last += 1;
657        if *skm_last > 255 {
658            break;
659        }
660        if let Some(t) = &km.first[*skm_last as usize] {
661            let m = [*skm_last as u8];
662            func(&m, Some(t), None);
663        }
664    }
665    // c:420 — `skm_func(k->nam, k->bind, k->str, skm_magic)`.
666    func(k_nam, k_bind, k_str);
667}
668
669/// Port of `openkeymap(char *name)` from Src/Zle/zle_keymap.c:428.
670pub fn openkeymap(name: &str) -> Option<Arc<Keymap>> {
671    // c:428
672    // c:428-431 — `n = keymapnamtab.getnode(name); return n ? n->keymap : NULL`.
673    keymapnamtab()
674        .lock()
675        .unwrap()
676        .get(name)
677        .map(|n| n.keymap.clone())
678}
679
680/// Port of `unlinkkeymap(char *name, int ignm)` from Src/Zle/zle_keymap.c:436.
681pub fn unlinkkeymap(name: &str, ignm: i32) -> i32 {
682    // c:436
683    // c:436-444 — `n = keymapnamtab.getnode(name); if (!n) return 2;
684    //               if (!ignm && (n->flags & KMN_IMMORTAL)) return 1;
685    //               keymapnamtab.freenode(removenode(name)); return 0`.
686    let mut tab = keymapnamtab().lock().unwrap();
687    match tab.get(name) {
688        None => 2,                                                  // c:440
689        Some(n) if ignm == 0 && (n.flags & KMN_IMMORTAL) != 0 => 1, // c:441
690        Some(_) => {
691            tab.remove(name); // c:443
692            0
693        }
694    }
695}
696
697/// Direct port of `int bindkey(Keymap km, const char *seq, Thingy
698/// bind, char *str)` from `Src/Zle/zle_keymap.c:566`. The single
699/// canonical entry — internal dispatch on (bind/str/seq.len())
700/// matches the C body's `if (!bind || ztrlen(seq) > 1)` branch.
701///
702/// Returns 0 on success, 1 if `km->flags & KM_IMMUTABLE`, 2 if `seq`
703/// is empty.
704///
705/// The `Keymap::bind_char` / `bind_seq` / `bind_str` methods are a
706/// Rust-only factoring of C's internal dispatch — every default-
707/// bindings site and every C-equivalent caller goes through this
708/// canonical `bindkey()` so the call-site coverage matches C.
709pub fn bindkey(km: &mut Keymap, seq: &[u8], bind: Option<Thingy>, str: Option<String>) -> i32 {
710    // c:566
711    // c:572 — `if (km->flags & KM_IMMUTABLE) return 1;`
712    if (km.flags & KM_IMMUTABLE) != 0 {
713        return 1;
714    }
715    // c:574 — `if (!*seq) return 2;`
716    if seq.is_empty() {
717        return 2;
718    }
719    // c:576 — `if (!bind || ztrlen(seq) > 1)` dispatch. Inlined
720    // (not delegated back to bindkey or to the `Keymap::bind_*`
721    // methods) to avoid (a) infinite recursion via the dispatch
722    // table and (b) round-tripping through the Rust-only method
723    // façade. The four arms match C's `c:600` single-byte arm and
724    // `c:631-641` multi-byte arm.
725    match (bind, str, seq.len()) {
726        (Some(t), None, 1) => {
727            // c:600 — `km->first[f] = bind; return 0;`
728            km.first[seq[0] as usize] = Some(t);
729            0
730        }
731        (Some(t), None, _) => {
732            // c:631-641 — multi-char Thingy binding. Mark prefixes
733            // first (so getkeymapcmd's trie walk knows to keep
734            // reading bytes), then insert the full-seq binding.
735            for i in 1..seq.len() {
736                km.multi
737                    .entry(seq[..i].to_vec())
738                    .and_modify(|kb| kb.prefixct += 1)
739                    .or_insert(KeyBinding {
740                        bind: None,
741                        str: None,
742                        prefixct: 1,
743                    });
744            }
745            km.multi.insert(
746                seq.to_vec(),
747                KeyBinding {
748                    bind: Some(t),
749                    str: None,
750                    prefixct: 0,
751                },
752            );
753            0
754        }
755        (None, Some(s), _) => {
756            // c:614-641 — send-string `bindkey -s` form.
757            for i in 1..seq.len() {
758                km.multi
759                    .entry(seq[..i].to_vec())
760                    .and_modify(|kb| kb.prefixct += 1)
761                    .or_insert(KeyBinding {
762                        bind: None,
763                        str: None,
764                        prefixct: 1,
765                    });
766            }
767            km.multi.insert(
768                seq.to_vec(),
769                KeyBinding {
770                    bind: None,
771                    str: Some(s),
772                    prefixct: 0,
773                },
774            );
775            0
776        }
777        (None, None, _) => {
778            // c:574 — `bindkey -r` unbind: bind to t_undefinedkey.
779            if seq.len() == 1 {
780                km.first[seq[0] as usize] = Some(Thingy::builtin("undefined-key"));
781            } else {
782                for i in 1..seq.len() {
783                    km.multi
784                        .entry(seq[..i].to_vec())
785                        .and_modify(|kb| kb.prefixct += 1)
786                        .or_insert(KeyBinding {
787                            bind: None,
788                            str: None,
789                            prefixct: 1,
790                        });
791                }
792                km.multi.insert(
793                    seq.to_vec(),
794                    KeyBinding {
795                        bind: Some(Thingy::builtin("undefined-key")),
796                        str: None,
797                        prefixct: 0,
798                    },
799                );
800            }
801            0
802        }
803        (Some(_), Some(_), _) => {
804            // C signature doesn't allow both. Caller bug.
805            -1
806        }
807    }
808}
809
810/// Port of `linkkeymap(Keymap km, char *name, int imm)` from Src/Zle/zle_keymap.c:449.
811pub fn linkkeymap(km: Arc<Keymap>, name: &str, imm: i32) -> i32 {
812    // c:449
813    // c:449-466 — `n = keymapnamtab.getnode(name); if (n) { ... }
814    //               else { n = makekeymapnamnode(km); ... addnode }
815    //               refkeymap_by_name(n); return 0`.
816    let mut tab = keymapnamtab().lock().unwrap();
817    if let Some(existing) = tab.get_mut(name) {
818        // c:453-454 — `if (n->flags & KMN_IMMORTAL) return 1`.
819        if existing.flags & KMN_IMMORTAL != 0 {
820            return 1;
821        }
822        // c:455-456 — `if (n->keymap == km) return 0`.
823        if Arc::ptr_eq(&existing.keymap, &km) {
824            return 0;
825        }
826        // c:457-458 — `unrefkeymap_by_name(n); n->keymap = km`.
827        existing.keymap = km;
828    } else {
829        // c:459-463 — `n = makekeymapnamnode(km); if (imm)
830        //              n->flags |= KMN_IMMORTAL; addnode(name, n)`.
831        let mut n = KeymapName {
832            nam: name.to_string(),
833            flags: 0,
834            keymap: km,
835        };
836        if imm != 0 {
837            n.flags |= KMN_IMMORTAL;
838        }
839        tab.insert(name.to_string(), n);
840    }
841    drop(tab);
842    refkeymap_by_name(name); // c:465
843    0 // c:466
844}
845
846/// Port of `refkeymap(Keymap km)` from `Src/Zle/zle_keymap.c:471`.
847/// ```c
848/// void
849/// refkeymap(Keymap km)
850/// {
851///     km->rc++;
852/// }
853/// ```
854/// Bump the reference count on a keymap.
855pub fn refkeymap(km: &mut Keymap) {
856    // c:471
857    km.rc += 1; // c:471 km->rc++
858}
859
860/// Port of `unrefkeymap(Keymap km)` from `Src/Zle/zle_keymap.c:479`.
861/// ```c
862/// int
863/// unrefkeymap(Keymap km)
864/// {
865///     if (!--km->rc) {
866///         deletekeymap(km);
867///         return 0;
868///     }
869///     return km->rc;
870/// }
871/// ```
872/// Drop a reference; returns the new rc, or 0 if the keymap was
873/// deleted. The Rust port returns the new rc — callers can compare
874/// to 0 to detect deletion. The actual delete-on-zero path is
875/// indicated via the `should_delete` out flag (the caller is expected
876/// to drop the Keymap; Rust ownership doesn't allow self-deletion
877/// from the &mut reference).
878pub fn unrefkeymap(km: &mut Keymap) -> i32 {
879    // c:480
880    km.rc -= 1; // c:480 --km->rc
881    if km.rc == 0 {
882        // c:483 — `deletekeymap(km)`. Rust caller drops the Keymap;
883        // we just signal by returning 0.
884        return 0; // c:484
885    }
886    km.rc // c:487 return km->rc
887}
888
889// Select a keymap as the current ZLE keymap.  Can optionally fall back    // c:495
890// on the guaranteed safe keymap if it fails.                              // c:495
891/// Port of `selectkeymap(char *name, int fb)` from Src/Zle/zle_keymap.c:495.
892pub fn selectkeymap(name: &str, fb: i32) -> i32 {
893    // c:495
894    // C body (c:497-521): `Keymap km = openkeymap(name); if (!km) {
895    //   showmsg + if (!fb) return 1; km = openkeymap(".safe"); }
896    //   if (name != curkeymapname) { ... curkeymapname = ztrdup(name);
897    //   if (zleactive && oldname && strcmp...) zlecallhook(...); }
898    //   curkeymap = km; return 0`.
899    let mut km = openkeymap(name); // c:497
900    let mut resolved = name.to_string();
901    if km.is_none() {
902        // c:498
903        if fb == 0 {
904            return 1; // c:506
905        }
906        km = openkeymap(".safe"); // c:508
907        if km.is_none() {
908            return 1;
909        }
910        resolved = ".safe".to_string();
911    }
912    // c:513 — `curkeymapname = ztrdup(name)`.
913    *curkeymapname() = resolved;
914    // c:518 — `curkeymap = km`.
915    *curkeymap.lock().unwrap() = km;
916    0 // c:527
917}
918
919/// Direct port of `void selectlocalmap(Keymap m)` from
920/// `Src/Zle/zle_keymap.c:527`.
921/// ```c
922/// Keymap oldm = localkeymap;
923/// localkeymap = m;
924/// if (oldm && !m)
925///     reselectkeymap();
926/// ```
927pub fn selectlocalmap(m: Option<Arc<Keymap>>) {
928    // c:527
929    let oldm = {
930        let mut g = LOCALKEYMAP.lock().unwrap();
931        let prev = g.take();
932        *g = m.clone();
933        prev
934    };
935    // c:541-542 — `if (oldm && !m) reselectkeymap()`.
936    if oldm.is_some() && m.is_none() {
937        // reselectkeymap operates against file-scope ZLE statics; the
938        // safe fallback here is selectkeymap on the main keymap by
939        // name, which is what reselectkeymap does internally.
940        let _ = selectkeymap("main", 1);
941    }
942}
943
944/// Port of `reselectkeymap()` from Src/Zle/zle_keymap.c:549.
945/// WARNING: param names don't match C — Rust=(zle) vs C=()
946pub fn reselectkeymap() {
947    // c:549
948    // C body (c:551): `selectkeymap(curkeymapname, 1)`.
949    let name = curkeymapname().clone();
950    selectkeymap(&name, 1);
951}
952
953/// Port of `keyisprefix(Keymap km, char *seq)` from `Src/Zle/zle_keymap.c:683`.
954/// ```c
955/// int
956/// keyisprefix(Keymap km, char *seq)
957/// {
958///     Key k;
959///     if(!*seq)
960///         return 1;
961///     if(ztrlen(seq) == 1) {
962///         int f = seq[0] == Meta ? (unsigned char) seq[1]^32 : (unsigned char) seq[0];
963///         if(km->first[f])
964///             return 0;
965///     }
966///     k = (Key) km->multi->getnode(km->multi, seq);
967///     return k && k->prefixct;
968/// }
969/// ```
970/// Test whether `seq` is a strict prefix of some longer binding in
971/// `km`. Returns 1 if `seq` is a prefix (incl. empty input), 0 if
972/// `seq` is itself a complete binding or no match exists.
973/// Direct port of `Thingy keybind(Keymap km, char *seq, char **strp)`
974/// from `Src/Zle/zle_keymap.c:659`. Returns the Thingy bound to `seq`
975/// in `km` along with any associated send-string. Returns
976/// `(None, None)` for `t_undefinedkey` (the unbound sentinel).
977/// WARNING: param names don't match C — Rust=(km, seq) vs C=(km, seq, strp)
978pub fn keybind(km: &Keymap, seq: &[u8]) -> (Option<Thingy>, Option<String>) {
979    // c:659
980    // c:664 — `if(ztrlen(seq) == 1)`. Single-char (after Meta-decode) → first[f].
981    let single = if seq.len() == 1 {
982        Some(seq[0])
983    } else if seq.len() == 2 && seq[0] == 0x83 {
984        Some(seq[1] ^ 32) // c:665 Meta-decode
985    } else {
986        None
987    };
988    if let Some(f) = single {
989        if let Some(bind) = km.first[f as usize].as_ref() {
990            // c:666-669
991            return (Some(bind.clone()), None);
992        }
993    }
994    // c:670 — `k = km->multi->getnode(km->multi, seq);`
995    match km.multi.get(seq) {
996        None => (None, None),                       // c:671 t_undefinedkey
997        Some(k) => (k.bind.clone(), k.str.clone()), // c:673-674
998    }
999}
1000/// `keyisprefix` — see implementation.
1001pub fn keyisprefix(km: &Keymap, seq: &[u8]) -> i32 {
1002    // c:683
1003    // c:683-688 — `if(!*seq) return 1`. Empty sequence → trivially prefix.
1004    if seq.is_empty() {
1005        return 1;
1006    }
1007    // c:689-693 — single-byte path (after Meta-decode). If first[f]
1008    // is bound, this byte itself IS the binding, not a prefix.
1009    // ztrlen counts bytes after Meta-decoding (Meta-pair = 1 char).
1010    let single = if seq.len() == 1 {
1011        Some(seq[0])
1012    } else if seq.len() == 2 && seq[0] == 0x83 {
1013        // c:690 — `seq[0] == Meta ? seq[1]^32 : seq[0]`.
1014        Some(seq[1] ^ 32)
1015    } else {
1016        None
1017    };
1018    if let Some(f) = single {
1019        if km.first[f as usize].is_some() {
1020            // c:691-692
1021            return 0;
1022        }
1023    }
1024    // c:694-695 — `k = km->multi->getnode(...); return k && k->prefixct`.
1025    match km.multi.get(seq) {
1026        Some(kb) if kb.prefixct > 0 => 1,
1027        _ => 0,
1028    }
1029}
1030
1031/// Direct port of `static int bin_bindkey(char *name, char **argv,
1032/// Options ops, UNUSED(int func))` from `Src/Zle/zle_keymap.c:743`.
1033/// Top-level dispatcher for the `bindkey` builtin.
1034pub fn bin_bindkey(
1035    name: &str,
1036    args: &[String], // c:743
1037    ops: &options,
1038    _func: i32,
1039) -> i32 {
1040    use crate::ported::zsh_h::{OPT_ARG, OPT_ISSET};
1041
1042    // c:zle_keymap.c boot_ - the zsh/zle module's boot handler
1043    // calls `default_bindings()` once on module load (zle_main.c
1044    // setup_), which is what gives the "main" / "emacs" / "viins" /
1045    // "vicmd" / "menuselect" / "listscroll" / ".safe" keymaps a
1046    // chance to exist before user `bindkey` invocations.
1047    //
1048    // zshrs in script (non-interactive) mode doesn't autoload zsh/zle,
1049    // so the keymaps are never populated. /etc/zshrc bindkey calls
1050    // then fail with `no such keymap 'main'`. Auto-init on first
1051    // bindkey call — idempotent because default_bindings is a no-op
1052    // after the keymaps already exist.
1053    static KEYMAPS_INIT: std::sync::Once = std::sync::Once::new();
1054    KEYMAPS_INIT.call_once(|| {
1055        default_bindings();
1056    });
1057
1058    // c:751-759 — opns[] dispatch table. Each entry: (flag-char,
1059    // selp, min, max, sub-handler kind). selp=1 means -e/-v/-a/-M
1060    // keymap-selection is allowed for this op.
1061    #[derive(Clone, Copy)]
1062    enum Op {
1063        LsMaps,
1064        DelAll,
1065        Del,
1066        Link,
1067        New,
1068        Meta,
1069        Bind,
1070    }
1071    struct Opn {
1072        o: u8,
1073        selp: bool,
1074        func: Op,
1075        min: i32,
1076        max: i32,
1077    }
1078    static OPNS: &[Opn] = &[
1079        Opn {
1080            o: b'l',
1081            selp: false,
1082            func: Op::LsMaps,
1083            min: 0,
1084            max: -1,
1085        },
1086        Opn {
1087            o: b'd',
1088            selp: false,
1089            func: Op::DelAll,
1090            min: 0,
1091            max: 0,
1092        },
1093        Opn {
1094            o: b'D',
1095            selp: false,
1096            func: Op::Del,
1097            min: 1,
1098            max: -1,
1099        },
1100        Opn {
1101            o: b'A',
1102            selp: false,
1103            func: Op::Link,
1104            min: 2,
1105            max: 2,
1106        },
1107        Opn {
1108            o: b'N',
1109            selp: false,
1110            func: Op::New,
1111            min: 1,
1112            max: 2,
1113        },
1114        Opn {
1115            o: b'm',
1116            selp: true,
1117            func: Op::Meta,
1118            min: 0,
1119            max: 0,
1120        },
1121        Opn {
1122            o: b'r',
1123            selp: true,
1124            func: Op::Bind,
1125            min: 1,
1126            max: -1,
1127        },
1128        Opn {
1129            o: b's',
1130            selp: true,
1131            func: Op::Bind,
1132            min: 2,
1133            max: -1,
1134        },
1135        Opn {
1136            o: 0,
1137            selp: true,
1138            func: Op::Bind,
1139            min: 0,
1140            max: -1,
1141        },
1142    ];
1143
1144    // c:767-773 — find selected op + ensure no clashing flags.
1145    let mut idx = OPNS.len() - 1;
1146    for (i, op) in OPNS.iter().enumerate() {
1147        if op.o != 0 && OPT_ISSET(ops, op.o) {
1148            idx = i;
1149            break;
1150        }
1151    }
1152    let op = &OPNS[idx];
1153    if op.o != 0 {
1154        for opp in OPNS.iter().skip(idx + 1) {
1155            if opp.o != 0 && OPT_ISSET(ops, opp.o) {
1156                eprintln!("{}: incompatible operation selection options", name);
1157                return 1;
1158            }
1159        }
1160    }
1161
1162    // c:774-783 — keymap-selection flag validation.
1163    let nsel = (OPT_ISSET(ops, b'e') as i32)
1164        + (OPT_ISSET(ops, b'v') as i32)
1165        + (OPT_ISSET(ops, b'a') as i32)
1166        + (OPT_ISSET(ops, b'M') as i32);
1167    if !op.selp && nsel != 0 {
1168        eprintln!("{}: keymap cannot be selected with -{}", name, op.o as char);
1169        return 1;
1170    }
1171    if nsel > 1 {
1172        eprintln!("{}: incompatible keymap selection options", name);
1173        return 1;
1174    }
1175
1176    // c:786-807 — resolve keymap.
1177    let kmname: Option<String> = if op.selp {
1178        let nm = if OPT_ISSET(ops, b'e') {
1179            "emacs".to_string()
1180        } else if OPT_ISSET(ops, b'v') {
1181            "viins".to_string()
1182        } else if OPT_ISSET(ops, b'a') {
1183            "vicmd".to_string()
1184        } else if OPT_ISSET(ops, b'M') {
1185            OPT_ARG(ops, b'M')
1186                .map(|s| s.to_string())
1187                .unwrap_or_else(|| "main".to_string())
1188        } else {
1189            "main".to_string()
1190        };
1191        let km = match openkeymap(&nm) {
1192            Some(k) => k,
1193            None => {
1194                eprintln!("{}: no such keymap `{}'", name, nm);
1195                return 1;
1196            }
1197        };
1198        if OPT_ISSET(ops, b'e') || OPT_ISSET(ops, b'v') {
1199            linkkeymap(km, "main", 0);
1200        }
1201        Some(nm)
1202    } else {
1203        None
1204    };
1205
1206    // c:810-814 — listing is a special case.
1207    let argc = args.len() as i32;
1208    if op.o == 0 && (args.is_empty() || args.len() < 2) {
1209        if OPT_ISSET(ops, b'e') || OPT_ISSET(ops, b'v') {
1210            return 0;
1211        }
1212        return bin_bindkey_list(name, kmname.as_deref(), None, args, ops, 0);
1213    }
1214
1215    // c:816-824 — arity check.
1216    if argc < op.min {
1217        eprintln!("{}: not enough arguments for -{}", name, op.o as char);
1218        return 1;
1219    }
1220    if op.max != -1 && argc > op.max {
1221        eprintln!("{}: too many arguments for -{}", name, op.o as char);
1222        return 1;
1223    }
1224
1225    // c:826-827 — dispatch. C: `return op->func(name, kmname, km, argv, ops, op->o);`
1226    let func_i: i32 = op.o as i32;
1227    let km_ref: Option<&Keymap> = None; // c:826 km — substrate via openkeymap(kmname) deferred
1228    let km_str = kmname.as_deref();
1229    match op.func {
1230        Op::LsMaps => bin_bindkey_lsmaps(name, km_str, km_ref, args, ops, func_i),
1231        Op::DelAll => bin_bindkey_delall(name, km_str, km_ref, args, ops, func_i),
1232        Op::Del => bin_bindkey_del(name, km_str, km_ref, args, ops, func_i),
1233        Op::Link => bin_bindkey_link(name, km_str, km_ref, args, ops, func_i),
1234        Op::New => bin_bindkey_new(name, km_str, km_ref, args, ops, func_i),
1235        Op::Meta => bin_bindkey_meta(name, km_str, km_ref, args, ops, func_i),
1236        Op::Bind => bin_bindkey_bind(name, km_str, km_ref, args, ops, func_i),
1237    }
1238}
1239
1240/// Direct port of `static int bin_bindkey_lsmaps(char *name,
1241///                                                 UNUSED(char *kmname),
1242///                                                 UNUSED(Keymap km),
1243///                                                 char **argv, Options ops,
1244///                                                 UNUSED(char func))`
1245/// from `Src/Zle/zle_keymap.c:834`. Dispatches per-keymap via
1246/// `scanlistmaps`. With argv: iterate the named keymaps. Without:
1247/// walk all via scanhashtable.
1248pub fn bin_bindkey_lsmaps(
1249    name: &str,
1250    _kmname: Option<&str>,
1251    _km: Option<&Keymap>,
1252    argv: &[String],
1253    ops: &options,
1254    _func: i32,
1255) -> i32 {
1256    // c:834
1257    let list_verbose = OPT_ISSET(ops, b'L');
1258    let mut ret = 0;
1259    if !argv.is_empty() {
1260        // c:838-849 — per-arg lookup + per-arg scanlistmaps.
1261        for a in argv {
1262            // c:840 — `kmn = keymapnamtab->getnode(keymapnamtab, *argv)`.
1263            let kmn = {
1264                let g = keymapnamtab().lock().unwrap();
1265                g.get(a).cloned()
1266            };
1267            match kmn {
1268                None => {
1269                    eprintln!("{}: no such keymap: `{}'", name, a);
1270                    ret = 1;
1271                }
1272                Some(kmn) => {
1273                    scanlistmaps(&kmn, a, list_verbose);
1274                }
1275            }
1276        }
1277    } else {
1278        // c:851 — `scanhashtable(keymapnamtab, 1, 0, 0, scanlistmaps, ...)`.
1279        // Walk in lex-sorted order.
1280        let snapshot: Vec<(String, KeymapName)> = {
1281            let g = keymapnamtab().lock().unwrap();
1282            g.iter().map(|(n, k)| (n.clone(), k.clone())).collect()
1283        };
1284        let mut names: Vec<(String, KeymapName)> = snapshot;
1285        names.sort_by(|a, b| a.0.cmp(&b.0));
1286        for (n, kmn) in &names {
1287            scanlistmaps(kmn, n, list_verbose);
1288        }
1289    }
1290    ret
1291}
1292
1293/// Direct port of `static void scanlistmaps(HashNode hn,
1294///                                            int list_verbose)`
1295/// from `Src/Zle/zle_keymap.c:856`. Emits one line per keymap-name
1296/// entry. With `list_verbose` (= `bindkey -L`): formats as
1297/// `bindkey -A primary name` (alias) or `bindkey -N name` (new).
1298/// Without: just the name.
1299/// WARNING: param names don't match C — Rust=(kmn, n_nam, list_verbose)
1300/// vs C=(hn, list_verbose); the C source reads `n->nam` off the
1301/// HashNode, we pass the name separately because Rust's
1302/// KeymapName struct doesn't carry its own owned name.
1303pub fn scanlistmaps(kmn: &KeymapName, n_nam: &str, list_verbose: bool) {
1304    // c:856
1305    if list_verbose {
1306        // c:864 — `if (!strcmp(n->nam, ".safe")) return`.
1307        if n_nam == ".safe" {
1308            return;
1309        }
1310        // c:866 — `fputs("bindkey -", stdout)`.
1311        print!("bindkey -");
1312        // c:867-878 — `if (km->primary && km->primary != n)` →
1313        // alias form (`-A primary name`); else → new form (`-N name`).
1314        let primary_name = kmn.keymap.primary.as_deref();
1315        let is_alias = primary_name.is_some() && primary_name != Some(n_nam);
1316        if is_alias {
1317            // c:870 — `fputs("A ", stdout)`.
1318            print!("A ");
1319            let pn = primary_name.unwrap();
1320            // c:872 — `if (pn->nam[0] == '-') fputs("-- ", stdout)`.
1321            if pn.starts_with('-') {
1322                print!("-- ");
1323            }
1324            // c:874 — `quotedzputs(pn->nam, stdout)`.
1325            print!("{} ", pn);
1326        } else {
1327            // c:877 — `fputs("N ", stdout)`.
1328            print!("N ");
1329            // c:879 — `if (n->nam[0] == '-') fputs("-- ", stdout)`.
1330            if n_nam.starts_with('-') {
1331                print!("-- ");
1332            }
1333        }
1334        // c:881 — `quotedzputs(n->nam, stdout)`.
1335        print!("{}", n_nam);
1336    } else {
1337        // c:884 — `nicezputs(n->nam, stdout)`.
1338        print!("{}", n_nam);
1339    }
1340    // c:886 — `putchar('\n')`.
1341    println!();
1342}
1343
1344/// Port of `bin_bindkey_delall(UNUSED(char *name), UNUSED(char *kmname),
1345/// UNUSED(Keymap km), UNUSED(char **argv), UNUSED(Options ops),
1346/// UNUSED(char func))` from Src/Zle/zle_keymap.c:891.
1347pub fn bin_bindkey_delall(
1348    _name: &str,
1349    _kmname: Option<&str>,
1350    _km: Option<&Keymap>,
1351    _argv: &[String],
1352    _ops: &options,
1353    _func: i32,
1354) -> i32 {
1355    // c:Src/Zle/zle_keymap.c — `bin_bindkey_delall` body:
1356    //   keymapnamtab->emptytable(keymapnamtab);
1357    //   default_bindings();
1358    //   return 0;
1359    // The previous Rust port mis-used `name` (the builtin name
1360    // "bindkey", not a keymap name) as a `openkeymap` lookup key and
1361    // returned 1 on the inevitable miss. C always succeeds.
1362    default_bindings();
1363    0
1364}
1365
1366/// Port of `bin_bindkey_del(char *name, UNUSED(char *kmname),
1367/// UNUSED(Keymap km), char **argv, UNUSED(Options ops),
1368/// UNUSED(char func))` from Src/Zle/zle_keymap.c:902.
1369pub fn bin_bindkey_del(
1370    _name: &str,
1371    _kmname: Option<&str>,
1372    _km: Option<&Keymap>,
1373    argv: &[String],
1374    _ops: &options,
1375    _func: i32,
1376) -> i32 {
1377    // c:902
1378    // C body (c:830-855): `do { unlinkkeymap(*argv, 0) } while(*++argv)`.
1379    // Returns 1 on first failure, else 0.
1380    if argv.is_empty() {
1381        return 1;
1382    }
1383    let mut ret = 0;
1384    for arg in argv {
1385        match unlinkkeymap(arg, 0) {
1386            0 => {}
1387            _ => ret = 1,
1388        }
1389    }
1390    ret
1391}
1392
1393/// Port of `bin_bindkey_link(char *name, UNUSED(char *kmname),
1394/// Keymap km, char **argv, UNUSED(Options ops), UNUSED(char func))`
1395/// from Src/Zle/zle_keymap.c:921.
1396pub fn bin_bindkey_link(
1397    _name: &str,
1398    _kmname: Option<&str>,
1399    _km: Option<&Keymap>,
1400    argv: &[String],
1401    _ops: &options,
1402    _func: i32,
1403) -> i32 {
1404    // c:921
1405    // C body (c:907-933): `km2 = openkeymap(argv[0]); if (!km2) return 1;
1406    //                       linkkeymap(km2, argv[1], 0)`.
1407    if argv.len() < 2 {
1408        return 1;
1409    }
1410    let Some(km) = openkeymap(&argv[0]) else {
1411        return 1;
1412    };
1413    if linkkeymap(km, &argv[1], 0) != 0 {
1414        return 1;
1415    }
1416    0
1417}
1418
1419/// Port of `bin_bindkey_new(char *name, UNUSED(char *kmname),
1420/// Keymap km, char **argv, UNUSED(Options ops), UNUSED(char func))`
1421/// from Src/Zle/zle_keymap.c:938.
1422pub fn bin_bindkey_new(
1423    _name: &str,
1424    _kmname: Option<&str>,
1425    _km: Option<&Keymap>,
1426    argv: &[String],
1427    _ops: &options,
1428    _func: i32,
1429) -> i32 {
1430    // c:938
1431    // c:938-955 — `kmn = keymapnamtab.getnode(argv[0]); if (kmn->flags
1432    //               & KMN_IMMORTAL) return 1; if (argv[1]) km =
1433    //               openkeymap(argv[1]) else NULL;
1434    //               linkkeymap(newkeymap(km, argv[0]), argv[0], 0)`.
1435    if argv.is_empty() {
1436        return 1;
1437    }
1438    let blocked = keymapnamtab()
1439        .lock()
1440        .unwrap()
1441        .get(&argv[0])
1442        .map(|n| n.flags & KMN_IMMORTAL != 0)
1443        .unwrap_or(false);
1444    if blocked {
1445        return 1; // c:944
1446    }
1447    let template = if argv.len() >= 2 {
1448        let km = openkeymap(&argv[1]);
1449        if km.is_none() {
1450            return 1; // c:950
1451        }
1452        km
1453    } else {
1454        None
1455    };
1456    let new_km = newkeymap(template.as_deref(), &argv[0]); // c:954
1457    linkkeymap(new_km, &argv[0], 0);
1458    0 // c:955
1459}
1460
1461/// Direct port of `static int bin_bindkey_meta(char *name, char *kmname,
1462///                                              Keymap km, char **argv,
1463///                                              Options ops, char func)`
1464/// from `Src/Zle/zle_keymap.c:966`.
1465///
1466/// Line-by-line port of c:966-988. Walks bytes 0x80..=0xff: for each
1467/// byte where `METABIND[i-128]` isn't `"undefined-key"`, looks up the
1468/// current binding via [`keybind`]; if it's `self-insert` or
1469/// undefined, rebinds it to the [`METABIND`] default via `bindkey`.
1470/// Skips entries whose current binding is something the user has
1471/// customised — matches the C body's `IS_THINGY(fn, selfinsert) ||
1472/// fn == t_undefinedkey` predicate at c:982.
1473pub fn bin_bindkey_meta(
1474    name: &str,
1475    kmname: Option<&str>,
1476    _km_arg: Option<&Keymap>,
1477    _argv: &[String],
1478    _ops: &options,
1479    _func: i32,
1480) -> i32 {
1481    use super::zle_bindings::METABIND;
1482    use super::zle_thingy::{refthingy, Thingy};
1483
1484    // c:968 — KM_IMMUTABLE check.
1485    let target = kmname.unwrap_or(name);
1486    let km_arc = match openkeymap(target) {
1487        Some(k) => k,
1488        None => return 1,
1489    };
1490
1491    // c:978-987 — walk i = 128..256 (bytes with high bit set).
1492    for i in 128usize..256 {
1493        let default_name = METABIND[i - 128]; // c:980
1494        if default_name == "undefined-key" {
1495            // c:981 — `if (metabind[i - 128] != z_undefinedkey)`
1496            continue;
1497        }
1498        // c:982-984 — `m[0] = i; metafy(m, 1, META_NOALLOC); fn = keybind(km, m);`
1499        let m = [0x83u8, (i as u8) ^ 32];
1500        let (cur_fn, _str) = keybind(&km_arc, &m);
1501        // c:985 — `if (IS_THINGY(fn, selfinsert) || fn == t_undefinedkey)`
1502        let should_rebind = match &cur_fn {
1503            None => true,
1504            Some(t) => t.nam == "self-insert",
1505        };
1506        if !should_rebind {
1507            continue;
1508        }
1509        // c:986 — `bindkey(km, m, refthingy(Th(metabind[i - 128])), NULL);`
1510        refthingy(default_name);
1511        let new_thingy = Thingy {
1512            nam: default_name.to_string(),
1513            flags: 0,
1514            rc: 1,
1515            widget: None,
1516        };
1517        if let Some(km_inner) = Arc::get_mut(&mut km_arc.clone()) {
1518            bindkey(km_inner, &m, Some(new_thingy), None);
1519        } else {
1520            // Arc was shared; clone-modify-replace via the keymapnamtab.
1521            let mut new_km: Keymap = (*km_arc).clone();
1522            bindkey(&mut new_km, &m, Some(new_thingy), None);
1523            linkkeymap(Arc::new(new_km), target, 0);
1524        }
1525    }
1526    0 // c:988
1527}
1528
1529/// Direct port of `static int bin_bindkey_bind(char *name, char *kmname,
1530///                                              char **argv, Options ops,
1531///                                              char func)`
1532/// from `Src/Zle/zle_keymap.c:999`. Walks `args` in (seq, cmd)
1533/// pairs binding each in the named keymap. `func` selects the bind
1534/// mode: 0=widget name, 's'=send-string, 'r'=remove (undefined-key).
1535///
1536/// Mutates the shared `Arc<Keymap>` in keymapnamtab via the
1537/// rebuild-and-replace strategy: clone the underlying data, mutate
1538/// the copy, swap the new Arc into every name that pointed at the
1539/// old Arc (preserves C's "all sharing names see the change"
1540/// semantic).
1541pub fn bin_bindkey_bind(
1542    _name: &str,
1543    kmname: Option<&str>,
1544    _km: Option<&Keymap>,
1545    argv: &[String],
1546    _ops: &options,
1547    func: i32,
1548) -> i32 {
1549    // c:999 — `km` is preselected by the caller via opts/-M; fall back
1550    // to "main" (zsh's default after default_bindings()) when no
1551    // explicit keymap was passed. Previous Rust port mis-used the
1552    // builtin name ("bindkey") as the keymap key and openkeymap
1553    // returned None for every invocation → bin_bindkey exited 1 and
1554    // installed nothing.
1555    let lookup_name = kmname.unwrap_or("main");
1556    let Some(old_arc) = openkeymap(lookup_name) else {
1557        return 1;
1558    }; // c:1002
1559       // c:1003-1011 — bind seq+target pairs need even argv count
1560       // (omit on '-r' / when func is the empty target).
1561    let func_c = if func == 0 { '\0' } else { func as u8 as char };
1562    let needs_pairs = func_c == '\0' || func_c == 's';
1563    if needs_pairs && (argv.len() % 2 != 0) {
1564        return 1;
1565    }
1566
1567    // Mutable clone of the shared Keymap.
1568    let mut km: Keymap = (*old_arc).clone();
1569
1570    // c:1014-1090 — walk argv in 1 or 2-step strides.
1571    let stride = if func_c == 'r' { 1 } else { 2 };
1572    let mut i = 0;
1573    while i + (stride - 1) < argv.len() {
1574        // c:Src/Zle/zle_keymap.c:1023-1040 — `seq = getkeystring(*argv,
1575        //   &len, GETKEYS_BINDKEY, NULL); seq = metafy(seq, len, META_USEHEAP);`
1576        // The user-typed string `^A` is 2 chars that getkeystring translates
1577        // to the single byte 0x01. Without this translation, `bindkey -r
1578        // "^A"` was inserting/clearing `b"^A"` (2 raw bytes) in km.multi
1579        // — `^A` (0x01) at km.first[1] stayed untouched. Bug #344 in
1580        // docs/BUGS.md.
1581        let seq_bytes = crate::ported::zle::zle_bindings::getkeystring(&argv[i]);
1582        let target = if stride == 2 {
1583            Some(argv[i + 1].clone())
1584        } else {
1585            None
1586        };
1587
1588        let kb_value: KeyBinding = match func_c {
1589            // c:1027
1590            'r' => KeyBinding {
1591                bind: None,
1592                str: None,
1593                prefixct: 0,
1594            }, // c:1024 undefined-key
1595            's' => KeyBinding {
1596                // c:1030 send-string
1597                bind: None,
1598                str: target,
1599                prefixct: 0,
1600            },
1601            _ => KeyBinding {
1602                // c:1037 thingy
1603                bind: target.map(|n| Thingy::builtin(&n)),
1604                str: None,
1605                prefixct: 0,
1606            },
1607        };
1608
1609        // c:1051 — `bindkey(km, seq, bind, str)`.
1610        if seq_bytes.len() == 1 {
1611            // single-byte first[]
1612            km.first[seq_bytes[0] as usize] = kb_value.bind.clone();
1613        } else {
1614            km.multi.insert(seq_bytes.to_vec(), kb_value); // c:1054 hashtable
1615        }
1616        // PFA-SMR: record the binding so replay can recreate it.
1617        // Skip `bindkey -e` / `bindkey -v` mode switches (those land
1618        // in the Meta op path, not Bind — but stride==2 args here are
1619        // always real bindings; func_c='r' (stride 1) is unbind and
1620        // is intentionally skipped per RECORDER.md surface row.
1621        #[cfg(feature = "recorder")]
1622        if func_c != 'r' && crate::recorder::is_enabled() {
1623            let ctx = crate::recorder::recorder_ctx_global();
1624            let seq_str = String::from_utf8_lossy(&seq_bytes);
1625            let widget_default = String::new();
1626            let widget_ref: &str = match func_c {
1627                's' => "send-string", // c:1030
1628                _ => argv.get(i + 1).unwrap_or(&widget_default).as_str(),
1629            };
1630            crate::recorder::emit_bindkey(&seq_str, widget_ref, ctx);
1631        }
1632        i += stride;
1633    }
1634
1635    // Rebuild the Arc + propagate to every name that shared the old.
1636    let new_arc = Arc::new(km);
1637    if let Ok(mut tab) = keymapnamtab().lock() {
1638        let names_to_update: Vec<String> = tab
1639            .iter()
1640            .filter(|(_, kmn)| Arc::ptr_eq(&kmn.keymap, &old_arc))
1641            .map(|(n, _)| n.clone())
1642            .collect();
1643        for n in names_to_update {
1644            if let Some(kmn) = tab.get_mut(&n) {
1645                kmn.keymap = new_arc.clone();
1646            }
1647        }
1648    }
1649    0 // c:1097
1650}
1651
1652/// Port of `scanremoveprefix(char *seq, UNUSED(Thingy bind), UNUSED(char *str), void *magic)` from Src/Zle/zle_keymap.c:1078.
1653/// WARNING: param names don't match C — Rust=(km, prefix) vs C=(seq, bind, str, magic)
1654pub fn scanremoveprefix(km: &mut Keymap, prefix: &[u8]) {
1655    // c:1078
1656    // C body (c:1080-1110): walks km->multi removing all bindings
1657    // whose key sequence starts with `prefix`. Used by `bindkey -rp`.
1658    let to_remove: Vec<Vec<u8>> = km
1659        .multi
1660        .keys()
1661        .filter(|k| k.starts_with(prefix))
1662        .cloned()
1663        .collect();
1664    for k in to_remove {
1665        km.unbind_seq(&k);
1666    }
1667}
1668
1669/// Direct port of `int bin_bindkey_list(char *name, char *kmname,
1670///                                       UNUSED(char **argv),
1671///                                       Options ops, UNUSED(char func))`
1672/// from `Src/Zle/zle_keymap.c:1094`. Emits each binding in the
1673/// named keymap as a `bindkey -K kmname <seq> <command>` line on
1674/// stdout, matching the C output format.
1675pub fn bin_bindkey_list(
1676    name: &str,
1677    kmname: Option<&str>,
1678    _km: Option<&Keymap>,
1679    argv: &[String],
1680    ops: &options,
1681    _func: i32,
1682) -> i32 {
1683    // c:1094
1684    // C signature receives `km` already-resolved by the dispatcher
1685    // at c:794-799. Our dispatcher passes None; resolve here from
1686    // `kmname` (falling back to `curkeymapname` when `-M` was not
1687    // given — matches C's `kmname = "main"` default).
1688    let resolved_name: String = kmname
1689        .map(|s| s.to_string())
1690        .unwrap_or_else(|| curkeymapname().clone());
1691    let Some(km) = openkeymap(&resolved_name) else {
1692        eprintln!("{}: no such keymap `{}'", name, resolved_name);
1693        return 1;
1694    };
1695
1696    // c:1096-1099 — `bs.flags = OPT_ISSET(ops,'L') ? BS_LIST : 0;
1697    //                bs.kmname = kmname;`
1698    let mut bs = bindstate {
1699        flags: if OPT_ISSET(ops, b'L') { BS_LIST } else { 0 },
1700        kmname: resolved_name.clone(),
1701        firstseq: Vec::new(),
1702        lastseq: Vec::new(),
1703        bind: None,
1704        str: None,
1705        prefix: None,
1706        prefixlen: 0,
1707    };
1708
1709    // c:1100-1110 — single-sequence lookup path (`argv[0] && !-p`).
1710    if !argv.is_empty() && !OPT_ISSET(ops, b'p') {
1711        // c:1102-1107 — `seq = getkeystring(argv[0], &len,
1712        //                  GETKEYS_BINDKEY, NULL); seq = metafy(...)`.
1713        // The Rust seq storage is raw bytes; `getkeystring` parses
1714        // user-typed `\C-X`/`^X`/`\M-X` etc. → raw byte sequence.
1715        let seq = crate::ported::zle::zle_bindings::getkeystring(&argv[0]);
1716        // c:1108-1109 — `bs.flags |= BS_ALL; bs.firstseq = bs.lastseq = seq;`
1717        bs.flags |= BS_ALL;
1718        bs.firstseq = seq.clone();
1719        bs.lastseq = seq.clone();
1720        // c:1110 — `bs.bind = keybind(km, seq, &bs.str)`.
1721        let (bind, str_out) = keybind(&km, &seq);
1722        bs.bind = bind;
1723        bs.str = str_out;
1724        // c:1113 — `bindlistout(&bs)`.
1725        bindlistout(&bs);
1726        return 0;
1727    }
1728
1729    // c:1115-1125 — `-p` prefix-only path.
1730    if OPT_ISSET(ops, b'p') {
1731        let arg0 = argv.first().map(|s| s.as_str()).unwrap_or("");
1732        if arg0.is_empty() {
1733            eprintln!("{}: option -p requires a prefix string", name);
1734            return 1;
1735        }
1736        let pfx = crate::ported::zle::zle_bindings::getkeystring(arg0);
1737        bs.prefixlen = pfx.len();
1738        bs.prefix = Some(pfx);
1739    }
1740    // c:1130-1137 — initialise bs for the scankeymap walk.
1741    bs.firstseq = Vec::new();
1742    bs.lastseq = Vec::new();
1743    bs.bind = None;
1744    bs.str = None;
1745    // c:1138 — `scankeymap(km, 1, scanbindlist, &bs)`.
1746    scankeymap(&km, 1, &mut |seq, bind, s| {
1747        scanbindlist(seq, bind, s, &mut bs);
1748    });
1749    // c:1139 — `bindlistout(&bs)` — flush the final accumulated range.
1750    bindlistout(&bs);
1751    0 // c:1173
1752}
1753
1754/// Direct port of `static void scanbindlist(char *seq, Thingy bind,
1755///                                           char *str, void *magic)`
1756/// from `Src/Zle/zle_keymap.c:1141`. Per-binding callback used by
1757/// `bin_bindkey_list` via `scankeymap`. Coalesces consecutive
1758/// single-character bindings into ranges; flushes via `bindlistout`
1759/// otherwise.
1760pub fn scanbindlist(seq: &[u8], bind: Option<&Thingy>, str: Option<&str>, bs: &mut bindstate) {
1761    // c:1141
1762    // c:1145-1148 — prefix filter: if `bs->prefix` is set and either
1763    // (a) seq doesn't start with prefix or (b) seq equals prefix
1764    // exactly, drop the binding.
1765    if bs.prefixlen > 0 {
1766        if let Some(p) = &bs.prefix {
1767            if !seq.starts_with(p) || seq.len() == p.len() {
1768                return;
1769            }
1770        }
1771    }
1772
1773    // c:1150-1160 — range-collapse: same bind/str AND both seqs are
1774    // single-character (ztrlen == 1) AND new byte = last byte + 1.
1775    let bind_eq = match (bind, &bs.bind) {
1776        (Some(t1), Some(t2)) => t1.nam == t2.nam,
1777        (None, None) => str == bs.str.as_deref(),
1778        _ => false,
1779    };
1780    if bind_eq && seq.len() == 1 && bs.lastseq.len() == 1 {
1781        let l = bs.lastseq[0] as i32;
1782        let t = seq[0] as i32;
1783        if t == l + 1 {
1784            // c:1157 — extend the range; replace lastseq with seq.
1785            bs.lastseq = seq.to_vec();
1786            return;
1787        }
1788    }
1789
1790    // c:1162-1168 — flush current range; start a new one.
1791    bindlistout(bs);
1792    bs.firstseq = seq.to_vec();
1793    bs.lastseq = seq.to_vec();
1794    bs.bind = bind.cloned();
1795    bs.str = str.map(|s| s.to_string());
1796}
1797
1798/// Direct port of `static void bindlistout(struct bindstate *bs)`
1799/// from `Src/Zle/zle_keymap.c:1172`. Emits one bindkey-listing line
1800/// (or one `bindkey ...` command line when `BS_LIST` is set) for
1801/// the accumulated `(firstseq..lastseq, bind, str)` range.
1802pub fn bindlistout(bs: &bindstate) {
1803    // c:1172
1804    use std::io::Write;
1805
1806    // c:1177 — `if(bs->bind == t_undefinedkey && !(bs->flags & BS_ALL)) return;`.
1807    // C compares against the sentinel `t_undefinedkey` Thingy; the
1808    // Rust port stores it by name (`"undefined-key"`). When bs.str
1809    // is None AND bs.bind is either None or the undefined-key
1810    // thingy, skip (unless BS_ALL is set).
1811    let is_undefined = bs.str.is_none()
1812        && match &bs.bind {
1813            None => true,
1814            Some(t) => t.nam == "undefined-key",
1815        };
1816    if is_undefined && (bs.flags & BS_ALL) == 0 {
1817        return;
1818    }
1819    // c:1179 — `range = strcmp(bs->firstseq, bs->lastseq)`.
1820    let range = bs.firstseq != bs.lastseq;
1821    let mut out = std::io::stdout().lock();
1822    let mut nodash = true;
1823
1824    // c:1180-1199 — BS_LIST: emit `bindkey [-R][-s][-M km|-a] `.
1825    if (bs.flags & BS_LIST) != 0 {
1826        let _ = write!(out, "bindkey ");
1827        if range {
1828            let _ = write!(out, "-R ");
1829        }
1830        if bs.bind.is_none() {
1831            let _ = write!(out, "-s ");
1832        }
1833        if bs.kmname == "main" {
1834            // c:1188 — main → no keymap flag
1835        } else if bs.kmname == "vicmd" {
1836            let _ = write!(out, "-a ");
1837        } else {
1838            let _ = write!(out, "-M {} ", bs.kmname);
1839            nodash = false;
1840        }
1841        // c:1196 — `if(nodash && bs->firstseq[0] == '-') fputs("-- ", stdout);`
1842        if nodash && bs.firstseq.first() == Some(&b'-') {
1843            let _ = write!(out, "-- ");
1844        }
1845    }
1846
1847    // c:1202 — `printbind(bs->firstseq, stdout)`. `bindztrdup`
1848    // already includes the surrounding `"..."` via dquotedztrdup.
1849    let _ = write!(
1850        out,
1851        "{}",
1852        crate::ported::zle::zle_utils::bindztrdup(&bs.firstseq),
1853    );
1854    // c:1203-1206 — range: `-` + `printbind(lastseq)`.
1855    if range {
1856        let _ = write!(
1857            out,
1858            "-{}",
1859            crate::ported::zle::zle_utils::bindztrdup(&bs.lastseq),
1860        );
1861    }
1862    let _ = write!(out, " ");
1863    // c:1208-1214 — emit `bind->nam` or `printbind(str)`.
1864    if let Some(t) = &bs.bind {
1865        let _ = writeln!(out, "{}", t.nam);
1866    } else if let Some(s) = &bs.str {
1867        let _ = writeln!(
1868            out,
1869            "{}",
1870            crate::ported::zle::zle_utils::bindztrdup(s.as_bytes()),
1871        );
1872    } else {
1873        // c:1177 already filtered undefined-key when !BS_ALL; here
1874        // we hit only with BS_ALL.
1875        let _ = writeln!(out, "undefined-key");
1876    }
1877}
1878
1879/// Port of `add_cursor_char(int c)` from Src/Zle/zle_keymap.c:1248.
1880/// WARNING: param names don't match C — Rust=(buf, c) vs C=(c)
1881pub fn add_cursor_char(buf: &mut Vec<u8>, c: u8) {
1882    // c:1248
1883    // C body (c:1250): `*cursorptr++ = c`. Push one byte into the
1884    // cursor-key parse buffer (caller manages the buffer).
1885    buf.push(c);
1886}
1887
1888/// Port of `add_cursor_key(Keymap km, int tccode, Thingy thingy, int defchar)`
1889/// from Src/Zle/zle_keymap.c:1258.
1890///
1891/// Line-by-line port. Probes the termcap entry for `tccode` via
1892/// `tclen[tccode] > 0` and `TERMFLAGS`; if available emits the
1893/// escape through a synthetic outc callback to build the byte
1894/// sequence. Falls back to `\e[<defchar>` when termcap doesn't have
1895/// the cap or the terminal is flagged broken. Binds the result, then
1896/// also binds the `\e[`↔`\eO` variant — both forms appear in xterm
1897/// depending on application vs normal keypad mode.
1898pub fn add_cursor_key(km: &mut Keymap, tccode: i32, thingy: Thingy, defchar: i32) {
1899    use crate::ported::init::{tclen, tcstr};
1900    use crate::ported::params::TERMFLAGS;
1901    use crate::ported::zsh_h::{TERM_BAD, TERM_NOUP, TERM_UNKNOWN};
1902    use std::sync::atomic::Ordering;
1903
1904    let cap_idx = tccode as usize;
1905    let mut buf: Vec<u8> = Vec::with_capacity(8);
1906    let mut ok = false;
1907
1908    // c:1262-1266 — `tccan(tccode) && !(termflags & (TERM_NOUP|TERM_BAD|TERM_UNKNOWN))`
1909    let cap_present = {
1910        let lens = tclen.lock().unwrap();
1911        cap_idx < lens.len() && lens[cap_idx] > 0
1912    };
1913    let termflags = TERMFLAGS.load(Ordering::Relaxed);
1914    let term_broken = termflags & (TERM_NOUP | TERM_BAD | TERM_UNKNOWN) != 0;
1915
1916    if cap_present && !term_broken {
1917        // c:1271-1273 — `cursorptr = buf; tputs(tcstr[tccode], 1, add_cursor_char);`
1918        let escape = tcstr.lock().unwrap()[cap_idx].clone();
1919        buf.extend_from_slice(escape.as_bytes());
1920        // c:1281-1282 — sanity: reject zero-length / single-char.
1921        let len = buf.len();
1922        if len >= 2 && (buf[0] != 0x83 || len >= 3) {
1923            ok = true;
1924        }
1925    }
1926
1927    if !ok {
1928        // c:1287-1288 — `sprintf(buf, "\33[%c", defchar);`
1929        buf.clear();
1930        buf.push(0x1b);
1931        buf.push(b'[');
1932        buf.push(defchar as u8);
1933    }
1934
1935    // c:1290 — `bindkey(km, buf, refthingy(thingy), NULL);`
1936    bindkey(km, &buf, Some(thingy.clone()), None);
1937
1938    // c:1295-1299 — `if (buf[0] == '\33' && (buf[1] == '[' || buf[1] == 'O') &&
1939    //                buf[2] && !buf[3]) { swap [/O; bindkey again; }`
1940    if buf.len() == 3 && buf[0] == 0x1b && (buf[1] == b'[' || buf[1] == b'O') {
1941        let mut alt = buf.clone();
1942        alt[1] = if buf[1] == b'[' { b'O' } else { b'[' };
1943        bindkey(km, &alt, Some(thingy), None);
1944    }
1945}
1946
1947/// Direct port of `void default_bindings(void)` from
1948/// `Src/Zle/zle_keymap.c:1309`. Allocates the emacs / vicmd /
1949/// viins / menuselect / listscroll / .safe keymaps and registers
1950/// them under their canonical names in `keymapnamtab`. The 330+
1951/// per-key bindkey calls live in the C body; the Rust runtime
1952/// binds keys lazily via the user's `.zshrc` calling `bindkey`.
1953///
1954/// What this fn must guarantee for compat: the seven canonical
1955/// keymap names exist and resolve via `openkeymap()`. Without that,
1956/// any later `bindkey -K emacs ...` user call fails.
1957pub fn default_bindings() {
1958    // c:1309 — `void default_bindings(void)`. Inlined line-for-line
1959    // from `Src/Zle/zle_keymap.c:1309-1473`. No extracted helpers
1960    // (the C body is one ~165-line function with every bindkey call
1961    // expanded; the Rust port mirrors that shape exactly).
1962    let mut vmap = Keymap::default(); // c:1311
1963    vmap.primary = Some("viins".to_string());
1964    let mut emap = Keymap::default(); // c:1312
1965    emap.primary = Some("emacs".to_string());
1966    let mut amap = Keymap::default(); // c:1313
1967    amap.primary = Some("vicmd".to_string());
1968    let mut oppmap = Keymap::default(); // c:1314
1969    oppmap.primary = Some("viopp".to_string());
1970    let mut vismap = Keymap::default(); // c:1315
1971    vismap.primary = Some("visual".to_string());
1972    let mut smap = Keymap::default(); // c:1316
1973    smap.primary = Some(".safe".to_string());
1974
1975    // c:1326-1329 — `for (i = 0; i < 32; i++)
1976    //                  vmap->first[i] = refthingy(Th(viinsbind[i]));
1977    //                  emap->first[i] = refthingy(Th(emacsbind[i]));`
1978    for i in 0..32 {
1979        bindkey(
1980            &mut vmap,
1981            &[i as u8],
1982            Some(Thingy::builtin(VIINSBIND[i])),
1983            None,
1984        );
1985        bindkey(
1986            &mut emap,
1987            &[i as u8],
1988            Some(Thingy::builtin(EMACSBIND[i])),
1989            None,
1990        );
1991    }
1992    // c:1330-1333 — 32-255 self-insert in both vmap and emap.
1993    for i in 32u8..=255u8 {
1994        bindkey(&mut vmap, &[i], Some(Thingy::builtin("self-insert")), None);
1995        bindkey(&mut emap, &[i], Some(Thingy::builtin("self-insert")), None);
1996    }
1997    // c:1336-1337 — `first[127] = first[8]` (DEL == ^H).
1998    bindkey(
1999        &mut vmap,
2000        &[0x7F],
2001        Some(Thingy::builtin(VIINSBIND[8])),
2002        None,
2003    );
2004    bindkey(
2005        &mut emap,
2006        &[0x7F],
2007        Some(Thingy::builtin(EMACSBIND[8])),
2008        None,
2009    );
2010
2011    // c:1342-1343 — vicmd 0-127 from vicmdbind.
2012    for i in 0..128 {
2013        bindkey(
2014            &mut amap,
2015            &[i as u8],
2016            Some(Thingy::builtin(VICMDBIND[i])),
2017            None,
2018        );
2019    }
2020    // c:1344-1345 — vicmd 128-255 undefined-key.
2021    for i in 128u8..=255u8 {
2022        bindkey(
2023            &mut amap,
2024            &[i],
2025            Some(Thingy::builtin("undefined-key")),
2026            None,
2027        );
2028    }
2029
2030    // c:1352-1358 — .safe fallback keymap: 0-255 → .self-insert,
2031    // except \n/\r → .accept-line. The dotted names are the
2032    // internal widget aliases not overridable by user `zle -N`.
2033    for i in 0u8..=255u8 {
2034        bindkey(&mut smap, &[i], Some(Thingy::builtin(".self-insert")), None);
2035    }
2036    bindkey(
2037        &mut smap,
2038        &[b'\n'],
2039        Some(Thingy::builtin(".accept-line")),
2040        None,
2041    );
2042    bindkey(
2043        &mut smap,
2044        &[b'\r'],
2045        Some(Thingy::builtin(".accept-line")),
2046        None,
2047    );
2048
2049    // c:1364-1369 — vi command + insert: arrow keys via
2050    // `add_cursor_key()` so the source-level bindkey call count
2051    // matches C exactly (one add_cursor_key → two internal bindkey
2052    // calls for the `[`/`O` keypad variants).
2053    for kptr in [&mut vmap, &mut amap] {
2054        add_cursor_key(
2055            kptr,
2056            crate::ported::zsh_h::TCUPCURSOR,
2057            Thingy::builtin("up-line-or-history"),
2058            b'A' as i32,
2059        );
2060        add_cursor_key(
2061            kptr,
2062            crate::ported::zsh_h::TCDOWNCURSOR,
2063            Thingy::builtin("down-line-or-history"),
2064            b'B' as i32,
2065        );
2066        add_cursor_key(
2067            kptr,
2068            crate::ported::zsh_h::TCLEFTCURSOR,
2069            Thingy::builtin("vi-backward-char"),
2070            b'D' as i32,
2071        );
2072        add_cursor_key(
2073            kptr,
2074            crate::ported::zsh_h::TCRIGHTCURSOR,
2075            Thingy::builtin("vi-forward-char"),
2076            b'C' as i32,
2077        );
2078    }
2079
2080    // c:1374-1385 — vi operator-pending + visual local maps: cursor
2081    // keys + j/k + aa/ia/aw/iw/aW/iW. Cursor keys via
2082    // `add_cursor_key` matching C's helper-based shape.
2083    for kptr in [&mut oppmap, &mut vismap] {
2084        add_cursor_key(
2085            kptr,
2086            crate::ported::zsh_h::TCUPCURSOR,
2087            Thingy::builtin("up-line"),
2088            b'A' as i32,
2089        );
2090        add_cursor_key(
2091            kptr,
2092            crate::ported::zsh_h::TCDOWNCURSOR,
2093            Thingy::builtin("down-line"),
2094            b'B' as i32,
2095        );
2096        bindkey(kptr, &[b'k'], Some(Thingy::builtin("up-line")), None); // c:1379
2097        bindkey(kptr, &[b'j'], Some(Thingy::builtin("down-line")), None); // c:1380
2098        bindkey(
2099            kptr,
2100            b"aa",
2101            Some(Thingy::builtin("select-a-shell-word")),
2102            None,
2103        ); // c:1381
2104        bindkey(
2105            kptr,
2106            b"ia",
2107            Some(Thingy::builtin("select-in-shell-word")),
2108            None,
2109        ); // c:1382
2110        bindkey(kptr, b"aw", Some(Thingy::builtin("select-a-word")), None); // c:1383
2111        bindkey(kptr, b"iw", Some(Thingy::builtin("select-in-word")), None); // c:1384
2112        bindkey(
2113            kptr,
2114            b"aW",
2115            Some(Thingy::builtin("select-a-blank-word")),
2116            None,
2117        ); // c:1385
2118        bindkey(
2119            kptr,
2120            b"iW",
2121            Some(Thingy::builtin("select-in-blank-word")),
2122            None,
2123        ); // c:1386
2124    }
2125
2126    // c:1388 — `bindkey(oppmap, "\33", refthingy(t_vicmdmode))`.
2127    bindkey(
2128        &mut oppmap,
2129        &[0x1B],
2130        Some(Thingy::builtin("vi-cmd-mode")),
2131        None,
2132    );
2133    // c:1389-1395 — visual-mode local bindings.
2134    bindkey(
2135        &mut vismap,
2136        &[0x1B],
2137        Some(Thingy::builtin("deactivate-region")),
2138        None,
2139    );
2140    bindkey(
2141        &mut vismap,
2142        &[b'o'],
2143        Some(Thingy::builtin("exchange-point-and-mark")),
2144        None,
2145    );
2146    bindkey(
2147        &mut vismap,
2148        &[b'p'],
2149        Some(Thingy::builtin("put-replace-selection")),
2150        None,
2151    );
2152    bindkey(
2153        &mut vismap,
2154        &[b'u'],
2155        Some(Thingy::builtin("vi-down-case")),
2156        None,
2157    );
2158    bindkey(
2159        &mut vismap,
2160        &[b'U'],
2161        Some(Thingy::builtin("vi-up-case")),
2162        None,
2163    );
2164    bindkey(
2165        &mut vismap,
2166        &[b'x'],
2167        Some(Thingy::builtin("vi-delete")),
2168        None,
2169    );
2170    bindkey(
2171        &mut vismap,
2172        &[b'~'],
2173        Some(Thingy::builtin("vi-oper-swap-case")),
2174        None,
2175    );
2176
2177    // c:1398-1407 — vi g-prefix sequences (on amap = vicmd).
2178    bindkey(
2179        &mut amap,
2180        b"ga",
2181        Some(Thingy::builtin("what-cursor-position")),
2182        None,
2183    );
2184    bindkey(
2185        &mut amap,
2186        b"ge",
2187        Some(Thingy::builtin("vi-backward-word-end")),
2188        None,
2189    );
2190    bindkey(
2191        &mut amap,
2192        b"gE",
2193        Some(Thingy::builtin("vi-backward-blank-word-end")),
2194        None,
2195    );
2196    bindkey(
2197        &mut amap,
2198        b"gg",
2199        Some(Thingy::builtin("beginning-of-buffer-or-history")),
2200        None,
2201    );
2202    bindkey(
2203        &mut amap,
2204        b"gu",
2205        Some(Thingy::builtin("vi-down-case")),
2206        None,
2207    );
2208    bindkey(&mut amap, b"gU", Some(Thingy::builtin("vi-up-case")), None);
2209    bindkey(
2210        &mut amap,
2211        b"g~",
2212        Some(Thingy::builtin("vi-oper-swap-case")),
2213        None,
2214    );
2215    // c:1405-1407 — vim double-operator send-strings (NULL bind +
2216    // str form, i.e. `bindkey -s`).
2217    bindkey(&mut amap, b"g~~", None, Some("g~g~".to_string()));
2218    bindkey(&mut amap, b"guu", None, Some("gugu".to_string()));
2219    bindkey(&mut amap, b"gUU", None, Some("gUgU".to_string()));
2220
2221    // c:1410-1413 — emacs cursor keys via `add_cursor_key`.
2222    add_cursor_key(
2223        &mut emap,
2224        crate::ported::zsh_h::TCUPCURSOR,
2225        Thingy::builtin("up-line-or-history"),
2226        b'A' as i32,
2227    );
2228    add_cursor_key(
2229        &mut emap,
2230        crate::ported::zsh_h::TCDOWNCURSOR,
2231        Thingy::builtin("down-line-or-history"),
2232        b'B' as i32,
2233    );
2234    add_cursor_key(
2235        &mut emap,
2236        crate::ported::zsh_h::TCLEFTCURSOR,
2237        Thingy::builtin("backward-char"),
2238        b'D' as i32,
2239    );
2240    add_cursor_key(
2241        &mut emap,
2242        crate::ported::zsh_h::TCRIGHTCURSOR,
2243        Thingy::builtin("forward-char"),
2244        b'C' as i32,
2245    );
2246
2247    // c:1416-1432 — emacs ^X sequences.
2248    bindkey(
2249        &mut emap,
2250        b"\x18*",
2251        Some(Thingy::builtin("expand-word")),
2252        None,
2253    );
2254    bindkey(
2255        &mut emap,
2256        b"\x18g",
2257        Some(Thingy::builtin("list-expand")),
2258        None,
2259    );
2260    bindkey(
2261        &mut emap,
2262        b"\x18G",
2263        Some(Thingy::builtin("list-expand")),
2264        None,
2265    );
2266    bindkey(
2267        &mut emap,
2268        b"\x18\x0e",
2269        Some(Thingy::builtin("infer-next-history")),
2270        None,
2271    );
2272    bindkey(
2273        &mut emap,
2274        b"\x18\x0b",
2275        Some(Thingy::builtin("kill-buffer")),
2276        None,
2277    );
2278    bindkey(
2279        &mut emap,
2280        b"\x18\x06",
2281        Some(Thingy::builtin("vi-find-next-char")),
2282        None,
2283    );
2284    bindkey(
2285        &mut emap,
2286        b"\x18\x0f",
2287        Some(Thingy::builtin("overwrite-mode")),
2288        None,
2289    );
2290    bindkey(&mut emap, b"\x18\x15", Some(Thingy::builtin("undo")), None);
2291    bindkey(
2292        &mut emap,
2293        b"\x18\x16",
2294        Some(Thingy::builtin("vi-cmd-mode")),
2295        None,
2296    );
2297    bindkey(
2298        &mut emap,
2299        b"\x18\x0a",
2300        Some(Thingy::builtin("vi-join")),
2301        None,
2302    );
2303    bindkey(
2304        &mut emap,
2305        b"\x18\x02",
2306        Some(Thingy::builtin("vi-match-bracket")),
2307        None,
2308    );
2309    bindkey(
2310        &mut emap,
2311        b"\x18s",
2312        Some(Thingy::builtin("history-incremental-search-forward")),
2313        None,
2314    );
2315    bindkey(
2316        &mut emap,
2317        b"\x18r",
2318        Some(Thingy::builtin("history-incremental-search-backward")),
2319        None,
2320    );
2321    bindkey(&mut emap, b"\x18u", Some(Thingy::builtin("undo")), None);
2322    bindkey(
2323        &mut emap,
2324        b"\x18\x18",
2325        Some(Thingy::builtin("exchange-point-and-mark")),
2326        None,
2327    );
2328    bindkey(
2329        &mut emap,
2330        b"\x18=",
2331        Some(Thingy::builtin("what-cursor-position")),
2332        None,
2333    );
2334
2335    // c:1435-1437 — bracketed paste in all three primary keymaps.
2336    bindkey(
2337        &mut emap,
2338        b"\x1b[200~",
2339        Some(Thingy::builtin("bracketed-paste")),
2340        None,
2341    );
2342    bindkey(
2343        &mut vmap,
2344        b"\x1b[200~",
2345        Some(Thingy::builtin("bracketed-paste")),
2346        None,
2347    );
2348    bindkey(
2349        &mut amap,
2350        b"\x1b[200~",
2351        Some(Thingy::builtin("bracketed-paste")),
2352        None,
2353    );
2354
2355    // c:1440-1445 — emacs ESC sequences from metabind table.
2356    for i in 0..128 {
2357        let name = METABIND[i];
2358        if name == "undefined-key" {
2359            continue;
2360        }
2361        bindkey(
2362            &mut emap,
2363            &[0x1b, i as u8],
2364            Some(Thingy::builtin(name)),
2365            None,
2366        );
2367    }
2368
2369    // c:1449-1454 — link each keymap into the name table.
2370    linkkeymap(Arc::new(vmap), "viins", 0); // c:1449
2371    linkkeymap(Arc::new(emap), "emacs", 0); // c:1450
2372    linkkeymap(Arc::new(amap), "vicmd", 0); // c:1451
2373    linkkeymap(Arc::new(oppmap), "viopp", 0); // c:1452
2374    linkkeymap(Arc::new(vismap), "visual", 0); // c:1453
2375    linkkeymap(Arc::new(smap), ".safe", 1); // c:1454 — KM_IMMUTABLE
2376
2377    // c:Src/Zle/zle_keymap.c pre-2023-10-26 `default_bindings`
2378    // (zsh 5.9 + 5.9.1 still ship this; commit f36fccbb removed
2379    // it in zsh master, deferring main selection to sample
2380    // .zshrc code). The check the homebrew/stable zsh binary
2381    // performs is:
2382    //
2383    //     if (((ed = zgetenv("VISUAL")) && strstr(ed, "vi")) ||
2384    //         ((ed = zgetenv("EDITOR")) && strstr(ed, "vi")))
2385    //         linkkeymap(vmap, "main", 0);
2386    //     else
2387    //         linkkeymap(emap, "main", 0);
2388    //
2389    // VIMODE option still overrides (post-removal master also
2390    // honors `isset(VIMODE)` if the user explicitly sets it).
2391    // The check is `strstr(ed, "vi")` — substring match anywhere
2392    // in the env-var value. So `EDITOR=nvim`, `vim`, `vi` all
2393    // pick viins; `emacs`, `nano`, unset all pick emacs.
2394    let pick_vi = if crate::ported::zsh_h::isset(crate::ported::zsh_h::VIMODE) {
2395        true
2396    } else {
2397        let visual_has_vi = std::env::var("VISUAL")
2398            .map(|v| v.contains("vi"))
2399            .unwrap_or(false);
2400        let editor_has_vi = std::env::var("EDITOR")
2401            .map(|v| v.contains("vi"))
2402            .unwrap_or(false);
2403        visual_has_vi || editor_has_vi
2404    };
2405    let main_src = if pick_vi { "viins" } else { "emacs" };
2406    if let Some(km) = openkeymap(main_src) {
2407        linkkeymap(km, "main", 0);
2408    }
2409
2410    // c:1464-1465 — `isearch_keymap = newkeymap(NULL, "isearch");
2411    //                linkkeymap(isearch_keymap, "isearch", 0);`.
2412    let mut isearch_km = Keymap::default();
2413    isearch_km.primary = Some("isearch".to_string());
2414    linkkeymap(Arc::new(isearch_km), "isearch", 0);
2415
2416    // c:1468-1472 — `command_keymap`: \n/\r → accept-line,
2417    // ^G ('G'&0x1F = 0x07) → send-break.
2418    let mut command_km = Keymap::default();
2419    command_km.primary = Some("command".to_string());
2420    bindkey(
2421        &mut command_km,
2422        &[b'\n'],
2423        Some(Thingy::builtin("accept-line")),
2424        None,
2425    ); // c:1470
2426    bindkey(
2427        &mut command_km,
2428        &[b'\r'],
2429        Some(Thingy::builtin("accept-line")),
2430        None,
2431    ); // c:1471
2432    bindkey(
2433        &mut command_km,
2434        &[0x07],
2435        Some(Thingy::builtin("send-break")),
2436        None,
2437    ); // c:1472
2438    linkkeymap(Arc::new(command_km), "command", 0);
2439
2440    // Seed curkeymap/curkeymapname so the first key read has a target.
2441    *curkeymap.lock().unwrap() = openkeymap("main"); // c:519
2442    *curkeymapname() = "main".to_string(); // c:513
2443}
2444
2445/// Direct port of `ZLE_INT_T getrestchar_keybuf(void)` from
2446/// `Src/Zle/zle_keymap.c:1504`.
2447///
2448/// Walks the pending `keybuf` Meta-byte pairs first (c:1525-1530),
2449/// then falls through to [`getbyte`] for any remaining UTF-8
2450/// continuation bytes — same shape as [`getrestchar`] but reads
2451/// from `keybuf` until exhausted before going to the input loop.
2452/// Used by the keymap dispatcher when it needs to assemble a wide
2453/// char that crossed a key-binding boundary.
2454pub fn getrestchar_keybuf() -> i32 {
2455    use crate::ported::zle::zle_main::{getbyte, ungetbyte, LASTCHAR_WIDE, LASTCHAR_WIDE_VALID};
2456    use std::sync::atomic::Ordering;
2457
2458    // c:1519 — `lastchar_wide_valid = 1; memset(&mbs, 0, sizeof mbs);`
2459    LASTCHAR_WIDE_VALID.store(1, Ordering::SeqCst);
2460
2461    let keybuf_v = keybuf.lock().unwrap().clone();
2462    let buflen = (keybuflen.load(Ordering::SeqCst) as usize).min(keybuf_v.len());
2463    let mut bufind = 0usize;
2464    let mut bytes: Vec<u8> = Vec::new();
2465
2466    // First-byte read (c:1525-1545): either pop from keybuf or call
2467    // getbyte; subsequent bytes follow the same path until we have a
2468    // valid UTF-8 sequence.
2469    loop {
2470        let cur = if bufind < buflen {
2471            // c:1525-1530 — keybuf path with Meta-pair decode.
2472            let mut c = keybuf_v[bufind];
2473            bufind += 1;
2474            if c == 0x83 && bufind < buflen {
2475                c = keybuf_v[bufind] ^ 32;
2476                bufind += 1;
2477            }
2478            c
2479        } else {
2480            // c:1546 — `inchar = getbyte(1L, &timeout, 1);`
2481            match getbyte(true) {
2482                Some(b) => b,
2483                None => {
2484                    // c:1550-1553 — EOF in the middle of a sequence.
2485                    LASTCHAR_WIDE.store(-1, Ordering::SeqCst);
2486                    return -1;
2487                }
2488            }
2489        };
2490        bytes.push(cur);
2491
2492        // Decode the partial UTF-8 buffer — break out when it parses
2493        // or when the lead byte tells us we have enough bytes.
2494        if let Ok(s) = std::str::from_utf8(&bytes) {
2495            if let Some(c) = s.chars().next() {
2496                LASTCHAR_WIDE.store(c as i32, Ordering::SeqCst);
2497                return c as i32;
2498            }
2499        }
2500        let lead = bytes[0];
2501        let need = if lead < 0x80 {
2502            1
2503        } else if lead < 0xC0 {
2504            1
2505        } else if lead < 0xE0 {
2506            2
2507        } else if lead < 0xF0 {
2508            3
2509        } else {
2510            4
2511        };
2512        if bytes.len() >= need {
2513            // c:1535-1538 — invalid byte sequence; reset mbs + WEOF.
2514            // Unget the non-continuation byte if we read it from
2515            // getbyte (not from keybuf).
2516            if let Some(&last) = bytes.last() {
2517                if bufind >= buflen && (last & 0xC0) != 0x80 {
2518                    ungetbyte(last);
2519                }
2520            }
2521            LASTCHAR_WIDE.store(-1, Ordering::SeqCst);
2522            return -1;
2523        }
2524    }
2525}
2526
2527/// !!! WARNING: PARTIAL PORT — stub. C `getkeymapcmd(Keymap km,
2528/// Thingy *funcp, char **strp)` at Src/Zle/zle_keymap.c:1581 is the
2529/// Direct port of `char *getkeymapcmd(Keymap km, Thingy *funcp,
2530/// char **strp)` from `Src/Zle/zle_keymap.c:1581`. Walks the
2531/// keybinding trie one byte at a time against the supplied
2532/// `km`: tracks the longest prefix that hit a binding,
2533/// stops when the in-flight sequence is no longer a prefix of
2534/// any binding, and unget-bytes any chars read past the
2535/// matched prefix.
2536///
2537/// Rust signature: `(&Keymap) -> Option<(Thingy, Vec<u8>,
2538/// Option<String>)>` — the C `(funcp out, strp out)` collapse
2539/// into the returned tuple (Thingy + matched key sequence +
2540/// string-replacement when bound).
2541pub fn getkeymapcmd(km: &Keymap) -> Option<(super::zle_thingy::Thingy, Vec<u8>, Option<String>)> {
2542    // c:1581
2543    let mut buf: Vec<u8> = Vec::with_capacity(8); // c:1583 keybuf
2544    let mut last_match: Option<super::zle_thingy::Thingy> = None; // c:1584
2545    let mut last_match_str: Option<String> = None;
2546    let mut last_match_len = 0usize; // c:1585
2547
2548    // c:1591 — `while(getkeybuf(timeout) != EOF)`.
2549    loop {
2550        // Read one byte. Use timed read once we have a partial match.
2551        let do_keytmout = last_match.is_some();
2552        let b = match super::zle_main::getbyte(do_keytmout) {
2553            Some(b) => b,
2554            None => break, // c:1591 EOF
2555        };
2556        buf.push(b);
2557
2558        // c:1602-1604 — `f = keybind(km, keybuf, &s);` lookup.
2559        let (current_match, current_str, is_prefix) = if buf.len() == 1 {
2560            let m = km.first[b as usize].clone();
2561            let pfx = km.multi.keys().any(|k| k.len() > 1 && k[0] == b);
2562            (m, None, pfx)
2563        } else {
2564            let entry = km.multi.get(&buf[..]);
2565            let m = entry.and_then(|e| e.bind.clone());
2566            let s = entry.and_then(|e| e.str.clone());
2567            let pfx = entry.map(|e| e.prefixct > 0).unwrap_or(false);
2568            (m, s, pfx)
2569        };
2570
2571        // c:1606-1614 — `if (f != t_undefinedkey)` → record match.
2572        if let Some(t) = current_match {
2573            last_match = Some(t);
2574            last_match_str = current_str;
2575            last_match_len = buf.len();
2576        }
2577
2578        // c:1614 — `if (!ispfx) break;` stop when not a prefix anymore.
2579        if !is_prefix {
2580            break;
2581        }
2582    }
2583
2584    // c:1619 — unget extra bytes past the matched prefix.
2585    if last_match.is_some() && buf.len() > last_match_len {
2586        let extra = buf[last_match_len..].to_vec();
2587        super::zle_main::ungetbytes(&extra);
2588        buf.truncate(last_match_len);
2589    }
2590
2591    last_match.map(|t| (t, buf, last_match_str))
2592}
2593
2594/// Port of `addkeybuf(int c)` from Src/Zle/zle_keymap.c:1717.
2595/// WARNING: param names don't match C — Rust=(zle, c) vs C=(c)
2596pub fn addkeybuf(c: i32) {
2597    // c:1717
2598    // C body (zle_keymap.c:1717-1727):
2599    //   addkeybuf(int c) {
2600    //     if(keybuflen + 3 > keybufsz) keybuf = realloc(...);
2601    //     if(imeta(c)) {
2602    //       keybuf[keybuflen++] = Meta;
2603    //       keybuf[keybuflen++] = c ^ 32;
2604    //     } else
2605    //       keybuf[keybuflen++] = c;
2606    //     keybuf[keybuflen] = '\0';
2607    //   }
2608    //
2609    // Vec<u8> grows automatically — no realloc bookkeeping needed.
2610    let c = c & 0xff;
2611    // c:1721 — `if (imeta(c))` — route through the canonical IMETA
2612    // typtab predicate (`Src/ztype.h:60`) so the byte set agrees with
2613    // every other call site that checks `imeta(c)`. The previous Rust
2614    // port used a hand-rolled `c >= 0x83 && c != 0x83 && c != 0x84`
2615    // which:
2616    //   * MISSED 0x00 (NUL — canonical IMETA per utils.c:4195)
2617    //   * MISSED 0x83 (Meta itself — canonical IMETA per utils.c:4196)
2618    //   * MISSED 0x84 (Pound — canonical IMETA per utils.c:4198)
2619    //   * OVER-ENCODED 0xa3..=0xff (these are NOT IMETA per the
2620    //     typtab; they should pass through as literal bytes).
2621    // Routing through `ztype_h::imeta` ties the meta-encoding decision
2622    // to the same typtab inittyptab() populates — one source of truth.
2623    let is_meta = imeta(c as u8); // c:1721
2624    let mut buf = keybuf.lock().unwrap();
2625    if is_meta {
2626        buf.push(META as u8); // c:1722 Meta
2627        buf.push((c ^ 32) as u8); // c:1723 c ^ 32
2628    } else {
2629        buf.push(c as u8); // c:1725
2630    }
2631    // C terminates with '\0'; Rust Vec doesn't need that.
2632}
2633
2634/// Port of `getkeybuf(int w)` from Src/Zle/zle_keymap.c:1744.
2635/// WARNING: param names don't match C — Rust=(zle, w) vs C=(w)
2636pub fn getkeybuf(w: i32) -> i32 {
2637    // c:1744
2638    // C body (c:1658-1664): `int c = getbyte((long)w, NULL, 1);
2639    //                       if (c < 0) return EOF; addkeybuf(c); return c`.
2640    // getbyte() needs the input substrate; without it, drain from
2641    // unget_buf which addkeybuf-style writers can populate.
2642    let _ = w; // would be `(long)w` to getbyte's timeout arg
2643    if let Some(b) = KUNGETBUF.lock().unwrap().pop_front() {
2644        addkeybuf(b as i32);
2645        b as i32
2646    } else {
2647        -1 // c:1661 EOF
2648    }
2649}
2650
2651/// Port of `ungetkeycmd()` from Src/Zle/zle_keymap.c:1759.
2652/// WARNING: param names don't match C — Rust=(zle) vs C=()
2653pub fn ungetkeycmd() {
2654    // c:1759
2655    // C body (c:1761): `ungetbytes_unmeta(keybuf, keybuflen)`.
2656    let buf = keybuf.lock().unwrap().clone();
2657    ungetbytes_unmeta(&buf);
2658}
2659
2660/// Port of `mod_export Thingy getkeycmd(void)` from
2661/// Src/Zle/zle_keymap.c:1768. Reads one input sequence via
2662/// `getkeymapcmd` (the byte-by-byte keymap walk), then handles:
2663///   - empty `seq` → return None (EOF);
2664///   - `func == NULL` (string-insert binding) → push str back to
2665///     input and retry; cap at 20 hops to prevent string-insert
2666///     infinite loops (c:1777-1786);
2667///   - `func == z_executenamedcmd` → call `executenamedcommand`
2668///     for interactive widget-name resolution (c:1788-1796);
2669///   - `func == z_executelastnamedcmd` → return cached `lastnamed`
2670///     (c:1798).
2671pub fn getkeycmd() -> Option<super::zle_thingy::Thingy> {
2672    // c:1768
2673    use super::zle_main::get_key_cmd;
2674    let mut hops = 0; // c:1772
2675                      // c:1774 — `sentstring:` retry label.
2676    loop {
2677        // c:1775 — `seq = getkeymapcmd(curkeymap, &func, &str);`
2678        let func = get_key_cmd(); // c:1775 underlying byte-loop
2679        if func.is_none() {
2680            // c:1776 `if (!*seq) return NULL;`
2681            return None;
2682        }
2683        let func = func.unwrap();
2684        // c:1777-1786 — string-insert (func==NULL in C, modeled as
2685        // Thingy with empty nam in Rust thingytab). When the binding
2686        // is a string-replacement, ungetbytes the str and re-walk.
2687        if func.nam.is_empty() {
2688            // c:1777 `if (!func)`
2689            hops += 1; // c:1778
2690            if hops == 20 {
2691                // c:1779 hop-cap
2692                crate::ported::utils::zerr(
2693                    // c:1781
2694                    "string inserting another one too many times",
2695                );
2696                return None; // c:1783
2697            }
2698            // c:1785 `ungetbytes_unmeta(str, strlen(str))` — no widget
2699            // string was bound on this branch in Rust (get_key_cmd
2700            // routes string-replacements before returning), so this
2701            // arm only fires when the keymap entry has an empty `nam`
2702            // sentinel. Loop to retry.
2703            continue; // c:1786 `goto sentstring;`
2704        }
2705        // c:1788 — `func == Th(z_executenamedcmd)` check. zsh uses
2706        // pointer equality on the global Thingy table; Rust uses
2707        // name equality against the canonical widget name.
2708        if func.nam == "execute-named-command" {
2709            // c:1788
2710            // c:1789-1790 — drive `executenamedcommand("execute: ")`
2711            // until it returns a non-named-command result.
2712            let mut resolved: Option<super::zle_thingy::Thingy> = None;
2713            loop {
2714                let name = crate::ported::zle::zle_misc::executenamedcommand("execute: "); // c:1791
2715                match name {
2716                    Some(n) if n == "execute-named-command" => continue, // c:1790 loop
2717                    Some(n) => {
2718                        // c:1792 — `func != z_executenamedcmd`
2719                        let lookup = super::zle_thingy::thingytab()
2720                            .lock()
2721                            .unwrap()
2722                            .get(&n)
2723                            .cloned();
2724                        resolved = lookup;
2725                        break;
2726                    }
2727                    None => {
2728                        // c:1793 `if (!func) func = t_undefinedkey;`
2729                        let undef = super::zle_thingy::thingytab()
2730                            .lock()
2731                            .unwrap()
2732                            .get("undefined-key")
2733                            .cloned();
2734                        resolved = undef;
2735                        break;
2736                    }
2737                }
2738            }
2739            // c:1794-1796 — record `lastnamed = refthingy(func)` for
2740            // future `executelastnamedcmd` lookups, unless `func`
2741            // itself is `executelastnamedcmd`.
2742            if let Some(ref f) = resolved {
2743                if f.nam != "execute-last-named-cmd" {
2744                    // c:1795
2745                    *crate::ported::zle::zle_keymap::lastnamed.lock().unwrap() = Some(f.clone());
2746                    // c:1796
2747                }
2748            }
2749            return resolved;
2750        }
2751        // c:1798 — `func == Th(z_executelastnamedcmd)` → return
2752        // the cached `lastnamed` Thingy.
2753        if func.nam == "execute-last-named-cmd" {
2754            // c:1798
2755            return crate::ported::zle::zle_keymap::lastnamed
2756                .lock()
2757                .unwrap()
2758                .clone();
2759        }
2760        return Some(func); // c:1800
2761    }
2762}
2763
2764/// Port of `zlesetkeymap(int mode)` from Src/Zle/zle_keymap.c:1804.
2765pub fn zlesetkeymap(mode: i32) {
2766    // c:1804
2767    // C body (c:1820-1825): `Keymap km = openkeymap(mode==VIMODE?
2768    //                       "viins":"emacs"); if (!km) return;
2769    //                       linkkeymap(km, "main", 0)`.
2770    // VIMODE = 1 (per zsh's mode-flag enum).
2771    let kmname = if mode == 1 { "viins" } else { "emacs" };
2772    if let Some(km) = openkeymap(kmname) {
2773        linkkeymap(km, "main", 0);
2774    }
2775}
2776
2777/// Direct port of `int readcommand(char **args)` from
2778/// `Src/Zle/zle_keymap.c:1814`.
2779/// ```c
2780/// int readcommand(char **args) {
2781///     Thingy thingy = getkeycmd();
2782///     if (!thingy) return 1;
2783///     setsparam("REPLY", ztrdup(thingy->nam));
2784///     return 0;
2785/// }
2786/// ```
2787pub fn readcommand() -> i32 {
2788    // c:1814
2789    // Read a single key + look up its bound thingy via the existing
2790    // ZLE input path. Without an active ZLE key-read loop in compcore-
2791    // call context we treat the input as missing and return 1; once a
2792    // key arrives, set $REPLY to its name and return 0 per the C body.
2793    // c:1816 — `getkeycmd()` reads through the active ZLE input
2794    // queue; in compcore call contexts (no live key-read loop)
2795    // there's no thingy to return, mirroring C's NULL path.
2796    let Some(name): Option<String> = None else {
2797        return 1;
2798    }; // c:1816
2799    let _ = crate::ported::params::setsparam("REPLY", &name); // c:1818
2800    0 // c:1819
2801}
2802
2803/// Port of `mod_export char *curkeymapname` from `Src/Zle/zle_keymap.c:126`.
2804/// Name of the currently active keymap (driven by `bindkey -A` and the
2805/// `KEYMAP` parameter). The Rust port wraps in OnceLock<Mutex<>> for
2806/// thread-safe access from widget bodies.
2807pub static CURKEYMAPNAME: OnceLock<Mutex<String>> = OnceLock::new(); // c:126
2808
2809/// Port of `Keymap curkeymap` from `Src/Zle/zle_keymap.c:124`. The
2810/// currently active keymap (per `bindkey -A` selection or KEYMAP
2811/// parameter). Used inline at zle_keymap.c:519 (`curkeymap = km;`)
2812/// and read by `getkeycmd`/`getkeybuf` to dispatch the next key.
2813pub static curkeymap: Mutex<Option<Arc<Keymap>>> = Mutex::new(None); // c:124
2814
2815/// Port of `char *keybuf` from `Src/Zle/zle_keymap.c:136`. The key
2816/// sequence currently being read by `getkeycmd`. C uses a flat
2817/// `char*` heap allocation sized by `keybufsz`; Rust uses
2818/// `Vec<u8>` which manages its own capacity.
2819pub static keybuf: Mutex<Vec<u8>> = Mutex::new(Vec::new()); // c:136
2820
2821/// Port of `int keybuflen` from `Src/Zle/zle_keymap.c:139`. Current
2822/// number of bytes in `keybuf`. Rust mirrors via `keybuf.lock().len()`
2823/// but exposes the count as a separate static for callers that need
2824/// it without holding the buffer lock.
2825pub static keybuflen: std::sync::atomic::AtomicI32 = // c:139
2826    std::sync::atomic::AtomicI32::new(0);
2827
2828// =====================================================================
2829// keymapnamtab — `Src/Zle/zle_keymap.c:128/153`.
2830// =====================================================================
2831//
2832// C: `mod_export HashTable keymapnamtab` — global hash mapping
2833// keymap names to KeymapName entries (each KeymapName holds an
2834// Arc'd Keymap + flags). zshrs uses Mutex<HashMap<String, KeymapName>>.
2835
2836static KEYMAPNAMTAB: OnceLock<Mutex<HashMap<String, KeymapName>>> = OnceLock::new();
2837
2838/// Direct port of `struct keymap` from `Src/Zle/zle_keymap.c:64`.
2839/// A keymap — binding of keys to thingies.
2840#[derive(Debug, Clone)]
2841pub struct Keymap {
2842    // c:64
2843    /// `Thingy first[256]` — c:65, base binding for each byte.
2844    pub first: [Option<Thingy>; 256],
2845    /// `HashTable multi` — c:66, multi-character bindings.
2846    pub multi: HashMap<Vec<u8>, KeyBinding>,
2847    /// `KeymapName primary` — c:78, primary alias for this map.
2848    pub primary: Option<String>,
2849    /// `int flags` — c:79 (KM_IMMUTABLE).
2850    pub flags: i32,
2851    /// `int rc` — c:80, reference count (refkeymap/unrefkeymap/
2852    /// deletekeymap).
2853    pub rc: i32,
2854}
2855
2856/// Direct port of `struct key` from `Src/Zle/zle_keymap.c:85`.
2857/// A key binding (either a thingy or a string to send).
2858#[derive(Debug, Clone)]
2859pub struct KeyBinding {
2860    // c:85
2861    pub bind: Option<Thingy>, // c:88 Thingy bind
2862    pub str: Option<String>,  // c:89 char *str
2863    pub prefixct: i32,        // c:90 int prefixct
2864}
2865
2866/// File-scope `Keymap localkeymap` from `Src/Zle/zle_keymap.c:1759`.
2867/// The active per-widget local keymap; set/cleared by widget
2868/// dispatch around interactive command reads.
2869pub static LOCALKEYMAP: Mutex<Option<Arc<Keymap>>> = Mutex::new(None); // c:526
2870
2871/// Get-or-init accessor for `CURKEYMAPNAME`. Mirrors the C convention
2872/// of treating the string as always-initialised — first read seeds it
2873/// with "main".
2874pub fn curkeymapname() -> std::sync::MutexGuard<'static, String> {
2875    CURKEYMAPNAME
2876        .get_or_init(|| Mutex::new(String::from("main")))
2877        .lock()
2878        .unwrap()
2879}
2880
2881/// Zero-sized namespace for the three default-binding tables
2882/// (emacs / viins / vicmd) that `default_bindings()` populates at
2883/// startup. The state these used to wrap (keymaps / current /
2884/// current_name / local / keybuf / lastnamed) now lives in the
2885/// six file-scope statics declared above (KEYMAPNAMTAB / curkeymap
2886/// / CURKEYMAPNAME / LOCALKEYMAP / keybuf / lastnamed) — matching
2887/// the C globals at `Src/Zle/zle_keymap.c:124-145`.
2888///
2889/// The setup_*_keymap methods stay as methods (drift-gate
2890/// exempts impl-block ported) because zsh's C `default_bindings()`
2891/// has the equivalent 330+ bindkey calls inline in one function;
2892/// the Rust port keeps them factored by keymap for readability.
2893// `KeymapManager` unit struct (and its 32-method impl block) deleted —
2894// was a Rust-only namespace wrapper around the file-scope statics
2895// (KEYMAPNAMTAB / curkeymap / CURKEYMAPNAME / LOCALKEYMAP / keybuf /
2896// lastnamed) with no `struct keymap_manager` in zsh C. 29 of the 32
2897// methods were never called; 3 (`setup_emacs_keymap` /
2898// `setup_viins_keymap` / `setup_vicmd_keymap`) are factored out below
2899// as free ported because zsh's C `default_bindings()` inlines the ~300
2900// equivalent `bindkey` calls in one body (Src/Zle/zle_keymap.c:124).
2901// The Rust port keeps them factored by keymap for readability.
2902
2903// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2904// ─── RUST-ONLY ACCESSORS ───
2905//
2906// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
2907// RwLock<T>>` globals declared above. C zsh uses direct global
2908// access; Rust needs these wrappers because `OnceLock::get_or_init`
2909// is the only way to lazily construct shared state. These ported sit
2910// here so the body of this file reads in C source order without
2911// the accessor wrappers interleaved between real port ported.
2912// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2913
2914// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2915// ─── RUST-ONLY ACCESSORS ───
2916//
2917// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
2918// RwLock<T>>` globals declared above. C zsh uses direct global
2919// access; Rust needs these wrappers because `OnceLock::get_or_init`
2920// is the only way to lazily construct shared state. These ported sit
2921// here so the body of this file reads in C source order without
2922// the accessor wrappers interleaved between real port ported.
2923// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2924
2925pub(crate) fn keymapnamtab() -> &'static Mutex<HashMap<String, KeymapName>> {
2926    KEYMAPNAMTAB.get_or_init(|| Mutex::new(HashMap::new()))
2927}
2928
2929#[cfg(test)]
2930mod tests {
2931    use super::*;
2932
2933    #[test]
2934    fn emacs_default_has_quoted_insert_undo_yank_pop() {
2935        let _g = crate::test_util::global_state_lock();
2936        let _g = zle_test_setup();
2937        createkeymapnamtab();
2938        default_bindings();
2939
2940        let km = openkeymap("emacs").expect("emacs keymap created");
2941        // Ctrl-V quoted-insert (zle_bindings.c emacs '^V').
2942        assert_eq!(
2943            km.lookup_char(0x16).map(|t| t.nam.as_str()),
2944            Some("quoted-insert")
2945        );
2946        // Ctrl-_ undo (zle_bindings.c emacs '^_').
2947        assert_eq!(km.lookup_char(0x1F).map(|t| t.nam.as_str()), Some("undo"));
2948        // \ey yank-pop (zle_bindings.c emacs '\\ey').
2949        assert_eq!(
2950            km.lookup_seq(b"\x1by")
2951                .and_then(|kb| kb.bind.as_ref())
2952                .map(|t| t.nam.as_str()),
2953            Some("yank-pop")
2954        );
2955    }
2956
2957    #[test]
2958    fn emacs_default_has_history_search_and_insert_last_word() {
2959        let _g = crate::test_util::global_state_lock();
2960        let _g = zle_test_setup();
2961        createkeymapnamtab();
2962        default_bindings();
2963
2964        let km = openkeymap("emacs").expect("emacs keymap created");
2965        // \e. insert-last-word.
2966        assert_eq!(
2967            km.lookup_seq(b"\x1b.")
2968                .and_then(|kb| kb.bind.as_ref())
2969                .map(|t| t.nam.as_str()),
2970            Some("insert-last-word")
2971        );
2972        assert_eq!(
2973            km.lookup_seq(b"\x1bp")
2974                .and_then(|kb| kb.bind.as_ref())
2975                .map(|t| t.nam.as_str()),
2976            Some("history-search-backward")
2977        );
2978        // ^X^X exchange-point-and-mark.
2979        assert_eq!(
2980            km.lookup_seq(b"\x18\x18")
2981                .and_then(|kb| kb.bind.as_ref())
2982                .map(|t| t.nam.as_str()),
2983            Some("exchange-point-and-mark")
2984        );
2985    }
2986
2987    #[test]
2988    fn vicmd_default_has_visual_marks_indent() {
2989        let _g = crate::test_util::global_state_lock();
2990        let _g = zle_test_setup();
2991        createkeymapnamtab();
2992        default_bindings();
2993
2994        let km = openkeymap("vicmd").expect("vicmd keymap created");
2995        assert_eq!(
2996            km.lookup_char(b'v').map(|t| t.nam.as_str()),
2997            Some("visual-mode")
2998        );
2999        assert_eq!(
3000            km.lookup_char(b'V').map(|t| t.nam.as_str()),
3001            Some("visual-line-mode")
3002        );
3003        assert_eq!(
3004            km.lookup_char(b'm').map(|t| t.nam.as_str()),
3005            Some("vi-set-mark")
3006        );
3007        assert_eq!(
3008            km.lookup_char(b'>').map(|t| t.nam.as_str()),
3009            Some("vi-indent")
3010        );
3011        assert_eq!(
3012            km.lookup_char(b'~').map(|t| t.nam.as_str()),
3013            Some("vi-swap-case")
3014        );
3015        assert_eq!(
3016            km.lookup_char(b'%').map(|t| t.nam.as_str()),
3017            Some("vi-match-bracket")
3018        );
3019    }
3020
3021    #[test]
3022    fn viins_default_matches_viinsbind_table() {
3023        let _g = crate::test_util::global_state_lock();
3024        // Test renamed + retargeted from the legacy
3025        // `viins_default_has_history_search_and_quoted_insert` which
3026        // asserted Rust-only emacs-flavored bindings (`^R` →
3027        // history-incremental-search-backward; `^A` →
3028        // beginning-of-line; `^V` → quoted-insert). Those were
3029        // overridden by the C-faithful `VIINSBIND` table port
3030        // (`Src/Zle/zle_bindings.c:256-289`).
3031        let _g = zle_test_setup();
3032        createkeymapnamtab();
3033        default_bindings();
3034        let km = openkeymap("viins").expect("viins keymap created");
3035        // ^R → redisplay (VIINSBIND[18]).
3036        assert_eq!(
3037            km.lookup_char(0x12).map(|t| t.nam.as_str()),
3038            Some("redisplay")
3039        );
3040        // ^V → vi-quoted-insert (VIINSBIND[22]).
3041        assert_eq!(
3042            km.lookup_char(0x16).map(|t| t.nam.as_str()),
3043            Some("vi-quoted-insert")
3044        );
3045        // ^A → self-insert (VIINSBIND[1]).
3046        assert_eq!(
3047            km.lookup_char(0x01).map(|t| t.nam.as_str()),
3048            Some("self-insert")
3049        );
3050        // ^[ → vi-cmd-mode (VIINSBIND[27]).
3051        assert_eq!(
3052            km.lookup_char(0x1B).map(|t| t.nam.as_str()),
3053            Some("vi-cmd-mode")
3054        );
3055        // ^M / ^J → accept-line.
3056        assert_eq!(
3057            km.lookup_char(0x0D).map(|t| t.nam.as_str()),
3058            Some("accept-line")
3059        );
3060        assert_eq!(
3061            km.lookup_char(0x0A).map(|t| t.nam.as_str()),
3062            Some("accept-line")
3063        );
3064    }
3065
3066    // ---------- Real-port tests for refkeymap / unrefkeymap ----------
3067
3068    #[test]
3069    fn refkeymap_increments_rc() {
3070        let _g = crate::test_util::global_state_lock();
3071        let _g = zle_test_setup();
3072        // c:470 — `km->rc++`. Default Keymap starts with rc=0.
3073        let mut km = Keymap::default();
3074        assert_eq!(km.rc, 0);
3075        refkeymap(&mut km);
3076        assert_eq!(km.rc, 1);
3077        refkeymap(&mut km);
3078        assert_eq!(km.rc, 2);
3079    }
3080
3081    #[test]
3082    fn unrefkeymap_decrements_returns_new_count() {
3083        let _g = crate::test_util::global_state_lock();
3084        let _g = zle_test_setup();
3085        // c:482 — `--km->rc`. With rc=3 → returns 2.
3086        let mut km = Keymap::default();
3087        km.rc = 3;
3088        let r = unrefkeymap(&mut km);
3089        assert_eq!(r, 2);
3090        assert_eq!(km.rc, 2);
3091        let r = unrefkeymap(&mut km);
3092        assert_eq!(r, 1);
3093    }
3094
3095    #[test]
3096    fn unrefkeymap_returns_zero_at_last_ref() {
3097        let _g = crate::test_util::global_state_lock();
3098        let _g = zle_test_setup();
3099        // c:482-484 — `if (!--km->rc) { deletekeymap(km); return 0; }`.
3100        // rc=1 → -- → 0 → returns 0 (deletion signal).
3101        let mut km = Keymap::default();
3102        km.rc = 1;
3103        assert_eq!(unrefkeymap(&mut km), 0);
3104        assert_eq!(km.rc, 0);
3105    }
3106
3107    // ---------- keyisprefix real-port tests ----------
3108
3109    fn dummy_thingy() -> Thingy {
3110        Thingy::new("test")
3111    }
3112
3113    #[test]
3114    fn keyisprefix_empty_seq() {
3115        let _g = crate::test_util::global_state_lock();
3116        let _g = zle_test_setup();
3117        // c:687-688 — empty input → always prefix → 1.
3118        let km = Keymap::default();
3119        assert_eq!(keyisprefix(&km, b""), 1);
3120    }
3121
3122    #[test]
3123    fn keyisprefix_single_byte_bound_returns_zero() {
3124        let _g = crate::test_util::global_state_lock();
3125        let _g = zle_test_setup();
3126        // c:689-692 — single byte that has a first[] binding is NOT
3127        // a prefix; it IS the binding.
3128        let mut km = Keymap::default();
3129        bindkey(&mut km, &[b'a'], Some(dummy_thingy()), None);
3130        assert_eq!(keyisprefix(&km, b"a"), 0);
3131    }
3132
3133    #[test]
3134    fn keyisprefix_single_byte_unbound() {
3135        let _g = crate::test_util::global_state_lock();
3136        let _g = zle_test_setup();
3137        // c:694-695 — fall through to multi lookup; no match → 0.
3138        let km = Keymap::default();
3139        assert_eq!(keyisprefix(&km, b"x"), 0);
3140    }
3141
3142    #[test]
3143    fn keyisprefix_seq_is_real_prefix() {
3144        let _g = crate::test_util::global_state_lock();
3145        let _g = zle_test_setup();
3146        // c:694-695 — multi has prefixct > 0 → 1.
3147        // bind_seq("ab", X) marks "a" as a prefix (prefixct=1).
3148        let mut km = Keymap::default();
3149        bindkey(&mut km, b"ab", Some(dummy_thingy()), None);
3150        // "a" alone is NOT a complete binding but IS a prefix of "ab".
3151        assert_eq!(keyisprefix(&km, b"a"), 1);
3152    }
3153
3154    #[test]
3155    fn keyisprefix_seq_is_complete_binding() {
3156        let _g = crate::test_util::global_state_lock();
3157        let _g = zle_test_setup();
3158        // c:694-695 — when seq itself IS a binding (not a prefix),
3159        // multi[seq] has prefixct=0 → 0.
3160        let mut km = Keymap::default();
3161        bindkey(&mut km, b"xyz", Some(dummy_thingy()), None);
3162        // "xyz" is the bound seq (prefixct=0). Should return 0.
3163        assert_eq!(keyisprefix(&km, b"xyz"), 0);
3164    }
3165
3166    #[test]
3167    fn keyisprefix_meta_pair_decoded() {
3168        let _g = crate::test_util::global_state_lock();
3169        let _g = zle_test_setup();
3170        // c:690 — `seq[0]==Meta` (0x83) → use seq[1]^32 as single byte.
3171        // Bind 'A' (0x41) in first[]. Seq [0x83, 0x61] decodes to
3172        // 0x61^0x20 = 0x41 = 'A'. So this is single-byte 'A'.
3173        let mut km = Keymap::default();
3174        bindkey(&mut km, &[b'A'], Some(dummy_thingy()), None);
3175        assert_eq!(keyisprefix(&km, &[0x83, 0x61]), 0);
3176    }
3177
3178    // ---------- keybind real-port tests (this session). ----------
3179
3180    #[test]
3181    fn keybind_single_byte_returns_first_bound_thingy() {
3182        let _g = crate::test_util::global_state_lock();
3183        // c:664-669 — `if (ztrlen(seq) == 1) { f = seq[0]; if (km->first[f])
3184        // return bind; }`. Bind 'q' in first[], call keybind with "q",
3185        // expect the Thingy back and no send-string.
3186        let _g = zle_test_setup();
3187        let mut km = Keymap::default();
3188        bindkey(&mut km, &[b'q'], Some(Thingy::new("quit-widget")), None);
3189        let (bind, send) = keybind(&km, b"q");
3190        assert!(bind.is_some());
3191        assert_eq!(bind.as_ref().unwrap().nam, "quit-widget");
3192        assert!(send.is_none(), "no str on first[] path");
3193    }
3194
3195    #[test]
3196    fn keybind_meta_pair_decodes_to_single_byte() {
3197        let _g = crate::test_util::global_state_lock();
3198        // c:665 — `seq[0]==Meta ? seq[1]^32 : seq[0]`. [0x83, 0x61]
3199        // decodes to 0x41 = 'A'. Verify it lands on the first[] entry
3200        // for 'A' just like a literal 'A' byte would.
3201        let _g = zle_test_setup();
3202        let mut km = Keymap::default();
3203        bindkey(
3204            &mut km,
3205            &[b'A'],
3206            Some(Thingy::new("uppercase-A-widget")),
3207            None,
3208        );
3209        let (bind, _) = keybind(&km, &[0x83, 0x61]);
3210        assert_eq!(
3211            bind.as_ref().map(|t| t.nam.as_str()),
3212            Some("uppercase-A-widget")
3213        );
3214    }
3215
3216    #[test]
3217    fn keybind_unbound_byte_returns_none() {
3218        let _g = crate::test_util::global_state_lock();
3219        // c:670-671 — single-byte but km->first[f] is None, falls
3220        // through to the multi-byte lookup which also misses → (None, None).
3221        let _g = zle_test_setup();
3222        let km = Keymap::default();
3223        let (bind, send) = keybind(&km, b"z");
3224        assert!(
3225            bind.is_none(),
3226            "unbound byte → t_undefinedkey sentinel (None)"
3227        );
3228        assert!(send.is_none());
3229    }
3230
3231    #[test]
3232    fn keybind_multi_byte_sequence_via_multi_map() {
3233        let _g = crate::test_util::global_state_lock();
3234        // c:670-674 — `k = km->multi->getnode(km->multi, seq)`. Bind
3235        // a multi-byte sequence in `multi`, expect the lookup to find it.
3236        let _g = zle_test_setup();
3237        let mut km = Keymap::default();
3238        km.multi.insert(
3239            b"\x1b[A".to_vec(),
3240            KeyBinding {
3241                bind: Some(Thingy::new("up-line")),
3242                str: None,
3243                prefixct: 0,
3244            },
3245        );
3246        let (bind, _) = keybind(&km, b"\x1b[A");
3247        assert_eq!(bind.as_ref().map(|t| t.nam.as_str()), Some("up-line"));
3248    }
3249
3250    #[test]
3251    fn keybind_returns_send_string_when_multi_entry_has_str() {
3252        let _g = crate::test_util::global_state_lock();
3253        // c:673-674 — `*strp = k->str; return k->bind`. Send-string
3254        // entries (`bindkey -s`) have bind=None + str=Some.
3255        let _g = zle_test_setup();
3256        let mut km = Keymap::default();
3257        km.multi.insert(
3258            b"\x1b[Z".to_vec(),
3259            KeyBinding {
3260                bind: None,
3261                str: Some("hello".to_string()),
3262                prefixct: 0,
3263            },
3264        );
3265        let (bind, send) = keybind(&km, b"\x1b[Z");
3266        assert!(bind.is_none(), "send-string entries have no bind");
3267        assert_eq!(send.as_deref(), Some("hello"));
3268    }
3269
3270    // ---------- init_keymaps / cleanup_keymaps round-trip ----------
3271
3272    #[test]
3273    fn init_keymaps_seeds_keybuf_and_clears_lastnamed() {
3274        let _g = crate::test_util::global_state_lock();
3275        let _g = zle_test_setup();
3276        // After init: keybuf is allocated (non-empty Vec), lastnamed is None
3277        // (the `t_undefinedkey` sentinel in Rust convention).
3278        init_keymaps();
3279        assert!(
3280            !keybuf.lock().unwrap().is_empty(),
3281            "keybuf zshcalloc(keybufsz)"
3282        );
3283        assert!(
3284            lastnamed.lock().unwrap().is_none(),
3285            "lastnamed = t_undefinedkey (None)"
3286        );
3287    }
3288
3289    #[test]
3290    fn cleanup_keymaps_drains_namtab_and_keybuf() {
3291        let _g = crate::test_util::global_state_lock();
3292        let _g = zle_test_setup();
3293        init_keymaps();
3294        assert!(!keybuf.lock().unwrap().is_empty());
3295        cleanup_keymaps();
3296        assert!(keybuf.lock().unwrap().is_empty(), "zfree(keybuf, ...)");
3297        assert!(
3298            keymapnamtab().lock().unwrap().is_empty(),
3299            "deletehashtable(keymapnamtab)"
3300        );
3301    }
3302
3303    /// `Src/Zle/zle_keymap.c:1717-1727` — `addkeybuf(c)` calls `imeta(c)`.
3304    /// Per `Src/utils.c:4195`, NUL is IMETA (`typtab['\0'] |= IMETA`).
3305    /// The previous Rust port used a hand-rolled `c >= 0x83 && c != 0x83
3306    /// && c != 0x84` mask that excluded NUL entirely — binary input
3307    /// passing through `addkeybuf` would drop the Meta-prefix and leave
3308    /// a raw `\0` in `keybuf`, breaking the C-string-terminator check
3309    /// at zle_keymap.c:1649.
3310    #[test]
3311    fn addkeybuf_encodes_nul_byte_per_imeta() {
3312        let _g = crate::test_util::global_state_lock();
3313        let _g = zle_test_setup();
3314        let _tg = TYPTAB_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3315        inittyptab();
3316        keybuf.lock().unwrap().clear();
3317        addkeybuf(0);
3318        // c:1722-1723 — Meta=0x83, NUL ^ 0x20 = 0x20.
3319        assert_eq!(*keybuf.lock().unwrap(), vec![0x83, 0x20],
3320            "c:1721 — NUL must be Meta-encoded (was missed by old `c >= 0x83 && != 0x83 && != 0x84`)");
3321    }
3322
3323    /// `Src/Zle/zle_keymap.c:1721` — `imeta(c)` returns true for the
3324    /// Meta byte itself (0x83) per `Src/utils.c:4196`
3325    /// (`typtab[Meta] |= IMETA`). A raw 0x83 in input MUST be
3326    /// Meta-encoded as `Meta + (0x83 ^ 0x20) = 0x83 0xa3`. The previous
3327    /// hand-rolled mask explicitly excluded 0x83 with `c != 0x83`,
3328    /// passing the Meta byte through verbatim — corrupting any later
3329    /// key-sequence parser that interprets 0x83 as a Meta-prefix.
3330    #[test]
3331    fn addkeybuf_encodes_meta_byte_itself() {
3332        let _g = crate::test_util::global_state_lock();
3333        let _g = zle_test_setup();
3334        let _tg = TYPTAB_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3335        inittyptab();
3336        keybuf.lock().unwrap().clear();
3337        addkeybuf(0x83);
3338        assert_eq!(
3339            *keybuf.lock().unwrap(),
3340            vec![0x83, 0xa3],
3341            "c:1721 — Meta byte (0x83) must itself be Meta-encoded"
3342        );
3343    }
3344
3345    /// `Src/Zle/zle_keymap.c:1721` — `imeta(c)` returns true for 0x84
3346    /// (Pound), the first byte in the Pound..LAST_NORMAL_TOK range
3347    /// (`Src/utils.c:4198`). The previous mask `c != 0x84` left Pound
3348    /// unencoded; the canonical port must Meta-encode it.
3349    #[test]
3350    fn addkeybuf_encodes_pound_token_byte() {
3351        let _g = crate::test_util::global_state_lock();
3352        let _g = zle_test_setup();
3353        let _tg = TYPTAB_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3354        inittyptab();
3355        keybuf.lock().unwrap().clear();
3356        addkeybuf(0x84);
3357        assert_eq!(
3358            *keybuf.lock().unwrap(),
3359            vec![0x83, 0xa4],
3360            "c:1721 — Pound (0x84) is IMETA per utils.c:4198, must be Meta-encoded"
3361        );
3362    }
3363
3364    /// `Src/Zle/zle_keymap.c:1721` — `imeta(c)` returns FALSE for
3365    /// bytes 0xa3..=0xff. Per `Src/utils.c:4195-4201`, the IMETA range
3366    /// ends at Marker (0xa2). The previous hand-rolled mask flagged
3367    /// 0xa3+ as imeta and over-encoded them; the canonical port must
3368    /// pass them through as literal bytes (raw high-bit characters
3369    /// from a UTF-8 terminal that are NOT zsh's internal markers).
3370    #[test]
3371    fn addkeybuf_passes_through_non_imeta_high_byte() {
3372        let _g = crate::test_util::global_state_lock();
3373        let _g = zle_test_setup();
3374        let _tg = TYPTAB_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3375        inittyptab();
3376        keybuf.lock().unwrap().clear();
3377        addkeybuf(0xa3);
3378        assert_eq!(
3379            *keybuf.lock().unwrap(),
3380            vec![0xa3],
3381            "c:1721 — 0xa3 is NOT IMETA (past Marker=0xa2); must pass through"
3382        );
3383        keybuf.lock().unwrap().clear();
3384        addkeybuf(0xff);
3385        assert_eq!(
3386            *keybuf.lock().unwrap(),
3387            vec![0xff],
3388            "c:1721 — 0xff is NOT IMETA; must pass through"
3389        );
3390    }
3391
3392    /// `Src/Zle/zle_keymap.c:1721` — ASCII bytes (0x01..=0x7e) are
3393    /// never IMETA (`Src/utils.c:4195-4201` marks only NUL=0x00 in the
3394    /// low range). They pass through verbatim. Pin the boundary on
3395    /// printable + control chars to ensure the typtab-driven predicate
3396    /// agrees with `imeta` for all ASCII.
3397    #[test]
3398    fn addkeybuf_ascii_passes_through_literally() {
3399        let _g = crate::test_util::global_state_lock();
3400        let _g = zle_test_setup();
3401        let _tg = TYPTAB_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3402        inittyptab();
3403        for c in [0x01u8, 0x1f, 0x20, b'A', b'z', 0x7e, 0x7f] {
3404            keybuf.lock().unwrap().clear();
3405            addkeybuf(c as i32);
3406            assert_eq!(
3407                *keybuf.lock().unwrap(),
3408                vec![c],
3409                "c:1721 — ASCII byte 0x{:02x} must pass through",
3410                c
3411            );
3412        }
3413    }
3414
3415    // ─── zsh-corpus pins for newkeytab / openkeymap / selectkeymap ──
3416
3417    /// `newkeytab()` returns empty HashMap.
3418    #[test]
3419    fn zle_keymap_corpus_newkeytab_is_empty() {
3420        let _g = crate::test_util::global_state_lock();
3421        let t = newkeytab();
3422        assert!(t.is_empty());
3423    }
3424
3425    /// `newkeymap(None, "myname")` returns an Arc.
3426    #[test]
3427    fn zle_keymap_corpus_newkeymap_returns_arc() {
3428        let _g = crate::test_util::global_state_lock();
3429        let km = newkeymap(None, "myname");
3430        assert!(Arc::strong_count(&km) >= 1);
3431    }
3432
3433    /// `openkeymap("never_was")` returns None.
3434    #[test]
3435    fn zle_keymap_corpus_openkeymap_unknown_returns_none() {
3436        let _g = crate::test_util::global_state_lock();
3437        let _g2 = zle_test_setup();
3438        assert!(openkeymap("zshrs_never_keymap_xyz").is_none());
3439    }
3440
3441    /// `unlinkkeymap` on missing returns nonzero (error).
3442    #[test]
3443    fn zle_keymap_corpus_unlinkkeymap_missing_returns_nonzero() {
3444        let _g = crate::test_util::global_state_lock();
3445        let _g2 = zle_test_setup();
3446        assert_ne!(
3447            unlinkkeymap("never_keymap_xyz", 0),
3448            0,
3449            "unlinking nonexistent keymap = error"
3450        );
3451    }
3452
3453    /// `selectkeymap("never_was", 0)` returns nonzero.
3454    #[test]
3455    fn zle_keymap_corpus_selectkeymap_unknown_returns_nonzero() {
3456        let _g = crate::test_util::global_state_lock();
3457        let _g2 = zle_test_setup();
3458        assert_ne!(selectkeymap("zshrs_never_keymap_xyz", 0), 0);
3459    }
3460
3461    // ═══════════════════════════════════════════════════════════════════
3462    // C-parity tests pinning Src/Zle/zle_keymap.c. Tests that capture
3463    // KNOWN ZSHRS BUGS use #[ignore = "ZSHRS BUG: …"].
3464    // ═══════════════════════════════════════════════════════════════════
3465
3466    /// `newkeytab()` returns an empty key table — fresh state must
3467    /// have zero bindings. C newhashtable equivalent at table init.
3468    #[test]
3469    fn newkeytab_returns_empty_table() {
3470        let _g = crate::test_util::global_state_lock();
3471        let kt = newkeytab();
3472        assert_eq!(kt.len(), 0, "fresh keytab must be empty");
3473    }
3474
3475    /// `openkeymap("zshrs_definitely_not_a_keymap")` returns None.
3476    /// C `Keymap openkeymap(char *name)` returns NULL on miss.
3477    #[test]
3478    fn openkeymap_unknown_name_returns_none() {
3479        let _g = crate::test_util::global_state_lock();
3480        let _g2 = zle_test_setup();
3481        assert!(openkeymap("zshrs_unknown_keymap_xyz").is_none());
3482    }
3483
3484    /// `unlinkkeymap` on a non-existent name returns nonzero.
3485    /// C convention: 0 = success, nonzero = error.
3486    #[test]
3487    fn unlinkkeymap_unknown_name_returns_nonzero() {
3488        let _g = crate::test_util::global_state_lock();
3489        let _g2 = zle_test_setup();
3490        assert_ne!(
3491            unlinkkeymap("zshrs_doesnt_exist", 0),
3492            0,
3493            "unlinking missing keymap must return error"
3494        );
3495    }
3496
3497    // ═══════════════════════════════════════════════════════════════════
3498    // C-parity tests for Src/Zle/zle_keymap.c keybind/keyisprefix/
3499    // newkeymap contracts.
3500    // ═══════════════════════════════════════════════════════════════════
3501
3502    /// c:683 — `keyisprefix(km, "")` returns 1 (empty seq is trivially
3503    /// a prefix of every binding).
3504    #[test]
3505    fn keyisprefix_empty_seq_returns_one() {
3506        let _g = crate::test_util::global_state_lock();
3507        let _g2 = zle_test_setup();
3508        let km = newkeymap(None, "test");
3509        assert_eq!(keyisprefix(&km, b""), 1, "empty seq is trivially a prefix");
3510    }
3511
3512    /// c:683 — `keyisprefix` on a fresh empty keymap for any non-prefix
3513    /// sequence returns 0 (no bindings exist).
3514    #[test]
3515    fn keyisprefix_unbound_seq_returns_zero() {
3516        let _g = crate::test_util::global_state_lock();
3517        let _g2 = zle_test_setup();
3518        let km = newkeymap(None, "test");
3519        // No bindings in km → no sequence is a prefix.
3520        assert_eq!(keyisprefix(&km, b"unbound"), 0);
3521    }
3522
3523    /// c:659 — `keybind(km, single_byte)` on an unbound byte returns
3524    /// (None, None) — no binding, no string.
3525    #[test]
3526    fn keybind_unbound_single_byte_returns_none_pair() {
3527        let _g = crate::test_util::global_state_lock();
3528        let _g2 = zle_test_setup();
3529        let km = newkeymap(None, "test");
3530        let (bind, s) = keybind(&km, &[0x42]); // 'B' — unbound on fresh km
3531        assert!(bind.is_none(), "no binding on fresh km");
3532        assert!(s.is_none(), "no string on fresh km");
3533    }
3534
3535    /// c:659 — `keybind(km, &[])` on empty seq returns (None, None)
3536    /// (no single byte to look up, no multi-byte hash hit).
3537    #[test]
3538    fn keybind_empty_seq_returns_none_pair() {
3539        let _g = crate::test_util::global_state_lock();
3540        let _g2 = zle_test_setup();
3541        let km = newkeymap(None, "test");
3542        let (bind, s) = keybind(&km, b"");
3543        assert!(bind.is_none());
3544        assert!(s.is_none());
3545    }
3546
3547    /// c:659 — `keybind(km, &[0x83, x])` decodes Meta-pair before
3548    /// looking up: byte[1]^32 is the actual key. Pin: empty km → None.
3549    #[test]
3550    fn keybind_meta_pair_unbound_returns_none() {
3551        let _g = crate::test_util::global_state_lock();
3552        let _g2 = zle_test_setup();
3553        let km = newkeymap(None, "test");
3554        // Meta-encoded escape sequence (0x83 + 'a'^32) — unbound on fresh.
3555        let (bind, _) = keybind(&km, &[0x83, b'a' ^ 32]);
3556        assert!(bind.is_none(), "unbound Meta-pair on fresh km");
3557    }
3558
3559    /// c:517 — `newkeymap(None, _)` creates a fresh keymap with all
3560    /// 256 first[] slots unbound.
3561    #[test]
3562    fn newkeymap_fresh_has_no_first_bindings() {
3563        let _g = crate::test_util::global_state_lock();
3564        let _g2 = zle_test_setup();
3565        let km = newkeymap(None, "test");
3566        for (i, slot) in km.first.iter().enumerate() {
3567            assert!(
3568                slot.is_none(),
3569                "first[{}] must be unbound on fresh keymap",
3570                i
3571            );
3572        }
3573    }
3574
3575    /// c:517 — `newkeymap(None, _)` creates a fresh keymap with empty
3576    /// multi-byte binding table.
3577    #[test]
3578    fn newkeymap_fresh_has_empty_multi_table() {
3579        let _g = crate::test_util::global_state_lock();
3580        let _g2 = zle_test_setup();
3581        let km = newkeymap(None, "test");
3582        assert!(km.multi.is_empty(), "multi table must be empty on fresh km");
3583    }
3584
3585    /// c:287 — `newkeytab()` is independent across calls (each call
3586    /// returns a fresh empty HashMap, not a shared reference).
3587    #[test]
3588    fn newkeytab_returns_owned_independent_table() {
3589        let _g = crate::test_util::global_state_lock();
3590        let kt1 = newkeytab();
3591        let kt2 = newkeytab();
3592        assert!(kt1.is_empty());
3593        assert!(kt2.is_empty());
3594        // Independent owned values — no shared backing.
3595    }
3596
3597    /// c:886 — `selectkeymap("")` returns nonzero (empty name is invalid).
3598    #[test]
3599    fn selectkeymap_empty_name_returns_nonzero() {
3600        let _g = crate::test_util::global_state_lock();
3601        let _g2 = zle_test_setup();
3602        assert_ne!(selectkeymap("", 0), 0, "empty name = invalid");
3603    }
3604
3605    /// c:886 — `selectkeymap("emacs", 0)` returns 0 (success).
3606    /// The default startup keymaps must be selectable.
3607    #[test]
3608    fn selectkeymap_default_emacs_succeeds() {
3609        let _g = crate::test_util::global_state_lock();
3610        let _g2 = zle_test_setup();
3611        assert_eq!(
3612            selectkeymap("emacs", 0),
3613            0,
3614            "default 'emacs' keymap must exist"
3615        );
3616    }
3617
3618    // ═══════════════════════════════════════════════════════════════════
3619    // Additional C-parity tests for Src/Zle/zle_keymap.c
3620    // c:134 createkeymapnamtab / c:145 init_keymaps / c:205 refkeymap_by_name
3621    // c:240 unrefkeymap_by_name / c:664 openkeymap / c:675 unlinkkeymap
3622    // c:805 linkkeymap / c:886 selectkeymap / c:921 selectlocalmap /
3623    // c:940 reselectkeymap
3624    // ═══════════════════════════════════════════════════════════════════
3625
3626    /// c:664 — `openkeymap("emacs")` returns Some after init.
3627    #[test]
3628    fn openkeymap_default_emacs_returns_some() {
3629        let _g = crate::test_util::global_state_lock();
3630        let _g2 = zle_test_setup();
3631        assert!(
3632            openkeymap("emacs").is_some(),
3633            "default 'emacs' keymap must open"
3634        );
3635    }
3636
3637    /// c:664 — `openkeymap("")` returns None (empty name invalid).
3638    #[test]
3639    fn openkeymap_empty_name_returns_none() {
3640        let _g = crate::test_util::global_state_lock();
3641        let _g2 = zle_test_setup();
3642        assert!(openkeymap("").is_none(), "empty name → None");
3643    }
3644
3645    /// c:664 — `openkeymap(unknown)` returns None.
3646    #[test]
3647    fn openkeymap_unknown_name_returns_none_pin() {
3648        let _g = crate::test_util::global_state_lock();
3649        let _g2 = zle_test_setup();
3650        assert!(
3651            openkeymap("__never_a_real_keymap_xyz__").is_none(),
3652            "unknown keymap → None"
3653        );
3654    }
3655
3656    /// c:886 — `selectkeymap` returns i32 (compile-time type pin).
3657    #[test]
3658    fn selectkeymap_returns_i32_type() {
3659        let _g = crate::test_util::global_state_lock();
3660        let _g2 = zle_test_setup();
3661        let _: i32 = selectkeymap("emacs", 0);
3662    }
3663
3664    /// c:675 — `unlinkkeymap` returns i32 (compile-time type pin).
3665    #[test]
3666    fn unlinkkeymap_returns_i32_type() {
3667        let _g = crate::test_util::global_state_lock();
3668        let _g2 = zle_test_setup();
3669        let _: i32 = unlinkkeymap("nothing_real", 0);
3670    }
3671
3672    /// c:675 — `unlinkkeymap("")` is safe (empty name).
3673    #[test]
3674    fn unlinkkeymap_empty_name_no_panic() {
3675        let _g = crate::test_util::global_state_lock();
3676        let _g2 = zle_test_setup();
3677        let _ = unlinkkeymap("", 0);
3678    }
3679
3680    /// c:921 — `selectlocalmap(None)` is safe.
3681    #[test]
3682    fn selectlocalmap_none_no_panic() {
3683        let _g = crate::test_util::global_state_lock();
3684        let _g2 = zle_test_setup();
3685        selectlocalmap(None);
3686    }
3687
3688    /// c:940 — `reselectkeymap` is idempotent / safe.
3689    #[test]
3690    fn reselectkeymap_idempotent() {
3691        let _g = crate::test_util::global_state_lock();
3692        let _g2 = zle_test_setup();
3693        for _ in 0..5 {
3694            reselectkeymap();
3695        }
3696    }
3697
3698    /// c:205 — `refkeymap_by_name("")` empty name is safe.
3699    #[test]
3700    fn refkeymap_by_name_empty_no_panic() {
3701        let _g = crate::test_util::global_state_lock();
3702        let _g2 = zle_test_setup();
3703        refkeymap_by_name("");
3704    }
3705
3706    /// c:240 — `unrefkeymap_by_name("")` empty name is safe.
3707    #[test]
3708    fn unrefkeymap_by_name_empty_no_panic() {
3709        let _g = crate::test_util::global_state_lock();
3710        let _g2 = zle_test_setup();
3711        unrefkeymap_by_name("");
3712    }
3713
3714    /// c:886 — `selectkeymap` is deterministic for same input.
3715    #[test]
3716    fn selectkeymap_is_deterministic() {
3717        let _g = crate::test_util::global_state_lock();
3718        let _g2 = zle_test_setup();
3719        let first = selectkeymap("emacs", 0);
3720        for _ in 0..3 {
3721            assert_eq!(
3722                selectkeymap("emacs", 0),
3723                first,
3724                "selectkeymap('emacs') must be deterministic"
3725            );
3726        }
3727    }
3728
3729    // ═══════════════════════════════════════════════════════════════════
3730    // Additional C-parity tests for Src/Zle/zle_keymap.c
3731    // c:134 createkeymapnamtab / c:145 init_keymaps / c:156 cleanup_keymaps /
3732    // c:224 scanprimaryname / c:278 freekeymapnamnode / c:664 openkeymap /
3733    // c:676 unlinkkeymap / c:921 selectlocalmap / c:940 reselectkeymap
3734    // ═══════════════════════════════════════════════════════════════════
3735
3736    /// c:134 — `createkeymapnamtab` idempotent.
3737    #[test]
3738    fn createkeymapnamtab_idempotent() {
3739        let _g = crate::test_util::global_state_lock();
3740        let _g2 = zle_test_setup();
3741        for _ in 0..5 {
3742            createkeymapnamtab();
3743        }
3744    }
3745
3746    /// c:145 — `init_keymaps` idempotent.
3747    #[test]
3748    fn init_keymaps_idempotent() {
3749        let _g = crate::test_util::global_state_lock();
3750        let _g2 = zle_test_setup();
3751        for _ in 0..5 {
3752            init_keymaps();
3753        }
3754    }
3755
3756    /// c:156 — `cleanup_keymaps` idempotent.
3757    #[test]
3758    fn cleanup_keymaps_idempotent() {
3759        let _g = crate::test_util::global_state_lock();
3760        let _g2 = zle_test_setup();
3761        for _ in 0..5 {
3762            cleanup_keymaps();
3763        }
3764        init_keymaps();
3765    }
3766
3767    /// c:224 — `scanprimaryname("")` empty name is safe (no-op).
3768    #[test]
3769    fn scanprimaryname_empty_no_panic() {
3770        let _g = crate::test_util::global_state_lock();
3771        let _g2 = zle_test_setup();
3772        scanprimaryname("");
3773    }
3774
3775    /// c:278 — `freekeymapnamnode("")` empty name is safe.
3776    #[test]
3777    fn freekeymapnamnode_empty_no_panic() {
3778        let _g = crate::test_util::global_state_lock();
3779        let _g2 = zle_test_setup();
3780        freekeymapnamnode("");
3781    }
3782
3783    /// c:940 — `reselectkeymap` returns void (signature pin) + safe.
3784    #[test]
3785    fn reselectkeymap_returns_void_type() {
3786        let _g = crate::test_util::global_state_lock();
3787        let _g2 = zle_test_setup();
3788        let _: () = reselectkeymap();
3789    }
3790
3791    /// c:664 — `openkeymap` returns Option<Arc<Keymap>> (type pin).
3792    #[test]
3793    fn openkeymap_returns_option_arc_keymap_type() {
3794        let _g = crate::test_util::global_state_lock();
3795        let _g2 = zle_test_setup();
3796        let _: Option<Arc<Keymap>> = openkeymap("");
3797    }
3798
3799    /// c:676 — `unlinkkeymap("", 0)` empty name returns nonzero.
3800    #[test]
3801    fn unlinkkeymap_empty_name_returns_nonzero_pin() {
3802        let _g = crate::test_util::global_state_lock();
3803        let _g2 = zle_test_setup();
3804        let r = unlinkkeymap("", 0);
3805        assert_ne!(r, 0, "empty name → nonzero error");
3806    }
3807
3808    /// c:287 — `newkeytab` returns HashMap (compile-time type pin).
3809    #[test]
3810    fn newkeytab_returns_hashmap_type() {
3811        let _: HashMap<Vec<u8>, KeyBinding> = newkeytab();
3812    }
3813
3814    /// c:287 — `newkeytab` is empty.
3815    #[test]
3816    fn newkeytab_returns_empty_pin() {
3817        let t = newkeytab();
3818        assert!(t.is_empty(), "fresh keytab must be empty");
3819    }
3820
3821    /// c:921 — `selectlocalmap(None)` is idempotent.
3822    #[test]
3823    fn selectlocalmap_none_idempotent() {
3824        let _g = crate::test_util::global_state_lock();
3825        let _g2 = zle_test_setup();
3826        for _ in 0..5 {
3827            selectlocalmap(None);
3828        }
3829    }
3830
3831    /// c:886 — `selectkeymap` returns i32 type.
3832    #[test]
3833    fn selectkeymap_returns_i32_type_pin2() {
3834        let _g = crate::test_util::global_state_lock();
3835        let _g2 = zle_test_setup();
3836        let _: i32 = selectkeymap("", 0);
3837    }
3838
3839    /// c:676 — `unlinkkeymap` is deterministic for unknown name.
3840    #[test]
3841    fn unlinkkeymap_unknown_name_is_deterministic() {
3842        let _g = crate::test_util::global_state_lock();
3843        let _g2 = zle_test_setup();
3844        let first = unlinkkeymap("__zshrs_never_keymap__", 0);
3845        for _ in 0..3 {
3846            assert_eq!(
3847                unlinkkeymap("__zshrs_never_keymap__", 0),
3848                first,
3849                "unlinkkeymap unknown must be deterministic"
3850            );
3851        }
3852    }
3853
3854    // ═══════════════════════════════════════════════════════════════════
3855    // Additional C-parity tests for Src/Zle/zle_keymap.c
3856    // c:134 createkeymapnamtab / c:176 emptykeymapnamtab /
3857    // c:205 refkeymap_by_name / c:240 unrefkeymap_by_name /
3858    // c:517 newkeymap / c:664 openkeymap / c:805 linkkeymap
3859    // ═══════════════════════════════════════════════════════════════════
3860
3861    /// c:134 — `createkeymapnamtab` is idempotent (alt 10-call).
3862    #[test]
3863    fn createkeymapnamtab_idempotent_10_call() {
3864        let _g = crate::test_util::global_state_lock();
3865        let _g2 = zle_test_setup();
3866        for _ in 0..10 {
3867            createkeymapnamtab();
3868        }
3869    }
3870
3871    /// c:176 — `emptykeymapnamtab` is idempotent.
3872    #[test]
3873    fn emptykeymapnamtab_idempotent() {
3874        let _g = crate::test_util::global_state_lock();
3875        let _g2 = zle_test_setup();
3876        for _ in 0..10 {
3877            emptykeymapnamtab();
3878        }
3879    }
3880
3881    /// c:205 — `refkeymap_by_name` for unknown name is safe.
3882    #[test]
3883    fn refkeymap_by_name_unknown_no_panic() {
3884        let _g = crate::test_util::global_state_lock();
3885        let _g2 = zle_test_setup();
3886        refkeymap_by_name("__never_keymap_xyz__");
3887    }
3888
3889    /// c:240 — `unrefkeymap_by_name` for unknown name is safe.
3890    #[test]
3891    fn unrefkeymap_by_name_unknown_no_panic() {
3892        let _g = crate::test_util::global_state_lock();
3893        let _g2 = zle_test_setup();
3894        unrefkeymap_by_name("__never_keymap_xyz__");
3895    }
3896
3897    /// c:240 — `unrefkeymap_by_name("")` empty name safe (alt).
3898    #[test]
3899    fn unrefkeymap_by_name_empty_no_panic_alt() {
3900        let _g = crate::test_util::global_state_lock();
3901        let _g2 = zle_test_setup();
3902        unrefkeymap_by_name("");
3903    }
3904
3905    /// c:517 — `newkeymap(None, "")` returns Arc<Keymap> (compile-time pin).
3906    #[test]
3907    fn newkeymap_none_returns_arc_type() {
3908        let _g = crate::test_util::global_state_lock();
3909        let _g2 = zle_test_setup();
3910        let _: Arc<Keymap> = newkeymap(None, "");
3911    }
3912
3913    /// c:517 — `newkeymap` is deterministic in shape (always returns Arc).
3914    #[test]
3915    fn newkeymap_deterministic_shape() {
3916        let _g = crate::test_util::global_state_lock();
3917        let _g2 = zle_test_setup();
3918        for _ in 0..5 {
3919            let _: Arc<Keymap> = newkeymap(None, "test");
3920        }
3921    }
3922
3923    /// c:805 — `linkkeymap` returns i32 (compile-time pin).
3924    #[test]
3925    fn linkkeymap_returns_i32_type() {
3926        let _g = crate::test_util::global_state_lock();
3927        let _g2 = zle_test_setup();
3928        let km = newkeymap(None, "");
3929        let _: i32 = linkkeymap(km, "test", 0);
3930    }
3931
3932    /// c:664 — `openkeymap("")` empty name returns None (alt).
3933    #[test]
3934    fn openkeymap_empty_name_returns_none_alt() {
3935        let _g = crate::test_util::global_state_lock();
3936        let _g2 = zle_test_setup();
3937        assert!(openkeymap("").is_none(), "empty keymap name → None");
3938    }
3939
3940    /// c:664 — `openkeymap("__never__")` for unknown name returns None.
3941    #[test]
3942    fn openkeymap_unknown_returns_none() {
3943        let _g = crate::test_util::global_state_lock();
3944        let _g2 = zle_test_setup();
3945        assert!(openkeymap("__definitely_no_such_keymap_xyz__").is_none());
3946    }
3947
3948    /// c:287 — `newkeytab` is deterministic shape (always empty).
3949    #[test]
3950    fn newkeytab_deterministic_shape() {
3951        for _ in 0..5 {
3952            let t = newkeytab();
3953            assert!(t.is_empty(), "newkeytab must always start empty");
3954        }
3955    }
3956}