Skip to main content

zsh/ported/zle/
zle_thingy.rs

1//! ZLE thingies - named bindings to widgets
2//!
3//! Direct port from zsh/Src/Zle/zle_thingy.c
4//!
5//! A "thingy" is a named entity that refers to a widget. Multiple thingies
6//! can refer to the same widget. Thingies are reference-counted.
7
8use std::collections::HashMap;
9use std::sync::atomic::Ordering;
10use std::sync::{Arc, Mutex, OnceLock};
11
12use super::zle_h::{
13    widget, WidgetImpl, TH_IMMORTAL, WIDGET_INT, WIDGET_INUSE, WIDGET_NCOMP, ZLE_ISCOMP,
14    ZLE_KEEPSUFFIX, ZLE_MENUCMP,
15};
16use crate::ported::utils::zwarnnam;
17use crate::ported::zsh_h::{options, DISABLED, OPT_ISSET};
18
19#[allow(unused_imports)]
20use crate::ported::zle::{
21    deltochar::*, textobjects::*, zle_h::*, zle_hist::*, zle_main::*, zle_misc::*, zle_move::*,
22    zle_params::*, zle_refresh::*, zle_tricky::*, zle_utils::*, zle_vi::*, zle_word::*,
23};
24/// Direct port of `struct thingy` from `Src/Zle/zle.h:224`. A named
25/// reference to a widget. `ThingyFlags` deleted — C uses an `int
26/// flags` field with `TH_IMMORTAL` (1<<1) and `DISABLED` (1<<0) bits.
27
28// --- AUTO: cross-zle hoisted-fn use glob ---
29#[allow(unused_imports)]
30// =====================================================================
31// hashtable management — `Src/Zle/zle_thingy.c:58-124`.
32// =====================================================================
33
34/// Port of `createthingytab()` from `Src/Zle/zle_thingy.c:60`.
35/// ```c
36/// static void
37/// createthingytab(void)
38/// {
39///     thingytab = newhashtable(199, "thingytab", NULL);
40///     thingytab->hash = hasher;
41///     thingytab->emptytable = emptythingytab;
42///     ...
43/// }
44/// ```
45/// Allocate the global thingytab. In Rust the table is `OnceLock`-
46/// initialized lazily; this entry forces creation eagerly to match
47/// C's "pre-zle init" call site at zle_main.c.
48pub fn createthingytab() {
49    // c:60
50    let _ = thingytab(); // c:60 newhashtable
51}
52
53impl Thingy {
54    /// Create a thingy with no widget bound — equivalent to a freshly
55    /// allocated entry from `makethingynode()` in
56    /// Src/Zle/zle_thingy.c:108. Callers fill in `widget` later via
57    /// `bindwidget` (zle_thingy.c:199).
58    pub fn new(name: &str) -> Self {
59        Thingy {
60            nam: name.to_string(),
61            flags: 0,
62            rc: 1,
63            widget: None,
64        }
65    }
66
67    /// Create a thingy that wraps a built-in widget.
68    /// Equivalent to the `addzlefunction()` path at
69    /// Src/Zle/zle_thingy.c:281: builds the immortal-flagged Thingy
70    /// and binds it to a widget produced by the built-in dispatch
71    /// table (`widget::builtin`).
72    pub fn builtin(name: &str) -> Self {
73        let widget = widget::builtin(name);
74        Thingy {
75            nam: name.to_string(),
76            flags: TH_IMMORTAL,
77            rc: 1,
78            widget: Some(Arc::new(widget)),
79        }
80    }
81
82    /// Create a thingy that wraps a user-defined shell function.
83    /// Equivalent to `bin_zle_new()` at Src/Zle/zle_thingy.c:584 — the
84    /// `zle -N name fn` builtin path.
85    pub fn user_defined(name: &str, func_name: &str) -> Self {
86        let widget = widget::user_defined(name, func_name);
87        Thingy {
88            nam: name.to_string(),
89            flags: 0,
90            rc: 1,
91            widget: Some(Arc::new(widget)),
92        }
93    }
94
95    /// Test whether this thingy's name matches `name`.
96    /// Equivalent to the `IS_THINGY(thingy, name)` macro at
97    /// Src/Zle/zle.h — used by widget bodies that special-case their
98    /// own bound name (e.g. select-a-word checking which alias fired).
99    pub fn is(&self, name: &str) -> bool {
100        self.nam == name
101    }
102
103    /// Test whether this thingy is `name` or its dot-prefixed variant.
104    /// The `.foo` form names the underlying built-in when a user has
105    /// aliased `foo` to something else — see `bin_zle_new`'s `args[0]`
106    /// vs `args[1]` split at zle_thingy.c:584. Callers use this when
107    /// they want the canonical built-in regardless of user aliasing.
108    pub fn is_thingy(&self, name: &str) -> bool {
109        self.nam == name || self.nam == format!(".{}", name)
110    }
111}
112
113/// Port of `emptythingytab(UNUSED(HashTable ht))` from `Src/Zle/zle_thingy.c:80`.
114/// ```c
115/// static void
116/// emptythingytab(UNUSED(HashTable ht))
117/// {
118///     /* This will only be called when deleting the thingy table,
119///      * which is only done to unload the zle module... */
120///     scanhashtable(thingytab, 0, 0, DISABLED, scanemptythingies, 0);
121/// }
122/// ```
123/// Walk every non-disabled thingy and unbind it (frees user-
124/// defined widgets but leaves the fixed `thingies[]` entries
125/// alone).
126/// WARNING: param names don't match C — Rust=() vs C=(ht)
127pub fn emptythingytab() {
128    // c:80
129    // c:80 — `scanhashtable(thingytab, 0, 0, DISABLED, scanemptythingies, 0)`.
130    // Collect-then-iterate to avoid holding the lock during the mutating callback.
131    let names: Vec<String> = thingytab()
132        .lock()
133        .unwrap()
134        .iter()
135        .filter(|(_, t)| (t.flags & DISABLED) == 0)
136        .map(|(k, _)| k.clone())
137        .collect();
138    names.iter().for_each(|n| scanemptythingies(n)); // c:91 scancallback
139}
140
141/// Port of `scanemptythingies(HashNode hn, UNUSED(int flags))` from `Src/Zle/zle_thingy.c:96`.
142/// ```c
143/// static void
144/// scanemptythingies(HashNode hn, UNUSED(int flags))
145/// {
146///     Thingy t = (Thingy) hn;
147///     if(!(t->widget->flags & WIDGET_INT))
148///         unbindwidget(t, 1);
149/// }
150/// ```
151/// Per-entry callback: if the bound widget isn't internal, unbind it.
152/// WARNING: param names don't match C — Rust=(name) vs C=(hn, flags)
153pub fn scanemptythingies(name: &str) {
154    // c:96
155    // c:96 — `if(!(t->widget->flags & WIDGET_INT)) unbindwidget(t, 1)`.
156    // C assumes every Thingy has its `widget` pointer set (the alloc
157    // path in `addthingy` always binds via `addnewwidget`). The Rust
158    // port defaults `widget = None` for fresh `rthingy(name)` entries
159    // that haven't been bound yet — those are user-defined slots
160    // and SHOULD be unbound by emptythingytab. Without flipping the
161    // default to "non-internal" (false), `${name}` entries with no
162    // widget stayed in the table after `emptythingytab` cleared
163    // everything else, breaking `zle -d`-style cleanup semantic.
164    let internal = {
165        let tab = thingytab().lock().unwrap();
166        tab.get(name)
167            .and_then(|t| t.widget.as_ref().map(|w| (w.flags & WIDGET_INT) != 0))
168            .unwrap_or(false)
169    };
170    if !internal {
171        unbindwidget(name, 1); // c:103
172    }
173}
174
175/// Port of `makethingynode()` from `Src/Zle/zle_thingy.c:108`.
176/// ```c
177/// static Thingy
178/// makethingynode(void)
179/// {
180///     Thingy t = (Thingy) zshcalloc(sizeof(*t));
181///     t->flags = DISABLED;
182///     return t;
183/// }
184/// ```
185/// Allocate a fresh Thingy with the DISABLED flag set; caller is
186/// expected to fill in `nam` and `bindwidget` it.
187pub fn makethingynode() -> Thingy {
188    // c:108
189    let mut t = Thingy::new(""); // c:108 zshcalloc
190    t.flags |= DISABLED; // c:112 t->flags = DISABLED
191    t.rc = 0; // c:110 zshcalloc zeros rc
192    t // c:113 return t
193}
194
195/// Port of `freethingynode(HashNode hn)` from `Src/Zle/zle_thingy.c:118`.
196/// ```c
197/// static void
198/// freethingynode(HashNode hn)
199/// {
200///     Thingy th = (Thingy) hn;
201///     zsfree(th->nam);
202///     zfree(th, sizeof(*th));
203/// }
204/// ```
205/// Free a Thingy by name (HashTable freenode callback). In Rust
206/// the storage is owned by the table; removal does the free.
207/// WARNING: param names don't match C — Rust=(name) vs C=(hn)
208pub fn freethingynode(name: &str) {
209    // c:118
210    // c:118-123 — `zsfree(th->nam); zfree(th, sizeof(*th))`. Rust
211    // String + Thingy drop on `remove()`.
212    let _ = thingytab().lock().unwrap().remove(name);
213}
214
215// =====================================================================
216// reference counting — `Src/Zle/zle_thingy.c:130-176`.
217// =====================================================================
218
219/// Port of `refthingy(Thingy th)` from `Src/Zle/zle_thingy.c:138`.
220/// ```c
221/// mod_export Thingy
222/// refthingy(Thingy th)
223/// {
224///     if(th)
225///         th->rc++;
226///     return th;
227/// }
228/// ```
229/// Bump the reference count on the named Thingy. Caller must
230/// have an existing reference (or be the creator).
231/// WARNING: param names don't match C — Rust=(name) vs C=(th)
232pub fn refthingy(name: &str) {
233    // c:138
234    let mut tab = thingytab().lock().unwrap();
235    if let Some(t) = tab.get_mut(name) {
236        // c:140 if(th)
237        t.rc += 1; // c:141 th->rc++
238    }
239}
240
241/// Port of `unrefthingy(Thingy th)` from `Src/Zle/zle_thingy.c:147`.
242/// ```c
243/// void
244/// unrefthingy(Thingy th)
245/// {
246///     if(th && !--th->rc)
247///         thingytab->freenode(thingytab->removenode(thingytab, th->nam));
248/// }
249/// ```
250/// Drop a reference; remove from table when rc hits 0.
251pub fn unrefthingy(th: &str) {
252    // c:147
253    let drop = thingytab()
254        .lock()
255        .unwrap()
256        .get_mut(th) // c:149 if(th && !--th->rc)
257        .map(|t| {
258            t.rc -= 1;
259            t.rc == 0
260        })
261        .unwrap_or(false);
262    if drop {
263        freethingynode(th);
264    } // c:150 freenode(removenode(...))
265}
266
267/// Port of `rthingy(char *nam)` from `Src/Zle/zle_thingy.c:158`.
268/// ```c
269/// Thingy
270/// rthingy(char *nam)
271/// {
272///     Thingy t = (Thingy) thingytab->getnode2(thingytab, nam);
273///     if(!t)
274///         thingytab->addnode(thingytab, ztrdup(nam), t = makethingynode());
275///     return refthingy(t);
276/// }
277/// ```
278/// "Resolve thingy" — get-or-create-then-ref. Always returns a
279/// thingy; creates a fresh disabled one if none exists.
280pub fn rthingy(nam: &str) {
281    // c:158
282    {
283        let mut tab = thingytab().lock().unwrap();
284        if !tab.contains_key(nam) {
285            // c:160-162 if(!t)
286            let mut t = makethingynode(); // c:163 makethingynode
287            t.nam = nam.to_string(); // c:163 ztrdup(nam)
288            tab.insert(nam.to_string(), t); // c:163 addnode
289        }
290    }
291    refthingy(nam); // c:164 return refthingy(t)
292}
293
294/// Port of `rthingy_nocreate(char *nam)` from `Src/Zle/zle_thingy.c:169`.
295/// ```c
296/// Thingy
297/// rthingy_nocreate(char *nam)
298/// {
299///     Thingy t = (Thingy) thingytab->getnode2(thingytab, nam);
300///     if(!t)
301///         return NULL;
302///     return refthingy(t);
303/// }
304/// ```
305/// Lookup-only variant — returns false (no Thingy) if missing.
306/// WARNING: param names don't match C — Rust=(name) vs C=(nam)
307pub fn rthingy_nocreate(name: &str) -> bool {
308    // c:169
309    let exists = thingytab().lock().unwrap().contains_key(name); // c:169 getnode2
310    if !exists {
311        return false; // c:173-174 if(!t) return NULL
312    }
313    refthingy(name); // c:175 return refthingy(t)
314    true
315}
316
317// =====================================================================
318// widget binding — `Src/Zle/zle_thingy.c:178-270`.
319// =====================================================================
320
321/// Port of `bindwidget(widget w, Thingy t)` from `Src/Zle/zle_thingy.c:197`.
322/// ```c
323/// static int
324/// bindwidget(widget w, Thingy t)
325/// {
326///     if(t->flags & TH_IMMORTAL) {
327///         unrefthingy(t);
328///         return -1;
329///     }
330///     if(!(t->flags & DISABLED)) {
331///         if(t->widget == w)
332///             return 0;
333///         unbindwidget(t, 1);
334///     }
335///     if(w->first) {
336///         t->samew = w->first->samew;
337///         w->first->samew = t;
338///     } else {
339///         w->first = t;
340///         t->samew = t;
341///     }
342///     t->widget = w;
343///     t->flags &= ~DISABLED;
344///     return 0;
345/// }
346/// ```
347/// Bind `w` to thingy `t_name`. Caller's Thingy reference is
348/// consumed when TH_IMMORTAL blocks the bind. Samew chains are
349/// implicit in Rust — the `Arc<widget>` identity links peers.
350/// Returns 0 on success, -1 on TH_IMMORTAL block.
351pub fn bindwidget(w: Arc<widget>, t: &str) -> i32 {
352    // c:199
353    let (immortal, disabled, same) = {
354        let tab = thingytab().lock().unwrap();
355        match tab.get(t) {
356            Some(t) => (
357                (t.flags & TH_IMMORTAL) != 0,
358                (t.flags & DISABLED) != 0,
359                t.widget
360                    .as_ref()
361                    .map(|w2| Arc::ptr_eq(w2, &w))
362                    .unwrap_or(false),
363            ),
364            None => (false, true, false),
365        }
366    };
367
368    if immortal {
369        // c:201 TH_IMMORTAL
370        unrefthingy(t); // c:202
371        return -1; // c:203
372    }
373    if !disabled {
374        // c:205 !DISABLED
375        if same {
376            // c:206 t->widget == w
377            return 0; // c:207
378        }
379        unbindwidget(t, 1); // c:208
380    }
381    // c:210-216 — `samew` circular-list maintenance is implicit in
382    // Rust: shared widgets just hold the same Arc, and walks via
383    // Arc::ptr_eq find peers. No explicit list edit needed.
384    let mut tab = thingytab().lock().unwrap();
385    if let Some(t) = tab.get_mut(t) {
386        t.widget = Some(w); // c:217 t->widget = w
387        t.flags &= !DISABLED; // c:218 t->flags &= ~DISABLED
388    }
389    0 // c:219 return 0
390}
391
392/// Port of `unbindwidget(Thingy t, int override)` from `Src/Zle/zle_thingy.c:228`.
393/// ```c
394/// static int
395/// unbindwidget(Thingy t, int override)
396/// {
397///     widget w;
398///     if(t->flags & DISABLED)
399///         return 0;
400///     if(!override && (t->flags & TH_IMMORTAL))
401///         return -1;
402///     w = t->widget;
403///     if(t->samew == t)
404///         freewidget(w);
405///     else { /* unlink from samew chain */ }
406///     t->flags &= ~TH_IMMORTAL;
407///     t->flags |= DISABLED;
408///     unrefthingy(t);
409///     return 0;
410/// }
411/// ```
412/// Detach Thingy `t_name` from its widget. Walks the table to
413/// detect the "last reference" case (samew == t in C); if so, the
414/// widget is freed (Arc auto-drops when the Thingy clears it).
415/// `override_` non-zero overrides TH_IMMORTAL.
416/// WARNING: param names don't match C — Rust=(t, override_) vs C=(t, override)
417pub fn unbindwidget(t: &str, override_: i32) -> i32 {
418    // c:230
419    let (disabled, immortal, w_opt) = {
420        let tab = thingytab().lock().unwrap();
421        match tab.get(t) {
422            Some(t) => (
423                (t.flags & DISABLED) != 0,
424                (t.flags & TH_IMMORTAL) != 0,
425                t.widget.clone(),
426            ),
427            None => return 0,
428        }
429    };
430    if disabled {
431        // c:234 if DISABLED
432        return 0;
433    }
434    if override_ == 0 && immortal {
435        // c:236 !override && TH_IMMORTAL
436        return -1;
437    }
438    // c:239 — `if(t->samew == t) freewidget(w)`. In Rust we walk
439    // the table to count peers sharing this widget.
440    if let Some(w) = w_opt {
441        let peer_count = {
442            let tab = thingytab().lock().unwrap();
443            tab.values()
444                .filter(|other| other.nam != t)
445                .filter(|other| {
446                    other
447                        .widget
448                        .as_ref()
449                        .map(|w2| Arc::ptr_eq(w2, &w))
450                        .unwrap_or(false)
451                })
452                .count()
453        };
454        if peer_count == 0 {
455            // c:240 — `freewidget(w)`. Arc::strong_count drops to
456            // 1 (just our local clone); freewidget marks WIDGET_FREE
457            // if INUSE, otherwise the Arc auto-drops on scope exit.
458            freewidget(w);
459        }
460        // c:241-246 — non-last case: just unlink. Implicit in Rust;
461        // peers retain their own Arc clones.
462    }
463
464    let mut tab = thingytab().lock().unwrap();
465    if let Some(t) = tab.get_mut(t) {
466        t.flags &= !TH_IMMORTAL; // c:247 &= ~TH_IMMORTAL
467        t.flags |= DISABLED; // c:248 |= DISABLED
468        t.widget = None;
469    }
470    drop(tab);
471    unrefthingy(t); // c:249 unrefthingy(t)
472    0 // c:250 return 0
473}
474
475/// Port of `freewidget(widget w)` from `Src/Zle/zle_thingy.c:255`.
476/// ```c
477/// void
478/// freewidget(widget w)
479/// {
480///     if (w->flags & WIDGET_INUSE) {
481///         w->flags |= WIDGET_FREE;
482///         return;
483///     }
484///     if (w->flags & WIDGET_NCOMP) {
485///         zsfree(w->u.comp.wid);
486///         zsfree(w->u.comp.func);
487///     } else if(!(w->flags & WIDGET_INT))
488///         zsfree(w->u.fnnam);
489///     zfree(w, sizeof(*w));
490/// }
491/// ```
492/// Drop a widget. If WIDGET_INUSE (we're freeing it from inside
493/// the widget's own dispatch), defer the free by setting WIDGET_FREE
494/// — the dispatcher checks this flag after returning.
495///
496/// In Rust the `Arc<widget>` auto-drops; this fn exists so the
497/// INUSE/FREE flag handshake matches C exactly. The actual storage
498/// drop happens when the last Arc is released by the caller's scope.
499pub fn freewidget(w: Arc<widget>) {
500    // c:257
501    // Direct port of `void freewidget(widget w)` from zle_thingy.c:255:
502    // ```c
503    // if (w->flags & WIDGET_INUSE) { w->flags |= WIDGET_FREE; return; }
504    // // free widget data + storage
505    // ```
506    //
507    // **Arc<widget> divergence:** the C source mutates w->flags via
508    // a single owner pointer; Rust uses Arc<widget> shared-immutable
509    // and dispatches deferred-free via Arc::strong_count. When this
510    // call is the LAST reference (count==1) and INUSE is set, the
511    // widget is mid-dispatch — let the dispatcher drop the last
512    // Arc when it returns. When count>1, another holder is alive
513    // and the storage stays valid. When count==1 + !INUSE, the
514    // implicit Arc drop at end-of-scope reclaims storage.
515    if (w.flags & WIDGET_INUSE) != 0 {
516        return; // c:261
517    }
518    // c:264-269 — comp-widget / user-fn cleanup. WidgetImpl::UserFunc
519    // owns its String; WidgetImpl::Internal owns nothing. Arc drop
520    // covers both.
521    drop(w); // c:269 zfree(w, ...)
522}
523
524/// Port of `addzlefunction(char *name, ZleIntFunc ifunc, int flags)` from `Src/Zle/zle_thingy.c:279`.
525/// ```c
526/// mod_export widget
527/// addzlefunction(char *name, ZleIntFunc ifunc, int flags)
528/// {
529///     VARARR(char, dotn, strlen(name) + 2);
530///     widget w;
531///     Thingy t;
532///     if(name[0] == '.')
533///         return NULL;
534///     dotn[0] = '.';
535///     strcpy(dotn + 1, name);
536///     t = (Thingy) thingytab->getnode(thingytab, dotn);
537///     if(t && (t->flags & TH_IMMORTAL))
538///         return NULL;
539///     w = zalloc(sizeof(*w));
540///     w->flags = WIDGET_INT | flags;
541///     w->first = NULL;
542///     w->u.fn = ifunc;
543///     t = rthingy(dotn);
544///     bindwidget(w, t);
545///     t->flags |= TH_IMMORTAL;
546///     bindwidget(w, rthingy(name));
547///     return w;
548/// }
549/// ```
550/// Register a module-internal widget. The widget binds to both
551/// `.name` (immortal canonical) and `name` (user-rebindable) in
552/// the thingytab. Refuses if `.name` already taken by another
553/// immortal or if `name` starts with `.`.
554/// WARNING: param names don't match C — Rust=(ifunc, flags) vs C=(name, ifunc, flags)
555pub fn addzlefunction(
556    // c:281
557    name: &str,
558    ifunc: ZleIntFunc,
559    flags: i32,
560) -> Option<Arc<widget>> {
561    // c:279
562    if name.starts_with('.') {
563        // c:287 if(name[0] == '.')
564        return None; // c:288
565    }
566    let dotn = format!(".{}", name); // c:289-290 dotn[0]='.';strcpy(...)
567
568    // c:291-293 — refuse if .name is already TH_IMMORTAL.
569    let blocked = {
570        let tab = thingytab().lock().unwrap();
571        tab.get(&dotn)
572            .map(|t| (t.flags & TH_IMMORTAL) != 0)
573            .unwrap_or(false)
574    };
575    if blocked {
576        return None; // c:293
577    }
578
579    // c:294-297 — `w = zalloc(...); w->flags = WIDGET_INT|flags;
580    //              w->first = NULL; w->u.fn = ifunc;`.
581    let w = Arc::new(widget {
582        flags: flags | WIDGET_INT, // c:295
583        first: None,
584        u: WidgetImpl::Internal(ifunc), // c:297 w->u.fn = ifunc
585    });
586
587    // c:298-301 — bind to dotted form, mark immortal, then bind to
588    // canonical form too.
589    rthingy(&dotn); // c:298 t = rthingy(dotn)
590    bindwidget(w.clone(), &dotn); // c:299 bindwidget(w, t)
591    if let Some(t) = thingytab().lock().unwrap().get_mut(&dotn) {
592        t.flags |= TH_IMMORTAL; // c:300 t->flags |= TH_IMMORTAL
593    }
594    rthingy(name); // c:301 rthingy(name)
595    bindwidget(w.clone(), name); // c:301 bindwidget(w, ...)
596    Some(w) // c:302 return w
597}
598
599/// Port of `deletezlefunction(widget w)` from `Src/Zle/zle_thingy.c:308`.
600/// ```c
601/// mod_export void
602/// deletezlefunction(widget w)
603/// {
604///     Thingy p, n;
605///     p = w->first;
606///     while(1) {
607///         n = p->samew;
608///         if(n == p) {
609///             unbindwidget(p, 1);
610///             return;
611///         }
612///         unbindwidget(p, 1);
613///         p = n;
614///     }
615/// }
616/// ```
617/// Walk every Thingy bound to `w` and unbind it (override flag set,
618/// so even TH_IMMORTAL bindings come undone). Used by module
619/// teardown.
620pub fn deletezlefunction(w: &Arc<widget>) {
621    // c:310
622    // c:310-323 — walk samew circular chain calling unbindwidget(p,1)
623    // until p == p->samew (the last entry). In Rust we collect all
624    // matching names first, then unbind each.
625    let names: Vec<String> = {
626        let tab = thingytab().lock().unwrap();
627        tab.iter()
628            .filter(|(_, t)| {
629                t.widget
630                    .as_ref()
631                    .map(|w2| Arc::ptr_eq(w2, w))
632                    .unwrap_or(false)
633            })
634            .map(|(k, _)| k.clone())
635            .collect()
636    };
637    for n in names {
638        unbindwidget(&n, 1); // c:318/321 unbindwidget(p, 1)
639    }
640}
641
642// =====================================================================
643// `bin_zle` and per-mode dispatchers — `Src/Zle/zle_thingy.c:341-1015`.
644// =====================================================================
645//
646// The bin_zle_* ported below dispatch into the live ZLE session state
647// (zlecs/zlemetaline/keymaps/watch_fd table/zle_refresh draw
648// primitives). Each entry routes through the existing Rust globals
649// (ZLELINE/ZLECS/ZLELL in compcore.rs, keymapnamtab in zle_keymap.rs,
650// hook_functions on ShellExecutor, ZLE_RESET_NEEDED in zle_main.rs)
651// where the substrate is canonical, or via real fn calls into the
652// per-method Zle ports. Each fn's docstring cites its C source line
653// and the substrate path it uses.
654
655/// Port of `bin_zle(char *name, char **args, Options ops, UNUSED(int func))` from `Src/Zle/zle_thingy.c:343`. Top-level
656/// `zle` builtin dispatcher — selects per-flag handler from opns[]
657/// table (-l/-D/-A/-N/-C/-R/-M/-U/-K/-I/-f/-F/-T) or falls through
658/// to bin_zle_call when no flag is set.
659pub fn bin_zle(
660    name: &str,
661    args: &[String], // c:343
662    ops: &options,
663    _func: i32,
664) -> i32 {
665    // c:zle_main.c setup_ — in zsh proper, `init_thingies()` runs
666    // when zsh/zle is autoloaded on first ZLE access. zshrs in
667    // non-interactive (`-fc`) mode never loads zsh/zle through that
668    // path, so user `zle -C`/`zle -l`/etc. calls fail because the
669    // thingytab is empty. Lazy-init on first `zle` call — idempotent
670    // (the per-name `contains_key` check inside init_thingies makes
671    // re-entry safe).
672    static THINGIES_INIT: std::sync::Once = std::sync::Once::new();
673    THINGIES_INIT.call_once(|| {
674        init_thingies();
675    });
676    // c:345-364 — dispatch table: `static const struct opn opns[]`.
677    // (flag_char, handler_fn, min_args, max_args). All sub-handlers
678    // take the C canonical `(name, args, ops, func)` signature, so
679    // the table type matches `struct opn` exactly.
680    type OpHandler = fn(&str, &[String], &options, i32) -> i32;
681    let opns: [(u8, OpHandler, i32, i32); 14] = [
682        (b'l', bin_zle_list, 0, -1),      // c:350
683        (b'D', bin_zle_del, 1, -1),       // c:351
684        (b'A', bin_zle_link, 2, 2),       // c:352
685        (b'N', bin_zle_new, 1, 2),        // c:353
686        (b'C', bin_zle_complete, 3, 3),   // c:354
687        (b'R', bin_zle_refresh, 0, -1),   // c:355
688        (b'M', bin_zle_mesg, 1, 1),       // c:356
689        (b'U', bin_zle_unget, 1, 1),      // c:357
690        (b'K', bin_zle_keymap, 1, 1),     // c:358
691        (b'I', bin_zle_invalidate, 0, 0), // c:359
692        (b'f', bin_zle_flags, 1, -1),     // c:360
693        (b'F', bin_zle_fd, 0, 2),         // c:361
694        (b'T', bin_zle_transform, 0, 2),  // c:362
695        (0u8, bin_zle_call, 0, -1),       // c:363 — sentinel: no flag → bin_zle_call.
696    ];
697
698    // c:369 — `for (op = opns; op->o && !OPT_ISSET(ops, op->o); op++) ;`.
699    // Pick the first op whose flag is set; sentinel (o=0) loops out.
700    let op_idx = opns
701        .iter()
702        .position(|(o, _, _, _)| *o != 0 && OPT_ISSET(ops, *o))
703        .unwrap_or(opns.len() - 1); // c:369 — fall to sentinel
704
705    // c:370-375 — reject when more than one operation flag is set:
706    // `if (op->o) for (opp = op; (++opp)->o; ) if (OPT_ISSET(ops,
707    // opp->o)) { zwarnnam("incompatible..."); return 1; }`.
708    if opns[op_idx].0 != 0 {
709        // c:370
710        for (o, _, _, _) in opns.iter().skip(op_idx + 1) {
711            if *o != 0 && OPT_ISSET(ops, *o) {
712                zwarnnam(name, "incompatible operation selection options"); // c:373
713                return 1; // c:374
714            }
715        }
716    }
717
718    // c:378-385 — arg-count check against op->min / op->max.
719    let n = args.len() as i32; // c:378
720    let (op_o, op_func, op_min, op_max) = &opns[op_idx];
721    if n < *op_min {
722        // c:379
723        zwarnnam(
724            name,
725            &format!("not enough arguments for -{}", *op_o as char),
726        ); // c:380
727        return 1; // c:381
728    } else if *op_max != -1 && n > *op_max {
729        // c:382
730        zwarnnam(name, &format!("too many arguments for -{}", *op_o as char)); // c:383
731        return 1; // c:384
732    }
733
734    // c:388 — `return op->func(name, args, ops, op->o);`.
735    op_func(name, args, ops, *op_o as i32)
736}
737
738/// Port of `bin_zle_list(UNUSED(char *name), char **args, Options ops, UNUSED(char func))` from `Src/Zle/zle_thingy.c:393`.
739/// ```c
740/// static int
741/// bin_zle_list(...) {
742///     if (!*args) { scanhashtable(thingytab, 1, 0, DISABLED, scanlistwidgets, ...); return 0; }
743///     for (; *args && !ret; args++) {
744///         HashNode hn = thingytab->getnode2(thingytab, *args);
745///         if (!t || (!ALL && t->widget->flags & WIDGET_INT)) ret = 1;
746///         else if (LONG) scanlistwidgets(hn, 1);
747///     }
748///     return ret;
749/// }
750/// ```
751/// `zle -l` — list widget bindings (or check existence per arg).
752pub fn bin_zle_list(_name: &str, args: &[String], ops: &options, _func: i32) -> i32 {
753    // c:393
754    // c:393-413 — `if (!*args) scan all` else look up each in turn.
755    // Returns 0 if all found and listable; 1 if any missing.
756    // c:Src/Zle/zle_thingy.c:396-397 — list mode is
757    //   `OPT_ISSET(ops,'a') ? -1 : OPT_ISSET(ops,'L')`.
758    //   -a (`-la`) → -1: emit raw name, INCLUDE internal widgets.
759    //   -L (`-lL`) → 1:  `zle -N name [fn]` reproducible form.
760    //   default (`-l`) → 0: abbreviated `name (fn)` form, hide internals.
761    let list_mode: i32 = if OPT_ISSET(ops, b'a') {
762        -1
763    } else if OPT_ISSET(ops, b'L') {
764        1
765    } else {
766        0
767    };
768    if args.is_empty() {
769        // c:396-397 — walk thingytab, call scanlistwidgets per node.
770        let _ = scanlistwidgets(list_mode);
771        return 0;
772    }
773    let mut ret = 0;
774    for arg in args {
775        // c:403-411
776        let exists = thingytab().lock().unwrap().contains_key(arg);
777        if !exists {
778            ret = 1;
779            break;
780        }
781    }
782    ret // c:412
783}
784
785/// Direct port of `int bin_zle_refresh(char *name, char **args,
786///                                      Options ops, UNUSED(char func))`
787/// from `Src/Zle/zle_thingy.c:416-454`.
788/// ```c
789/// if (!zleactive) { zwarnnam(name, "no line editor"); return 1; }
790/// // optional statusline/listlist install via -p flag
791/// zrefresh();
792/// return 0;
793/// ```
794///
795/// **Substrate tradeoff:** `zrefresh()` is a free fn in
796/// zle_refresh.rs reading the file-scope ZLE statics. To keep this
797/// bin_zle_refresh path lightweight (and to drop work to the next
798/// zlecore tick when it's available), we set the `ZLE_RESET_NEEDED`
799/// Port of `bin_zle_refresh(UNUSED(char *name), char **args, Options ops, UNUSED(char func))` from `Src/Zle/zle_thingy.c:418`.
800pub fn bin_zle_refresh(_name: &str, args: &[String], ops: &options, _func: i32) -> i32 {
801    // c:418
802    // c:420-421 — `char *s = statusline; int ocl = clearlist;`. Save
803    // pre-call state so the function can restore it on exit.
804    let s_save: Option<String> = STATUSLINE.lock().unwrap().clone(); // c:420
805    let ocl: i32 = CLEARLIST.load(Ordering::Relaxed); // c:421
806
807    if crate::ported::builtins::sched::zleactive.load(Ordering::Relaxed) == 0 {
808        // c:423
809        return 1; // c:424
810    }
811    // c:425 — `statusline = NULL;`
812    *STATUSLINE.lock().unwrap() = None;
813    if !args.is_empty() {
814        // c:426
815        // c:427-428 — `if (**args) statusline = *args;` — empty arg
816        // means "clear statusline", non-empty replaces it.
817        if !args[0].is_empty() {
818            // c:427
819            *STATUSLINE.lock().unwrap() = Some(args[0].clone()); // c:428
820        }
821        if args.len() > 1 {
822            // c:429 — second-and-following args form a list to display.
823            let zmultsav: i32 = crate::ported::zle::compcore::ZMULT.load(Ordering::Relaxed); // c:431
824                                                                                             // c:433-434 — `for (; *args; args++) addlinknode(l, *args);`.
825            let list: Vec<String> = args[1..].to_vec(); // c:434
826            crate::ported::zle::compcore::ZMULT.store(1, Ordering::Relaxed); // c:436
827                                                                             // c:437 — `listlist(l)`. Rust port takes (&[String], cols);
828                                                                             // 0 cols defers width to listlist's internal calc.
829            listlist(&list, 0); // c:437
830            if STATUSLINE.lock().unwrap().is_some() {
831                // c:438
832                LASTLISTLEN.fetch_add(1, Ordering::Relaxed); // c:439
833            }
834            // c:440 — `showinglist = clearlist = 0;`.
835            SHOWINGLIST.store(0, Ordering::Relaxed);
836            CLEARLIST.store(0, Ordering::Relaxed);
837            // c:441 — restore zmult.
838            crate::ported::zle::compcore::ZMULT.store(zmultsav, Ordering::Relaxed);
839        } else if OPT_ISSET(ops, b'c') {
840            // c:442 — single positional + `-c`: queue a clear.
841            CLEARLIST.store(1, Ordering::Relaxed); // c:443
842            LASTLISTLEN.store(0, Ordering::Relaxed); // c:444
843        }
844    } else if OPT_ISSET(ops, b'c') {
845        // c:446 — no positionals + `-c`: clear list immediately.
846        CLEARLIST.store(1, Ordering::Relaxed); // c:447
847        LISTSHOWN.store(1, Ordering::Relaxed); // c:447
848        LASTLISTLEN.store(0, Ordering::Relaxed); // c:448
849    }
850    zrefresh(); // c:450
851                // c:451-452 — `statusline = s; clearlist = ocl;` restore.
852    *STATUSLINE.lock().unwrap() = s_save; // c:451
853    CLEARLIST.store(ocl, Ordering::Relaxed); // c:452
854    0 // c:453
855}
856
857/// Port of `bin_zle_mesg(char *name, char **args, UNUSED(Options ops), UNUSED(char func))` from `Src/Zle/zle_thingy.c:459`.
858/// ```c
859/// static int
860/// bin_zle_mesg(...) {
861///     if (!zleactive) { zwarnnam; return 1; }
862///     showmsg(*args);
863///     if (sfcontext != SFC_WIDGET) zrefresh();
864///     return 0;
865/// }
866/// ```
867/// `zle -M msg` — display a transient message during widget run.
868pub fn bin_zle_mesg(name: &str, args: &[String], _ops: &options, _func: i32) -> i32 {
869    // c:459
870    if crate::ported::builtins::sched::zleactive.load(Ordering::Relaxed) == 0 {
871        crate::ported::utils::zwarnnam(name, "can only be called from widget function");
872        return 1; // c:463
873    }
874    if let Some(arg) = args.first() {
875        crate::ported::zle::zle_utils::showmsg(arg); // c:465
876    }
877    // c:467 — `if (sfcontext != SFC_WIDGET) zrefresh();`. SFC_WIDGET
878    // means the call came from a user widget body and the editor
879    // will redraw soon; outside that path, redraw now so the message
880    // is visible before the next event-loop tick.
881    use crate::ported::zsh_h::SFC_WIDGET;
882    if crate::ported::builtin::SFCONTEXT.load(std::sync::atomic::Ordering::Relaxed) != SFC_WIDGET {
883        crate::ported::zle::zle_refresh::zrefresh(); // c:467
884    }
885    0 // c:468
886}
887
888/// Port of `bin_zle_unget(char *name, char **args, UNUSED(Options ops), UNUSED(char func))` from `Src/Zle/zle_thingy.c:473`.
889/// ```c
890/// static int
891/// bin_zle_unget(char *name, char **args, ...) {
892///     char *b = unmeta(*args), *p = b + strlen(b);
893///     if (!zleactive) { zwarnnam(name, "..."); return 1; }
894///     while (p > b)
895///         ungetbyte((int) *--p);
896///     return 0;
897/// }
898/// ```
899/// `zle -U str` — push string bytes back onto input queue in
900/// reverse so subsequent reads return them in original order.
901/// WARNING: param names don't match C — Rust=(zle, args) vs C=(name, args, ops, func)
902pub fn bin_zle_unget(_name: &str, args: &[String], _ops: &options, _func: i32) -> i32 {
903    // c:473
904    if crate::ported::builtins::sched::zleactive.load(Ordering::Relaxed) == 0 {
905        return 1; // c:479
906    }
907    if let Some(arg) = args.first() {
908        // c:481-482 — push bytes back in reverse.
909        for byte in arg.bytes().rev() {
910            ungetbyte(byte);
911        }
912    }
913    0 // c:483
914}
915
916/// Port of `bin_zle_keymap(char *name, char **args, UNUSED(Options ops), UNUSED(char func))` from `Src/Zle/zle_thingy.c:488`.
917/// ```c
918/// static int
919/// bin_zle_keymap(...) {
920///     if (!zleactive) { zwarnnam(name, "..."); return 1; }
921///     return selectkeymap(*args, 0);
922/// }
923/// ```
924/// `zle -K keymap` — switch the current keymap (only valid from
925/// inside a widget callback).
926/// WARNING: param names don't match C — Rust=(args) vs C=(name, args, ops, func)
927pub fn bin_zle_keymap(name: &str, args: &[String], _ops: &options, _func: i32) -> i32 {
928    // c:488
929    // c:489-491 — `if (!zleactive)` reject from outside ZLE.
930    if crate::ported::builtins::sched::zleactive.load(Ordering::Relaxed) == 0 {
931        crate::ported::utils::zwarnnam(name, "can only be called from widget function");
932        return 1; // c:491
933    }
934    // c:493 — `return selectkeymap(*args, 0)`.
935    if args.is_empty() {
936        return 1;
937    }
938    crate::ported::zle::zle_keymap::selectkeymap(&args[0], 0) // c:493
939}
940
941/// Direct port of `static void scanlistwidgets(HashNode hn, int list)`
942/// from `Src/Zle/zle_thingy.c:505`. Pretty-prints one Thingy: skips
943/// internal (WIDGET_INT) widgets, then either:
944///   - `list == 0`: emits `zle -N name [fn]` (re-definable shell form);
945///   - `list != 0`: emits `name (fn)` when fn != name, else just `name`.
946/// Output goes to stdout (C uses `putc('\n', stdout)`).
947/// WARNING: param names don't match C — Rust=(list) vs C=(hn, list).
948pub fn scanlistwidgets(list: i32) -> i32 {
949    // c:505
950    use std::io::Write;
951    let tab = thingytab().lock().unwrap();
952    // c:509-512 — `if (list < 0) { printf("%s\n", hn->nam); return; }`
953    // The `-la` path emits raw name, includes INTERNAL widgets, and
954    // does not filter or annotate. Bug #379.
955    if list < 0 {
956        let mut names: Vec<String> = tab.keys().cloned().collect();
957        drop(tab);
958        names.sort();
959        let stdout = std::io::stdout();
960        let mut handle = stdout.lock();
961        for n in &names {
962            let _ = writeln!(handle, "{}", n);
963        }
964        return 0;
965    }
966    let mut entries: Vec<(String, String)> = Vec::new();
967    for (name, t) in tab.iter() {
968        let w = match t.widget.as_ref() {
969            Some(w) => w,
970            None => continue,
971        };
972        // c:514-515 — skip internal widgets.
973        if (w.flags & WIDGET_INT) != 0 {
974            continue;
975        }
976        let fn_name = match &w.u {
977            WidgetImpl::UserFunc(s) => s.clone(),
978            // c:516-517 — non-user widgets (`zle -C`/`zle -A` linked)
979            // print with the same `zle -N name [body]` shape; treat
980            // internal-impl variants as bare-name entries.
981            _ => name.clone(),
982        };
983        entries.push((name.clone(), fn_name));
984    }
985    drop(tab);
986    // c:533-541 — emit. Sort by name for stable output (C iterates the
987    // hash table in addnode order; Rust HashMap has no order).
988    entries.sort_by(|a, b| a.0.cmp(&b.0));
989    let stdout = std::io::stdout();
990    let mut handle = stdout.lock();
991    for (name, fn_name) in &entries {
992        if list != 0 {
993            // c:539 — abbreviated `name (fn)` when distinct.
994            if &fn_name != &name {
995                let _ = writeln!(handle, "{} ({})", name, fn_name);
996            } else {
997                let _ = writeln!(handle, "{}", name);
998            }
999        } else {
1000            // c:534 — re-definable `zle -N name [fn]` form.
1001            if &fn_name != &name {
1002                let _ = writeln!(handle, "zle -N {} {}", name, fn_name);
1003            } else {
1004                let _ = writeln!(handle, "zle -N {}", name);
1005            }
1006        }
1007    }
1008    0
1009}
1010
1011/// Port of `bin_zle_del(char *name, char **args, UNUSED(Options ops), UNUSED(char func))` from `Src/Zle/zle_thingy.c:547`.
1012/// ```c
1013/// static int
1014/// bin_zle_del(char *name, char **args, ...) {
1015///     int ret = 0;
1016///     do {
1017///         Thingy t = thingytab->getnode(thingytab, *args);
1018///         if (!t) { zwarnnam(name, "no such widget"); ret = 1; }
1019///         else if (unbindwidget(t, 0)) {
1020///             zwarnnam(name, "widget name `%s' is protected"); ret = 1;
1021///         }
1022///     } while (*++args);
1023///     return ret;
1024/// }
1025/// ```
1026/// `zle -D widget...` — unbind one or more widgets from the
1027/// thingytab. Returns 1 if any widget was missing or protected
1028/// (TH_IMMORTAL), else 0.
1029/// WARNING: param names don't match C — Rust=(args) vs C=(name, args, ops, func)
1030pub fn bin_zle_del(_name: &str, args: &[String], _ops: &options, _func: i32) -> i32 {
1031    // c:548
1032    let mut ret = 0;
1033    for arg in args {
1034        // c:552-561 do-while
1035        let exists = thingytab().lock().unwrap().contains_key(arg);
1036        if !exists {
1037            ret = 1; // c:556
1038        } else if unbindwidget(arg, 0) != 0 {
1039            // c:557
1040            ret = 1; // c:559
1041        }
1042    }
1043    ret // c:562
1044}
1045
1046/// Port of `bin_zle_link(char *name, char **args, UNUSED(Options ops), UNUSED(char func))` from `Src/Zle/zle_thingy.c:567`.
1047/// ```c
1048/// static int
1049/// bin_zle_link(char *name, char **args, ...) {
1050///     Thingy t = thingytab->getnode(thingytab, args[0]);
1051///     if (!t) { zwarnnam(name, "no such widget `%s'", args[0]); return 1; }
1052///     else if (bindwidget(t->widget, rthingy(args[1]))) {
1053///         zwarnnam(name, "widget name `%s' is protected", args[1]);
1054///         return 1;
1055///     }
1056///     return 0;
1057/// }
1058/// ```
1059/// `zle -A old new` — alias `new` to point at the same widget as `old`.
1060/// WARNING: param names don't match C — Rust=(args) vs C=(name, args, ops, func)
1061pub fn bin_zle_link(_name: &str, args: &[String], _ops: &options, _func: i32) -> i32 {
1062    // c:567
1063    // c:567-578 — `t = thingytab.getnode(args[0]); if(!t) ret=1; else
1064    //              if(bindwidget(t->widget, rthingy(args[1]))) ret=1`.
1065    if args.len() < 2 {
1066        return 1;
1067    }
1068    let src = &args[0];
1069    let dst = &args[1];
1070    let widget = {
1071        let tab = thingytab().lock().unwrap();
1072        tab.get(src).and_then(|t| t.widget.clone())
1073    };
1074    let Some(w) = widget else {
1075        return 1; // c:573
1076    };
1077    rthingy(dst); // c:574 rthingy(args[1])
1078    if bindwidget(w, dst) != 0 {
1079        // c:574 bindwidget(...)
1080        return 1; // c:575
1081    }
1082    // PFA-SMR: `zle -A SRC DST` aliases an existing widget under a
1083    // new name. Record as a zle event with DST as the widget name
1084    // and SRC as the implementing-function reference so replay can
1085    // recreate the link.
1086    #[cfg(feature = "recorder")]
1087    if crate::recorder::is_enabled() {
1088        let ctx = crate::recorder::recorder_ctx_global();
1089        crate::recorder::emit_zle(dst, Some(src.as_str()), ctx);
1090    }
1091    0 // c:578
1092}
1093
1094/// Port of `bin_zle_new(char *name, char **args, UNUSED(Options ops), UNUSED(char func))` from `Src/Zle/zle_thingy.c:583`.
1095/// ```c
1096/// static int
1097/// bin_zle_new(char *name, char **args, ...) {
1098///     widget w = zalloc(sizeof(*w));
1099///     w->flags = 0;
1100///     w->first = NULL;
1101///     w->u.fnnam = ztrdup(args[1] ? args[1] : args[0]);
1102///     if (!bindwidget(w, rthingy(args[0]))) return 0;
1103///     freewidget(w);
1104///     zwarnnam(name, "widget name `%s' is protected", args[0]);
1105///     return 1;
1106/// }
1107/// ```
1108/// `zle -N name [func]` — bind a user-defined widget. `func`
1109/// defaults to `name` when omitted.
1110/// WARNING: param names don't match C — Rust=(args) vs C=(name, args, ops, func)
1111pub fn bin_zle_new(_name: &str, args: &[String], _ops: &options, _func: i32) -> i32 {
1112    // c:584
1113    // c:584-595 — `widget w = zalloc; w->flags=0; w->u.fnnam = ztrdup(args[1]?args[1]:args[0]);
1114    //              if(!bindwidget(w, rthingy(args[0]))) return 0;
1115    //              freewidget(w); zwarnnam(...); return 1;`.
1116    if args.is_empty() {
1117        return 1;
1118    }
1119    // c:590 — fn name is args[1] if present, else args[0].
1120    let fname = if args.len() >= 2 {
1121        args[1].clone()
1122    } else {
1123        args[0].clone()
1124    };
1125    let w = Arc::new(widget {
1126        flags: 0i32, // c:588
1127        first: None,
1128        u: WidgetImpl::UserFunc(fname), // c:590 fnnam
1129    });
1130    rthingy(&args[0]); // c:591 rthingy(args[0])
1131    if bindwidget(w.clone(), &args[0]) == 0 {
1132        // c:591 bindwidget(...)
1133        // PFA-SMR: record widget registration. `name` is the widget
1134        // identifier (args[0]); `func` is the implementing shell
1135        // function (args[1] or args[0] when omitted). One event per
1136        // successful `zle -N` invocation.
1137        #[cfg(feature = "recorder")]
1138        if crate::recorder::is_enabled() {
1139            let ctx = crate::recorder::recorder_ctx_global();
1140            let widget_name = &args[0];
1141            let fn_arg = if args.len() >= 2 {
1142                Some(args[1].as_str())
1143            } else {
1144                None
1145            };
1146            crate::recorder::emit_zle(widget_name, fn_arg, ctx);
1147        }
1148        return 0; // c:592
1149    }
1150    // c:593-594 — bindwidget failed (TH_IMMORTAL) → free + warn.
1151    freewidget(w);
1152    1 // c:595
1153}
1154
1155/// Port of `bin_zle_complete(char *name, char **args, UNUSED(Options ops), UNUSED(char func))` from `Src/Zle/zle_thingy.c:599`.
1156/// ```c
1157/// static int
1158/// bin_zle_complete(...) {
1159///     ...
1160///     t = rthingy((args[1][0] == '.') ? args[1] : dyncat(".", args[1]));
1161///     cw = t->widget; unrefthingy(t);
1162///     if (!cw || !(cw->flags & ZLE_ISCOMP)) { zwarnnam; return 1; }
1163///     w = zalloc(sizeof(*w));
1164///     w->flags = WIDGET_NCOMP|ZLE_MENUCMP|ZLE_KEEPSUFFIX;
1165///     w->u.comp.fn = cw->u.fn;
1166///     w->u.comp.wid = ztrdup(args[1]);
1167///     w->u.comp.func = ztrdup(args[2]);
1168///     if (bindwidget(w, rthingy(args[0]))) { freewidget(w); return 1; }
1169///     ...
1170/// }
1171/// ```
1172/// `zle -C name comp-widget func` — register a completion widget.
1173/// WARNING: param names don't match C — Rust=(args) vs C=(name, args, ops, func)
1174pub fn bin_zle_complete(_name: &str, args: &[String], _ops: &options, _func: i32) -> i32 {
1175    // c:600
1176    // c:600-629 — Load zsh/complete; resolve `args[1]` (or `.args[1]`)
1177    // to a Thingy; verify it's ZLE_ISCOMP; alloc a widget with
1178    // WIDGET_NCOMP|MENUCMP|KEEPSUFFIX flags and bind to args[0].
1179    if args.len() < 3 {
1180        return 1;
1181    }
1182    // c:609-611 — `t = rthingy(args[1] starts with '.' ? args[1] : ".args[1]")`.
1183    let lookup = if args[1].starts_with('.') {
1184        args[1].clone()
1185    } else {
1186        format!(".{}", args[1])
1187    };
1188    let comp_widget = {
1189        let tab = thingytab().lock().unwrap();
1190        tab.get(&lookup).and_then(|t| t.widget.clone())
1191    };
1192    let Some(cw) = comp_widget else {
1193        return 1; // c:613-614
1194    };
1195    // c:612 — `if (!cw || !(cw->flags & ZLE_ISCOMP)) return 1`.
1196    if (cw.flags & ZLE_ISCOMP) == 0 {
1197        return 1;
1198    }
1199    // c:619 — `w->u.comp.fn = cw->u.fn`. Extract the base widget's
1200    // internal fn pointer; bail if the base isn't WIDGET_INT (the
1201    // C check `cw->flags & ZLE_ISCOMP` guarantees this in practice
1202    // because every ZLE_ISCOMP widget in iwidgets.list is internal).
1203    let base_fn = match &cw.u {
1204        WidgetImpl::Internal(f) => *f,
1205        _ => return 1,
1206    };
1207    // c:616-621 — alloc new completion widget:
1208    //   w->flags = WIDGET_NCOMP | ZLE_MENUCMP | ZLE_KEEPSUFFIX;
1209    //   w->u.comp.fn   = cw->u.fn;
1210    //   w->u.comp.wid  = ztrdup(args[1]);
1211    //   w->u.comp.func = ztrdup(args[2]);
1212    let w = Arc::new(widget {
1213        flags: WIDGET_NCOMP | ZLE_MENUCMP | ZLE_KEEPSUFFIX,
1214        first: None,
1215        u: WidgetImpl::Comp {
1216            fn_: base_fn,          // c:619
1217            wid: args[1].clone(),  // c:620
1218            func: args[2].clone(), // c:621
1219        },
1220    });
1221    rthingy(&args[0]);
1222    if bindwidget(w.clone(), &args[0]) != 0 {
1223        // c:622
1224        freewidget(w);
1225        return 1; // c:625
1226    }
1227    0 // c:629
1228}
1229
1230/// Port of `zle_usable()` from `Src/Zle/zle_thingy.c:634`.
1231/// ```c
1232/// static int
1233/// zle_usable(void)
1234/// {
1235///     return zleactive && !incompctlfunc && !incompfunc;
1236/// }
1237/// ```
1238/// True iff a ZLE session is currently active and we're not
1239/// inside a compctl-fn or comp-fn call (zle widgets can't run
1240/// from inside completion functions).
1241pub fn zle_usable() -> i32 {
1242    // c:634
1243    let active = crate::ported::builtins::sched::zleactive.load(Ordering::Relaxed) != 0;
1244    let incompctlfunc = crate::ported::zle::compctl::INCOMPCTLFUNC // c:636
1245        .with(|c| c.get());
1246    let incompfunc = crate::ported::zle::complete::INCOMPFUNC.load(Ordering::Relaxed) != 0;
1247    if active && !incompctlfunc && !incompfunc {
1248        1
1249    } else {
1250        0
1251    }
1252}
1253
1254/// Port of `bin_zle_flags(char *name, char **args, UNUSED(Options ops), UNUSED(char func))` from `Src/Zle/zle_thingy.c:650`.
1255/// ```c
1256/// static int
1257/// bin_zle_flags(...) {
1258///     if (!zle_usable()) { zwarnnam(...); return 1; }
1259///     if (bindk) { widget w = bindk->widget;
1260///         for (flag = args; *flag; flag++) {
1261///             if      (!strcmp(*flag, "yank"))       w->flags |= ZLE_YANKAFTER;
1262///             else if (!strcmp(*flag, "yankbefore")) w->flags |= ZLE_YANKBEFORE;
1263///             else if (!strcmp(*flag, "kill"))       w->flags |= ZLE_KILL;
1264///             ...
1265///         }
1266///     }
1267///     return ret;
1268/// }
1269/// ```
1270/// `zle -f flag...` — set widget-execution flags (yank/yankbefore/
1271/// kill) on the currently-running widget.
1272/// Rust idiom replacement: `Arc<widget>` is immutable in zshrs, so
1273/// the C `w->flags |= ZLE_*` mutation lives on the widget-execution
1274/// path itself; this entry validates args + returns success.
1275/// WARNING: param names don't match C — Rust=(args) vs C=(name, args, ops, func)
1276pub fn bin_zle_flags(_name: &str, args: &[String], _ops: &options, _func: i32) -> i32 {
1277    // c:651
1278    // c:653-654 — locals.
1279    let mut ret: i32 = 0; // c:653
1280                          // c:656-659 — !zle_usable early-return.
1281    if zle_usable() == 0 {
1282        zwarnnam("zle", "can only set flags from a widget"); // c:657
1283        return 1; // c:658
1284    }
1285    // c:661-663 — `if (bindk) { Widget w = bindk->widget; if (w) { ... } }`.
1286    // BINDK holds the Thingy bound by the active key. When unset (no
1287    // active key sequence), the c:661 guard skips the whole loop.
1288    let bindk_present = BINDK.lock().map(|b| b.is_some()).unwrap_or(false);
1289    if bindk_present {
1290        // c:661
1291        // c:664-693 — `for (flag = args; *flag; flag++) { ... }`.
1292        for flag in args {
1293            // c:664
1294            match flag.as_str() {
1295                "yank" => {
1296                    // c:665 — `w->flags |= ZLE_YANKAFTER;`. !!! WARNING:
1297                    // PARTIAL PORT — current Thingy.widget shape is
1298                    // `Option<Arc<widget>>` (immutable through Arc),
1299                    // so flag bits are validated but the mutation
1300                    // back into widget.flags is dropped. Faithful
1301                    // port needs Arc<Mutex<widget>> across the tree.
1302                    // For now this matches "validation only".
1303                }
1304                "yankbefore" => {
1305                    // c:667 — `w->flags |= ZLE_YANKBEFORE;`. Same gap.
1306                }
1307                "kill" => {
1308                    // c:669 — `w->flags |= ZLE_KILL;`. Same gap.
1309                }
1310                // c:672-680 — menucmp/linemove/keepsuffix branches are
1311                // commented out in C ("These won't do anything yet,
1312                // because of how execzlefunc handles user widgets").
1313                // We mirror that — recognized as valid flag-names but
1314                // no-op.
1315                "menucmp" | "linemove" | "keepsuffix" => {
1316                    // c:674/676/678
1317                }
1318                "vichange" => {
1319                    // c:682 — `if (invicmdmode()) startvichange(-1); ...`
1320                    if invicmdmode(&crate::ported::zle::zle_keymap::curkeymapname()) {
1321                        // c:683
1322                        startvichange(-1); // c:684
1323                                           // c:685-688 — if a numeric arg is active and a
1324                                           // PM_SPECIAL `NUMERIC` param exists, clear its
1325                                           // PM_UNSET bit so the value becomes visible to
1326                                           // the widget.
1327                        let zm_flags = ZMOD.lock().unwrap().flags;
1328                        if (zm_flags & (MOD_MULT | MOD_TMULT)) != 0 {
1329                            // c:685 — numeric arg present.
1330                            if let Ok(mut tab) = crate::ported::params::paramtab().write() {
1331                                if let Some(pm) = tab.get_mut("NUMERIC") {
1332                                    if (pm.node.flags as u32 & crate::ported::zsh_h::PM_SPECIAL)
1333                                        != 0
1334                                    {
1335                                        // c:687 — clear PM_UNSET so widget sees value.
1336                                        pm.node.flags &= !(crate::ported::zsh_h::PM_UNSET as i32);
1337                                    }
1338                                }
1339                            }
1340                        }
1341                    }
1342                }
1343                _ => {
1344                    // c:691-693 — unknown flag name.
1345                    zwarnnam("zle", &format!("invalid flag `{}' given to zle -f", flag)); // c:692
1346                    ret = 1; // c:693
1347                }
1348            }
1349        }
1350    }
1351    ret // c:697
1352}
1353
1354/// Port of `bin_zle_call(char *name, char **args, UNUSED(Options ops), UNUSED(char func))` from `Src/Zle/zle_thingy.c:702`.
1355/// ```c
1356/// static int
1357/// bin_zle_call(...) {
1358///     ...
1359///     char *wname = *args++;
1360///     if (!wname) return !zle_usable();
1361///     if (!zle_usable()) { zwarnnam(name, "..."); return 1; }
1362///     ...
1363/// }
1364/// ```
1365/// Bare-args invocation of `zle widget args...` from inside another
1366/// widget. The full path (flag parse + execzlefunc) needs ZLE
1367/// session substrate; this port covers the empty-args probe and
1368/// Faithful port of `bin_zle_call(char *name, char **args, Options ops,
1369/// UNUSED(char func))` from Src/Zle/zle_thingy.c:703.
1370/// WARNING: param names don't match C — Rust=(args) vs C=(name, args, ops, func)
1371pub fn bin_zle_call(_name: &str, args: &[String], _ops: &options, _func: i32) -> i32 {
1372    // c:703
1373    // c:706-709 — locals.
1374    let modsave: modifier = (*ZMOD.lock().unwrap()).clone(); // c:706 struct modifier modsave = zmod
1375    let mut saveflag = 0i32; // c:707
1376    let mut setbindk = 0i32; // c:707
1377    let mut setlbindk = 0i32; // c:707
1378                              // c:707 `remetafy` collapses in Rust (UTF-8 storage).
1379    let mut keymap_restore: Option<String> = None; // c:708
1380                                                   // c:708 — `char *wname = *args++;`. Consume first arg as widget name.
1381    let mut argv: Vec<String> = args.to_vec();
1382    let wname = if argv.is_empty() {
1383        None
1384    } else {
1385        Some(argv.remove(0))
1386    };
1387
1388    // c:710-711 — `if (!wname) return !zle_usable();`
1389    if wname.is_none() {
1390        // c:710
1391        return if zle_usable() != 0 { 0 } else { 1 }; // c:711
1392    }
1393    let wname = wname.unwrap();
1394
1395    // c:713-716 — `if (!zle_usable()) { zwarnnam; return 1; }`.
1396    if zle_usable() == 0 {
1397        // c:713
1398        zwarnnam("zle", "widgets can only be called when ZLE is active"); // c:714
1399        return 1; // c:715
1400    }
1401
1402    // c:722-726 — `if (zlemetaline) { unmetafy_line(); remetafy = 1; }
1403    //               else remetafy = 0;`. Rust stores ZLE as UTF-8;
1404    // the meta-line bookkeeping is a no-op.
1405
1406    // c:728-798 — flag-parsing loop. C iterates while `**args == '-'`
1407    // and consumes the option characters one at a time. Supports
1408    //   -f nolast      → setlbindk = 1
1409    //   -n NUM         → zmod.mult = NUM, MOD_MULT |= 1
1410    //   -N             → zmod.mult = 1, MOD_MULT &= ~1 (reset count)
1411    //   -K keymap      → selectkeymap(keymap, 0)
1412    //   -w             → setbindk = 1
1413    // The C trick `skip_this_arg = "x"` substitutes a dummy when an
1414    // attached-value (like `-nNUM` form) consumed the operand inline.
1415    while !argv.is_empty() && argv[0].starts_with('-') {
1416        // c:728
1417        let cur = argv[0].clone();
1418        // c:732-734 — `-` or `--` terminates flag parsing.
1419        if cur.len() == 1 || (cur.len() == 2 && cur.as_bytes()[1] == b'-') {
1420            // c:732
1421            argv.remove(0); // c:733 args++
1422            break; // c:734
1423        }
1424        let mut byte_idx = 1usize; // skip leading '-'
1425        let mut consumed_next = false;
1426        let cur_bytes = cur.as_bytes();
1427        while byte_idx < cur_bytes.len() {
1428            // c:736 `while (*++(*args))`
1429            let c = cur_bytes[byte_idx];
1430            byte_idx += 1;
1431            match c {
1432                b'f' => {
1433                    // c:738-750 — `-f nolast`.
1434                    // c:739 — `flag = args[0][1] ? args[0]+1 : args[1];`
1435                    let flag: Option<String> = if byte_idx < cur_bytes.len() {
1436                        // c:739 attached form `-fXXX`
1437                        Some(
1438                            std::str::from_utf8(&cur_bytes[byte_idx..])
1439                                .unwrap_or("")
1440                                .to_string(),
1441                        )
1442                    } else if argv.len() > 1 {
1443                        // c:739 separate form `-f XXX`
1444                        Some(argv[1].clone())
1445                    } else {
1446                        None
1447                    };
1448                    if flag.as_deref() != Some("nolast") {
1449                        // c:740
1450                        zwarnnam("zle", "'nolast' expected after -f"); // c:741
1451                        return 1; // c:744
1452                    }
1453                    // c:746-747 — consume separate-form operand.
1454                    if byte_idx >= cur_bytes.len() {
1455                        argv.remove(1); // c:746-747
1456                        consumed_next = true;
1457                    }
1458                    setlbindk = 1; // c:749
1459                    byte_idx = cur_bytes.len(); // exit inner loop
1460                }
1461                b'n' => {
1462                    // c:751-764 — `-n NUM`. Set zmod.mult.
1463                    let num: Option<String> = if byte_idx < cur_bytes.len() {
1464                        Some(
1465                            std::str::from_utf8(&cur_bytes[byte_idx..])
1466                                .unwrap_or("")
1467                                .to_string(),
1468                        )
1469                    } else if argv.len() > 1 {
1470                        Some(argv[1].clone())
1471                    } else {
1472                        None
1473                    };
1474                    if num.is_none() {
1475                        // c:753
1476                        zwarnnam("zle", &format!("number expected after -{}", c as char)); // c:754
1477                        return 1; // c:757
1478                    }
1479                    if byte_idx >= cur_bytes.len() {
1480                        argv.remove(1);
1481                        consumed_next = true;
1482                    }
1483                    saveflag = 1; // c:761
1484                    let n: i32 = num.unwrap().parse().unwrap_or(0); // c:762 atoi
1485                    let mut zm = ZMOD.lock().unwrap();
1486                    zm.mult = n; // c:762
1487                    zm.flags |= MOD_MULT; // c:763
1488                    byte_idx = cur_bytes.len();
1489                }
1490                b'N' => {
1491                    // c:765-768 — `-N` reset count modifier.
1492                    saveflag = 1; // c:766
1493                    let mut zm = ZMOD.lock().unwrap();
1494                    zm.mult = 1; // c:767
1495                    zm.flags &= !MOD_MULT; // c:768
1496                }
1497                b'K' => {
1498                    // c:770-786 — `-K keymap`.
1499                    let keymap_tmp: Option<String> = if byte_idx < cur_bytes.len() {
1500                        Some(
1501                            std::str::from_utf8(&cur_bytes[byte_idx..])
1502                                .unwrap_or("")
1503                                .to_string(),
1504                        )
1505                    } else if argv.len() > 1 {
1506                        Some(argv[1].clone())
1507                    } else {
1508                        None
1509                    };
1510                    if keymap_tmp.is_none() {
1511                        // c:772
1512                        zwarnnam("zle", &format!("keymap expected after -{}", c as char)); // c:773
1513                        return 1; // c:776
1514                    }
1515                    if byte_idx >= cur_bytes.len() {
1516                        argv.remove(1);
1517                        consumed_next = true;
1518                    }
1519                    keymap_restore = Some(crate::ported::zle::zle_keymap::curkeymapname().clone()); // c:780
1520                    if crate::ported::zle::zle_keymap::selectkeymap(&keymap_tmp.unwrap(), 0) != 0 {
1521                        // c:781
1522                        return 1; // c:784
1523                    }
1524                    byte_idx = cur_bytes.len();
1525                }
1526                b'w' => {
1527                    // c:787-789 — `-w`.
1528                    setbindk = 1; // c:788
1529                }
1530                _ => {
1531                    // c:790-794 — unknown option.
1532                    zwarnnam("zle", &format!("unknown option: {}", cur)); // c:791
1533                    return 1; // c:794
1534                }
1535            }
1536        }
1537        argv.remove(0); // c:797 — args++.
1538        let _ = consumed_next; // already adjusted via argv.remove(1) above
1539    }
1540
1541    // c:800-807 — `t = rthingy(wname); ... ret = execzlefunc(t, args,
1542    //   setbindk, setlbindk); unrefthingy(t);`. Rust execzlefunc takes
1543    // (name, args); setbindk/setlbindk plumbing pending the wider sig.
1544    rthingy(&wname); // c:800
1545                     // c:806 — `ret = execzlefunc(t, args, setbindk, setlbindk)`.
1546                     // Now that execzlefunc takes the 4-arg C sig, thread the flags
1547                     // collected from `-w` (setbindk) and `-f nolast` (setlbindk).
1548    let ret = execzlefunc(&wname, &argv, setbindk, setlbindk); // c:806
1549    unrefthingy(&wname); // c:807
1550
1551    // c:808-809 — `if (saveflag) zmod = modsave;`.
1552    if saveflag != 0 {
1553        *ZMOD.lock().unwrap() = modsave; // c:809
1554    }
1555    // c:810-811 — `if (keymap_restore) selectkeymap(keymap_restore, 0);`.
1556    if let Some(k) = keymap_restore {
1557        // c:810
1558        crate::ported::zle::zle_keymap::selectkeymap(&k, 0); // c:811
1559    }
1560    // c:812-813 — remetafy collapses in Rust.
1561    ret // c:814
1562}
1563
1564/// Direct port of `int bin_zle_invalidate(char *name, char **args,
1565///                                         Options ops, UNUSED(char func))`
1566/// from `Src/Zle/zle_thingy.c:828-852`.
1567/// ```c
1568/// if (zleactive) {
1569///     int wastrashed = trashedzle;
1570///     trashzle();
1571///     if (!wastrashed) { settyinfo(&shttyinfo); fetchttyinfo = 1; }
1572///     return 0;
1573/// }
1574/// return 1;
1575/// ```
1576///
1577/// **Substrate tradeoff:** `trashzle` is a free fn at
1578/// zle_main.rs:1111 that reads the file-scope ZLE statics; the
1579/// `wastrashed`/`shttyinfo`/`fetchttyinfo` path is part of the
1580/// active editor's tty state machine. From compcore-call-context
1581/// we flag `ZLE_RESET_NEEDED` so the next zlecore tick observes
1582/// the invalidation and re-enters `trashzle`.
1583/// Port of `bin_zle_invalidate(UNUSED(char *name), UNUSED(char **args), UNUSED(Options ops), UNUSED(char func))` from `Src/Zle/zle_thingy.c:830`.
1584/// WARNING: param names don't match C — Rust=() vs C=(name, args, ops, func)
1585pub fn bin_zle_invalidate(_name: &str, _args: &[String], _ops: &options, _func: i32) -> i32 {
1586    // c:830
1587    if crate::ported::builtins::sched::zleactive.load(Ordering::Relaxed) != 0 {
1588        // c:837 — `trashzle()` via the reset-flag bridge.
1589        ZLE_RESET_NEEDED.store(1, Ordering::SeqCst);
1590        0 // c:850
1591    } else {
1592        1 // c:852
1593    }
1594}
1595
1596/// Port of `bin_zle_fd(char *name, char **args, Options ops, UNUSED(char func))` from `Src/Zle/zle_thingy.c:857`.
1597/// `zle -F fd handler` — register an fd watcher invoked when the
1598/// fd becomes readable while the editor is idle.
1599/// Direct port of `int bin_zle_fd(char *name, char **args, Options ops,
1600///                                 UNUSED(char func))` from
1601/// `Src/Zle/zle_thingy.c:857`. Manages the per-Zle `watch_fds`
1602/// table: `-d` removes, single-arg lists, two-args register a
1603/// handler.
1604///
1605/// Mutates the global `WATCH_FDS` (`Src/Zle/zle_main.c:204`)
1606/// directly so the poll loop in `zle_main::raw_getbyte` sees the
1607/// new registration on the next iteration.
1608/// Rust idiom replacement: WATCH_FDS `Mutex<HashMap>` covers the C
1609/// `watch_fds` LinkList add/remove; the poll loop in `raw_getbyte`
1610/// reads the map directly, no callback-table indirection needed.
1611/// WARNING: param names don't match C — Rust=(args) vs C=(name, args, ops, func)
1612pub fn bin_zle_fd(_name: &str, args: &[String], ops: &options, _func: i32) -> i32 {
1613    // c:857
1614    // c:859 — locals.
1615    let mut fd: i32 = 0; // c:859
1616    let mut found: bool = false; // c:859
1617
1618    // c:862-869 — parse fd if given. C uses zstrtol(*args, &endptr, 10)
1619    // and rejects when trailing garbage exists (*endptr != '\0') OR
1620    // fd < 0.
1621    if !args.is_empty() {
1622        // c:862
1623        match args[0].parse::<i32>() {
1624            // c:863 zstrtol
1625            Ok(n) if n >= 0 => fd = n,
1626            _ => {
1627                // c:865 — `*endptr || fd < 0`
1628                zwarnnam(
1629                    "zle",
1630                    &format!("Bad file descriptor number for -F: {}", args[0]),
1631                ); // c:866
1632                return 1; // c:867
1633            }
1634        }
1635    }
1636
1637    // c:871-887 — `-L` listing branch, OR no-args list-all.
1638    if OPT_ISSET(ops, b'L') || args.is_empty() {
1639        // c:871
1640        if !args.is_empty() && args.len() > 1 {
1641            // c:873
1642            zwarnnam("zle", "too many arguments for -FL"); // c:874
1643            return 1; // c:875
1644        }
1645        if let Ok(tab) = WATCH_FDS.lock() {
1646            // c:877 — `for (i = 0; i < nwatch; i++)`
1647            for w in tab.iter() {
1648                if !args.is_empty() && w.fd != fd {
1649                    // c:879
1650                    continue; // c:880
1651                }
1652                found = true; // c:881
1653                              // c:882 — `printf("%s -F %s%d %s\n", name, widget ? "-w " : "", fd, func);`
1654                let w_flag = if w.widget != 0 { "-w " } else { "" };
1655                println!("zle -F {}{} {}", w_flag, w.fd, w.func);
1656            }
1657        }
1658        // c:885-886 — return 1 if fd was given and not found.
1659        return if !args.is_empty() && !found { 1 } else { 0 }; // c:886
1660    }
1661
1662    if args.len() > 1 {
1663        // c:889 — adding/replacing a handler.
1664        let funcnam = args[1].clone(); // c:891 ztrdup
1665        if let Ok(mut tab) = WATCH_FDS.lock() {
1666            // c:892 — `if (nwatch) for (...) if (fd matches) replace`.
1667            for w in tab.iter_mut() {
1668                // c:893
1669                if w.fd == fd {
1670                    // c:895
1671                    w.func = funcnam.clone(); // c:897
1672                    w.widget = if OPT_ISSET(ops, b'w') { 1 } else { 0 }; // c:898
1673                    found = true; // c:899
1674                    break; // c:900
1675                }
1676            }
1677            if !found {
1678                // c:904 — append new entry.
1679                tab.push(watch_fd {
1680                    // c:910-913
1681                    fd,                                               // c:911
1682                    func: funcnam,                                    // c:912
1683                    widget: if OPT_ISSET(ops, b'w') { 1 } else { 0 }, // c:913
1684                });
1685                // c:914 — `nwatch = newnwatch;` (Vec.len() tracks
1686                // nwatch implicitly).
1687            }
1688        }
1689    } else {
1690        // c:916 — deleting a handler (one positional, no value).
1691        if let Ok(mut tab) = WATCH_FDS.lock() {
1692            let len_before = tab.len();
1693            tab.retain(|w| w.fd != fd); // c:920-940 memcpy-shrink
1694            found = tab.len() < len_before; // c:940
1695        }
1696        if !found {
1697            // c:944 — `if (!found) zwarnnam(name, "No handler installed for fd %d", fd);`
1698            zwarnnam("zle", &format!("No handler installed for fd {}", fd)); // c:945
1699            return 1; // c:946
1700        }
1701    }
1702
1703    0 // c:952
1704}
1705
1706/// Direct port of `int bin_zle_transform(char *name, char **args,
1707///                                       Options ops, UNUSED(char func))`
1708/// from `Src/Zle/zle_thingy.c:955`.
1709/// ```c
1710/// // -L: list installed transformations
1711/// // 0 args: clear all
1712/// // 1 arg: clear specific (tcfn name)
1713/// // 2 args: install transformation tcfn -> fn
1714/// ```
1715///
1716/// Registers the transformation via `ShellExecutor.hook_functions`
1717/// under the synthetic hook name `zle-transform-<tcfn>` so the
1718/// redisplay path can find it. Args validate first.
1719pub fn bin_zle_transform(_name: &str, args: &[String], ops: &options, _func: i32) -> i32 {
1720    // c:955
1721    // c:957-963 — badargs convention:
1722    //   -1: too few arguments
1723    //    0: just right
1724    //    1: too many arguments
1725    //    2: first argument not recognised
1726    let mut badargs: i32 = 0; // c:963
1727
1728    if OPT_ISSET(ops, b'L') {
1729        // c:965 — `-L`: list the current tc handler.
1730        if !args.is_empty() {
1731            // c:966
1732            if args.len() > 1 {
1733                // c:967
1734                badargs = 1; // c:968
1735            } else if args[0] != "tc" {
1736                // c:969
1737                badargs = 2; // c:970
1738            }
1739        }
1740        if badargs == 0 {
1741            // c:973
1742            let cur = TCOUT_FUNC_NAME.lock().ok().and_then(|n| n.clone());
1743            if let Some(fname) = cur {
1744                // c:973
1745                print!("zle -T tc "); // c:974
1746                print!("{}", crate::ported::utils::quotedzputs(&fname)); // c:975
1747                println!(); // c:976
1748            }
1749        }
1750    } else if OPT_ISSET(ops, b'r') {
1751        // c:978 — `-r`: reset the tc handler.
1752        if args.is_empty() {
1753            // c:979
1754            badargs = -1; // c:980
1755        } else if args.len() > 1 {
1756            // c:981
1757            badargs = 1; // c:982
1758        } else if args[0] == "tc" {
1759            // c:983 — `if (tcout_func_name) { zsfree; tcout_func_name = NULL; }`.
1760            // The C `if (tcout_func_name)` guard avoids a double-free
1761            // before the value is reset.
1762            if let Ok(mut name) = TCOUT_FUNC_NAME.lock() {
1763                if name.is_some() {
1764                    // c:983
1765                    *name = None; // c:985 zsfree + NULL
1766                }
1767            }
1768        } else {
1769            // C falls through silently when args[0] != "tc"; the only
1770            // `tc` transform exists, so anything else is a no-op.
1771            badargs = 2;
1772        }
1773    } else {
1774        // c:987 — default `zle -T name fname` form.
1775        if args.is_empty() || args.len() < 2 {
1776            // c:988
1777            badargs = -1; // c:989 — we've already checked args <= 2.
1778        } else {
1779            // c:991
1780            if args[0] == "tc" {
1781                // c:992
1782                if let Ok(mut name) = TCOUT_FUNC_NAME.lock() {
1783                    // c:993 — `if (tcout_func_name) zsfree(tcout_func_name);`.
1784                    *name = Some(args[1].clone()); // c:996 ztrdup
1785                }
1786            } else {
1787                badargs = 2; // c:998
1788            }
1789        }
1790    }
1791
1792    if badargs != 0 {
1793        // c:1003
1794        if badargs == 2 {
1795            // c:1004
1796            zwarnnam(
1797                "zle",
1798                &format!("-T: no such transformation '{}'", args[0]), // c:1005
1799            );
1800        } else {
1801            // c:1006
1802            let way = if badargs > 0 { "many" } else { "few" }; // c:1007
1803            zwarnnam("zle", &format!("too {} arguments for option -T", way));
1804            // c:1008
1805        }
1806        return 1; // c:1010
1807    }
1808
1809    0 // c:1013
1810}
1811
1812/// Port of `init_thingies()` from `Src/Zle/zle_thingy.c:1022`.
1813/// Boot-time thingytab population from the built-in widget table.
1814/// Walks the static `thingies[]` array in zle_thingy.c and inserts
1815/// each into the table marked TH_IMMORTAL.
1816pub fn init_thingies() -> i32 {
1817    // c:1022
1818    // c:1026 — `createthingytab();`.
1819    createthingytab();
1820    // c:1027-1028 — `for (t = thingies; t->nam; t++)
1821    //                  thingytab->addnode(thingytab, t->nam, t);`.
1822    // The C `thingies[]` array at zle_bindings.c:72 is generated from
1823    // `Src/Zle/thingies.list`, which itself is generated from
1824    // `iwidgets.list` (`Makefile:1057-1075`). Each iwidgets.list line
1825    // produces TWO thingy entries: a bare `name` (mortal, can be
1826    // rebound by `zle -A`/`zle -C`) and a `.name` (TH_IMMORTAL, the
1827    // internal anchor). Both point at the same shared widget.
1828    let names = crate::ported::zle::zle_bindings::IWIDGET_NAMES;
1829    let mut tab = thingytab().lock().unwrap();
1830    for nam in names {
1831        // Build the shared widget for this name.
1832        // c:widgets.list — each iwidgets.list line emits a
1833        // `W(ZLE_FLAGS, t_firstname, functionname)` widget entry.
1834        // The Rust analog: WIDGET_INT + iwidget_flags(name) +
1835        // u = Internal(iwidget_lookup(name)).
1836        let fn_ptr = crate::ported::zle::zle_bindings::iwidget_lookup(nam);
1837        // c:widgets.list — look up the per-widget ZLE_FLAGS column
1838        // for this name.
1839        let extra_flags = crate::ported::zle::zle_bindings::IWIDGET_FLAGS
1840            .iter()
1841            .find(|(n, _)| *n == *nam)
1842            .map(|(_, f)| *f)
1843            .unwrap_or(0);
1844        // c:zle_bindings.c:72 — every `thingies[]` entry generated from
1845        // iwidgets.list binds a real `widgets[]` struct, so every
1846        // thingy is enabled and appears in `$widgets` as "builtin".
1847        // Names whose C body has no Rust port yet still get an
1848        // Internal widget here (undefined-key body as placeholder)
1849        // so enumeration matches; the real body is a later port.
1850        let f = fn_ptr.unwrap_or(|_| crate::ported::zle::zle_misc::undefinedkey());
1851        let w = Some(Arc::new(widget {
1852            flags: WIDGET_INT | extra_flags,
1853            first: None,
1854            u: WidgetImpl::Internal(f),
1855        }));
1856
1857        // Bare `name` thingy — mortal.
1858        if !tab.contains_key(*nam) {
1859            let mut t = makethingynode();
1860            t.nam = nam.to_string(); // c:163 ztrdup(nam)
1861            t.widget = w.clone(); // c:229
1862            tab.insert(nam.to_string(), t);
1863        }
1864        // Dotted `.name` thingy — TH_IMMORTAL, the internal anchor
1865        // used by `zle -C BASE` lookups (`bin_zle_complete` c:610
1866        // prepends `.` when looking up the base widget).
1867        let dotted = format!(".{}", nam);
1868        if !tab.contains_key(&dotted) {
1869            let mut t = makethingynode();
1870            t.nam = dotted.clone();
1871            t.flags |= TH_IMMORTAL; // c:1027 — `.NAME` entries are immortal
1872            t.widget = w;
1873            tab.insert(dotted, t);
1874        }
1875    }
1876    0
1877}
1878/// `Thingy` — see fields for layout.
1879#[derive(Debug, Clone)]
1880pub struct Thingy {
1881    // c:224
1882    pub nam: String,                 // c:226 char *nam
1883    pub flags: i32,                  // c:227 int flags
1884    pub rc: i32,                     // c:228 int rc
1885    pub widget: Option<Arc<widget>>, // c:229 widget widget
1886}
1887
1888// `pub mod names` removed — Rust-fabricated namespace wrapping
1889// thingy-name string literals. C source uses bare `"accept-line"`/
1890// `"self-insert"`/etc. directly at `zle_thingy.c` registration
1891// sites; no namespace, no helper consts. The mod had no callers.
1892
1893// =====================================================================
1894// thingytab — `Src/Zle/zle_thingy.c:52`.
1895// =====================================================================
1896//
1897// C: `mod_export HashTable thingytab;`. One global hash keyed by
1898// thingy name; each entry is a `Thingy` struct (rc + flags + widget
1899// + samew circular-list pointer). Allocated by `createthingytab()`
1900// at zle init and torn down by `cleanup_zle()`.
1901//
1902// Rust: `Mutex<HashMap<String, Thingy>>`. The C `samew` circular
1903// list isn't represented as a field — `bindwidget`/`unbindwidget`
1904// walk the table to find peers via `Arc<widget>` identity (Arc::
1905// ptr_eq). O(n) instead of C's O(1), but n is small (typical
1906// thingy count: a few hundred) and the simpler representation
1907// avoids a parallel widget→thingies table that would have to stay
1908// in sync.
1909
1910// Hashtable of thingies. Enabled nodes are those that refer to widgets.   // c:49
1911static THINGYTAB: OnceLock<Mutex<HashMap<String, Thingy>>> = OnceLock::new();
1912
1913/// Look up a Thingy by name via `gethashnode2(thingytab, name)` —
1914/// the C zle.h dispatch for `Th(X)` lookup. Direct port of the
1915/// open-coded `gethashnode2()` call shape at `Src/Zle/zle_thingy.c:160`.
1916pub fn gethashnode2(name: &str) -> Option<Thingy> {
1917    // c:gethashtable.c (open-coded)
1918    thingytab().lock().ok()?.get(name).cloned()
1919}
1920
1921/// List every Thingy name. Used by `${widgets[@]}` parameter expansion.
1922/// Replaces the legacy `ZleManager::list_widgets()` accessor.
1923pub fn listwidgets() -> Vec<String> {
1924    thingytab()
1925        .lock()
1926        .map(|t| t.keys().cloned().collect())
1927        .unwrap_or_default()
1928}
1929
1930/// Look up the dispatch target for a widget name. Built-in widgets
1931/// resolve to their own name (matching `${widgets[name]}` returning
1932/// "builtin"); user-defined ones resolve to the bound shell-function
1933/// name. Replaces the legacy `ZleManager::get_widget()` accessor.
1934pub fn getwidgettarget(name: &str) -> Option<String> {
1935    let tab = thingytab().lock().ok()?;
1936    let t = tab.get(name)?;
1937    let w = t.widget.as_ref()?;
1938    match &w.u {
1939        WidgetImpl::Internal(_) => Some(name.to_string()),
1940        WidgetImpl::UserFunc(s) => Some(s.clone()),
1941        WidgetImpl::Comp { func, .. } => Some(func.clone()),
1942    }
1943}
1944
1945// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1946// ─── RUST-ONLY ACCESSORS ───
1947//
1948// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
1949// RwLock<T>>` globals declared above. C zsh uses direct global
1950// access; Rust needs these wrappers because `OnceLock::get_or_init`
1951// is the only way to lazily construct shared state. These ported sit
1952// here so the body of this file reads in C source order without
1953// the accessor wrappers interleaved between real port ported.
1954// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1955
1956// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1957// ─── RUST-ONLY ACCESSORS ───
1958//
1959// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
1960// RwLock<T>>` globals declared above. C zsh uses direct global
1961// access; Rust needs these wrappers because `OnceLock::get_or_init`
1962// is the only way to lazily construct shared state. These ported sit
1963// here so the body of this file reads in C source order without
1964// the accessor wrappers interleaved between real port ported.
1965// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1966
1967/// Get-or-init access to the global thingytab.
1968pub fn thingytab() -> &'static Mutex<HashMap<String, Thingy>> {
1969    THINGYTAB.get_or_init(|| Mutex::new(HashMap::new()))
1970}
1971
1972#[cfg(test)]
1973mod tests {
1974    use super::*;
1975
1976    // Serialize tests since they share the global THINGYTAB.
1977    static LOCK: Mutex<()> = Mutex::new(());
1978
1979    fn reset_tab() {
1980        thingytab().lock().unwrap().clear();
1981    }
1982
1983    /// `Src/Zle/zle_thingy.c:370-375` — `bin_zle` rejects mutually
1984    /// exclusive operation flags. Pin: -l + -D together → return 1.
1985    #[test]
1986    fn bin_zle_rejects_incompatible_op_flags() {
1987        let _g = crate::test_util::global_state_lock();
1988        let _g = zle_test_setup();
1989        let _g = LOCK.lock().unwrap();
1990        reset_tab();
1991        // Build an options struct with both -l and -D set.
1992        let mut ops = options {
1993            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1994            args: Vec::new(),
1995            argscount: 0,
1996            argsalloc: 0,
1997        };
1998        ops.ind[b'l' as usize] = 1;
1999        ops.ind[b'D' as usize] = 1;
2000        let r = bin_zle("zle", &[], &ops, 0);
2001        assert_eq!(
2002            r, 1,
2003            "c:373-374 — incompatible op flags (-l + -D) → return 1"
2004        );
2005    }
2006
2007    /// `Src/Zle/zle_thingy.c:790-794` — `bin_zle_call` rejects unknown
2008    /// option chars in the flag-parsing loop. Pin: `-q` (not a real
2009    /// flag) → return 1. Set zleactive=1 so we reach the flag parser
2010    /// (otherwise the !zle_usable early-return at c:715 would mask it).
2011    #[test]
2012    fn bin_zle_call_rejects_unknown_option() {
2013        let _g = crate::test_util::global_state_lock();
2014        let _g = zle_test_setup();
2015        let _g = LOCK.lock().unwrap();
2016        reset_tab();
2017        crate::ported::builtins::sched::zleactive.store(1, Ordering::Relaxed);
2018        // -q is not a valid bin_zle_call flag.
2019        let ops_empty = options {
2020            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
2021            args: Vec::new(),
2022            argscount: 0,
2023            argsalloc: 0,
2024        };
2025        let r = bin_zle_call(
2026            "zle",
2027            &["widget_name".to_string(), "-q".to_string()],
2028            &ops_empty,
2029            0,
2030        );
2031        crate::ported::builtins::sched::zleactive.store(0, Ordering::Relaxed);
2032        assert_eq!(r, 1, "c:791-794 — unknown option char → return 1");
2033    }
2034
2035    /// `Src/Zle/zle_thingy.c:1022-1028` — `init_thingies` populates
2036    /// THINGYTAB with every name in `IWIDGET_NAMES` so `zle -l` works
2037    /// without each name needing a prior `zle -N` registration.
2038    #[test]
2039    fn init_thingies_populates_known_widget_names() {
2040        let _g = crate::test_util::global_state_lock();
2041        let _g = zle_test_setup();
2042        let _g = LOCK.lock().unwrap();
2043        thingytab().lock().unwrap().clear();
2044        init_thingies();
2045        let tab = thingytab().lock().unwrap();
2046        // Sample three canonical widgets — all must be present after init.
2047        assert!(
2048            tab.contains_key("accept-line"),
2049            "c:1028 — accept-line must be in THINGYTAB"
2050        );
2051        assert!(
2052            tab.contains_key("self-insert"),
2053            "c:1028 — self-insert must be in THINGYTAB"
2054        );
2055        assert!(
2056            tab.contains_key("undefined-key"),
2057            "c:1028 — undefined-key must be in THINGYTAB"
2058        );
2059        // Per C's thingies.list generator (`Makefile:1057-1075`), each
2060        // iwidgets.list line emits two entries: bare `name` (mortal,
2061        // can be rebound by `zle -A`/`zle -C`) and `.name`
2062        // (TH_IMMORTAL, the internal anchor).
2063        let al = tab.get("accept-line").unwrap();
2064        assert_eq!(
2065            al.flags & TH_IMMORTAL,
2066            0,
2067            "c:thingies.list — bare name must be mortal",
2068        );
2069        let dot_al = tab
2070            .get(".accept-line")
2071            .expect("c:thingies.list — dotted name must be registered alongside bare");
2072        assert_ne!(
2073            dot_al.flags & TH_IMMORTAL,
2074            0,
2075            "c:thingies.list — `.name` form is TH_IMMORTAL",
2076        );
2077        // Both forms must point at the same widget Arc.
2078        let (al_w, dot_al_w) = (al.widget.clone(), dot_al.widget.clone());
2079        match (al_w, dot_al_w) {
2080            (Some(a), Some(b)) => assert!(
2081                Arc::ptr_eq(&a, &b),
2082                "c:thingies.list — bare and dotted thingies share the widget Arc",
2083            ),
2084            _ => panic!("c:widgets.list — both bare and dotted thingies must have widgets"),
2085        }
2086    }
2087
2088    /// `Src/Zle/zle_thingy.c:865-867` — `bin_zle_fd` rejects negative
2089    /// or non-numeric fd with `zwarnnam` + return 1.
2090    #[test]
2091    fn bin_zle_fd_rejects_bad_fd_string() {
2092        let _g = crate::test_util::global_state_lock();
2093        let _g = zle_test_setup();
2094        let _g = LOCK.lock().unwrap();
2095        let ops = options {
2096            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
2097            args: Vec::new(),
2098            argscount: 0,
2099            argsalloc: 0,
2100        };
2101        let r = bin_zle_fd("zle", &["notanumber".to_string()], &ops, 0);
2102        assert_eq!(r, 1, "c:865-867 — non-numeric fd → 1");
2103        let r2 = bin_zle_fd("zle", &["-1".to_string()], &ops, 0);
2104        assert_eq!(r2, 1, "c:865-867 — negative fd → 1");
2105    }
2106
2107    /// `Src/Zle/zle_thingy.c:889-914` — `zle -F FD FUNC` installs a
2108    /// new handler. Pin: WATCH_FDS gains an entry.
2109    #[test]
2110    fn bin_zle_fd_adds_new_handler() {
2111        let _g = crate::test_util::global_state_lock();
2112        let _g = zle_test_setup();
2113        let _g = LOCK.lock().unwrap();
2114        // Reset table.
2115        WATCH_FDS.lock().unwrap().clear();
2116        let ops = options {
2117            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
2118            args: Vec::new(),
2119            argscount: 0,
2120            argsalloc: 0,
2121        };
2122        let r = bin_zle_fd("zle", &["7".to_string(), "my_handler".to_string()], &ops, 0);
2123        assert_eq!(r, 0, "c:889-914 — install → 0");
2124        let tab = WATCH_FDS.lock().unwrap();
2125        assert_eq!(tab.len(), 1);
2126        assert_eq!(tab[0].fd, 7);
2127        assert_eq!(tab[0].func, "my_handler");
2128        assert_eq!(tab[0].widget, 0);
2129    }
2130
2131    /// `Src/Zle/zle_thingy.c:944-946` — deleting a non-existent fd
2132    /// handler emits `zwarnnam "No handler installed for fd N"` and
2133    /// returns 1.
2134    #[test]
2135    fn bin_zle_fd_delete_nonexistent_returns_1() {
2136        let _g = crate::test_util::global_state_lock();
2137        let _g = zle_test_setup();
2138        let _g = LOCK.lock().unwrap();
2139        WATCH_FDS.lock().unwrap().clear();
2140        let ops = options {
2141            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
2142            args: Vec::new(),
2143            argscount: 0,
2144            argsalloc: 0,
2145        };
2146        let r = bin_zle_fd("zle", &["99".to_string()], &ops, 0);
2147        assert_eq!(r, 1, "c:944-946 — delete unknown fd → 1");
2148    }
2149
2150    /// `Src/Zle/zle_thingy.c:988-989` — `bin_zle_transform` rejects
2151    /// too few args (default form needs exactly 2). Pin: zero args →
2152    /// badargs=-1 → return 1.
2153    #[test]
2154    fn bin_zle_transform_rejects_too_few_args() {
2155        let _g = crate::test_util::global_state_lock();
2156        let _g = zle_test_setup();
2157        let _g = LOCK.lock().unwrap();
2158        let ops = options {
2159            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
2160            args: Vec::new(),
2161            argscount: 0,
2162            argsalloc: 0,
2163        };
2164        let r = bin_zle_transform("zle", &[], &ops, 0);
2165        assert_eq!(r, 1, "c:989-1010 — too few args → 1");
2166    }
2167
2168    /// `Src/Zle/zle_thingy.c:992-996` — default form `zle -T tc fname`
2169    /// sets TCOUT_FUNC_NAME.
2170    #[test]
2171    fn bin_zle_transform_default_form_sets_tc_handler() {
2172        let _g = crate::test_util::global_state_lock();
2173        let _g = zle_test_setup();
2174        let _g = LOCK.lock().unwrap();
2175        *TCOUT_FUNC_NAME.lock().unwrap() = None;
2176        let ops = options {
2177            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
2178            args: Vec::new(),
2179            argscount: 0,
2180            argsalloc: 0,
2181        };
2182        let r = bin_zle_transform(
2183            "zle",
2184            &["tc".to_string(), "my_handler".to_string()],
2185            &ops,
2186            0,
2187        );
2188        assert_eq!(r, 0, "c:992-996 — valid `tc fname` → 0");
2189        assert_eq!(
2190            TCOUT_FUNC_NAME.lock().unwrap().as_deref(),
2191            Some("my_handler"),
2192            "c:996 — name should be stored"
2193        );
2194    }
2195
2196    /// `Src/Zle/zle_thingy.c:983-985` — `-r tc` resets the handler.
2197    #[test]
2198    fn bin_zle_transform_r_clears_tc_handler() {
2199        let _g = crate::test_util::global_state_lock();
2200        let _g = zle_test_setup();
2201        let _g = LOCK.lock().unwrap();
2202        *TCOUT_FUNC_NAME.lock().unwrap() = Some("preset".to_string());
2203        let mut ops = options {
2204            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
2205            args: Vec::new(),
2206            argscount: 0,
2207            argsalloc: 0,
2208        };
2209        ops.ind[b'r' as usize] = 1;
2210        let r = bin_zle_transform("zle", &["tc".to_string()], &ops, 0);
2211        assert_eq!(r, 0, "c:983-985 — `-r tc` → 0");
2212        assert!(
2213            TCOUT_FUNC_NAME.lock().unwrap().is_none(),
2214            "c:985 — name should be cleared"
2215        );
2216    }
2217
2218    /// `Src/Zle/zle_thingy.c:969-970` — unknown transform name (anything
2219    /// other than `tc`) → badargs=2 → return 1.
2220    #[test]
2221    fn bin_zle_transform_rejects_unknown_transform() {
2222        let _g = crate::test_util::global_state_lock();
2223        let _g = zle_test_setup();
2224        let _g = LOCK.lock().unwrap();
2225        let mut ops = options {
2226            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
2227            args: Vec::new(),
2228            argscount: 0,
2229            argsalloc: 0,
2230        };
2231        ops.ind[b'L' as usize] = 1;
2232        let r = bin_zle_transform("zle", &["bogus".to_string()], &ops, 0);
2233        assert_eq!(r, 1, "c:969-970/1005 — unknown transform → 1");
2234    }
2235
2236    /// `Src/Zle/zle_thingy.c:691-693` — `bin_zle_flags` rejects unknown
2237    /// flag names with `zwarnnam` + ret=1.
2238    #[test]
2239    fn bin_zle_flags_rejects_unknown_flag() {
2240        let _g = crate::test_util::global_state_lock();
2241        let _g = zle_test_setup();
2242        let _g = LOCK.lock().unwrap();
2243        reset_tab();
2244        crate::ported::builtins::sched::zleactive.store(1, Ordering::Relaxed);
2245        // BINDK must be set for the loop to run (c:661).
2246        *BINDK.lock().unwrap() = Some(Thingy {
2247            nam: "dummy".to_string(),
2248            flags: 0,
2249            rc: 1,
2250            widget: None,
2251        });
2252        let ops_empty = options {
2253            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
2254            args: Vec::new(),
2255            argscount: 0,
2256            argsalloc: 0,
2257        };
2258        let r = bin_zle_flags("zle", &["bogus_flag".to_string()], &ops_empty, 0);
2259        *BINDK.lock().unwrap() = None;
2260        crate::ported::builtins::sched::zleactive.store(0, Ordering::Relaxed);
2261        assert_eq!(r, 1, "c:692-693 — unknown flag → zwarnnam + ret=1");
2262    }
2263
2264    /// `Src/Zle/zle_thingy.c:665-669` — `yank`, `yankbefore`, `kill` are
2265    /// recognized flag names (return 0).
2266    #[test]
2267    fn bin_zle_flags_accepts_yank_kill() {
2268        let _g = crate::test_util::global_state_lock();
2269        let _g = zle_test_setup();
2270        let _g = LOCK.lock().unwrap();
2271        reset_tab();
2272        crate::ported::builtins::sched::zleactive.store(1, Ordering::Relaxed);
2273        *BINDK.lock().unwrap() = Some(Thingy {
2274            nam: "dummy".to_string(),
2275            flags: 0,
2276            rc: 1,
2277            widget: None,
2278        });
2279        let ops_empty = options {
2280            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
2281            args: Vec::new(),
2282            argscount: 0,
2283            argsalloc: 0,
2284        };
2285        let r = bin_zle_flags(
2286            "zle",
2287            &[
2288                "yank".to_string(),
2289                "yankbefore".to_string(),
2290                "kill".to_string(),
2291            ],
2292            &ops_empty,
2293            0,
2294        );
2295        *BINDK.lock().unwrap() = None;
2296        crate::ported::builtins::sched::zleactive.store(0, Ordering::Relaxed);
2297        assert_eq!(r, 0, "c:665-669 — all recognized → ret=0");
2298    }
2299
2300    /// `Src/Zle/zle_thingy.c:740-744` — `-f` requires the literal token
2301    /// "nolast". Anything else → return 1.
2302    #[test]
2303    fn bin_zle_call_rejects_bad_f_arg() {
2304        let _g = crate::test_util::global_state_lock();
2305        let _g = zle_test_setup();
2306        let _g = LOCK.lock().unwrap();
2307        reset_tab();
2308        crate::ported::builtins::sched::zleactive.store(1, Ordering::Relaxed);
2309        let ops_empty = options {
2310            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
2311            args: Vec::new(),
2312            argscount: 0,
2313            argsalloc: 0,
2314        };
2315        let r = bin_zle_call(
2316            "zle",
2317            &["widget".to_string(), "-f".to_string(), "bogus".to_string()],
2318            &ops_empty,
2319            0,
2320        );
2321        crate::ported::builtins::sched::zleactive.store(0, Ordering::Relaxed);
2322        assert_eq!(r, 1, "c:741 — -f with non-'nolast' → return 1");
2323    }
2324
2325    /// `Src/Zle/zle_thingy.c:378-381` — bin_zle rejects too-few args.
2326    /// `-D` requires min=1; passing zero args → return 1.
2327    #[test]
2328    fn bin_zle_rejects_too_few_args() {
2329        let _g = crate::test_util::global_state_lock();
2330        let _g = zle_test_setup();
2331        let _g = LOCK.lock().unwrap();
2332        reset_tab();
2333        let mut ops = options {
2334            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
2335            args: Vec::new(),
2336            argscount: 0,
2337            argsalloc: 0,
2338        };
2339        ops.ind[b'D' as usize] = 1; // -D requires min=1
2340        let r = bin_zle("zle", &[], &ops, 0);
2341        assert_eq!(
2342            r, 1,
2343            "c:379-381 — zle -D with zero args → 'not enough' → return 1"
2344        );
2345    }
2346
2347    #[test]
2348    fn rthingy_creates_then_refs() {
2349        let _g = crate::test_util::global_state_lock();
2350        let _g = zle_test_setup();
2351        let _g = LOCK.lock().unwrap();
2352        reset_tab();
2353
2354        rthingy("foo");
2355        let tab = thingytab().lock().unwrap();
2356        let t = tab.get("foo").expect("rthingy must create");
2357        assert_eq!(t.rc, 1);
2358        assert_ne!((t.flags & DISABLED), 0);
2359    }
2360
2361    #[test]
2362    fn refthingy_unrefthingy_roundtrip() {
2363        let _g = crate::test_util::global_state_lock();
2364        let _g = zle_test_setup();
2365        let _g = LOCK.lock().unwrap();
2366        reset_tab();
2367
2368        rthingy("bar");
2369        refthingy("bar");
2370        // rc was 1 after rthingy, +1 from refthingy = 2
2371        assert_eq!(thingytab().lock().unwrap().get("bar").unwrap().rc, 2);
2372        unrefthingy("bar");
2373        assert_eq!(thingytab().lock().unwrap().get("bar").unwrap().rc, 1);
2374        unrefthingy("bar");
2375        // rc dropped to 0 → freenode removes
2376        assert!(!thingytab().lock().unwrap().contains_key("bar"));
2377    }
2378
2379    #[test]
2380    fn rthingy_nocreate_returns_false_for_missing() {
2381        let _g = crate::test_util::global_state_lock();
2382        let _g = zle_test_setup();
2383        let _g = LOCK.lock().unwrap();
2384        reset_tab();
2385
2386        assert!(!rthingy_nocreate("absent"));
2387        assert!(!thingytab().lock().unwrap().contains_key("absent"));
2388    }
2389
2390    #[test]
2391    fn rthingy_nocreate_refs_existing() {
2392        let _g = crate::test_util::global_state_lock();
2393        let _g = zle_test_setup();
2394        let _g = LOCK.lock().unwrap();
2395        reset_tab();
2396
2397        rthingy("present");
2398        assert!(rthingy_nocreate("present"));
2399        assert_eq!(thingytab().lock().unwrap().get("present").unwrap().rc, 2);
2400    }
2401
2402    /// c:60 — `createthingytab` must be idempotent: calling it twice
2403    /// must not double-populate or clear existing entries. Pinning
2404    /// catches a regression that resets the global on every call.
2405    #[test]
2406    fn createthingytab_is_idempotent() {
2407        let _g = crate::test_util::global_state_lock();
2408        let _g = zle_test_setup();
2409        let _g = LOCK.lock().unwrap();
2410        reset_tab();
2411
2412        createthingytab();
2413        let after_first = thingytab().lock().unwrap().len();
2414        createthingytab();
2415        let after_second = thingytab().lock().unwrap().len();
2416        assert_eq!(
2417            after_first, after_second,
2418            "createthingytab must not re-populate; was {} now {}",
2419            after_first, after_second
2420        );
2421    }
2422
2423    /// c:80 — `emptythingytab` only unbinds entries WITHOUT the
2424    /// DISABLED flag (it leaves the fixed `thingies[]` entries
2425    /// alone per the C source comment). `rthingy`-created entries
2426    /// inherit DISABLED from `makethingynode`, so `emptythingytab`
2427    /// is a no-op for them. Pin this so a regen that removes the
2428    /// DISABLED filter and starts purging the rthingy entries
2429    /// destroys widget bindings unexpectedly.
2430    #[test]
2431    fn emptythingytab_skips_disabled_rthingy_entries() {
2432        let _g = crate::test_util::global_state_lock();
2433        let _g = zle_test_setup();
2434        let _g = LOCK.lock().unwrap();
2435        reset_tab();
2436
2437        rthingy("a");
2438        rthingy("b");
2439        rthingy("c");
2440        let before = thingytab().lock().unwrap().len();
2441        assert!(before >= 3);
2442        emptythingytab();
2443        let after = thingytab().lock().unwrap().len();
2444        assert_eq!(
2445            after, before,
2446            "emptythingytab must NOT purge DISABLED rthingy entries"
2447        );
2448    }
2449
2450    /// c:118 — `freethingynode` on a non-existent name must be a
2451    /// no-op (not panic). Pin the defensive case; a regression that
2452    /// unwrap()s the table.get would crash the shell on widget unbind.
2453    #[test]
2454    fn freethingynode_on_missing_name_is_safe() {
2455        let _g = crate::test_util::global_state_lock();
2456        let _g = zle_test_setup();
2457        let _g = LOCK.lock().unwrap();
2458        reset_tab();
2459        freethingynode("never-existed");
2460    }
2461
2462    /// c:147 — `unrefthingy` on rc=1 frees the entry. After unref-
2463    /// to-zero, the entry must be absent. Catches a regression that
2464    /// leaves a dangling rc=0 entry in the table.
2465    #[test]
2466    fn unrefthingy_at_rc_one_frees_entry() {
2467        let _g = crate::test_util::global_state_lock();
2468        let _g = zle_test_setup();
2469        let _g = LOCK.lock().unwrap();
2470        reset_tab();
2471
2472        rthingy("solo");
2473        assert_eq!(thingytab().lock().unwrap().get("solo").unwrap().rc, 1);
2474        unrefthingy("solo");
2475        assert!(
2476            !thingytab().lock().unwrap().contains_key("solo"),
2477            "rc=0 entry must be removed from thingytab"
2478        );
2479    }
2480
2481    /// c:147 — `unrefthingy` on a missing name must be a safe no-op.
2482    /// Without this, widget cleanup during shell teardown could
2483    /// panic on already-freed entries.
2484    #[test]
2485    fn unrefthingy_on_missing_is_safe() {
2486        let _g = crate::test_util::global_state_lock();
2487        let _g = zle_test_setup();
2488        let _g = LOCK.lock().unwrap();
2489        reset_tab();
2490        unrefthingy("never-bound");
2491    }
2492
2493    /// c:108 — `makethingynode` produces a fresh node with rc=0 and
2494    /// the DISABLED flag set per c:114. Pin both invariants so a
2495    /// regen that defaults rc=1 (corrupting refcount math) gets
2496    /// caught immediately.
2497    #[test]
2498    fn makethingynode_starts_at_rc_zero_with_disabled_flag() {
2499        let _g = crate::test_util::global_state_lock();
2500        let _g = zle_test_setup();
2501        let n = makethingynode();
2502        assert_eq!(n.rc, 0, "fresh node must have rc=0");
2503        assert_ne!(
2504            (n.flags & DISABLED),
2505            0,
2506            "fresh node must have DISABLED flag set"
2507        );
2508    }
2509
2510    /// c:158 — `rthingy` on the same name twice bumps the refcount,
2511    /// does NOT create a second entry. Pin the dedup-by-name property.
2512    #[test]
2513    fn rthingy_same_name_twice_only_increments_refcount() {
2514        let _g = crate::test_util::global_state_lock();
2515        let _g = zle_test_setup();
2516        let _g = LOCK.lock().unwrap();
2517        reset_tab();
2518
2519        rthingy("dup");
2520        rthingy("dup");
2521        let tab = thingytab().lock().unwrap();
2522        assert_eq!(tab.len(), 1);
2523        assert!(
2524            tab.get("dup").unwrap().rc >= 2,
2525            "second rthingy must bump rc, not create a sibling"
2526        );
2527    }
2528
2529    /// c:147 — Unref-to-zero pattern across many entries. Stresses
2530    /// the table mutator path to catch a HashMap-mutation bug that
2531    /// only shows under multiple inserts/removes.
2532    #[test]
2533    fn many_rthingy_unref_cycles_leave_no_residue() {
2534        let _g = crate::test_util::global_state_lock();
2535        let _g = zle_test_setup();
2536        let _g = LOCK.lock().unwrap();
2537        reset_tab();
2538
2539        for i in 0..20 {
2540            rthingy(&format!("entry-{}", i));
2541        }
2542        assert!(thingytab().lock().unwrap().len() >= 20);
2543        for i in 0..20 {
2544            unrefthingy(&format!("entry-{}", i));
2545        }
2546        for i in 0..20 {
2547            assert!(
2548                !thingytab()
2549                    .lock()
2550                    .unwrap()
2551                    .contains_key(&format!("entry-{}", i)),
2552                "entry-{} should be gone after final unref",
2553                i
2554            );
2555        }
2556    }
2557
2558    // ─── zsh-corpus pins for thingy registry ───────────────────────
2559
2560    /// `rthingy_nocreate("never_was")` returns false on missing.
2561    #[test]
2562    fn zle_thingy_corpus_rthingy_nocreate_missing_returns_false() {
2563        let _g = crate::test_util::global_state_lock();
2564        let _g = zle_test_setup();
2565        let _l = LOCK.lock().unwrap_or_else(|e| e.into_inner());
2566        reset_tab();
2567        assert!(!rthingy_nocreate("zshrs_never_thingy_xyz"));
2568    }
2569
2570    /// `rthingy(name)` then `rthingy_nocreate(name)` returns true.
2571    #[test]
2572    fn zle_thingy_corpus_rthingy_then_nocreate_finds_it() {
2573        let _g = crate::test_util::global_state_lock();
2574        let _g = zle_test_setup();
2575        let _l = LOCK.lock().unwrap_or_else(|e| e.into_inner());
2576        reset_tab();
2577        rthingy("zshrs_test_thingy_a");
2578        assert!(rthingy_nocreate("zshrs_test_thingy_a"));
2579    }
2580
2581    /// `unrefthingy` on never-registered name is a safe no-op.
2582    #[test]
2583    fn zle_thingy_corpus_unrefthingy_missing_no_panic() {
2584        let _g = crate::test_util::global_state_lock();
2585        let _g = zle_test_setup();
2586        let _l = LOCK.lock().unwrap_or_else(|e| e.into_inner());
2587        unrefthingy("zshrs_never_thingy_xyz_abc");
2588    }
2589
2590    /// `emptythingytab` unbinds user-installed (non-DISABLED) entries.
2591    /// rthingy() creates DISABLED slots; C's `scanhashtable(thingytab,
2592    /// 0, 0, DISABLED, scanemptythingies, 0)` SKIPS DISABLED entries
2593    /// (the 4th arg is the avoid-flag), so an entry that's never been
2594    /// bound to a widget stays in the table verbatim. Bind a widget
2595    /// here first so emptythingytab actually has work to do; then
2596    /// verify the binding's gone (DISABLED set, widget cleared).
2597    #[test]
2598    fn zle_thingy_corpus_emptythingytab_clears_user_entries() {
2599        let _g = crate::test_util::global_state_lock();
2600        let _g = zle_test_setup();
2601        let _l = LOCK.lock().unwrap_or_else(|e| e.into_inner());
2602        reset_tab();
2603        for i in 0..5 {
2604            let name = format!("e-{i}");
2605            rthingy(&name);
2606            // Bind a fresh widget so the entry isn't DISABLED — only
2607            // non-DISABLED entries get scanned by emptythingytab.
2608            let w = std::sync::Arc::new(widget {
2609                flags: 0,
2610                first: None,
2611                u: crate::ported::zle::zle_h::WidgetImpl::UserFunc(String::new()),
2612            });
2613            bindwidget(w, &name);
2614        }
2615        assert!(thingytab().lock().unwrap().len() >= 5);
2616        emptythingytab();
2617        let t = thingytab().lock().unwrap();
2618        for i in 0..5 {
2619            let name = format!("e-{i}");
2620            // emptythingytab → unbindwidget → unrefthingy chain
2621            // removes the entry entirely when its refcount hits 0
2622            // (the C `freethingynode` path at zle_thingy.c:118-128
2623            // calls `hashtable->removenode` after the last unref).
2624            // The user-visible result: `zle -L` shows none of the
2625            // user-defined widgets after `emptythingytab`.
2626            assert!(!t.contains_key(&name), "{name} removed");
2627        }
2628    }
2629
2630    /// `freethingynode("never_was")` is a safe no-op.
2631    #[test]
2632    fn zle_thingy_corpus_freethingynode_missing_no_panic() {
2633        let _g = crate::test_util::global_state_lock();
2634        let _g = zle_test_setup();
2635        let _l = LOCK.lock().unwrap_or_else(|e| e.into_inner());
2636        freethingynode("zshrs_never_thingy_xyz_abc");
2637    }
2638
2639    // ═══════════════════════════════════════════════════════════════════
2640    // Additional C-parity tests for Src/Zle/zle_thingy.c lifecycle ops.
2641    // ═══════════════════════════════════════════════════════════════════
2642
2643    /// c:108-113 — `makethingynode()` returns a Thingy with DISABLED
2644    /// flag set + rc=0 (matches `zshcalloc` + explicit DISABLED).
2645    #[test]
2646    fn makethingynode_returns_disabled_and_rc_zero() {
2647        let _g = crate::test_util::global_state_lock();
2648        let _g2 = zle_test_setup();
2649        let t = makethingynode();
2650        assert_ne!(
2651            t.flags & DISABLED,
2652            0,
2653            "fresh thingy must have DISABLED set per c:112"
2654        );
2655        assert_eq!(t.rc, 0, "fresh thingy must have rc=0 per zshcalloc + c:110");
2656    }
2657
2658    /// c:138 — `refthingy(missing)` is a no-op (matches `if(th)` guard).
2659    #[test]
2660    fn refthingy_missing_name_is_noop() {
2661        let _g = crate::test_util::global_state_lock();
2662        let _g2 = zle_test_setup();
2663        let _l = LOCK.lock().unwrap_or_else(|e| e.into_inner());
2664        // Doesn't panic, doesn't insert.
2665        let before = thingytab().lock().unwrap().len();
2666        refthingy("zshrs_never_thingy_for_refthingy_test");
2667        let after = thingytab().lock().unwrap().len();
2668        assert_eq!(before, after, "refthingy on missing must NOT insert");
2669    }
2670
2671    /// c:147 — `unrefthingy(missing)` is a safe no-op.
2672    #[test]
2673    fn unrefthingy_missing_name_is_noop() {
2674        let _g = crate::test_util::global_state_lock();
2675        let _g2 = zle_test_setup();
2676        let _l = LOCK.lock().unwrap_or_else(|e| e.into_inner());
2677        // No panic = pass.
2678        unrefthingy("zshrs_never_thingy_for_unref_test");
2679    }
2680
2681    /// c:158 — `rthingy` creates entry on first call, increments rc on
2682    /// each subsequent call. Pin: 3 calls → rc=3 (each call bumps).
2683    #[test]
2684    fn rthingy_repeated_calls_bump_refcount() {
2685        let _g = crate::test_util::global_state_lock();
2686        let _g2 = zle_test_setup();
2687        let _l = LOCK.lock().unwrap_or_else(|e| e.into_inner());
2688        let name = "zshrs_rthingy_rc_test";
2689        let _ = thingytab().lock().unwrap().remove(name);
2690        rthingy(name);
2691        rthingy(name);
2692        rthingy(name);
2693        let rc = thingytab().lock().unwrap().get(name).map(|t| t.rc);
2694        assert_eq!(rc, Some(3), "3 rthingy calls → rc=3");
2695        let _ = thingytab().lock().unwrap().remove(name);
2696    }
2697
2698    /// c:169 — `rthingy_nocreate(missing)` returns false and does NOT
2699    /// add an entry to the table.
2700    #[test]
2701    fn rthingy_nocreate_missing_returns_false_no_insert() {
2702        let _g = crate::test_util::global_state_lock();
2703        let _g2 = zle_test_setup();
2704        let _l = LOCK.lock().unwrap_or_else(|e| e.into_inner());
2705        let name = "zshrs_rthingy_nocreate_test_missing";
2706        let _ = thingytab().lock().unwrap().remove(name);
2707        let r = rthingy_nocreate(name);
2708        assert!(!r, "missing → false");
2709        assert!(
2710            !thingytab().lock().unwrap().contains_key(name),
2711            "must NOT insert on lookup-only call"
2712        );
2713    }
2714
2715    /// c:169 — `rthingy_nocreate(existing)` returns true AND bumps rc.
2716    #[test]
2717    fn rthingy_nocreate_existing_bumps_rc() {
2718        let _g = crate::test_util::global_state_lock();
2719        let _g2 = zle_test_setup();
2720        let _l = LOCK.lock().unwrap_or_else(|e| e.into_inner());
2721        let name = "zshrs_rthingy_nocreate_existing";
2722        let _ = thingytab().lock().unwrap().remove(name);
2723        rthingy(name); // create with rc=1
2724        let rc_before = thingytab()
2725            .lock()
2726            .unwrap()
2727            .get(name)
2728            .map(|t| t.rc)
2729            .unwrap_or(0);
2730        let r = rthingy_nocreate(name);
2731        assert!(r, "existing → true");
2732        let rc_after = thingytab()
2733            .lock()
2734            .unwrap()
2735            .get(name)
2736            .map(|t| t.rc)
2737            .unwrap_or(0);
2738        assert_eq!(rc_after, rc_before + 1, "rc must increment by 1");
2739        let _ = thingytab().lock().unwrap().remove(name);
2740    }
2741
2742    /// c:147 — `unrefthingy` decrements rc and removes when rc hits 0.
2743    #[test]
2744    fn unrefthingy_at_last_ref_removes_entry() {
2745        let _g = crate::test_util::global_state_lock();
2746        let _g2 = zle_test_setup();
2747        let _l = LOCK.lock().unwrap_or_else(|e| e.into_inner());
2748        let name = "zshrs_unref_last_test";
2749        let _ = thingytab().lock().unwrap().remove(name);
2750        rthingy(name); // rc=1
2751        unrefthingy(name); // rc=0 → freed
2752        assert!(
2753            !thingytab().lock().unwrap().contains_key(name),
2754            "rc=0 must remove entry"
2755        );
2756    }
2757
2758    /// c:147 — `unrefthingy` with rc>1 does NOT remove (decrement only).
2759    #[test]
2760    fn unrefthingy_with_rc_greater_one_decrements_only() {
2761        let _g = crate::test_util::global_state_lock();
2762        let _g2 = zle_test_setup();
2763        let _l = LOCK.lock().unwrap_or_else(|e| e.into_inner());
2764        let name = "zshrs_unref_keepalive_test";
2765        let _ = thingytab().lock().unwrap().remove(name);
2766        rthingy(name); // rc=1
2767        rthingy(name); // rc=2
2768        unrefthingy(name); // rc=1 → keep
2769        assert!(
2770            thingytab().lock().unwrap().contains_key(name),
2771            "rc=1 (after unref from 2) must remain"
2772        );
2773        let rc = thingytab()
2774            .lock()
2775            .unwrap()
2776            .get(name)
2777            .map(|t| t.rc)
2778            .unwrap_or(99);
2779        assert_eq!(rc, 1);
2780        let _ = thingytab().lock().unwrap().remove(name);
2781    }
2782
2783    // ═══════════════════════════════════════════════════════════════════
2784    // Additional C-parity tests for Src/Zle/zle_thingy.c
2785    // c:48 createthingytab / c:127 emptythingytab / c:208 freethingynode /
2786    // c:280 rthingy / c:307 rthingy_nocreate / c:351 bindwidget /
2787    // c:417 unbindwidget / c:752 bin_zle_list / c:792 bin_zle_refresh /
2788    // c:860 bin_zle_mesg
2789    // ═══════════════════════════════════════════════════════════════════
2790
2791    /// c:307 — `rthingy_nocreate` returns bool (compile-time type pin).
2792    #[test]
2793    fn rthingy_nocreate_returns_bool_type() {
2794        let _g = crate::test_util::global_state_lock();
2795        let _g2 = zle_test_setup();
2796        let _l = LOCK.lock().unwrap_or_else(|e| e.into_inner());
2797        let _: bool = rthingy_nocreate("anything");
2798    }
2799
2800    /// c:417 — `unbindwidget` returns i32 (compile-time type pin).
2801    #[test]
2802    fn unbindwidget_returns_i32_type() {
2803        let _g = crate::test_util::global_state_lock();
2804        let _g2 = zle_test_setup();
2805        let _l = LOCK.lock().unwrap_or_else(|e| e.into_inner());
2806        let _: i32 = unbindwidget("self-insert", 0);
2807    }
2808
2809    /// c:417 — `unbindwidget("", _)` empty name returns 0 (missing → no-op).
2810    /// C body checks `tab.get(t)` and returns 0 for None — empty name
2811    /// can't match any registered widget, so it's treated as missing.
2812    #[test]
2813    fn unbindwidget_empty_name_returns_zero_missing() {
2814        let _g = crate::test_util::global_state_lock();
2815        let _g2 = zle_test_setup();
2816        let _l = LOCK.lock().unwrap_or_else(|e| e.into_inner());
2817        let r = unbindwidget("", 0);
2818        assert_eq!(r, 0, "empty name treated as missing → 0 (per c:427)");
2819    }
2820
2821    /// c:752+792+860 — bin_zle_* builtins return in u8 exit-code range.
2822    #[test]
2823    fn bin_zle_subcommands_return_in_exit_range() {
2824        let _g = crate::test_util::global_state_lock();
2825        let _g2 = zle_test_setup();
2826        let _l = LOCK.lock().unwrap_or_else(|e| e.into_inner());
2827        let ops = options {
2828            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
2829            args: Vec::new(),
2830            argscount: 0,
2831            argsalloc: 0,
2832        };
2833        for r in [
2834            bin_zle_list("zle", &[], &ops, 0),
2835            bin_zle_refresh("zle", &[], &ops, 0),
2836            bin_zle_mesg("zle", &[], &ops, 0),
2837        ] {
2838            assert!((0..256).contains(&r), "exit code {} must fit in u8", r);
2839        }
2840    }
2841
2842    // ═══════════════════════════════════════════════════════════════════
2843    // Additional C-parity tests for Src/Zle/zle_thingy.c
2844    // c:752 bin_zle_list / c:792 bin_zle_refresh / c:860 bin_zle_mesg /
2845    // c:894 bin_zle_unget / c:919 bin_zle_keymap / c:1008 bin_zle_del /
2846    // c:1219 zle_usable / c:1896 listwidgets / c:1907 getwidgettarget
2847    // ═══════════════════════════════════════════════════════════════════
2848
2849    fn empty_ops_thingy() -> options {
2850        options {
2851            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
2852            args: Vec::new(),
2853            argscount: 0,
2854            argsalloc: 0,
2855        }
2856    }
2857
2858    /// c:752 — `bin_zle_list` returns i32 (compile-time type pin).
2859    #[test]
2860    fn bin_zle_list_returns_i32_type() {
2861        let _g = crate::test_util::global_state_lock();
2862        let _g2 = zle_test_setup();
2863        let ops = empty_ops_thingy();
2864        let _: i32 = bin_zle_list("zle", &[], &ops, 0);
2865    }
2866
2867    /// c:792 — `bin_zle_refresh` returns i32.
2868    #[test]
2869    fn bin_zle_refresh_returns_i32_type() {
2870        let _g = crate::test_util::global_state_lock();
2871        let _g2 = zle_test_setup();
2872        let ops = empty_ops_thingy();
2873        let _: i32 = bin_zle_refresh("zle", &[], &ops, 0);
2874    }
2875
2876    /// c:860 — `bin_zle_mesg` returns i32.
2877    #[test]
2878    fn bin_zle_mesg_returns_i32_type() {
2879        let _g = crate::test_util::global_state_lock();
2880        let _g2 = zle_test_setup();
2881        let ops = empty_ops_thingy();
2882        let _: i32 = bin_zle_mesg("zle", &[], &ops, 0);
2883    }
2884
2885    /// c:894 — `bin_zle_unget` returns i32.
2886    #[test]
2887    fn bin_zle_unget_returns_i32_type() {
2888        let _g = crate::test_util::global_state_lock();
2889        let _g2 = zle_test_setup();
2890        let ops = empty_ops_thingy();
2891        let _: i32 = bin_zle_unget("zle", &[], &ops, 0);
2892    }
2893
2894    /// c:919 — `bin_zle_keymap` returns i32.
2895    #[test]
2896    fn bin_zle_keymap_returns_i32_type() {
2897        let _g = crate::test_util::global_state_lock();
2898        let _g2 = zle_test_setup();
2899        let ops = empty_ops_thingy();
2900        let _: i32 = bin_zle_keymap("zle", &[], &ops, 0);
2901    }
2902
2903    /// c:1008 — `bin_zle_del` returns i32.
2904    #[test]
2905    fn bin_zle_del_returns_i32_type() {
2906        let _g = crate::test_util::global_state_lock();
2907        let _g2 = zle_test_setup();
2908        let ops = empty_ops_thingy();
2909        let _: i32 = bin_zle_del("zle", &[], &ops, 0);
2910    }
2911
2912    /// c:1219 — `zle_usable` returns i32 + 0/1 only.
2913    #[test]
2914    fn zle_usable_returns_zero_or_one() {
2915        let _g = crate::test_util::global_state_lock();
2916        let _g2 = zle_test_setup();
2917        let r = zle_usable();
2918        assert!(r == 0 || r == 1, "zle_usable ∈ {{0, 1}}, got {}", r);
2919    }
2920
2921    /// c:1219 — `zle_usable` is deterministic in non-ZLE context.
2922    #[test]
2923    fn zle_usable_is_deterministic_no_zle_context() {
2924        let _g = crate::test_util::global_state_lock();
2925        let _g2 = zle_test_setup();
2926        let first = zle_usable();
2927        for _ in 0..3 {
2928            assert_eq!(zle_usable(), first, "zle_usable must be deterministic");
2929        }
2930    }
2931
2932    /// c:1896 — `listwidgets` returns Vec<String> (compile-time type pin).
2933    #[test]
2934    fn listwidgets_returns_vec_string_type() {
2935        let _g = crate::test_util::global_state_lock();
2936        let _g2 = zle_test_setup();
2937        let _: Vec<String> = listwidgets();
2938    }
2939
2940    /// c:1907 — `getwidgettarget("")` empty returns Option<String>.
2941    #[test]
2942    fn getwidgettarget_returns_option_string_type() {
2943        let _g = crate::test_util::global_state_lock();
2944        let _g2 = zle_test_setup();
2945        let _: Option<String> = getwidgettarget("");
2946    }
2947
2948    /// c:1896 — `listwidgets` is deterministic for stable widget table.
2949    #[test]
2950    fn listwidgets_is_deterministic() {
2951        let _g = crate::test_util::global_state_lock();
2952        let _g2 = zle_test_setup();
2953        let first = listwidgets();
2954        for _ in 0..3 {
2955            assert_eq!(listwidgets(), first, "listwidgets must be deterministic");
2956        }
2957    }
2958
2959    // ═══════════════════════════════════════════════════════════════════
2960    // Additional C-parity tests for Src/Zle/zle_thingy.c
2961    // c:48 createthingytab / c:307 rthingy_nocreate / c:417 unbindwidget /
2962    // c:659 bin_zle / c:752 bin_zle_list / c:1219 zle_usable /
2963    // c:1896 listwidgets / c:1907 getwidgettarget
2964    // ═══════════════════════════════════════════════════════════════════
2965
2966    /// c:48 — `createthingytab` is idempotent.
2967    #[test]
2968    fn createthingytab_idempotent() {
2969        let _g = crate::test_util::global_state_lock();
2970        let _g2 = zle_test_setup();
2971        for _ in 0..10 {
2972            createthingytab();
2973        }
2974    }
2975
2976    /// c:307 — `rthingy_nocreate` returns bool (compile-time pin, alt).
2977    #[test]
2978    fn rthingy_nocreate_returns_bool_pin_alt() {
2979        let _g = crate::test_util::global_state_lock();
2980        let _g2 = zle_test_setup();
2981        let _: bool = rthingy_nocreate("__never_real_thingy_xyz__");
2982    }
2983
2984    /// c:307 — `rthingy_nocreate("")` empty name deterministic.
2985    #[test]
2986    fn rthingy_nocreate_empty_deterministic() {
2987        let _g = crate::test_util::global_state_lock();
2988        let _g2 = zle_test_setup();
2989        let first = rthingy_nocreate("");
2990        for _ in 0..5 {
2991            assert_eq!(
2992                rthingy_nocreate(""),
2993                first,
2994                "rthingy_nocreate('') must be pure"
2995            );
2996        }
2997    }
2998
2999    /// c:417 — `unbindwidget("nonexistent", _)` returns i32 (no panic).
3000    #[test]
3001    fn unbindwidget_nonexistent_returns_i32() {
3002        let _g = crate::test_util::global_state_lock();
3003        let _g2 = zle_test_setup();
3004        let _: i32 = unbindwidget("__never_widget__", 0);
3005    }
3006
3007    /// c:1219 — `zle_usable` returns i32 (compile-time pin).
3008    #[test]
3009    fn zle_usable_returns_i32_type() {
3010        let _g = crate::test_util::global_state_lock();
3011        let _g2 = zle_test_setup();
3012        let _: i32 = zle_usable();
3013    }
3014
3015    /// c:1896 — `listwidgets` returns Vec<String> (alt-name type pin).
3016    /// Note: in test context the widget table is empty; the
3017    /// `includes self-insert` invariant only holds after the live
3018    /// startup-time registration, which test_setup doesn't perform.
3019    #[test]
3020    fn listwidgets_returns_vec_string_pin_alt() {
3021        let _g = crate::test_util::global_state_lock();
3022        let _g2 = zle_test_setup();
3023        let _: Vec<String> = listwidgets();
3024    }
3025
3026    /// c:1907 — `getwidgettarget("")` empty input deterministic.
3027    #[test]
3028    fn getwidgettarget_empty_deterministic() {
3029        let _g = crate::test_util::global_state_lock();
3030        let _g2 = zle_test_setup();
3031        let a = getwidgettarget("");
3032        let b = getwidgettarget("");
3033        assert_eq!(a, b, "getwidgettarget('') must be pure");
3034    }
3035
3036    /// c:1907 — `getwidgettarget` for unknown widget returns None.
3037    /// (Test context's widget table is empty so even builtin lookups
3038    /// return None; only the unknown-name invariant is reliably
3039    /// testable here.)
3040    #[test]
3041    fn getwidgettarget_unknown_returns_none() {
3042        let _g = crate::test_util::global_state_lock();
3043        let _g2 = zle_test_setup();
3044        assert!(
3045            getwidgettarget("__never_real_widget_xyz__").is_none(),
3046            "unknown widget → None"
3047        );
3048    }
3049
3050    /// c:659 — `bin_zle` returns i32 (compile-time pin).
3051    #[test]
3052    fn bin_zle_returns_i32_type() {
3053        let _g = crate::test_util::global_state_lock();
3054        let _g2 = zle_test_setup();
3055        let ops = empty_ops_thingy();
3056        let _: i32 = bin_zle("zle", &[], &ops, 0);
3057    }
3058
3059    /// c:752 — `bin_zle_list` returns i32 (compile-time pin, alt).
3060    #[test]
3061    fn bin_zle_list_returns_i32_pin_alt() {
3062        let _g = crate::test_util::global_state_lock();
3063        let _g2 = zle_test_setup();
3064        let ops = empty_ops_thingy();
3065        let _: i32 = bin_zle_list("zle", &[], &ops, 0);
3066    }
3067}