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}