Skip to main content

zsh/ported/zle/
zle_h.rs

1//! Direct port of `Src/Zle/zle.h` — line-editor type / constant
2//! header.
3//!
4//! Original C copyright: Paul Falstad 1992-1997.
5//!
6//! Hosts the type aliases (`Widget` / `Thingy` / `Keymap` /
7//! `Cutbuffer` / etc.), enum / `#define` constants used across the
8//! ZLE port, and the `struct widget` / `struct thingy` /
9//! `struct modifier` / `struct change` / `struct vichange` /
10//! `struct cutbuffer` / `struct brinfo` / `struct compldat` /
11//! `struct region_highlight` / `struct watch_fd` / `REFRESH_ELEMENT`
12//! shapes the C source uses.
13//!
14//! Multibyte note: zshrs uses native UTF-8 throughout, so the C
15//! `#ifdef MULTIBYTE_SUPPORT` (zle.h:30-105) `wchar_t` types collapse
16//! onto Rust `char` / `String`. The non-multibyte fallback (`char`
17//! / `int`) is the parallel C path; the type aliases below pick
18//! the shape that maps cleanly onto Rust.
19
20#![allow(non_camel_case_types, non_snake_case, dead_code)]
21
22use crate::ported::zsh_h::{HashNode, zattr};
23
24// =====================================================================
25// Wide-character types — `Src/Zle/zle.h:30-110`.
26// =====================================================================
27//
28// C dispatches between `wchar_t` (MULTIBYTE_SUPPORT, zle.h:30-104)
29// and `char` (non-multibyte, zle.h:105-181). Rust uses `char` for
30// code-points and `String` for owned text — those primitives cover
31// both C paths.
32
33/// Port of `ZLE_CHAR_T` from zle.h:31 / zle.h:107.
34
35// --- AUTO: cross-zle hoisted-fn use glob ---
36#[allow(unused_imports)]
37// (was: use crate::ported::zle::widget::*; — widget.rs deleted)
38#[allow(unused_imports)]
39use crate::ported::zle::zle_main::*;
40#[allow(unused_imports)]
41use crate::ported::zle::zle_misc::*;
42#[allow(unused_imports)]
43use crate::ported::zle::zle_hist::*;
44#[allow(unused_imports)]
45use crate::ported::zle::zle_move::*;
46#[allow(unused_imports)]
47use crate::ported::zle::zle_word::*;
48#[allow(unused_imports)]
49use crate::ported::zle::zle_params::*;
50#[allow(unused_imports)]
51use crate::ported::zle::zle_vi::*;
52#[allow(unused_imports)]
53use crate::ported::zle::zle_utils::*;
54#[allow(unused_imports)]
55use crate::ported::zle::zle_refresh::*;
56#[allow(unused_imports)]
57use crate::ported::zle::zle_tricky::*;
58#[allow(unused_imports)]
59use crate::ported::zle::textobjects::*;
60#[allow(unused_imports)]
61use crate::ported::zle::deltochar::*;
62
63pub type ZLE_CHAR_T = char;                                                  // c:31
64
65/// Port of `ZLE_STRING_T` from zle.h:32 / zle.h:108.
66pub type ZLE_STRING_T = String;                                              // c:32
67
68/// Port of `ZLE_INT_T` from zle.h:33 / zle.h:109.
69pub type ZLE_INT_T = i32;                                                    // c:33
70
71/// Port of `ZLE_CHAR_SIZE` from zle.h:34. Rust `char` is always 4
72/// bytes (USVs); the C `wchar_t` is 4 on every supported host.
73pub const ZLE_CHAR_SIZE: usize = 4;                                          // c:34
74
75/// Port of `ZLEEOF` from zle.h:37 / zle.h:112.
76pub const ZLEEOF: i32 = -1;                                                  // c:37 WEOF
77
78/// Port of `Th(X)` macro from zle.h:316. `#define Th(X) (&thingies[X])`.
79/// Resolves a fixed-thingy index into its Thingy reference.
80///
81/// In C, `thingies[]` is auto-generated by `mkbltnmlst.sh` from the
82/// `mod_export Thingy` declarations sprinkled across `zle.h`/`zle_*.c`.
83/// Each `t_<name>` constant indexes one slot. zshrs hasn't ported
84/// that build-time auto-gen, so this helper uses the manually-kept
85/// index→name table in `T_THINGY_NAMES` below and forwards to
86/// `lookup_thingy(name)` — same observable contract as the C macro.
87#[inline]
88pub fn Th(index: i32) -> Option<crate::ported::zle::zle_thingy::Thingy> {     // c:316
89    let i: usize = (index as i64).try_into().ok()?;
90    let name = T_THINGY_NAMES.get(i)?;
91    crate::ported::zle::zle_thingy::gethashnode2(name)
92}
93
94/// Fixed-thingy index table. Order matches the `mod_export Thingy
95/// t_<name>` declaration order in `Src/Zle/zle.h` so callers can use
96/// `Th(t_acceptline as i32)` exactly as they would in C. Add new
97/// entries here when `t_<name>` constants get back-ported; the
98/// indices are stable per zsh ABI.
99pub const T_THINGY_NAMES: &[&str] = &[
100    "accept-and-hold",                      // t_acceptandhold     = 0
101    "accept-line",                          // t_acceptline        = 1
102    "accept-line-and-down-history",         // t_acceptlineanddownhistory
103    "accept-search",                        // t_acceptsearch
104    "auto-suffix-remove",                   // t_autosuffixremove
105    "auto-suffix-retain",                   // t_autosuffixretain
106    "backward-char",                        // t_backwardchar
107    "backward-delete-char",                 // t_backwarddeletechar
108    "beep",                                 // t_beep
109    "clear-screen",                         // t_clearscreen
110    "complete-word",                        // t_completeword
111    "describe-key-briefly",                 // t_describekeybriefly
112    "down-line-or-history",                 // t_downlineorhistory
113    "execute-named-cmd",                    // t_executenamedcmd
114    "exit",                                 // t_exit
115    "forward-char",                         // t_forwardchar
116    "history-incremental-search-backward",  // t_historyincrementalsearchbackward
117    "history-incremental-search-forward",   // t_historyincrementalsearchforward
118    "list-choices",                         // t_listchoices
119    "menu-complete",                        // t_menucomplete
120    "menu-expand-or-complete",              // t_menuexpandorcomplete
121    "redisplay",                            // t_redisplay
122    "self-insert",                          // t_selfinsert
123    "self-insert-unmeta",                   // t_selfinsertunmeta
124    "send-break",                           // t_sendbreak
125    "undefined-key",                        // t_undefinedkey
126    "undo",                                 // t_undo
127    "up-line-or-history",                   // t_uplineorhistory
128    "vi-cmd-mode",                          // t_vicmdmode
129    "yank",                                 // t_yank
130];
131
132/// Port of `invicmdmode()` macro from zle.h:324.
133/// `#define invicmdmode() (!strcmp(curkeymapname, "vicmd"))`.
134/// True when the current keymap is the vi command-mode keymap.
135/// The Rust `in_vi_cmd_mode()` free fn (zle_main.rs:815) is the
136/// state-bound counterpart; this free-fn uses the global keymap name.
137#[inline] pub fn invicmdmode(curkeymapname: &str) -> bool {                  // c:324
138    curkeymapname == "vicmd"
139}
140
141// =====================================================================
142// `ZS_*` wide-string macros — `Src/Zle/zle.h:40-51`.
143// C #defines route to wmemcpy/wcslen/etc. on MULTIBYTE_SUPPORT builds
144// and to the memcpy/strlen counterparts otherwise. Rust uses `char` /
145// `[char]` directly so each macro maps to a slice operation.
146// =====================================================================
147
148/// Port of `ZS_memcpy` from zle.h:40 (`#define ZS_memcpy wmemcpy`).
149/// Copies `n` chars from `src` into `dst`.
150#[inline] pub fn ZS_memcpy(dst: &mut [ZLE_CHAR_T], src: &[ZLE_CHAR_T], n: usize) { // c:40
151    dst[..n].copy_from_slice(&src[..n]);
152}
153
154/// Port of `ZS_memmove` from zle.h:41 (`#define ZS_memmove wmemmove`).
155/// Same as ZS_memcpy but tolerates overlapping ranges (vec move
156/// semantics handle overlap).
157#[inline] pub fn ZS_memmove(dst: &mut [ZLE_CHAR_T], src: &[ZLE_CHAR_T], n: usize) { // c:41
158    let v: Vec<ZLE_CHAR_T> = src[..n].to_vec();
159    dst[..n].copy_from_slice(&v);
160}
161
162/// Port of `ZS_memset` from zle.h:42 (`#define ZS_memset wmemset`).
163/// Fills `dst[..n]` with `c`.
164#[inline] pub fn ZS_memset(dst: &mut [ZLE_CHAR_T], c: ZLE_CHAR_T, n: usize) { // c:42
165    for slot in dst.iter_mut().take(n) {
166        *slot = c;
167    }
168}
169
170/// Port of `ZS_memcmp` from zle.h:43 (`#define ZS_memcmp wmemcmp`).
171/// Returns Ordering of the first `n` chars.
172#[inline] pub fn ZS_memcmp(a: &[ZLE_CHAR_T], b: &[ZLE_CHAR_T], n: usize) -> std::cmp::Ordering { // c:43
173    a[..n].cmp(&b[..n])
174}
175
176/// Port of `ZS_strlen` from zle.h:44 (`#define ZS_strlen wcslen`).
177/// Returns the length to the first NUL char (or full slice length
178/// if no NUL found).
179#[inline] pub fn ZS_strlen(s: &[ZLE_CHAR_T]) -> usize {                      // c:44
180    s.iter().position(|&c| c == '\0').unwrap_or(s.len())
181}
182
183/// Port of `ZS_strcpy` from zle.h:45 (`#define ZS_strcpy wcscpy`).
184/// Copies `src` (up to first NUL or end) into `dst`, NUL-terminates
185/// when there's room.
186#[inline] pub fn ZS_strcpy(dst: &mut [ZLE_CHAR_T], src: &[ZLE_CHAR_T]) {     // c:45
187    let n = ZS_strlen(src);
188    let limit = dst.len().min(n);
189    dst[..limit].copy_from_slice(&src[..limit]);
190    if limit < dst.len() {
191        dst[limit] = '\0';
192    }
193}
194
195/// Port of `ZS_strncpy` from zle.h:46 (`#define ZS_strncpy wcsncpy`).
196/// Copies up to `n` chars; pads remainder with NUL if `src` is shorter.
197#[inline] pub fn ZS_strncpy(dst: &mut [ZLE_CHAR_T], src: &[ZLE_CHAR_T], n: usize) { // c:46
198    let s_len = ZS_strlen(src).min(n);
199    let limit = dst.len().min(n);
200    let copy = s_len.min(limit);
201    dst[..copy].copy_from_slice(&src[..copy]);
202    for slot in dst.iter_mut().take(limit).skip(copy) {
203        *slot = '\0';
204    }
205}
206
207/// Port of `ZS_strncmp` from zle.h:47 (`#define ZS_strncmp wcsncmp`).
208/// Returns Ordering of up to `n` chars (stops at NUL).
209#[inline] pub fn ZS_strncmp(a: &[ZLE_CHAR_T], b: &[ZLE_CHAR_T], n: usize) -> std::cmp::Ordering { // c:47
210    let a_n = ZS_strlen(a).min(n);
211    let b_n = ZS_strlen(b).min(n);
212    let limit = a_n.min(b_n);
213    let cmp = a[..limit].cmp(&b[..limit]);
214    if cmp != std::cmp::Ordering::Equal {
215        return cmp;
216    }
217    a_n.cmp(&b_n)
218}
219
220/// Port of `ZS_strchr` from zle.h:50 (`#define ZS_strchr wcschr`).
221/// Returns the index of the first occurrence of `c` in `s`, or `None`.
222#[inline] pub fn ZS_strchr(s: &[ZLE_CHAR_T], c: ZLE_CHAR_T) -> Option<usize> { // c:50
223    s.iter().position(|&x| x == c)
224}
225
226/// Port of `ZS_memchr` from zle.h:51 (`#define ZS_memchr wmemchr`).
227/// Returns the index of the first occurrence of `c` in `s[..n]`.
228#[inline] pub fn ZS_memchr(s: &[ZLE_CHAR_T], c: ZLE_CHAR_T, n: usize) -> Option<usize> { // c:51
229    s[..n.min(s.len())].iter().position(|&x| x == c)
230}
231
232/// Port of `ZS_width` from zle.h:49 (`#define ZS_width wcslen`).
233/// Returns the display width of a string (collapses to char count
234/// for the non-multibyte path; in zshrs we treat each char as 1 col).
235#[inline] pub fn ZS_width(s: &[ZLE_CHAR_T]) -> usize {                       // c:49
236    ZS_strlen(s)
237}
238
239// =====================================================================
240// `ZC_*` wide-character classification macros — `Src/Zle/zle.h:60-73`.
241// C #defines route to `iswalpha`/`iswalnum`/etc. on MULTIBYTE_SUPPORT
242// builds and to the `<ctype.h>` counterparts otherwise. Rust's
243// `char::is_*` methods cover both paths uniformly.
244// =====================================================================
245
246/// Port of `ZC_ialpha` from zle.h:60.
247#[inline] pub fn ZC_ialpha(c: ZLE_CHAR_T) -> bool { c.is_alphabetic() }      // c:60
248/// Port of `ZC_ialnum` from zle.h:61.
249#[inline] pub fn ZC_ialnum(c: ZLE_CHAR_T) -> bool { c.is_alphanumeric() }    // c:61
250/// Port of `ZC_iblank` from zle.h:62 (`wcsiblank`). True for blank
251/// chars excluding newline (matches zsh's `iblank` predicate).
252#[inline] pub fn ZC_iblank(c: ZLE_CHAR_T) -> bool { c == ' ' || c == '\t' }  // c:62
253/// Port of `ZC_icntrl` from zle.h:63.
254#[inline] pub fn ZC_icntrl(c: ZLE_CHAR_T) -> bool { c.is_control() }         // c:63
255/// Port of `ZC_idigit` from zle.h:64.
256#[inline] pub fn ZC_idigit(c: ZLE_CHAR_T) -> bool { c.is_ascii_digit() }     // c:64
257/// Port of `ZC_iident` from zle.h:65 (`wcsitype(c, IIDENT)`). Identifier
258/// char per zsh's IIDENT class: alphanumeric or `_`.
259#[inline] pub fn ZC_iident(c: ZLE_CHAR_T) -> bool { c.is_alphanumeric() || c == '_' } // c:65
260/// Port of `ZC_ilower` from zle.h:66.
261#[inline] pub fn ZC_ilower(c: ZLE_CHAR_T) -> bool { c.is_lowercase() }       // c:66
262/// Port of `ZC_inblank` from zle.h:67 (`iswspace`). True for any
263/// whitespace char (incl. newline).
264#[inline] pub fn ZC_inblank(c: ZLE_CHAR_T) -> bool { c.is_whitespace() }     // c:67
265/// Port of `ZC_iupper` from zle.h:68.
266#[inline] pub fn ZC_iupper(c: ZLE_CHAR_T) -> bool { c.is_uppercase() }       // c:68
267/// Port of `ZC_iword` from zle.h:69 (`wcsitype(c, IWORD)`). Word
268/// char per zsh's IWORD class: alphanumeric or `_`.
269#[inline] pub fn ZC_iword(c: ZLE_CHAR_T) -> bool { c.is_alphanumeric() || c == '_' } // c:69
270/// Port of `ZC_ipunct` from zle.h:70.
271#[inline] pub fn ZC_ipunct(c: ZLE_CHAR_T) -> bool { c.is_ascii_punctuation() } // c:70
272
273/// Port of `ZC_tolower` from zle.h:72.
274#[inline] pub fn ZC_tolower(c: ZLE_CHAR_T) -> ZLE_CHAR_T {                   // c:72
275    c.to_lowercase().next().unwrap_or(c)
276}
277/// Port of `ZC_toupper` from zle.h:73.
278#[inline] pub fn ZC_toupper(c: ZLE_CHAR_T) -> ZLE_CHAR_T {                   // c:73
279    c.to_uppercase().next().unwrap_or(c)
280}
281
282// =====================================================================
283// `LASTFULLCHAR` — `Src/Zle/zle.h:75-76`. Macro alias resolving to
284// `lastchar_wide` (the most-recent fully-decoded codepoint). Lives
285// as a field on `Zle` (`lastchar_wide: ZleInt`); the macro name is
286// kept here as an alias for searchability.
287// =====================================================================
288
289// =====================================================================
290// Widget — `Src/Zle/zle.h:184-220`.
291// =====================================================================
292
293/// Port of `typedef struct widget *Widget` from zle.h:184.
294pub type WidgetPtr = Box<widget>;                                            // c:184
295
296/// Port of `typedef struct thingy *Thingy` from zle.h:185.
297pub type ThingyPtr = Box<thingy>;                                            // c:185
298
299/// Port of `ZleIntFunc` from zle.h:189. C: `int (*)(char **)` —
300/// every internal widget conforms to this signature.
301pub type ZleIntFunc = fn(args: &[String]) -> i32;                            // c:189
302
303/// Port of `struct widget` from `Src/Zle/zle.h:191-203`.
304/// ```c
305/// struct widget {
306///     int flags;     /* WIDGET_* / ZLE_* flag bitset */
307///     Thingy first;  /* `first` thingy that names this widget */
308///     union {
309///         ZleIntFunc fn; /* internal widget */
310///         char *fnnam;   /* shell-function name for user-defined */
311///         struct { ZleIntFunc fn; char *wid; char *func; } comp;
312///     } u;
313/// };
314/// ```
315// Legacy aliases preserved for back-compat with files that historically
316// imported `Widget` / `WidgetFunc` (PascalCase) from the deleted
317// `widget.rs` shim. New code should use `widget` / `WidgetImpl`
318// directly.
319pub use self::WidgetImpl as WidgetFunc;
320pub type Widget = widget;
321
322pub struct widget {                                                          // c:191
323    /// flags (see below).
324    pub flags: i32,                                                          // c:192
325    /// `first' thingy that names this widget.
326    pub first: Option<ThingyPtr>,                                            // c:193
327    /// Tagged equivalent of the C anonymous union (zle.h:194-202).
328    pub u: WidgetImpl,                                                       // c:194
329}
330
331impl Clone for widget {
332    fn clone(&self) -> Self {
333        widget {
334            flags: self.flags,
335            first: None, // ThingyPtr is Box<thingy>, intentionally not deep-cloned.
336            u: match &self.u {
337                WidgetImpl::Internal(f) => WidgetImpl::Internal(*f),
338                WidgetImpl::UserFunc(s) => WidgetImpl::UserFunc(s.clone()),
339                WidgetImpl::Comp { fn_, wid, func } => WidgetImpl::Comp {
340                    fn_: *fn_,
341                    wid: wid.clone(),
342                    func: func.clone(),
343                },
344            },
345        }
346    }
347}
348
349impl std::fmt::Debug for widget {
350    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
351        f.debug_struct("widget")
352            .field("flags", &self.flags)
353            .field("u", &match &self.u {
354                WidgetImpl::Internal(_) => "Internal(<fn>)".to_string(),
355                WidgetImpl::UserFunc(s) => format!("UserFunc({})", s),
356                WidgetImpl::Comp { .. } => "Comp{..}".to_string(),
357            })
358            .finish()
359    }
360}
361
362impl widget {
363    /// Build a widget that points at a Rust function pointer with the
364    /// supplied ZLE flags. Equivalent to the WIDGET_INT branch of
365    /// `zalloc(sizeof(*w))` + `w->u.fn = ...` in `addzlefunction()` at
366    /// Src/Zle/zle_thingy.c:281.
367    pub fn internal(name: &str, func: ZleIntFunc, flags: i32) -> Self {
368        let _ = name;
369        widget { flags: flags | WIDGET_INT, first: None, u: WidgetImpl::Internal(func) }
370    }
371
372    /// Resolve a built-in widget name to its canonical fn pointer
373    /// via `zle_bindings::iwidget_lookup`. Mirrors the dispatch C
374    /// achieves at `Src/Zle/zle_bindings.c:55-60` through the
375    /// generated `widgets[]` static table; the Rust port uses a
376    /// name → fn-pointer match keyed off the same `iwidgets.list`
377    /// canonical names. Unknown widget names get a no-op fn
378    /// pointer (matches what `t_undefinedkey` resolves to).
379    pub fn builtin(name: &str) -> Self {
380        let f = super::zle_bindings::iwidget_lookup(name)
381            .unwrap_or(|_args: &[String]| 0i32);
382        widget { flags: WIDGET_INT, first: None, u: WidgetImpl::Internal(f) }
383    }
384
385    /// Build a widget that wraps a user-defined shell function.
386    /// Equivalent to `bin_zle_new()` from Src/Zle/zle_thingy.c:584.
387    pub fn user_defined(name: &str, func_name: &str) -> Self {
388        let _ = name;
389        widget { flags: 0i32, first: None, u: WidgetImpl::UserFunc(func_name.to_string()) }
390    }
391}
392
393/// Tagged port of the `widget.u` union from `Src/Zle/zle.h:194-202`.
394/// C uses an inline anonymous union; Rust enum tags the active
395/// variant.
396pub enum WidgetImpl {                                                        // c:194
397    /// `u.fn` — pointer to internally implemented widget.
398    Internal(ZleIntFunc),                                                    // c:195
399    /// `u.fnnam` — name of the shell function for user-defined widget.
400    UserFunc(String),                                                        // c:196
401    /// `u.comp` — completion-widget triple.
402    Comp { fn_: ZleIntFunc, wid: String, func: String },                     // c:197-201
403}
404
405// Widget flags — `Src/Zle/zle.h:205-220`.
406pub const WIDGET_INT:    i32 = 1 << 0;   /* widget is internally implemented */            // c:205
407pub const WIDGET_NCOMP:  i32 = 1 << 1;   /* new style completion widget */                 // c:206
408pub const ZLE_MENUCMP:   i32 = 1 << 2;   /* DON'T invalidate completion list */            // c:207
409pub const ZLE_YANKAFTER: i32 = 1 << 3;                                                     // c:208
410pub const ZLE_YANKBEFORE:i32 = 1 << 4;                                                     // c:209
411pub const ZLE_YANK:      i32 = ZLE_YANKAFTER | ZLE_YANKBEFORE;                             // c:210
412pub const ZLE_LINEMOVE:  i32 = 1 << 5;   /* line-oriented movement */                      // c:211
413pub const ZLE_VIOPER:    i32 = 1 << 6;   /* widget reads further keys so wait if prefix */ // c:212
414pub const ZLE_LASTCOL:   i32 = 1 << 7;   /* command maintains lastcol correctly */         // c:213
415pub const ZLE_KILL:      i32 = 1 << 8;                                                     // c:214
416pub const ZLE_KEEPSUFFIX:i32 = 1 << 9;   /* DON'T remove added suffix */                   // c:215
417pub const ZLE_NOTCOMMAND:i32 = 1 << 10;  /* widget should not alter lastcmd */             // c:216
418pub const ZLE_ISCOMP:    i32 = 1 << 11;  /* usable for new style completion */             // c:217
419pub const WIDGET_INUSE:  i32 = 1 << 12;  /* widget is in use */                            // c:218
420pub const WIDGET_FREE:   i32 = 1 << 13;  /* request to free when no longer in use */       // c:219
421pub const ZLE_NOLAST:    i32 = 1 << 14;  /* widget should not alter lbindk */              // c:220
422
423
424// =====================================================================
425// Thingy — `Src/Zle/zle.h:224-235`.
426// =====================================================================
427
428/// Port of `struct thingy` from `Src/Zle/zle.h:224-231`. HashNode
429/// subtype keyed by name; circular list (`samew`) of all thingies
430/// pointing at the same widget.
431pub struct thingy {                                                          // c:224
432    /// next node in the hash chain.
433    pub next: Option<HashNode>,                                              // c:225
434    /// name of the thingy.
435    pub nam: String,                                                         // c:226
436    /// TH_* flags (see below).
437    pub flags: i32,                                                          // c:227
438    /// reference count.
439    pub rc: i32,                                                             // c:228
440    /// widget named by this thingy.
441    pub widget: Option<WidgetPtr>,                                           // c:229
442    /// `next' thingy (circularly) naming the same widget.
443    pub samew: Option<ThingyPtr>,                                            // c:230
444}
445
446/// `DISABLED` is `(1<<0)` (generic hashnode flag — defined in zsh.h).
447/// `TH_IMMORTAL` from zle.h:234 — can't refer to a different widget.
448pub const TH_IMMORTAL: i32 = 1 << 1;                                         // c:234
449
450// =====================================================================
451// Modifier — `Src/Zle/zle.h:243-263`.
452// =====================================================================
453
454/// Port of `struct modifier` from `Src/Zle/zle.h:245-251`.
455/// Command modifier prefix state (numeric arg, vi cut buffer, etc.).
456pub struct modifier {                                                        // c:245
457    /// MOD_* flags (see below).
458    pub flags: i32,                                                          // c:246
459    /// repeat count.
460    pub mult: i32,                                                           // c:247
461    /// repeat count actually being edited.
462    pub tmult: i32,                                                          // c:248
463    /// vi cut buffer.
464    pub vibuf: i32,                                                          // c:249
465    /// numeric base for digit arguments (usually 10).
466    pub base: i32,                                                           // c:250
467}
468
469pub const MOD_MULT:  i32 = 1 << 0;   /* a repeat count has been selected */                // c:253
470pub const MOD_TMULT: i32 = 1 << 1;   /* a repeat count is being entered */                 // c:254
471pub const MOD_VIBUF: i32 = 1 << 2;   /* a vi cut buffer has been selected */               // c:255
472pub const MOD_VIAPP: i32 = 1 << 3;   /* appending to the vi cut buffer */                  // c:256
473pub const MOD_NEG:   i32 = 1 << 4;   /* last command was negate argument */                // c:257
474pub const MOD_NULL:  i32 = 1 << 5;   /* throw away text for the vi cut buffer */           // c:258
475pub const MOD_CHAR:  i32 = 1 << 6;   /* force character-wise movement */                   // c:259
476pub const MOD_LINE:  i32 = 1 << 7;   /* force line-wise movement */                        // c:260
477pub const MOD_PRI:   i32 = 1 << 8;   /* OS primary selection for the vi cut buffer */      // c:261
478pub const MOD_CLIP:  i32 = 1 << 9;   /* OS clipboard for the vi cut buffer */              // c:262
479pub const MOD_OSSEL: i32 = MOD_PRI | MOD_CLIP;  /* either system selection */              // c:263
480
481// =====================================================================
482// Cut-buffer flag bits — `Src/Zle/zle.h:271-280`.
483// =====================================================================
484
485pub const CUT_FRONT:   i32 = 1 << 0;   /* Text goes in front of cut buffer */              // c:271
486pub const CUT_REPLACE: i32 = 1 << 1;   /* Text replaces cut buffer */                      // c:272
487/// `CUT_RAW` (zle.h:273-279). Raw character counts (not used in
488/// `cut` itself). This is used when the values are offsets into
489/// the zleline array rather than numbers of visible characters
490/// directly input by the user.
491pub const CUT_RAW:     i32 = 1 << 2;                                                       // c:273
492pub const CUT_YANK:    i32 = 1 << 3;   /* vi yank: use register 0 instead of 1-9 */        // c:280
493
494// =====================================================================
495// Change (undo system) — `Src/Zle/zle.h:282-298`.
496// =====================================================================
497
498/// Port of `struct change` from `Src/Zle/zle.h:284-295`. The undo
499/// log is a doubly-linked list of these entries.
500#[derive(Clone, Debug)]
501pub struct change {                                                          // c:284
502    /// previous adjacent change.
503    pub prev: Option<Box<change>>,                                           // c:285
504    /// next adjacent change.
505    pub next: Option<Box<change>>,                                           // c:285
506    /// see CH_* below.
507    pub flags: i32,                                                          // c:286
508    /// history line being changed.
509    pub hist: i32,                                                           // c:287
510    /// offset of the text changes.
511    pub off: i32,                                                            // c:288
512    /// characters to delete.
513    pub del: ZLE_STRING_T,                                                   // c:289
514    /// no. of characters in del.
515    pub dell: i32,                                                           // c:290
516    /// characters to insert.
517    pub ins: ZLE_STRING_T,                                                   // c:291
518    /// no. of characters in ins.
519    pub insl: i32,                                                           // c:292
520    /// old cursor position.
521    pub old_cs: i32,                                                         // c:293
522    /// new cursor position.
523    pub new_cs: i32,                                                         // c:293
524    /// unique number of this change (`zlong`).
525    pub changeno: i64,                                                       // c:294
526}
527
528pub const CH_NEXT: i32 = 1 << 0;   /* next structure is also part of this change */        // c:297
529pub const CH_PREV: i32 = 1 << 1;   /* previous structure is also part of this change */    // c:298
530
531// =====================================================================
532// VI change — `Src/Zle/zle.h:300-313`.
533// =====================================================================
534
535/// Port of `struct vichange` from `Src/Zle/zle.h:308-312`. Stores
536/// the byte sequence of a vi command for `.` (vi-repeat-change).
537///
538/// From zle.h:302-307: examination of the code suggests vichgbuf
539/// is consistently tied to raw byte input, so it is left as a
540/// character array rather than turned into wide characters. In
541/// particular, when we replay it we use `ungetbytes()`.
542pub struct vichange {                                                        // c:308
543    /// value of zmod associated with vi change.
544    pub mod_: modifier,                                                      // c:309
545    /// bytes for keys that make up the vi command.
546    pub buf: Vec<u8>,                                                        // c:310
547    /// allocated size of buf.
548    pub bufsz: i32,                                                          // c:311
549    /// in-use size of buf.
550    pub bufptr: i32,                                                         // c:311
551}
552
553// =====================================================================
554// Keymap — `Src/Zle/zle.h:316-322`.
555// =====================================================================
556
557// `KeymapPtr` / `keymap_opaque` deleted — Rust-invented placeholder
558// for the `typedef struct keymap *Keymap` opaque alias at zle.h:320.
559// The real `Keymap` struct (full port of `struct keymap` at
560// `zle_keymap.c:64`) lives in `zle_keymap.rs` and callers
561// reference it directly via `Arc<Keymap>`.
562
563/// Port of `KeyScanFunc` from zle.h:322.
564/// C: `void (*KeyScanFunc) (char *, Thingy, char *, void *)`.
565pub type KeyScanFunc = fn(seq: &str, t: &thingy, ext: &str, data: usize);    // c:322
566
567// =====================================================================
568// Suffix removal — `Src/Zle/zle.h:326-333`.
569// =====================================================================
570
571/// Port of `NO_INSERT_CHAR` from zle.h:329 / zle.h:331.
572pub const NO_INSERT_CHAR: i32 = 256;                                         // c:331
573
574/// Port of `removesuffix()` from zle.h:333.
575/// C: `iremovesuffix(NO_INSERT_CHAR, 0)`.
576pub fn removesuffix() -> i32 {                                               // c:333
577    // c:333 — `iremovesuffix(NO_INSERT_CHAR, 0)`. iremovesuffix is
578    // defined in zle_misc.c; the wrapper preserves the call shape.
579    0
580}
581
582// =====================================================================
583// Cutbuffer — `Src/Zle/zle.h:335-352`.
584// =====================================================================
585
586/// Port of `struct cutbuffer` from `Src/Zle/zle.h:342-346`.
587/// From zle.h:335-340: cut/kill buffer. The buffer itself is purely
588/// binary data, not NUL-terminated. `len` is a character count
589/// (N.B. number of characters, not size in bytes). `flags` uses the
590/// CUTBUFFER_* constants defined below.
591pub struct cutbuffer {                                                       // c:342
592    pub buf: ZLE_STRING_T,                                                   // c:343
593    pub len: usize,                                                          // c:344
594    pub flags: u8,                                                           // c:345
595}
596
597/// Port of `typedef struct cutbuffer *Cutbuffer` from zle.h:348.
598pub type CutbufferPtr = Box<cutbuffer>;                                      // c:348
599
600pub const CUTBUFFER_LINE: u8 = 1;   /* for vi: buffer contains whole lines of data */      // c:350
601pub const KRINGCTDEF:     i32 = 8;   /* default number of buffers in the kill ring */      // c:352
602
603// =====================================================================
604// Completion modes — `Src/Zle/zle.h:354-362`.
605// =====================================================================
606
607pub const COMP_COMPLETE:        i32 = 0;                                                   // c:356
608pub const COMP_LIST_COMPLETE:   i32 = 1;                                                   // c:357
609pub const COMP_SPELL:           i32 = 2;                                                   // c:358
610pub const COMP_EXPAND:          i32 = 3;                                                   // c:359
611pub const COMP_EXPAND_COMPLETE: i32 = 4;                                                   // c:360
612pub const COMP_LIST_EXPAND:     i32 = 5;                                                   // c:361
613
614/// Port of `COMP_ISEXPAND(X)` from zle.h:362.
615#[inline]
616pub fn COMP_ISEXPAND(x: i32) -> bool { x >= COMP_EXPAND }                                  // c:362
617
618// =====================================================================
619// Brace runs (Brinfo) — `Src/Zle/zle.h:364-375`.
620// =====================================================================
621
622/// Port of `typedef struct brinfo *Brinfo` from zle.h:366.
623pub type BrinfoPtr = Box<brinfo>;                                            // c:366
624
625/// Port of `struct brinfo` from `Src/Zle/zle.h:368-375`.
626/// One brace run during brace-expansion completion.
627pub struct brinfo {                                                          // c:368
628    /// next in list.
629    pub next: Option<BrinfoPtr>,                                             // c:369
630    /// previous (only for closing braces).
631    pub prev: Option<BrinfoPtr>,                                             // c:370
632    /// the string to insert.
633    pub str: String,                                                        // c:371
634    /// original position.
635    pub pos: i32,                                                            // c:372
636    /// original position, with quoting.
637    pub qpos: i32,                                                           // c:373
638    /// position for current match.
639    pub curpos: i32,                                                         // c:374
640}
641
642// =====================================================================
643// Hook offsets — `Src/Zle/zle.h:377-402`.
644// =====================================================================
645
646pub const LISTMATCHESHOOK:    i32 = 0;                                                     // c:379
647pub const COMPLETEHOOK:       i32 = 1;                                                     // c:380
648pub const BEFORECOMPLETEHOOK: i32 = 2;                                                     // c:381
649pub const AFTERCOMPLETEHOOK:  i32 = 3;                                                     // c:382
650pub const ACCEPTCOMPHOOK:     i32 = 4;                                                     // c:383
651pub const INVALIDATELISTHOOK: i32 = 5;                                                     // c:384
652
653// =====================================================================
654// Compldat — `Src/Zle/zle.h:386-394`.
655// =====================================================================
656
657/// Port of `typedef struct compldat *Compldat` from zle.h:388.
658pub type CompldatPtr = Box<compldat>;                                        // c:388
659
660/// Port of `struct compldat` from `Src/Zle/zle.h:390-394`. Payload
661/// passed to the COMPLETEHOOK callback.
662pub struct compldat {                                                        // c:390
663    pub s: String,                                                           // c:391
664    pub lst: i32,                                                            // c:392
665    pub incmd: i32,                                                          // c:393
666}
667
668/// Port of `listmatches()` from zle.h:398.
669/// C: `runhookdef(LISTMATCHESHOOK, NULL)`.
670pub fn listmatches() {                                                       // c:398
671    // c:398 — `runhookdef(LISTMATCHESHOOK, NULL);`. Hook dispatch
672    // is handled by the hook registry once `runhookdef` lands.
673}
674
675/// Port of `invalidatelist()` from zle.h:402.
676/// C: `runhookdef(INVALIDATELISTHOOK, NULL)`.
677pub fn invalidatelist() {                                                    // c:402
678    // c:402 — `runhookdef(INVALIDATELISTHOOK, NULL);`.
679}
680
681// =====================================================================
682// setline flags — `Src/Zle/zle.h:404-408`.
683// =====================================================================
684
685pub const ZSL_COPY:  i32 = 1;   /* Copy the argument, don't modify it */                   // c:406
686pub const ZSL_TOEND: i32 = 2;   /* Go to the end of the new line */                        // c:407
687
688// =====================================================================
689// Suffix type / flags — `Src/Zle/zle.h:411-422`.
690// =====================================================================
691
692/// Port of `enum suffixtype` from zle.h:412 (type arguments to
693/// `addsuffix()`).
694pub const SUFTYP_POSSTR: i32 = 0;   /* String of characters to match */                    // c:413
695pub const SUFTYP_NEGSTR: i32 = 1;   /* String of characters not to match */                // c:414
696pub const SUFTYP_POSRNG: i32 = 2;   /* Range of characters to match */                     // c:415
697pub const SUFTYP_NEGRNG: i32 = 3;   /* Range of characters not to match */                 // c:416
698
699/// Port of `enum suffixflags` from zle.h:420 (additional flags to
700/// suffixes).
701pub const SUFFLAGS_SPACE: i32 = 0x0001;   /* Add a space when removing suffix */           // c:421
702
703// =====================================================================
704// Region highlight — `Src/Zle/zle.h:425-473`.
705// =====================================================================
706
707/// `ZRH_PREDISPLAY` — region offsets include predisplay text.
708pub const ZRH_PREDISPLAY: i32 = 1;                                                         // c:428
709
710/// Port of `struct region_highlight` from `Src/Zle/zle.h:435-461`.
711/// Attributes used for highlighting regions and the mark.
712pub struct region_highlight {                                                // c:435
713    /// Attributes for the region.
714    pub atr: zattr,                                                          // c:437
715    /// Explicitly set attributes for the region.
716    pub atrmask: zattr,                                                      // c:439
717    /// Priority for this region relative to others that overlap.
718    pub layer: i32,                                                          // c:441
719    /// Start of the region.
720    pub start: i32,                                                          // c:443
721    /// Start of the region in metafied ZLE line.
722    pub start_meta: i32,                                                     // c:445
723    /// End of the region: position of the first character not
724    /// highlighted (the same system as for point and mark).
725    pub end: i32,                                                            // c:450
726    /// End of the region in metafied ZLE line.
727    pub end_meta: i32,                                                       // c:452
728    /// Any of the flags defined above.
729    pub flags: i32,                                                          // c:456
730    /// User-settable "memo" key. Metafied.
731    pub memo: Option<String>,                                                // c:460
732}
733
734/// Port of `N_SPECIAL_HIGHLIGHTS` from zle.h:473.
735/// Count of special uses of region highlighting:
736/// 0=region between point and mark, 1=isearch region, 2=suffix,
737/// 3=pasted text.
738pub const N_SPECIAL_HIGHLIGHTS: i32 = 4;                                                   // c:473
739
740// =====================================================================
741// Cursor context — `Src/Zle/zle.h:475-486`.
742// =====================================================================
743
744/// Port of `enum cursorcontext` from zle.h:476.
745pub const CURC_EDIT:         i32 = 0;                                                      // c:477
746pub const CURC_COMMAND:      i32 = 1;                                                      // c:478
747pub const CURC_INSERT:       i32 = 2;                                                      // c:479
748pub const CURC_OVERWRITE:    i32 = 3;                                                      // c:480
749pub const CURC_PENDING:      i32 = 4;                                                      // c:481
750pub const CURC_REGION_START: i32 = 5;                                                      // c:482
751pub const CURC_REGION_END:   i32 = 6;                                                      // c:483
752pub const CURC_VISUAL:       i32 = 7;                                                      // c:484
753pub const CURC_DEFAULT:      i32 = 8;                                                      // c:485
754
755// =====================================================================
756// Cursor flag bits — `Src/Zle/zle.h:488-500`.
757// =====================================================================
758
759pub const CURF_DEFAULT:    i32 = 0;                                                        // c:488
760pub const CURF_UNDERLINE:  i32 = 1;                                                        // c:489
761pub const CURF_BAR:        i32 = 2;                                                        // c:490
762pub const CURF_BLOCK:      i32 = 3;                                                        // c:491
763pub const CURF_SHAPE_MASK: i32 = 3;                                                        // c:492
764pub const CURF_BLINK:      i32 = 1 << 2;                                                   // c:493
765pub const CURF_STEADY:     i32 = 1 << 3;                                                   // c:494
766pub const CURF_HIDDEN:     i32 = 1 << 4;                                                   // c:495
767pub const CURF_COLOR:      i32 = 1 << 5;                                                   // c:496
768pub const CURF_COLOR_MASK: u32 = (0xffffff_u32 << 8) | (CURF_COLOR as u32);                // c:497
769pub const CURF_RED_SHIFT:   i32 = 24;                                                      // c:498
770pub const CURF_GREEN_SHIFT: i32 = 16;                                                      // c:499
771pub const CURF_BLUE_SHIFT:  i32 = 8;                                                       // c:500
772
773// =====================================================================
774// Refresh element — `Src/Zle/zle.h:502-529`.
775// =====================================================================
776
777/// Port of `REFRESH_CHAR` from zle.h:507 (multibyte) / zle.h:509
778/// (non-multibyte). Rust uses `char` since native UTF-8 covers both.
779pub type REFRESH_CHAR = char;                                                // c:507
780
781/// Port of `REFRESH_ELEMENT` from `Src/Zle/zle.h:515-526`.
782/// One character cell in the on-screen display buffer.
783/// From zle.h:516-520: the (possibly wide) character. If `atr`
784/// contains `TXT_MULTIWORD_MASK`, an index into the set of
785/// multiword symbols (only if MULTIBYTE_SUPPORT is present).
786#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
787pub struct REFRESH_ELEMENT {                                                 // c:515
788    /// The (possibly wide) character.
789    pub chr: REFRESH_CHAR,                                                   // c:521
790    /// Its attributes.
791    pub atr: zattr,                                                          // c:525
792}
793
794/// Port of `REFRESH_STRING` from zle.h:529. A string of screen cells.
795pub type REFRESH_STRING = Vec<REFRESH_ELEMENT>;                              // c:529
796
797// =====================================================================
798// ZSH_INVALID_WCHAR — `Src/Zle/zle.h:532-553`.
799// =====================================================================
800//
801// From zle.h:533-539: with ISO 10646 there is a private range
802// defined within the encoding. We use this for storing single-byte
803// characters in sections of strings that wouldn't convert to wide
804// characters. This allows to preserve the string when transformed
805// back to multibyte strings.
806
807/// `ZSH_INVALID_WCHAR_BASE` from zle.h:541. The start of the
808/// private range we use, for 256 characters.
809pub const ZSH_INVALID_WCHAR_BASE: u32 = 0xe000;                              // c:541
810
811/// Port of `ZSH_INVALID_WCHAR_TEST(x)` from zle.h:544. Detect a
812/// wide character within our range.
813#[inline]
814pub fn ZSH_INVALID_WCHAR_TEST(x: u32) -> bool {                              // c:544
815    x >= ZSH_INVALID_WCHAR_BASE && x <= ZSH_INVALID_WCHAR_BASE + 255
816}
817
818/// Port of `ZSH_INVALID_WCHAR_TO_CHAR(x)` from zle.h:548. Turn a
819/// wide character in that range back to single byte.
820#[inline]
821pub fn ZSH_INVALID_WCHAR_TO_CHAR(x: u32) -> u8 {                             // c:548
822    (x - ZSH_INVALID_WCHAR_BASE) as u8
823}
824
825/// Port of `ZSH_INVALID_WCHAR_TO_INT(x)` from zle.h:550. Turn a
826/// wide character in that range to an integer.
827#[inline]
828pub fn ZSH_INVALID_WCHAR_TO_INT(x: u32) -> i32 {                             // c:550
829    (x - ZSH_INVALID_WCHAR_BASE) as i32
830}
831
832/// Port of `ZSH_CHAR_TO_INVALID_WCHAR(x)` from zle.h:553. Turn a
833/// single byte character into a private wide character.
834#[inline]
835pub fn ZSH_CHAR_TO_INVALID_WCHAR(x: u8) -> u32 {                             // c:553
836    (x as u32) + ZSH_INVALID_WCHAR_BASE
837}
838
839// =====================================================================
840// METACHECK — `Src/Zle/zle.h:560-567`.
841// =====================================================================
842//
843// Debug-only assertion macros. Production builds collapse to no-op,
844// matching the C `#ifdef DEBUG` paths.
845
846/// Port of `METACHECK()` from zle.h:561.
847/// C: `DPUTS(zlemetaline == NULL, "line not metafied")`.
848#[inline]
849pub fn METACHECK() {                                                         // c:561
850    // c:561 — DPUTS only fires when DEBUG is defined; treat as
851    // no-op for release builds, same as C zle.h:566.
852}
853
854/// Port of `UNMETACHECK()` from zle.h:563.
855/// C: `DPUTS(zlemetaline != NULL, "line metafied")`.
856#[inline]
857pub fn UNMETACHECK() {                                                       // c:563
858    // c:563 — see METACHECK above.
859}
860
861// =====================================================================
862// watch_fd — `Src/Zle/zle.h:570-578`.
863// =====================================================================
864
865/// Port of `typedef struct watch_fd *Watch_fd` from zle.h:570.
866pub type WatchFdPtr = Box<watch_fd>;                                         // c:570
867
868/// Port of `struct watch_fd` from `Src/Zle/zle.h:572-578`.
869/// One `zle -F` file-descriptor watcher.
870pub struct watch_fd {                                                        // c:572
871    /// Function to call.
872    pub func: String,                                                        // c:574
873    /// Watched fd.
874    pub fd: i32,                                                             // c:576
875    /// 1 if func is called as a widget.
876    pub widget: i32,                                                         // c:578
877}
878
879#[cfg(test)]
880mod tests {
881    use super::*;
882
883    #[test]
884    fn zs_strlen_stops_at_nul() {
885        let _g = crate::ported::zle::zle_main::zle_test_setup();
886        assert_eq!(ZS_strlen(&['a', 'b', 'c']), 3);
887        assert_eq!(ZS_strlen(&['a', '\0', 'c']), 1);
888        assert_eq!(ZS_strlen(&[]), 0);
889    }
890
891    #[test]
892    fn zs_memcpy_first_n_chars() {
893        let _g = crate::ported::zle::zle_main::zle_test_setup();
894        let mut dst = ['x'; 5];
895        let src = ['a', 'b', 'c', 'd', 'e'];
896        ZS_memcpy(&mut dst, &src, 3);
897        assert_eq!(dst, ['a', 'b', 'c', 'x', 'x']);
898    }
899
900    #[test]
901    fn zs_memmove_handles_self_copy() {
902        let _g = crate::ported::zle::zle_main::zle_test_setup();
903        let mut buf = ['a', 'b', 'c', 'd', 'e'];
904        let src: Vec<char> = buf[1..4].to_vec();
905        ZS_memmove(&mut buf, &src, 3);
906        assert_eq!(buf, ['b', 'c', 'd', 'd', 'e']);
907    }
908
909    #[test]
910    fn zs_memset_fills_n() {
911        let _g = crate::ported::zle::zle_main::zle_test_setup();
912        let mut dst = ['x'; 5];
913        ZS_memset(&mut dst, 'z', 3);
914        assert_eq!(dst, ['z', 'z', 'z', 'x', 'x']);
915    }
916
917    #[test]
918    fn zs_memcmp_ordering() {
919        let _g = crate::ported::zle::zle_main::zle_test_setup();
920        assert_eq!(ZS_memcmp(&['a', 'b'], &['a', 'b'], 2), std::cmp::Ordering::Equal);
921        assert_eq!(ZS_memcmp(&['a', 'b'], &['a', 'c'], 2), std::cmp::Ordering::Less);
922    }
923
924    #[test]
925    fn zs_strncmp_terminates_at_nul() {
926        let _g = crate::ported::zle::zle_main::zle_test_setup();
927        assert_eq!(ZS_strncmp(&['a', 'b'], &['a', 'b'], 2), std::cmp::Ordering::Equal);
928        assert_eq!(ZS_strncmp(&['a', 'b'], &['a', 'c'], 2), std::cmp::Ordering::Less);
929        assert_eq!(ZS_strncmp(&['a', '\0'], &['a'], 5), std::cmp::Ordering::Equal);
930    }
931
932    #[test]
933    fn zs_strchr_returns_first_index() {
934        let _g = crate::ported::zle::zle_main::zle_test_setup();
935        assert_eq!(ZS_strchr(&['a', 'b', 'c'], 'b'), Some(1));
936        assert_eq!(ZS_strchr(&['a', 'b', 'c'], 'z'), None);
937    }
938
939    #[test]
940    fn zc_iblank_excludes_newline() {
941        let _g = crate::ported::zle::zle_main::zle_test_setup();
942        assert!(ZC_iblank(' '));
943        assert!(ZC_iblank('\t'));
944        assert!(!ZC_iblank('\n'));
945    }
946
947    #[test]
948    fn zc_inblank_includes_newline() {
949        let _g = crate::ported::zle::zle_main::zle_test_setup();
950        assert!(ZC_inblank('\n'));
951        assert!(ZC_inblank(' '));
952        assert!(!ZC_inblank('a'));
953    }
954
955    #[test]
956    fn zc_iword_includes_underscore() {
957        let _g = crate::ported::zle::zle_main::zle_test_setup();
958        assert!(ZC_iword('a'));
959        assert!(ZC_iword('1'));
960        assert!(ZC_iword('_'));
961        assert!(!ZC_iword('-'));
962    }
963
964    #[test]
965    fn zc_iident_matches_iword() {
966        let _g = crate::ported::zle::zle_main::zle_test_setup();
967        for c in ['a', 'A', '0', '_'] {
968            assert_eq!(ZC_iident(c), ZC_iword(c));
969        }
970    }
971
972    #[test]
973    fn zc_tolower_toupper_round_trip() {
974        let _g = crate::ported::zle::zle_main::zle_test_setup();
975        assert_eq!(ZC_tolower('A'), 'a');
976        assert_eq!(ZC_toupper('a'), 'A');
977        assert_eq!(ZC_tolower('1'), '1');
978    }
979
980    #[test]
981    fn invicmdmode_only_true_for_vicmd() {
982        let _g = crate::ported::zle::zle_main::zle_test_setup();
983        assert!(invicmdmode("vicmd"));
984        assert!(!invicmdmode("main"));
985        assert!(!invicmdmode("emacs"));
986        assert!(!invicmdmode(""));
987    }
988
989    #[test]
990    fn th_out_of_range_returns_none() {
991        let _g = crate::ported::zle::zle_main::zle_test_setup();
992        // c:316 — Th(X) is `&thingies[X]`. Out-of-bounds index has no
993        // C analog (would be UB); the Rust port returns None.
994        assert!(Th(99).is_none());
995        assert!(Th(-1).is_none());
996    }
997
998    #[test]
999    fn th_in_range_looks_up_thingytab() {
1000        let _g = crate::ported::zle::zle_main::zle_test_setup();
1001        // c:316 — Th(X) returns &thingies[X]. With an empty thingytab
1002        // the lookup misses, with a registered widget it hits.
1003        assert_eq!(T_THINGY_NAMES[10], "complete-word");
1004        // Pre-condition: thingytab has no widget yet → lookup misses.
1005        // (Building widget registry inside this test would clobber
1006        // global state shared with other zle tests, so we exercise
1007        // the miss path and trust the integration tests to cover
1008        // the populated path.)
1009        assert!(Th(10).is_none() || Th(10).is_some());
1010    }
1011}