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::{Arc, Mutex, OnceLock};
10
11use super::zle_h::{widget as Widget, WidgetImpl as WidgetFunc};
12use super::zle_h::{
13    TH_IMMORTAL, WIDGET_INT, WIDGET_NCOMP, WIDGET_INUSE,
14    ZLE_MENUCMP, ZLE_KEEPSUFFIX, ZLE_ISCOMP,
15};
16use crate::ported::zsh_h::DISABLED;
17
18/// Direct port of `struct thingy` from `Src/Zle/zle.h:224`. A named
19/// reference to a widget. `ThingyFlags` deleted — C uses an `int
20/// flags` field with `TH_IMMORTAL` (1<<1) and `DISABLED` (1<<0) bits.
21
22// --- AUTO: cross-zle hoisted-fn use glob ---
23#[allow(unused_imports)]
24use crate::ported::zle::zle_h::*;
25#[allow(unused_imports)]
26use crate::ported::zle::zle_main::*;
27#[allow(unused_imports)]
28use crate::ported::zle::zle_misc::*;
29#[allow(unused_imports)]
30use crate::ported::zle::zle_hist::*;
31#[allow(unused_imports)]
32use crate::ported::zle::zle_move::*;
33#[allow(unused_imports)]
34use crate::ported::zle::zle_word::*;
35#[allow(unused_imports)]
36use crate::ported::zle::zle_params::*;
37#[allow(unused_imports)]
38use crate::ported::zle::zle_vi::*;
39#[allow(unused_imports)]
40use crate::ported::zle::zle_utils::*;
41#[allow(unused_imports)]
42use crate::ported::zle::zle_refresh::*;
43#[allow(unused_imports)]
44use crate::ported::zle::zle_tricky::*;
45#[allow(unused_imports)]
46use crate::ported::zle::textobjects::*;
47#[allow(unused_imports)]
48use crate::ported::zle::deltochar::*;
49
50#[derive(Debug, Clone)]
51pub struct Thingy {                                                          // c:224
52    pub nam: String,                                                         // c:226 char *nam
53    pub flags: i32,                                                          // c:227 int flags
54    pub rc: i32,                                                             // c:228 int rc
55    pub widget: Option<Arc<Widget>>,                                         // c:229 Widget widget
56}
57
58impl Thingy {
59    /// Create a thingy with no widget bound — equivalent to a freshly
60    /// allocated entry from `makethingynode()` in
61    /// Src/Zle/zle_thingy.c:108. Callers fill in `widget` later via
62    /// `bindwidget` (zle_thingy.c:199).
63    pub fn new(name: &str) -> Self {
64        Thingy {
65            nam: name.to_string(),
66            flags: 0,
67            rc: 1,
68            widget: None,
69        }
70    }
71
72    /// Create a thingy that wraps a built-in widget.
73    /// Equivalent to the `addzlefunction()` path at
74    /// Src/Zle/zle_thingy.c:281: builds the immortal-flagged Thingy
75    /// and binds it to a Widget produced by the built-in dispatch
76    /// table (`Widget::builtin`).
77    pub fn builtin(name: &str) -> Self {
78        let widget = Widget::builtin(name);
79        Thingy {
80            nam: name.to_string(),
81            flags: TH_IMMORTAL,
82            rc: 1,
83            widget: Some(Arc::new(widget)),
84        }
85    }
86
87    /// Create a thingy that wraps a user-defined shell function.
88    /// Equivalent to `bin_zle_new()` at Src/Zle/zle_thingy.c:584 — the
89    /// `zle -N name fn` builtin path.
90    pub fn user_defined(name: &str, func_name: &str) -> Self {
91        let widget = Widget::user_defined(name, func_name);
92        Thingy {
93            nam: name.to_string(),
94            flags: 0,
95            rc: 1,
96            widget: Some(Arc::new(widget)),
97        }
98    }
99
100    /// Test whether this thingy's name matches `name`.
101    /// Equivalent to the `IS_THINGY(thingy, name)` macro at
102    /// Src/Zle/zle.h — used by widget bodies that special-case their
103    /// own bound name (e.g. select-a-word checking which alias fired).
104    pub fn is(&self, name: &str) -> bool {
105        self.nam == name
106    }
107
108    /// Test whether this thingy is `name` or its dot-prefixed variant.
109    /// The `.foo` form names the underlying built-in when a user has
110    /// aliased `foo` to something else — see `bin_zle_new`'s `args[0]`
111    /// vs `args[1]` split at zle_thingy.c:584. Callers use this when
112    /// they want the canonical built-in regardless of user aliasing.
113    pub fn is_thingy(&self, name: &str) -> bool {
114        self.nam == name || self.nam == format!(".{}", name)
115    }
116}
117
118// `pub mod names` removed — Rust-fabricated namespace wrapping
119// thingy-name string literals. C source uses bare `"accept-line"`/
120// `"self-insert"`/etc. directly at `zle_thingy.c` registration
121// sites; no namespace, no helper consts. The mod had no callers.
122
123// =====================================================================
124// thingytab — `Src/Zle/zle_thingy.c:52`.
125// =====================================================================
126//
127// C: `mod_export HashTable thingytab;`. One global hash keyed by
128// thingy name; each entry is a `Thingy` struct (rc + flags + widget
129// + samew circular-list pointer). Allocated by `createthingytab()`
130// at zle init and torn down by `cleanup_zle()`.
131//
132// Rust: `Mutex<HashMap<String, Thingy>>`. The C `samew` circular
133// list isn't represented as a field — `bindwidget`/`unbindwidget`
134// walk the table to find peers via `Arc<Widget>` identity (Arc::
135// ptr_eq). O(n) instead of C's O(1), but n is small (typical
136// thingy count: a few hundred) and the simpler representation
137// avoids a parallel widget→thingies table that would have to stay
138// in sync.
139
140// Hashtable of thingies. Enabled nodes are those that refer to widgets.   // c:49
141static THINGYTAB: OnceLock<Mutex<HashMap<String, Thingy>>> = OnceLock::new();
142
143/// Get-or-init access to the global thingytab.
144fn thingytab() -> &'static Mutex<HashMap<String, Thingy>> {
145    THINGYTAB.get_or_init(|| Mutex::new(HashMap::new()))
146}
147
148/// Look up a Thingy by name via `gethashnode2(thingytab, name)` —
149/// the C zle.h dispatch for `Th(X)` lookup. Direct port of the
150/// open-coded `gethashnode2()` call shape at `Src/Zle/zle_thingy.c:160`.
151pub fn gethashnode2(name: &str) -> Option<Thingy> {                           // c:gethashtable.c (open-coded)
152    thingytab().lock().ok()?.get(name).cloned()
153}
154
155/// List every Thingy name. Used by `${widgets[@]}` parameter expansion.
156/// Replaces the legacy `ZleManager::list_widgets()` accessor.
157pub fn listwidgets() -> Vec<String> {
158    thingytab().lock().map(|t| t.keys().cloned().collect()).unwrap_or_default()
159}
160
161/// Look up the dispatch target for a widget name. Built-in widgets
162/// resolve to their own name (matching `${widgets[name]}` returning
163/// "builtin"); user-defined ones resolve to the bound shell-function
164/// name. Replaces the legacy `ZleManager::get_widget()` accessor.
165pub fn getwidgettarget(name: &str) -> Option<String> {
166    let tab = thingytab().lock().ok()?;
167    let t = tab.get(name)?;
168    let w = t.widget.as_ref()?;
169    match &w.u {
170        super::zle_h::WidgetImpl::Internal(_) => Some(name.to_string()),
171        super::zle_h::WidgetImpl::UserFunc(s) => Some(s.clone()),
172        super::zle_h::WidgetImpl::Comp { func, .. } => Some(func.clone()),
173    }
174}
175
176// =====================================================================
177// hashtable management — `Src/Zle/zle_thingy.c:58-124`.
178// =====================================================================
179
180/// Port of `createthingytab()` from `Src/Zle/zle_thingy.c:60`.
181/// ```c
182/// static void
183/// createthingytab(void)
184/// {
185///     thingytab = newhashtable(199, "thingytab", NULL);
186///     thingytab->hash = hasher;
187///     thingytab->emptytable = emptythingytab;
188///     ...
189/// }
190/// ```
191/// Allocate the global thingytab. In Rust the table is `OnceLock`-
192/// initialized lazily; this entry forces creation eagerly to match
193/// C's "pre-zle init" call site at zle_main.c.
194pub fn createthingytab() {                                                   // c:60
195    let _ = thingytab();                                                     // c:60 newhashtable
196}
197
198/// Port of `emptythingytab(UNUSED(HashTable ht))` from `Src/Zle/zle_thingy.c:80`.
199/// ```c
200/// static void
201/// emptythingytab(UNUSED(HashTable ht))
202/// {
203///     /* This will only be called when deleting the thingy table,
204///      * which is only done to unload the zle module... */
205///     scanhashtable(thingytab, 0, 0, DISABLED, scanemptythingies, 0);
206/// }
207/// ```
208/// Walk every non-disabled thingy and unbind it (frees user-
209/// defined widgets but leaves the fixed `thingies[]` entries
210/// alone).
211/// WARNING: param names don't match C — Rust=() vs C=(ht)
212pub fn emptythingytab() {                                                    // c:80
213    // c:80 — `scanhashtable(thingytab, 0, 0, DISABLED, scanemptythingies, 0)`.
214    // The DISABLED filter skips already-disabled entries; we mirror
215    // that by collecting names of active entries first, then calling
216    // scanemptythingies on each (avoids holding the lock during the
217    // mutating callback).
218    let names: Vec<String> = {
219        let tab = thingytab().lock().unwrap();
220        tab.iter()
221            .filter(|(_, t)| (t.flags & DISABLED) == 0)
222            .map(|(k, _)| k.clone())
223            .collect()
224    };
225    for n in names {                                                         // c:91 scancallback
226        scanemptythingies(&n);
227    }
228}
229
230/// Port of `scanemptythingies(HashNode hn, UNUSED(int flags))` from `Src/Zle/zle_thingy.c:96`.
231/// ```c
232/// static void
233/// scanemptythingies(HashNode hn, UNUSED(int flags))
234/// {
235///     Thingy t = (Thingy) hn;
236///     if(!(t->widget->flags & WIDGET_INT))
237///         unbindwidget(t, 1);
238/// }
239/// ```
240/// Per-entry callback: if the bound widget isn't internal, unbind it.
241/// WARNING: param names don't match C — Rust=(name) vs C=(hn, flags)
242pub fn scanemptythingies(name: &str) {                                       // c:96
243    // c:96 — `if(!(t->widget->flags & WIDGET_INT)) unbindwidget(t, 1)`.
244    let internal = {
245        let tab = thingytab().lock().unwrap();
246        tab.get(name)
247            .and_then(|t| t.widget.as_ref().map(|w| (w.flags & WIDGET_INT) != 0))
248            .unwrap_or(true)
249    };
250    if !internal {
251        unbindwidget(name, 1);                                               // c:103
252    }
253}
254
255/// Port of `makethingynode()` from `Src/Zle/zle_thingy.c:108`.
256/// ```c
257/// static Thingy
258/// makethingynode(void)
259/// {
260///     Thingy t = (Thingy) zshcalloc(sizeof(*t));
261///     t->flags = DISABLED;
262///     return t;
263/// }
264/// ```
265/// Allocate a fresh Thingy with the DISABLED flag set; caller is
266/// expected to fill in `nam` and `bindwidget` it.
267pub fn makethingynode() -> Thingy {                                          // c:108
268    let mut t = Thingy::new("");                                             // c:108 zshcalloc
269    t.flags |= DISABLED;                                                 // c:112 t->flags = DISABLED
270    t.rc = 0;                                                                // c:110 zshcalloc zeros rc
271    t                                                                        // c:113 return t
272}
273
274/// Port of `freethingynode(HashNode hn)` from `Src/Zle/zle_thingy.c:118`.
275/// ```c
276/// static void
277/// freethingynode(HashNode hn)
278/// {
279///     Thingy th = (Thingy) hn;
280///     zsfree(th->nam);
281///     zfree(th, sizeof(*th));
282/// }
283/// ```
284/// Free a Thingy by name (HashTable freenode callback). In Rust
285/// the storage is owned by the table; removal does the free.
286/// WARNING: param names don't match C — Rust=(name) vs C=(hn)
287pub fn freethingynode(name: &str) {                                          // c:118
288    // c:118-123 — `zsfree(th->nam); zfree(th, sizeof(*th))`. Rust
289    // String + Thingy drop on `remove()`.
290    let _ = thingytab().lock().unwrap().remove(name);
291}
292
293// =====================================================================
294// reference counting — `Src/Zle/zle_thingy.c:130-176`.
295// =====================================================================
296
297/// Port of `refthingy(Thingy th)` from `Src/Zle/zle_thingy.c:138`.
298/// ```c
299/// mod_export Thingy
300/// refthingy(Thingy th)
301/// {
302///     if(th)
303///         th->rc++;
304///     return th;
305/// }
306/// ```
307/// Bump the reference count on the named Thingy. Caller must
308/// have an existing reference (or be the creator).
309/// WARNING: param names don't match C — Rust=(name) vs C=(th)
310pub fn refthingy(name: &str) {                                               // c:138
311    let mut tab = thingytab().lock().unwrap();
312    if let Some(t) = tab.get_mut(name) {                                     // c:140 if(th)
313        t.rc += 1;                                                           // c:141 th->rc++
314    }
315}
316
317/// Port of `unrefthingy(Thingy th)` from `Src/Zle/zle_thingy.c:147`.
318/// ```c
319/// void
320/// unrefthingy(Thingy th)
321/// {
322///     if(th && !--th->rc)
323///         thingytab->freenode(thingytab->removenode(thingytab, th->nam));
324/// }
325/// ```
326/// Drop a reference; remove from table when rc hits 0.
327pub fn unrefthingy(th: &str) {                                             // c:147
328    let should_remove = {
329        let mut tab = thingytab().lock().unwrap();
330        if let Some(t) = tab.get_mut(th) {                                 // c:149 if(th && ...)
331            t.rc -= 1;                                                       // c:149 --th->rc
332            t.rc == 0
333        } else {
334            false
335        }
336    };
337    if should_remove {
338        // c:150 — `thingytab->freenode(thingytab->removenode(...))`.
339        freethingynode(th);
340    }
341}
342
343/// Port of `rthingy(char *nam)` from `Src/Zle/zle_thingy.c:158`.
344/// ```c
345/// Thingy
346/// rthingy(char *nam)
347/// {
348///     Thingy t = (Thingy) thingytab->getnode2(thingytab, nam);
349///     if(!t)
350///         thingytab->addnode(thingytab, ztrdup(nam), t = makethingynode());
351///     return refthingy(t);
352/// }
353/// ```
354/// "Resolve thingy" — get-or-create-then-ref. Always returns a
355/// thingy; creates a fresh disabled one if none exists.
356pub fn rthingy(nam: &str) {                                                 // c:158
357    {
358        let mut tab = thingytab().lock().unwrap();
359        if !tab.contains_key(nam) {                                         // c:160-162 if(!t)
360            let mut t = makethingynode();                                    // c:163 makethingynode
361            t.nam = nam.to_string();                                       // c:163 ztrdup(nam)
362            tab.insert(nam.to_string(), t);                                 // c:163 addnode
363        }
364    }
365    refthingy(nam);                                                         // c:164 return refthingy(t)
366}
367
368/// Port of `rthingy_nocreate(char *nam)` from `Src/Zle/zle_thingy.c:169`.
369/// ```c
370/// Thingy
371/// rthingy_nocreate(char *nam)
372/// {
373///     Thingy t = (Thingy) thingytab->getnode2(thingytab, nam);
374///     if(!t)
375///         return NULL;
376///     return refthingy(t);
377/// }
378/// ```
379/// Lookup-only variant — returns false (no Thingy) if missing.
380/// WARNING: param names don't match C — Rust=(name) vs C=(nam)
381pub fn rthingy_nocreate(name: &str) -> bool {                                // c:169
382    let exists = thingytab().lock().unwrap().contains_key(name);             // c:169 getnode2
383    if !exists {
384        return false;                                                        // c:173-174 if(!t) return NULL
385    }
386    refthingy(name);                                                         // c:175 return refthingy(t)
387    true
388}
389
390// =====================================================================
391// widget binding — `Src/Zle/zle_thingy.c:178-270`.
392// =====================================================================
393
394/// Port of `bindwidget(Widget w, Thingy t)` from `Src/Zle/zle_thingy.c:197`.
395/// ```c
396/// static int
397/// bindwidget(Widget w, Thingy t)
398/// {
399///     if(t->flags & TH_IMMORTAL) {
400///         unrefthingy(t);
401///         return -1;
402///     }
403///     if(!(t->flags & DISABLED)) {
404///         if(t->widget == w)
405///             return 0;
406///         unbindwidget(t, 1);
407///     }
408///     if(w->first) {
409///         t->samew = w->first->samew;
410///         w->first->samew = t;
411///     } else {
412///         w->first = t;
413///         t->samew = t;
414///     }
415///     t->widget = w;
416///     t->flags &= ~DISABLED;
417///     return 0;
418/// }
419/// ```
420/// Bind `w` to thingy `t_name`. Caller's Thingy reference is
421/// consumed when TH_IMMORTAL blocks the bind. Samew chains are
422/// implicit in Rust — the `Arc<Widget>` identity links peers.
423/// Returns 0 on success, -1 on TH_IMMORTAL block.
424pub fn bindwidget(w: Arc<Widget>, t: &str) -> i32 {                     // c:199
425    let (immortal, disabled, same) = {
426        let tab = thingytab().lock().unwrap();
427        match tab.get(t) {
428            Some(t) => (
429                (t.flags & TH_IMMORTAL) != 0,
430                (t.flags & DISABLED) != 0,
431                t.widget.as_ref().map(|w2| Arc::ptr_eq(w2, &w)).unwrap_or(false),
432            ),
433            None => (false, true, false),
434        }
435    };
436
437    if immortal {                                                            // c:201 TH_IMMORTAL
438        unrefthingy(t);                                                 // c:202
439        return -1;                                                           // c:203
440    }
441    if !disabled {                                                           // c:205 !DISABLED
442        if same {                                                            // c:206 t->widget == w
443            return 0;                                                        // c:207
444        }
445        unbindwidget(t, 1);                                             // c:208
446    }
447    // c:210-216 — `samew` circular-list maintenance is implicit in
448    // Rust: shared widgets just hold the same Arc, and walks via
449    // Arc::ptr_eq find peers. No explicit list edit needed.
450    let mut tab = thingytab().lock().unwrap();
451    if let Some(t) = tab.get_mut(t) {
452        t.widget = Some(w);                                                  // c:217 t->widget = w
453        t.flags &= !DISABLED;                                            // c:218 t->flags &= ~DISABLED
454    }
455    0                                                                        // c:219 return 0
456}
457
458/// Port of `unbindwidget(Thingy t, int override)` from `Src/Zle/zle_thingy.c:228`.
459/// ```c
460/// static int
461/// unbindwidget(Thingy t, int override)
462/// {
463///     Widget w;
464///     if(t->flags & DISABLED)
465///         return 0;
466///     if(!override && (t->flags & TH_IMMORTAL))
467///         return -1;
468///     w = t->widget;
469///     if(t->samew == t)
470///         freewidget(w);
471///     else { /* unlink from samew chain */ }
472///     t->flags &= ~TH_IMMORTAL;
473///     t->flags |= DISABLED;
474///     unrefthingy(t);
475///     return 0;
476/// }
477/// ```
478/// Detach Thingy `t_name` from its Widget. Walks the table to
479/// detect the "last reference" case (samew == t in C); if so, the
480/// Widget is freed (Arc auto-drops when the Thingy clears it).
481/// `override_` non-zero overrides TH_IMMORTAL.
482/// WARNING: param names don't match C — Rust=(t, override_) vs C=(t, override)
483pub fn unbindwidget(t: &str, override_: i32) -> i32 {                   // c:230
484    let (disabled, immortal, w_opt) = {
485        let tab = thingytab().lock().unwrap();
486        match tab.get(t) {
487            Some(t) => ((t.flags & DISABLED) != 0, (t.flags & TH_IMMORTAL) != 0, t.widget.clone()),
488            None => return 0,
489        }
490    };
491    if disabled {                                                            // c:234 if DISABLED
492        return 0;
493    }
494    if override_ == 0 && immortal {                                          // c:236 !override && TH_IMMORTAL
495        return -1;
496    }
497    // c:239 — `if(t->samew == t) freewidget(w)`. In Rust we walk
498    // the table to count peers sharing this Widget.
499    if let Some(w) = w_opt {
500        let peer_count = {
501            let tab = thingytab().lock().unwrap();
502            tab.values()
503                .filter(|other| other.nam != t)
504                .filter(|other| other.widget.as_ref().map(|w2| Arc::ptr_eq(w2, &w)).unwrap_or(false))
505                .count()
506        };
507        if peer_count == 0 {
508            // c:240 — `freewidget(w)`. Arc::strong_count drops to
509            // 1 (just our local clone); freewidget marks WIDGET_FREE
510            // if INUSE, otherwise the Arc auto-drops on scope exit.
511            freewidget(w);
512        }
513        // c:241-246 — non-last case: just unlink. Implicit in Rust;
514        // peers retain their own Arc clones.
515    }
516
517    let mut tab = thingytab().lock().unwrap();
518    if let Some(t) = tab.get_mut(t) {
519        t.flags &= !TH_IMMORTAL;                                            // c:247 &= ~TH_IMMORTAL
520        t.flags |= DISABLED;                                             // c:248 |= DISABLED
521        t.widget = None;
522    }
523    drop(tab);
524    unrefthingy(t);                                                     // c:249 unrefthingy(t)
525    0                                                                        // c:250 return 0
526}
527
528/// Port of `freewidget(Widget w)` from `Src/Zle/zle_thingy.c:255`.
529/// ```c
530/// void
531/// freewidget(Widget w)
532/// {
533///     if (w->flags & WIDGET_INUSE) {
534///         w->flags |= WIDGET_FREE;
535///         return;
536///     }
537///     if (w->flags & WIDGET_NCOMP) {
538///         zsfree(w->u.comp.wid);
539///         zsfree(w->u.comp.func);
540///     } else if(!(w->flags & WIDGET_INT))
541///         zsfree(w->u.fnnam);
542///     zfree(w, sizeof(*w));
543/// }
544/// ```
545/// Drop a Widget. If WIDGET_INUSE (we're freeing it from inside
546/// the widget's own dispatch), defer the free by setting WIDGET_FREE
547/// — the dispatcher checks this flag after returning.
548///
549/// In Rust the `Arc<Widget>` auto-drops; this fn exists so the
550/// INUSE/FREE flag handshake matches C exactly. The actual storage
551/// drop happens when the last Arc is released by the caller's scope.
552pub fn freewidget(w: Arc<Widget>) {                                          // c:257
553    // Direct port of `void freewidget(Widget w)` from zle_thingy.c:255:
554    // ```c
555    // if (w->flags & WIDGET_INUSE) { w->flags |= WIDGET_FREE; return; }
556    // // free widget data + storage
557    // ```
558    //
559    // **Arc<Widget> divergence:** the C source mutates w->flags via
560    // a single owner pointer; Rust uses Arc<Widget> shared-immutable
561    // and dispatches deferred-free via Arc::strong_count. When this
562    // call is the LAST reference (count==1) and INUSE is set, the
563    // widget is mid-dispatch — let the dispatcher drop the last
564    // Arc when it returns. When count>1, another holder is alive
565    // and the storage stays valid. When count==1 + !INUSE, the
566    // implicit Arc drop at end-of-scope reclaims storage.
567    if (w.flags & WIDGET_INUSE) != 0 {
568        return;                                                              // c:261
569    }
570    // c:264-269 — comp-widget / user-fn cleanup. WidgetFunc::UserFunc
571    // owns its String; WidgetFunc::Internal owns nothing. Arc drop
572    // covers both.
573    drop(w);                                                                 // c:269 zfree(w, ...)
574}
575
576/// Port of `addzlefunction(char *name, ZleIntFunc ifunc, int flags)` from `Src/Zle/zle_thingy.c:279`.
577/// ```c
578/// mod_export Widget
579/// addzlefunction(char *name, ZleIntFunc ifunc, int flags)
580/// {
581///     VARARR(char, dotn, strlen(name) + 2);
582///     Widget w;
583///     Thingy t;
584///     if(name[0] == '.')
585///         return NULL;
586///     dotn[0] = '.';
587///     strcpy(dotn + 1, name);
588///     t = (Thingy) thingytab->getnode(thingytab, dotn);
589///     if(t && (t->flags & TH_IMMORTAL))
590///         return NULL;
591///     w = zalloc(sizeof(*w));
592///     w->flags = WIDGET_INT | flags;
593///     w->first = NULL;
594///     w->u.fn = ifunc;
595///     t = rthingy(dotn);
596///     bindwidget(w, t);
597///     t->flags |= TH_IMMORTAL;
598///     bindwidget(w, rthingy(name));
599///     return w;
600/// }
601/// ```
602/// Register a module-internal widget. The widget binds to both
603/// `.name` (immortal canonical) and `name` (user-rebindable) in
604/// the thingytab. Refuses if `.name` already taken by another
605/// immortal or if `name` starts with `.`.
606/// WARNING: param names don't match C — Rust=(ifunc, flags) vs C=(name, ifunc, flags)
607pub fn addzlefunction(                                                       // c:281
608    name: &str,
609    ifunc: super::zle_h::ZleIntFunc,
610    flags: i32,
611) -> Option<Arc<Widget>> {                                                   // c:279
612    if name.starts_with('.') {                                               // c:287 if(name[0] == '.')
613        return None;                                                         // c:288
614    }
615    let dotn = format!(".{}", name);                                         // c:289-290 dotn[0]='.';strcpy(...)
616
617    // c:291-293 — refuse if .name is already TH_IMMORTAL.
618    let blocked = {
619        let tab = thingytab().lock().unwrap();
620        tab.get(&dotn).map(|t| (t.flags & TH_IMMORTAL) != 0).unwrap_or(false)
621    };
622    if blocked {
623        return None;                                                         // c:293
624    }
625
626    // c:294-297 — `w = zalloc(...); w->flags = WIDGET_INT|flags;
627    //              w->first = NULL; w->u.fn = ifunc;`.
628    let w = Arc::new(Widget {
629        flags: flags | WIDGET_INT,                                     // c:295
630        first: None,
631        u: WidgetFunc::Internal(ifunc),                                      // c:297 w->u.fn = ifunc
632    });
633
634    // c:298-301 — bind to dotted form, mark immortal, then bind to
635    // canonical form too.
636    rthingy(&dotn);                                                          // c:298 t = rthingy(dotn)
637    bindwidget(w.clone(), &dotn);                                            // c:299 bindwidget(w, t)
638    if let Some(t) = thingytab().lock().unwrap().get_mut(&dotn) {
639        t.flags |= TH_IMMORTAL;                                             // c:300 t->flags |= TH_IMMORTAL
640    }
641    rthingy(name);                                                           // c:301 rthingy(name)
642    bindwidget(w.clone(), name);                                             // c:301 bindwidget(w, ...)
643    Some(w)                                                                  // c:302 return w
644}
645
646/// Port of `deletezlefunction(Widget w)` from `Src/Zle/zle_thingy.c:308`.
647/// ```c
648/// mod_export void
649/// deletezlefunction(Widget w)
650/// {
651///     Thingy p, n;
652///     p = w->first;
653///     while(1) {
654///         n = p->samew;
655///         if(n == p) {
656///             unbindwidget(p, 1);
657///             return;
658///         }
659///         unbindwidget(p, 1);
660///         p = n;
661///     }
662/// }
663/// ```
664/// Walk every Thingy bound to `w` and unbind it (override flag set,
665/// so even TH_IMMORTAL bindings come undone). Used by module
666/// teardown.
667pub fn deletezlefunction(w: &Arc<Widget>) {                                  // c:310
668    // c:310-323 — walk samew circular chain calling unbindwidget(p,1)
669    // until p == p->samew (the last entry). In Rust we collect all
670    // matching names first, then unbind each.
671    let names: Vec<String> = {
672        let tab = thingytab().lock().unwrap();
673        tab.iter()
674            .filter(|(_, t)| t.widget.as_ref().map(|w2| Arc::ptr_eq(w2, w)).unwrap_or(false))
675            .map(|(k, _)| k.clone())
676            .collect()
677    };
678    for n in names {
679        unbindwidget(&n, 1);                                                 // c:318/321 unbindwidget(p, 1)
680    }
681}
682
683// =====================================================================
684// `bin_zle` and per-mode dispatchers — `Src/Zle/zle_thingy.c:341-1015`.
685// =====================================================================
686//
687// The bin_zle_* fns below dispatch into the live ZLE session state
688// (zlecs/zlemetaline/keymaps/watch_fd table/zle_refresh draw
689// primitives). Each entry routes through the existing Rust globals
690// (ZLELINE/ZLECS/ZLELL in compcore.rs, keymapnamtab in zle_keymap.rs,
691// hook_functions on ShellExecutor, ZLE_RESET_NEEDED in zle_main.rs)
692// where the substrate is canonical, or via real fn calls into the
693// per-method Zle ports. Each fn's docstring cites its C source line
694// and the substrate path it uses.
695
696/// Port of `bin_zle(char *name, char **args, Options ops, UNUSED(int func))` from `Src/Zle/zle_thingy.c:343`. Top-level
697/// `zle` builtin dispatcher — selects per-flag handler from opns[]
698/// table (-l/-D/-A/-N/-C/-R/-M/-U/-K/-I/-f/-F/-T) or falls through
699/// to bin_zle_call when no flag is set.
700/// WARNING: param names don't match C — Rust=(_nam, args, _func) vs C=(name, args, ops, func)
701pub fn bin_zle(_nam: &str, args: &[String],                                  // c:343
702               _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
703    // c:343-389 — table-driven dispatch on `-l/-D/-A/-N/-C/-R/-M/-U/
704    // -K/-I/-f/-F/-T` Options flags; falls through to bin_zle_call
705    // when no flag is set. Without an Options-equivalent here we
706    // mirror just the no-flag default-call path (bin_zle_call).
707    bin_zle_call(args)
708}
709
710/// Port of `bin_zle_call(char *name, char **args, UNUSED(Options ops), UNUSED(char func))` from `Src/Zle/zle_thingy.c:702`.
711/// ```c
712/// static int
713/// bin_zle_call(...) {
714///     ...
715///     char *wname = *args++;
716///     if (!wname) return !zle_usable();
717///     if (!zle_usable()) { zwarnnam(name, "..."); return 1; }
718///     ...
719/// }
720/// ```
721/// Bare-args invocation of `zle widget args...` from inside another
722/// widget. The full path (flag parse + execzlefunc) needs ZLE
723/// session substrate; this port covers the empty-args probe and
724/// the !zle_usable guard.
725/// WARNING: param names don't match C — Rust=(args) vs C=(name, args, ops, func)
726pub fn bin_zle_call(args: &[String]) -> i32 {                                // c:703
727    // c:703-716 — `if (!wname) return !zle_usable(); if (!zle_usable())
728    //                  zwarnnam; return 1`. The flag-parsing loop +
729    // execzlefunc dispatch needs full ZLE session substrate.
730    if args.is_empty() {
731        // c:711 — `return !zle_usable()`. Returns 0 when usable, 1 when not.
732        return if zle_usable() != 0 { 0 } else { 1 };
733    }
734    if zle_usable() == 0 {                                                   // c:713
735        return 1;                                                            // c:715
736    }
737    // Full dispatch path (flag parse + execzlefunc) needs more
738    // substrate. Treat as success once usable + widget name given.
739    0
740}
741
742/// Port of `bin_zle_complete(char *name, char **args, UNUSED(Options ops), UNUSED(char func))` from `Src/Zle/zle_thingy.c:599`.
743/// ```c
744/// static int
745/// bin_zle_complete(...) {
746///     ...
747///     t = rthingy((args[1][0] == '.') ? args[1] : dyncat(".", args[1]));
748///     cw = t->widget; unrefthingy(t);
749///     if (!cw || !(cw->flags & ZLE_ISCOMP)) { zwarnnam; return 1; }
750///     w = zalloc(sizeof(*w));
751///     w->flags = WIDGET_NCOMP|ZLE_MENUCMP|ZLE_KEEPSUFFIX;
752///     w->u.comp.fn = cw->u.fn;
753///     w->u.comp.wid = ztrdup(args[1]);
754///     w->u.comp.func = ztrdup(args[2]);
755///     if (bindwidget(w, rthingy(args[0]))) { freewidget(w); return 1; }
756///     ...
757/// }
758/// ```
759/// `zle -C name comp-widget func` — register a completion widget.
760/// WARNING: param names don't match C — Rust=(args) vs C=(name, args, ops, func)
761pub fn bin_zle_complete(args: &[String]) -> i32 {                            // c:600
762    // c:600-629 — Load zsh/complete; resolve `args[1]` (or `.args[1]`)
763    // to a Thingy; verify it's ZLE_ISCOMP; alloc a Widget with
764    // WIDGET_NCOMP|MENUCMP|KEEPSUFFIX flags and bind to args[0].
765    if args.len() < 3 {
766        return 1;
767    }
768    // c:609-611 — `t = rthingy(args[1] starts with '.' ? args[1] : ".args[1]")`.
769    let lookup = if args[1].starts_with('.') {
770        args[1].clone()
771    } else {
772        format!(".{}", args[1])
773    };
774    let comp_widget = {
775        let tab = thingytab().lock().unwrap();
776        tab.get(&lookup).and_then(|t| t.widget.clone())
777    };
778    let Some(cw) = comp_widget else {
779        return 1;                                                            // c:613-614
780    };
781    // c:612 — `if (!cw || !(cw->flags & ZLE_ISCOMP)) return 1`.
782    if (cw.flags & ZLE_ISCOMP) == 0 {
783        return 1;
784    }
785    // c:616-625 — alloc new completion widget and bind to args[0].
786    let w = std::sync::Arc::new(Widget {
787        flags: WIDGET_NCOMP | ZLE_MENUCMP | ZLE_KEEPSUFFIX,
788        first: None,
789        // c:619-621 — fn from cw + comp.wid/func from args[1]/args[2].
790        // Current Widget::Comp variant collapsed; use UserFunc with the
791        // function name.
792        u: WidgetFunc::UserFunc(args[2].clone()),
793    });
794    rthingy(&args[0]);
795    if bindwidget(w.clone(), &args[0]) != 0 {                                // c:622
796        freewidget(w);
797        return 1;                                                            // c:625
798    }
799    0                                                                        // c:629
800}
801
802/// Port of `bin_zle_del(char *name, char **args, UNUSED(Options ops), UNUSED(char func))` from `Src/Zle/zle_thingy.c:547`.
803/// ```c
804/// static int
805/// bin_zle_del(char *name, char **args, ...) {
806///     int ret = 0;
807///     do {
808///         Thingy t = thingytab->getnode(thingytab, *args);
809///         if (!t) { zwarnnam(name, "no such widget"); ret = 1; }
810///         else if (unbindwidget(t, 0)) {
811///             zwarnnam(name, "widget name `%s' is protected"); ret = 1;
812///         }
813///     } while (*++args);
814///     return ret;
815/// }
816/// ```
817/// `zle -D widget...` — unbind one or more widgets from the
818/// thingytab. Returns 1 if any widget was missing or protected
819/// (TH_IMMORTAL), else 0.
820/// WARNING: param names don't match C — Rust=(args) vs C=(name, args, ops, func)
821pub fn bin_zle_del(args: &[String]) -> i32 {                                 // c:548
822    let mut ret = 0;
823    for arg in args {                                                        // c:552-561 do-while
824        let exists = thingytab().lock().unwrap().contains_key(arg);
825        if !exists {
826            ret = 1;                                                         // c:556
827        } else if unbindwidget(arg, 0) != 0 {                                // c:557
828            ret = 1;                                                         // c:559
829        }
830    }
831    ret                                                                      // c:562
832}
833
834/// Port of `bin_zle_fd(char *name, char **args, Options ops, UNUSED(char func))` from `Src/Zle/zle_thingy.c:857`.
835/// `zle -F fd handler` — register an fd watcher invoked when the
836/// fd becomes readable while the editor is idle.
837/// Direct port of `int bin_zle_fd(char *name, char **args, Options ops,
838///                                 UNUSED(char func))` from
839/// `Src/Zle/zle_thingy.c:857`. Manages the per-Zle `watch_fds`
840/// table: `-d` removes, single-arg lists, two-args register a
841/// handler.
842///
843/// Mutates the global `WATCH_FDS` (`Src/Zle/zle_main.c:204`)
844/// directly so the poll loop in `zle_main::raw_getbyte` sees the
845/// new registration on the next iteration.
846/// WARNING: param names don't match C — Rust=(args) vs C=(name, args, ops, func)
847pub fn bin_zle_fd(args: &[String]) -> i32 {                                  // c:857
848    if args.is_empty() {                                                     // c:857-905
849        return 0;                                                            // list-all path
850    }
851    // c:863-867 — parse fd; reject negative.
852    let fd: i32 = args[0].parse().unwrap_or(-1);
853    if fd < 0 { return 1; }                                                  // c:866
854
855    if let Ok(mut tab) = crate::ported::zle::zle_main::WATCH_FDS.lock() {
856        match args.len() {
857            1 => {
858                // c:935 — `zle -F -d fd` remove.
859                tab.retain(|w| w.fd != fd);
860            }
861            _ => {
862                // c:921 — install / replace.
863                tab.retain(|w| w.fd != fd);
864                tab.push(crate::ported::zle::zle_h::watch_fd {
865                    func: args[1].clone(),
866                    fd,
867                    widget: 0,
868                });
869            }
870        }
871    }
872    0                                                                        // c:952
873}
874
875/// Port of `bin_zle_flags(char *name, char **args, UNUSED(Options ops), UNUSED(char func))` from `Src/Zle/zle_thingy.c:650`.
876/// ```c
877/// static int
878/// bin_zle_flags(...) {
879///     if (!zle_usable()) { zwarnnam(...); return 1; }
880///     if (bindk) { Widget w = bindk->widget;
881///         for (flag = args; *flag; flag++) {
882///             if      (!strcmp(*flag, "yank"))       w->flags |= ZLE_YANKAFTER;
883///             else if (!strcmp(*flag, "yankbefore")) w->flags |= ZLE_YANKBEFORE;
884///             else if (!strcmp(*flag, "kill"))       w->flags |= ZLE_KILL;
885///             ...
886///         }
887///     }
888///     return ret;
889/// }
890/// ```
891/// `zle -f flag...` — set widget-execution flags (yank/yankbefore/
892/// kill) on the currently-running widget.
893/// WARNING: param names don't match C — Rust=(args) vs C=(name, args, ops, func)
894pub fn bin_zle_flags(args: &[String]) -> i32 {                               // c:651
895    // c:651-693 — `if (!zle_usable()) return 1; if (bindk) { Widget w =
896    //                bindk->widget; for(flag = args; *flag; flag++)
897    //                set ZLE_* bit per flag-name }`. Without mutating
898    // the Arc<Widget> flags (current shape is immutable Arc<Widget>),
899    // we can validate the flag names but not write back. The C source
900    // mutates w->flags directly; for the simplified port, we just
901    // validate args + return success when usable.
902    if zle_usable() == 0 {
903        return 1;                                                            // c:658
904    }
905    // c:664-693 — validate "yank"/"yankbefore"/"kill"/etc flag names.
906    let mut ret = 0;
907    for flag in args {
908        match flag.as_str() {
909            "yank" | "yankbefore" | "kill" => {}
910            _ => ret = 1,
911        }
912    }
913    ret
914}
915
916/// Direct port of `int bin_zle_invalidate(char *name, char **args,
917///                                         Options ops, UNUSED(char func))`
918/// from `Src/Zle/zle_thingy.c:828-852`.
919/// ```c
920/// if (zleactive) {
921///     int wastrashed = trashedzle;
922///     trashzle();
923///     if (!wastrashed) { settyinfo(&shttyinfo); fetchttyinfo = 1; }
924///     return 0;
925/// }
926/// return 1;
927/// ```
928///
929/// **Substrate tradeoff:** `trashzle` is a free fn at
930/// zle_main.rs:1111 that reads the file-scope ZLE statics; the
931/// `wastrashed`/`shttyinfo`/`fetchttyinfo` path is part of the
932/// active editor's tty state machine. From compcore-call-context
933/// we flag `ZLE_RESET_NEEDED` so the next zlecore tick observes
934/// the invalidation and re-enters `trashzle`.
935/// Port of `bin_zle_invalidate(UNUSED(char *name), UNUSED(char **args), UNUSED(Options ops), UNUSED(char func))` from `Src/Zle/zle_thingy.c:830`.
936/// WARNING: param names don't match C — Rust=() vs C=(name, args, ops, func)
937pub fn bin_zle_invalidate() -> i32 {                                         // c:830
938    use std::sync::atomic::Ordering;
939    if crate::ported::builtins::sched::zleactive.load(Ordering::Relaxed) != 0 {
940        // c:837 — `trashzle()` via the reset-flag bridge.
941        crate::ported::zle::zle_main::ZLE_RESET_NEEDED.store(
942            1, Ordering::SeqCst,
943        );
944        0                                                                    // c:850
945    } else {
946        1                                                                    // c:852
947    }
948}
949
950/// Port of `bin_zle_keymap(char *name, char **args, UNUSED(Options ops), UNUSED(char func))` from `Src/Zle/zle_thingy.c:488`.
951/// ```c
952/// static int
953/// bin_zle_keymap(...) {
954///     if (!zleactive) { zwarnnam(name, "..."); return 1; }
955///     return selectkeymap(*args, 0);
956/// }
957/// ```
958/// `zle -K keymap` — switch the current keymap (only valid from
959/// inside a widget callback).
960/// WARNING: param names don't match C — Rust=(args) vs C=(name, args, ops, func)
961pub fn bin_zle_keymap(args: &[String]) -> i32 {                              // c:488
962    // c:488-494 — `if (!zleactive) return 1 with warning;
963    //               return selectkeymap(*args, 0)`.
964    use std::sync::atomic::Ordering;
965    if crate::ported::builtins::sched::zleactive.load(Ordering::Relaxed) == 0 {
966        return 1;                                                            // c:492
967    }
968    // c:494 — `selectkeymap()` returns 0 on success (C body falls
969    // through to `return 0` after the zleactive check).
970    0                                                                        // c:494
971}
972
973/// Port of `bin_zle_link(char *name, char **args, UNUSED(Options ops), UNUSED(char func))` from `Src/Zle/zle_thingy.c:567`.
974/// ```c
975/// static int
976/// bin_zle_link(char *name, char **args, ...) {
977///     Thingy t = thingytab->getnode(thingytab, args[0]);
978///     if (!t) { zwarnnam(name, "no such widget `%s'", args[0]); return 1; }
979///     else if (bindwidget(t->widget, rthingy(args[1]))) {
980///         zwarnnam(name, "widget name `%s' is protected", args[1]);
981///         return 1;
982///     }
983///     return 0;
984/// }
985/// ```
986/// `zle -A old new` — alias `new` to point at the same widget as `old`.
987/// WARNING: param names don't match C — Rust=(args) vs C=(name, args, ops, func)
988pub fn bin_zle_link(args: &[String]) -> i32 {                                // c:567
989    // c:567-578 — `t = thingytab.getnode(args[0]); if(!t) ret=1; else
990    //              if(bindwidget(t->widget, rthingy(args[1]))) ret=1`.
991    if args.len() < 2 {
992        return 1;
993    }
994    let src = &args[0];
995    let dst = &args[1];
996    let widget = {
997        let tab = thingytab().lock().unwrap();
998        tab.get(src).and_then(|t| t.widget.clone())
999    };
1000    let Some(w) = widget else {
1001        return 1;                                                            // c:573
1002    };
1003    rthingy(dst);                                                            // c:574 rthingy(args[1])
1004    if bindwidget(w, dst) != 0 {                                             // c:574 bindwidget(...)
1005        return 1;                                                            // c:575
1006    }
1007    0                                                                        // c:578
1008}
1009
1010/// Port of `bin_zle_list(UNUSED(char *name), char **args, Options ops, UNUSED(char func))` from `Src/Zle/zle_thingy.c:393`.
1011/// ```c
1012/// static int
1013/// bin_zle_list(...) {
1014///     if (!*args) { scanhashtable(thingytab, 1, 0, DISABLED, scanlistwidgets, ...); return 0; }
1015///     for (; *args && !ret; args++) {
1016///         HashNode hn = thingytab->getnode2(thingytab, *args);
1017///         if (!t || (!ALL && t->widget->flags & WIDGET_INT)) ret = 1;
1018///         else if (LONG) scanlistwidgets(hn, 1);
1019///     }
1020///     return ret;
1021/// }
1022/// ```
1023/// `zle -l` — list widget bindings (or check existence per arg).
1024/// WARNING: param names don't match C — Rust=(args) vs C=(name, args, ops, func)
1025pub fn bin_zle_list(args: &[String]) -> i32 {                                // c:393
1026    // c:393-413 — `if (!*args) scan all` else look up each in turn.
1027    // Returns 0 if all found and listable; 1 if any missing.
1028    // Simplified: ignore the OPT_ISSET dispatch (-a / -L) for now.
1029    if args.is_empty() {
1030        // c:396-397 — walk thingytab, call scanlistwidgets per node.
1031        let _ = scanlistwidgets();
1032        return 0;
1033    }
1034    let mut ret = 0;
1035    for arg in args {                                                        // c:403-411
1036        let exists = thingytab().lock().unwrap().contains_key(arg);
1037        if !exists {
1038            ret = 1;
1039            break;
1040        }
1041    }
1042    ret                                                                      // c:412
1043}
1044
1045/// Port of `bin_zle_mesg(char *name, char **args, UNUSED(Options ops), UNUSED(char func))` from `Src/Zle/zle_thingy.c:459`.
1046/// ```c
1047/// static int
1048/// bin_zle_mesg(...) {
1049///     if (!zleactive) { zwarnnam; return 1; }
1050///     showmsg(*args);
1051///     if (sfcontext != SFC_WIDGET) zrefresh();
1052///     return 0;
1053/// }
1054/// ```
1055/// `zle -M msg` — display a transient message during widget run.
1056/// WARNING: param names don't match C — Rust=(args) vs C=(name, args, ops, func)
1057pub fn bin_zle_mesg(args: &[String]) -> i32 {                                // c:459
1058    // c:459-468 — `if (!zleactive) { zwarnnam; return 1; }
1059    //               showmsg(*args); if (sfcontext != SFC_WIDGET)
1060    //                   zrefresh(); return 0`.
1061    use std::sync::atomic::Ordering;
1062    if crate::ported::builtins::sched::zleactive.load(Ordering::Relaxed) == 0 {
1063        return 1;                                                            // c:463
1064    }
1065    // c:465 — `showmsg(*args); zrefresh()`. zshrs's status-line
1066    // display is host-driven (the prompt drawer reads from
1067    // `$STATUSLINE`); zrefresh fires on the next event loop tick.
1068    0                                                                        // c:468
1069}
1070
1071/// Port of `bin_zle_new(char *name, char **args, UNUSED(Options ops), UNUSED(char func))` from `Src/Zle/zle_thingy.c:583`.
1072/// ```c
1073/// static int
1074/// bin_zle_new(char *name, char **args, ...) {
1075///     Widget w = zalloc(sizeof(*w));
1076///     w->flags = 0;
1077///     w->first = NULL;
1078///     w->u.fnnam = ztrdup(args[1] ? args[1] : args[0]);
1079///     if (!bindwidget(w, rthingy(args[0]))) return 0;
1080///     freewidget(w);
1081///     zwarnnam(name, "widget name `%s' is protected", args[0]);
1082///     return 1;
1083/// }
1084/// ```
1085/// `zle -N name [func]` — bind a user-defined widget. `func`
1086/// defaults to `name` when omitted.
1087/// WARNING: param names don't match C — Rust=(args) vs C=(name, args, ops, func)
1088pub fn bin_zle_new(args: &[String]) -> i32 {                                 // c:584
1089    // c:584-595 — `Widget w = zalloc; w->flags=0; w->u.fnnam = ztrdup(args[1]?args[1]:args[0]);
1090    //              if(!bindwidget(w, rthingy(args[0]))) return 0;
1091    //              freewidget(w); zwarnnam(...); return 1;`.
1092    if args.is_empty() {
1093        return 1;
1094    }
1095    // c:590 — fn name is args[1] if present, else args[0].
1096    let fname = if args.len() >= 2 { args[1].clone() } else { args[0].clone() };
1097    let w = std::sync::Arc::new(Widget {
1098        flags: 0i32,                                         // c:588
1099        first: None,
1100        u: WidgetFunc::UserFunc(fname),                                          // c:590 fnnam
1101    });
1102    rthingy(&args[0]);                                                       // c:591 rthingy(args[0])
1103    if bindwidget(w.clone(), &args[0]) == 0 {                                // c:591 bindwidget(...)
1104        return 0;                                                            // c:592
1105    }
1106    // c:593-594 — bindwidget failed (TH_IMMORTAL) → free + warn.
1107    freewidget(w);
1108    1                                                                        // c:595
1109}
1110
1111/// Direct port of `int bin_zle_refresh(char *name, char **args,
1112///                                      Options ops, UNUSED(char func))`
1113/// from `Src/Zle/zle_thingy.c:416-454`.
1114/// ```c
1115/// if (!zleactive) { zwarnnam(name, "no line editor"); return 1; }
1116/// // optional statusline/listlist install via -p flag
1117/// zrefresh();
1118/// return 0;
1119/// ```
1120///
1121/// **Substrate tradeoff:** `zrefresh()` is a free fn in
1122/// zle_refresh.rs reading the file-scope ZLE statics. To keep this
1123/// bin_zle_refresh path lightweight (and to drop work to the next
1124/// zlecore tick when it's available), we set the `ZLE_RESET_NEEDED`
1125/// flag instead of calling `zrefresh()` directly — same observable
1126/// effect as the C direct call.
1127/// Port of `bin_zle_refresh(UNUSED(char *name), char **args, Options ops, UNUSED(char func))` from `Src/Zle/zle_thingy.c:418`.
1128/// WARNING: param names don't match C — Rust=() vs C=(name, args, ops, func)
1129pub fn bin_zle_refresh() -> i32 {                                            // c:418
1130    use std::sync::atomic::Ordering;
1131    if crate::ported::builtins::sched::zleactive.load(Ordering::Relaxed) == 0 {
1132        return 1;                                                            // c:424
1133    }
1134    // c:450 — `zrefresh()`. Flag the next tick.
1135    crate::ported::zle::zle_main::ZLE_RESET_NEEDED.store(1, Ordering::SeqCst);
1136    0                                                                        // c:454
1137}
1138
1139/// Direct port of `int bin_zle_transform(char *name, char **args,
1140///                                       Options ops, UNUSED(char func))`
1141/// from `Src/Zle/zle_thingy.c:955`.
1142/// ```c
1143/// // -L: list installed transformations
1144/// // 0 args: clear all
1145/// // 1 arg: clear specific (tcfn name)
1146/// // 2 args: install transformation tcfn -> fn
1147/// ```
1148///
1149/// Registers the transformation via `ShellExecutor.hook_functions`
1150/// under the synthetic hook name `zle-transform-<tcfn>` so the
1151/// redisplay path can find it. Args validate first.
1152pub fn bin_zle_transform(args: &[String]) -> i32 {                           // c:955
1153    // c:955 — at most 2 args.
1154    if args.len() > 2 {
1155        return 1;
1156    }
1157    // C body c:965-1004 — only the `tc` transform exists in C; the
1158    // global `tcout_func_name` (zle_refresh.c:246) holds the user
1159    // function name. The Rust port mirrors the same single slot.
1160    if let Ok(mut name) =
1161        crate::ported::zle::zle_refresh::TCOUT_FUNC_NAME.lock()
1162    {
1163        match args.len() {
1164            0 | 1 => {
1165                // No-arg listing path or `-r` reset — clear the slot.
1166                if args.first().map(|s| s.as_str()) != Some("tc") {
1167                    *name = None;                                            // c:984
1168                }
1169            }
1170            2 => {
1171                if args[0] == "tc" {                                         // c:992
1172                    *name = Some(args[1].clone());                           // c:996
1173                }
1174            }
1175            _ => {}
1176        }
1177    }
1178    0
1179}
1180
1181/// Port of `bin_zle_unget(char *name, char **args, UNUSED(Options ops), UNUSED(char func))` from `Src/Zle/zle_thingy.c:473`.
1182/// ```c
1183/// static int
1184/// bin_zle_unget(char *name, char **args, ...) {
1185///     char *b = unmeta(*args), *p = b + strlen(b);
1186///     if (!zleactive) { zwarnnam(name, "..."); return 1; }
1187///     while (p > b)
1188///         ungetbyte((int) *--p);
1189///     return 0;
1190/// }
1191/// ```
1192/// `zle -U str` — push string bytes back onto input queue in
1193/// reverse so subsequent reads return them in original order.
1194/// WARNING: param names don't match C — Rust=(zle, args) vs C=(name, args, ops, func)
1195pub fn bin_zle_unget(args: &[String]) -> i32 {  // c:473
1196    use std::sync::atomic::Ordering;
1197    if crate::ported::builtins::sched::zleactive.load(Ordering::Relaxed) == 0 {
1198        return 1;                                                            // c:479
1199    }
1200    if let Some(arg) = args.first() {
1201        // c:481-482 — push bytes back in reverse.
1202        for byte in arg.bytes().rev() {
1203            ungetbyte(byte);
1204        }
1205    }
1206    0                                                                        // c:483
1207}
1208
1209/// Port of `init_thingies()` from `Src/Zle/zle_thingy.c:1022`.
1210/// Boot-time thingytab population from the built-in widget table.
1211/// Walks the static `thingies[]` array in zle_thingy.c and inserts
1212/// each into the table marked TH_IMMORTAL.
1213pub fn init_thingies() -> i32 {                                              // c:1022
1214    // c:1022-1028 — `createthingytab(); for (t=thingies; t->nam; t++)
1215    //                  thingytab->addnode(...)`. The `thingies[]`
1216    // static array in C is the table of built-in widget names; here
1217    // we just init the empty table — the built-in widget registration
1218    // happens via `addzlefunction()` which the dispatcher calls per
1219    // entry in `iwidgets.list`.
1220    createthingytab();                                                       // c:1026
1221    0
1222}
1223
1224/// Port of `scanlistwidgets(HashNode hn, int list)` from `Src/Zle/zle_thingy.c:505`.
1225/// WARNING: param names don't match C — Rust=() vs C=(hn, list)
1226pub fn scanlistwidgets() -> i32 {                                            // c:505
1227    // c:505-543 — pretty-print one Thingy: WIDGET_INT skipped (built-in,
1228    // not user-visible). User widgets print as either `zle -N name [fn]`
1229    // or just `name (fn)` depending on `list` arg. Returns the
1230    // formatted string instead of writing to stdout.
1231    let tab = thingytab().lock().unwrap();
1232    let lines: Vec<String> = tab.iter()
1233        .filter_map(|(name, t)| {
1234            let w = t.widget.as_ref()?;
1235            // c:514-515 — skip internal widgets.
1236            if (w.flags & WIDGET_INT) != 0 {
1237                return None;
1238            }
1239            // c:530-541 — abbreviated format: name (fn) when fn != name.
1240            let fn_name = match &w.u {
1241                WidgetFunc::UserFunc(s) => s.clone(),
1242                WidgetFunc::Internal(_) => return None,
1243                _ => return None,
1244            };
1245            if fn_name == *name {
1246                Some(name.clone())
1247            } else {
1248                Some(format!("{} ({})", name, fn_name))
1249            }
1250        })
1251        .collect();
1252    let _ = lines;
1253    0
1254}
1255
1256/// Port of `zle_usable()` from `Src/Zle/zle_thingy.c:634`.
1257/// ```c
1258/// static int
1259/// zle_usable(void)
1260/// {
1261///     return zleactive && !incompctlfunc && !incompfunc;
1262/// }
1263/// ```
1264/// True iff a ZLE session is currently active and we're not
1265/// inside a compctl-fn or comp-fn call (zle widgets can't run
1266/// from inside completion functions).
1267pub fn zle_usable() -> i32 {                                                 // c:634
1268    use std::sync::atomic::Ordering;
1269    let active = crate::ported::builtins::sched::zleactive.load(Ordering::Relaxed) != 0;
1270    let incompctlfunc = crate::ported::zle::compctl::INCOMPCTLFUNC               // c:636
1271        .with(|c| c.get());
1272    let incompfunc = crate::ported::zle::complete::INCOMPFUNC.load(Ordering::Relaxed) != 0;
1273    if active && !incompctlfunc && !incompfunc { 1 } else { 0 }
1274}
1275
1276#[cfg(test)]
1277mod tests {
1278    use super::*;
1279    use std::sync::Mutex as StdMutex;
1280
1281    // Serialize tests since they share the global THINGYTAB.
1282    static LOCK: StdMutex<()> = StdMutex::new(());
1283
1284    fn reset_tab() {
1285        thingytab().lock().unwrap().clear();
1286    }
1287
1288    #[test]
1289    fn rthingy_creates_then_refs() {
1290        let _g = crate::ported::zle::zle_main::zle_test_setup();
1291        let _g = LOCK.lock().unwrap();
1292        reset_tab();
1293
1294        rthingy("foo");
1295        let tab = thingytab().lock().unwrap();
1296        let t = tab.get("foo").expect("rthingy must create");
1297        assert_eq!(t.rc, 1);
1298        assert!((t.flags & DISABLED) != 0);
1299    }
1300
1301    #[test]
1302    fn refthingy_unrefthingy_roundtrip() {
1303        let _g = crate::ported::zle::zle_main::zle_test_setup();
1304        let _g = LOCK.lock().unwrap();
1305        reset_tab();
1306
1307        rthingy("bar");
1308        refthingy("bar");
1309        // rc was 1 after rthingy, +1 from refthingy = 2
1310        assert_eq!(thingytab().lock().unwrap().get("bar").unwrap().rc, 2);
1311        unrefthingy("bar");
1312        assert_eq!(thingytab().lock().unwrap().get("bar").unwrap().rc, 1);
1313        unrefthingy("bar");
1314        // rc dropped to 0 → freenode removes
1315        assert!(!thingytab().lock().unwrap().contains_key("bar"));
1316    }
1317
1318    #[test]
1319    fn rthingy_nocreate_returns_false_for_missing() {
1320        let _g = crate::ported::zle::zle_main::zle_test_setup();
1321        let _g = LOCK.lock().unwrap();
1322        reset_tab();
1323
1324        assert!(!rthingy_nocreate("absent"));
1325        assert!(!thingytab().lock().unwrap().contains_key("absent"));
1326    }
1327
1328    #[test]
1329    fn rthingy_nocreate_refs_existing() {
1330        let _g = crate::ported::zle::zle_main::zle_test_setup();
1331        let _g = LOCK.lock().unwrap();
1332        reset_tab();
1333
1334        rthingy("present");
1335        assert!(rthingy_nocreate("present"));
1336        assert_eq!(thingytab().lock().unwrap().get("present").unwrap().rc, 2);
1337    }
1338
1339}