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}