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