Skip to main content

zsh/ported/modules/
curses.rs

1//! Curses module — port of `Src/Modules/curses.c`.
2//!
3//! Window is permanent (probably "stdscr")                                  // c:55
4//! Scrolling enabled                                                        // c:57
5//! Autogenerated keypad string/number mapping                               // c:209
6//!
7//! Implements the `zcurses` builtin: terminal UI windowing.
8//!
9//! The C source links libncurses for real curses primitives;
10//! zshrs doesn't yet take that dependency, so the per-window
11//! buffering and refresh emit ANSI escape sequences directly. The
12//! function names, signatures, control flow, validation pipeline,
13//! error-numbering, and module-static layout mirror `curses.c`
14//! line-by-line; the ncurses calls are the only deviation, marked
15//! with WARNING comments where they occur.
16//!
17//! Structure mirrors the C source:
18//!   - `enum zc_win_flags` (curses.c:54)
19//!   - `struct zc_win` (curses.c:63)
20//!   - `struct zcurses_namenumberpair` (curses.c:71)
21//!   - `struct zcurses_subcommand` (curses.c:83)
22//!   - module-statics `zcurses_windows` / `zcurses_colorpairs` /
23//!     `zc_errno` / `zc_color_phase` / `next_cp` (curses.c:90-113)
24//!   - error constants `ZCURSES_E{INVALID,DEFINED,UNDEFINED}`
25//!     (curses.c:102-104) and validation criteria `ZCURSES_UNUSED`/
26//!     `ZCURSES_USED` (curses.c:106-107)
27//!   - tables `zcurses_attributes[]` / `zcurses_colors[]`
28//!     (curses.c:120-143)
29//!   - validation helpers `zcurses_strerror` /
30//!     `zcurses_getwindowbyname` / `zcurses_validate_window` /
31//!     `zcurses_attrget` / `zcurses_color` (curses.c:233-328)
32//!   - `zccmd_*` family (curses.c:434-1567)
33//!   - `bin_zcurses()` dispatch table (curses.c:1568)
34//!   - module entries (curses.c:1744+)
35
36#![allow(non_camel_case_types)]
37#![allow(non_upper_case_globals)]
38#![allow(non_snake_case)]
39
40use std::collections::HashMap;
41use std::io::{self, Read, Write};
42use std::sync::{Mutex, OnceLock};
43
44use crate::ported::params::{getiparam, setaparam, setsparam};
45use crate::ported::utils::{zerrnam, zwarnnam};
46use crate::ported::zsh_h::{features, module, options};
47// =====================================================================
48// Port of `struct zc_win` from `Src/Modules/curses.c:63`.
49//
50// ```c
51// struct zc_win {
52//     WINDOW *win;       // ncurses window handle
53//     char *name;
54//     int flags;
55//     LinkList children;
56//     ZCWin parent;
57// };
58// ```
59//
60// `WINDOW *win` is replaced by an explicit cell buffer + cursor
61// position because libncurses isn't linked. The `children`/`parent`
62// chain is kept (subwin support per `zccmd_addwin` args[5]).
63// =====================================================================
64
65/// Per-window state. Port of `struct zc_win`.
66#[derive(Debug)]
67pub struct zc_win {
68    /// `name` field.
69    pub name: String,
70    /// `flags` field.
71    pub flags: u32,
72    /// `rows` field.
73    pub rows: usize,
74    /// `cols` field.
75    pub cols: usize,
76    /// `y` field.
77    pub y: usize,
78    /// `x` field.
79    pub x: usize,
80    /// `cursor_y` field.
81    pub cursor_y: usize,
82    /// `cursor_x` field.
83    pub cursor_x: usize,
84    /// `keypad` field.
85    pub keypad: bool,
86    /// `fg` field.
87    pub fg: i32,
88    /// `bg` field.
89    pub bg: i32,
90    /// `attrs` field.
91    pub attrs: u32,
92    /// Background `chtype` set by `zccmd_bg` (curses.c:908). Holds
93    /// the OR'd attribute / color-pair / character mask C `wbkgd`
94    /// receives.
95    pub bg_chtype: u32,
96    /// Per-window timeout in milliseconds. Port of the `wtimeout()`
97    /// state set by `zccmd_timeout` (curses.c:1255). `-1` = blocking.
98    pub timeout_ms: i32,
99    /// `parent` field.
100    pub parent: Option<String>,
101    /// `children` field.
102    pub children: Vec<String>,
103    /// `buffer` field.
104    buffer: Vec<Vec<char>>,
105}
106
107// =====================================================================
108// Port of `struct zcurses_namenumberpair` from `curses.c:71`.
109// =====================================================================
110
111/// Name → number pair entry. Used by attributes and colors lookup
112/// tables.
113#[derive(Debug, Clone, Copy)]
114pub struct zcurses_namenumberpair {
115    /// `name` field.
116    pub name: &'static str,
117    /// `number` field.
118    pub number: i32,
119}
120
121// =====================================================================
122// Port of `struct colorpairnode` from `Src/Modules/curses.c:76`.
123// =====================================================================
124
125/// Hash-table node mapping a color-pair name to its allocated short.
126/// C uses `struct hashnode` as the first field to support direct
127/// HashTable insertion; Rust uses (name → colorpairnode) HashMap so
128/// the node carries only the colorpair short here.
129///
130/// C definition (c:76-79):
131/// ```c
132/// struct colorpairnode {
133///     struct hashnode node;
134///     short colorpair;
135/// };
136/// typedef struct colorpairnode *Colorpairnode;
137/// ```
138#[derive(Debug, Clone, Copy)]
139pub struct colorpairnode {
140    // c:76
141    /// Color pair index (libncurses `init_pair` slot).
142    pub colorpair: i16, // c:78
143}
144
145// =====================================================================
146// Port of `struct zcurses_subcommand` (curses.c:83) + dispatch
147// table inside `bin_zcurses` (curses.c:1574).
148// =====================================================================
149
150/// Subcommand table entry. Port of `struct zcurses_subcommand`
151/// from `Src/Modules/curses.c:83`.
152struct zcurses_subcommand {
153    name: &'static str,
154    cmd: fn(&str, &[String]) -> i32, // c:84-85
155    minargs: i32,
156    maxargs: i32,
157}
158
159// =====================================================================
160// Module-static state — port of `curses.c:90-113`.
161//
162// Per PORT_PLAN.md these are bucket 2 (shell-wide) since multiple
163// callers (foreground evaluator + worker threads) may dispatch
164// `zcurses` simultaneously.
165// =====================================================================
166
167/// Port of `static LinkList zcurses_windows` from `curses.c:92`.
168/// HashMap keyed by window name; C uses an ordered linked list,
169/// the Rust port keeps insertion order in `WINDOW_ORDER` so
170/// `refresh` (no-arg) replays the same draw order.
171static zcurses_windows: OnceLock<Mutex<HashMap<String, zc_win>>> = OnceLock::new();
172
173/// Port of `static HashTable zcurses_colorpairs` from `curses.c:93`.
174/// Maps `"fg/bg"` → pair index.
175static zcurses_colorpairs: OnceLock<Mutex<HashMap<String, i16>>> = OnceLock::new();
176
177// =====================================================================
178// Port of `zccmd_mouse(const char *nam, char **args)` from `Src/Modules/curses.c:1294`.
179// =====================================================================
180
181/// `static int zcurses_flags` mouse mask, port of `curses.c:99`.
182static zcurses_flags: OnceLock<Mutex<u32>> = OnceLock::new();
183
184// =====================================================================
185// Error constants — port of `curses.c:102-110`.
186// =====================================================================
187
188/// `zc_errno` value: window name invalid (NULL or empty).
189pub const ZCURSES_EINVALID: i32 = 1;
190/// `zc_errno` value: window already defined (failed UNUSED check).
191pub const ZCURSES_EDEFINED: i32 = 2;
192/// `zc_errno` value: window undefined (failed USED check).
193pub const ZCURSES_EUNDEFINED: i32 = 3;
194
195impl zc_win {
196    /// WARNING: NOT IN CURSES.C — method on Rust-only `zc_win` wrapper.
197    /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
198    /// WARNING: NOT IN CURSES.C — Rust-only ctor; C uses
199    /// `(ZCWin)zshcalloc(sizeof(struct zc_win))` then field-by-field
200    /// assignment inside `zccmd_addwin` (curses.c:519).
201    fn new(name: &str, rows: usize, cols: usize, y: usize, x: usize) -> Self {
202        Self {
203            name: name.to_string(),
204            flags: 0,
205            rows,
206            cols,
207            y,
208            x,
209            cursor_y: 0,
210            cursor_x: 0,
211            keypad: false,
212            fg: -1,
213            bg: -1,
214            attrs: 0,
215            bg_chtype: 0,
216            timeout_ms: -1,
217            parent: None,
218            children: Vec::new(),
219            buffer: vec![vec![' '; cols]; rows],
220        }
221    }
222
223    /// WARNING: NOT IN CURSES.C — method on Rust-only `zc_win` wrapper.
224    /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
225    fn refresh(&self) -> io::Result<()> {
226        let mut stdout = io::stdout();
227        write!(stdout, "\x1b[{};{}H", self.y + 1, self.x + 1)?;
228        let attr_sgr = sgr_for_attrs(self.attrs);
229        write!(stdout, "{}", attr_sgr)?;
230        for (row_idx, row) in self.buffer.iter().enumerate() {
231            write!(stdout, "\x1b[{};{}H", self.y + row_idx + 1, self.x + 1)?;
232            let line: String = row.iter().collect();
233            write!(stdout, "{}", line)?;
234        }
235        write!(
236            stdout,
237            "\x1b[{};{}H",
238            self.y + self.cursor_y + 1,
239            self.x + self.cursor_x + 1
240        )?;
241        stdout.flush()
242    }
243}
244
245/// `zcurses_validate_window` criterion: must NOT already exist.
246pub const ZCURSES_UNUSED: i32 = 1;
247/// `zcurses_validate_window` criterion: must already exist.
248pub const ZCURSES_USED: i32 = 2;
249
250/// `zccmd_attr` mode: turn attribute on.
251pub const ZCURSES_ATTRON: i32 = 1;
252/// `zccmd_attr` mode: turn attribute off.
253pub const ZCURSES_ATTROFF: i32 = 2;
254
255/// Port of `static short next_cp` from `curses.c:113`. Counter for
256/// `init_pair()` allocations.
257static next_cp: OnceLock<Mutex<i16>> = OnceLock::new();
258
259/// Port of `struct zcurses_mouse_event` from `Src/Modules/curses.c:163`.
260/// Maps a libncurses button bit to a (button-number, event-kind) pair.
261///
262/// C definition (c:163-167):
263/// ```c
264/// struct zcurses_mouse_event {
265///     int button;
266///     int what;
267///     mmask_t event;
268/// };
269/// ```
270#[derive(Debug, Clone, Copy)]
271pub struct zcurses_mouse_event {
272    // c:163
273    /// Mouse button number (1..=5).
274    pub button: i32, // c:164
275    /// Event kind (`ZCME_*` from above).
276    pub what: i32, // c:165
277    /// libncurses mmask_t event bit (BUTTON1_PRESSED etc.).
278    pub event: u64, // c:166
279}
280
281/// `static mmask_t zcurses_mouse_mask`, port of `curses.c:205`.
282static zcurses_mouse_mask: OnceLock<Mutex<u32>> = OnceLock::new();
283
284// =====================================================================
285// Special-parameter accessors — `Src/Modules/curses.c:213`.
286// Each backs one of zsh's `$zcurses_*` magic-assoc/array params.
287// =====================================================================
288
289/// Direct port of `zcurses_pairs_to_array(nnps)` from `Src/Modules/
290/// curses.c:213`. C body (c:215-228): walk the namenumberpair table,
291/// allocate a `char **` of length+1, fill with `dupstring(name)` per
292/// entry, NULL-terminate. Rust returns `Vec<String>` directly.
293pub fn zcurses_pairs_to_array(nnps: &[zcurses_namenumberpair]) -> Vec<String> {
294    nnps.iter().map(|p| p.name.to_string()).collect() // c:222 dupstring
295}
296
297// =====================================================================
298// Port of `zcurses_strerror(int err)` from `Src/Modules/curses.c:233`.
299// =====================================================================
300
301/// Port of `zcurses_strerror(int err)` from `Src/Modules/curses.c:233`.
302///
303/// Map a `zc_errno` value to its human-readable message. C uses a
304/// static `errs[]` array keyed by errno; Rust port mirrors with
305/// the same four messages in the same index order.
306/// WARNING: param names don't match C — Rust=() vs C=(err)
307pub(crate) fn zcurses_strerror(err: i32) -> &'static str {
308    static ERRS: &[&str] = &[
309        "unknown error",
310        "window name invalid",
311        "window already defined",
312        "window undefined",
313    ];
314    let idx = if !(1..=3).contains(&err) {
315        0
316    } else {
317        err as usize
318    };
319    ERRS[idx]
320}
321
322// =====================================================================
323// Port of `zcurses_getwindowbyname(const char *name)` from `Src/Modules/curses.c:246`.
324// =====================================================================
325
326/// Port of `zcurses_getwindowbyname()` from
327/// `Src/Modules/curses.c:246`. Linear-search the windows list for a
328/// matching name. C returns the `LinkNode`; Rust port returns
329/// `bool` (presence) since the HashMap-backed lookup doesn't expose
330/// node iterators.
331/// WARNING: param names don't match C — Rust=() vs C=(name)
332pub(crate) fn zcurses_getwindowbyname(name: &str) -> bool {
333    windows_lock().lock().unwrap().contains_key(name)
334}
335
336// =====================================================================
337// Port of `zcurses_validate_window(char *win, int criteria)` from `Src/Modules/curses.c:259`.
338// =====================================================================
339
340/// Port of `zcurses_validate_window()` from
341/// `Src/Modules/curses.c:259`. Validate `win` per the criteria
342/// (`ZCURSES_UNUSED` = must not exist, `ZCURSES_USED` = must
343/// exist), set `zc_errno`, return `true` on success / `false` on
344/// failure.
345///
346/// Note: C returns the `LinkNode` (NULL = failure or
347/// not-yet-defined); the Rust port returns a bool because the
348/// HashMap doesn't expose a positional handle. Callers that need
349/// the existing window get it via `zcurses_getwindowbyname` after a
350/// successful USED check.
351/// WARNING: param names don't match C — Rust=(criteria) vs C=(win, criteria)
352pub(crate) fn zcurses_validate_window(win: &str, criteria: i32) -> bool {
353    if win.is_empty() {
354        zc_errno_set(ZCURSES_EINVALID);
355        return false;
356    }
357    let target_present = zcurses_getwindowbyname(win);
358    if target_present && (criteria & ZCURSES_UNUSED) != 0 {
359        zc_errno_set(ZCURSES_EDEFINED);
360        return false;
361    }
362    if !target_present && (criteria & ZCURSES_USED) != 0 {
363        zc_errno_set(ZCURSES_EUNDEFINED);
364        return false;
365    }
366    zc_errno_set(0);
367    target_present
368}
369
370// =====================================================================
371// Port of `zcurses_free_window(ZCWin w)` from `Src/Modules/curses.c:285`.
372// =====================================================================
373
374/// Port of `zcurses_free_window(ZCWin w)` from `Src/Modules/curses.c:285`.
375///
376/// Refuses to free permanent (`ZCWF_PERMANENT`) windows. Removes
377/// from the windows map and the order tracker.
378/// WARNING: param names don't match C — Rust=() vs C=(w)
379pub(crate) fn zcurses_free_window(name: &str) -> i32 {
380    let mut wins = windows_lock().lock().unwrap();
381    if let Some(w) = wins.get(name) {
382        if w.flags & ZCWF_PERMANENT != 0 {
383            return 1;
384        }
385    }
386    wins.remove(name);
387    let mut order = order_lock().lock().unwrap();
388    order.retain(|n| n != name);
389    0
390}
391
392// =====================================================================
393// Port of `zcurses_attrget(UNUSED(WINDOW *w), char *attr)` from `Src/Modules/curses.c:302`.
394// =====================================================================
395
396/// Port of `zcurses_attrget(UNUSED(WINDOW *w), char *attr)` from `Src/Modules/curses.c:302`.
397///
398/// Look up an attribute by user-facing name. Returns the matching
399/// table entry (with `name`, `number`) on hit, `None` on miss.
400/// WARNING: param names don't match C — Rust=(attr) vs C=(w, attr)
401pub(crate) fn zcurses_attrget(_w: &zc_win, attr: &str) -> Option<&'static zcurses_namenumberpair> {
402    if attr.is_empty() {
403        return None;
404    }
405    zcurses_attributes.iter().find(|p| p.name == attr)
406}
407
408// =====================================================================
409// Port of `zcurses_color(const char *color)` from `Src/Modules/curses.c:318`.
410// =====================================================================
411
412/// Port of `zcurses_color(const char *color)` from `Src/Modules/curses.c:318`.
413///
414/// Resolve a color name to its ncurses constant. Returns `-2` on
415/// miss (matching C's sentinel — `-1` is `default`, so the miss
416/// value sits one step further down).
417/// WARNING: param names don't match C — Rust=() vs C=(color)
418pub(crate) fn zcurses_color(color: &str) -> i32 {
419    for c in zcurses_colors {
420        if c.name == color {
421            return c.number;
422        }
423    }
424    -2
425}
426
427/// Port of `zcurses_colorget(const char *nam, char *colorpair)` from `Src/Modules/curses.c:331`.
428/// C body parses `"fg/bg"` (numeric or named on each side), resolves
429/// both via `zcurses_color`, allocates a new color-pair slot via
430/// `init_pair(next_cp, fg, bg)`, and caches the result keyed by the
431/// `"fg/bg"` string in `zcurses_colorpairs`. Returns the pair index.
432///
433/// C signature: `static Colorpairnode zcurses_colorget(const char *nam,
434/// char *colorpair)`. Rust port returns `Option<i16>` (the pair slot)
435/// since the Colorpairnode struct is collapsed into the
436/// `HashMap<String, i16>` colorpairs store.
437pub fn zcurses_colorget(nam: &str, colorpair: &str) -> Option<i16> {
438    // c:331
439    // c:331-342 — cache hit?
440    {
441        let g = colorpairs_lock().lock().ok()?;
442        if let Some(&cp) = g.get(colorpair) {
443            // c:342 gethashnode2
444            return Some(cp);
445        }
446    }
447    // c:347 — parse "fg/bg".
448    let (cp_str, bg_str) = match colorpair.split_once('/') {
449        // c:351
450        Some(t) => t,
451        None => return None, // c:354
452    };
453    // c:359-369 — resolve foreground.
454    let f: i16 = if !cp_str.is_empty() && cp_str.chars().next().unwrap_or('x').is_ascii_digit() {
455        // c:360
456        cp_str.parse::<i16>().unwrap_or(-2) // c:361 atoi
457    } else {
458        zcurses_color(cp_str) as i16 // c:363
459    };
460    // c:365-369 — resolve background.
461    let b: i16 = if !bg_str.is_empty() && bg_str.chars().next().unwrap_or('x').is_ascii_digit() {
462        // c:365
463        bg_str.parse::<i16>().unwrap_or(-2) // c:366
464    } else {
465        zcurses_color(bg_str) as i16 // c:368
466    };
467    // c:370-378 — error on unknown names.
468    if f == -2 || b == -2 {
469        // c:370
470        if f == -2 {
471            zwarnnam(
472                nam, // c:372
473                &format!("foreground color `{}' not known", cp_str),
474            );
475        }
476        if b == -2 {
477            zwarnnam(
478                nam, // c:374
479                &format!("background color `{}' not known", bg_str),
480            );
481        }
482        return None; // c:377
483    }
484    // c:381 — ++next_cp.
485    let mut np = next_cp_lock().lock().ok()?;
486    *np += 1; // c:381
487    let cp_slot = *np;
488    // c:382 — COLOR_PAIRS limit / init_pair check. Without a live
489    // ncurses link we accept any slot up to a large bound and skip
490    // the actual init_pair call; the colorpairs map is what callers
491    // consult to know the slot exists.
492    if cp_slot < 0 || cp_slot >= 1024 {
493        // c:382 COLOR_PAIRS
494        return None;
495    }
496    let _ = (f, b); // init_pair would consume
497    drop(np);
498    // c:387-394 — calloc cpn + addhashnode.
499    let mut g = colorpairs_lock().lock().ok()?;
500    g.insert(colorpair.to_string(), cp_slot); // c:393 addhashnode
501    Some(cp_slot) // c:397
502}
503
504/// Direct port of `zcurses_colornode(HashNode hn, int cp)` from `Src/Modules/curses.c:402`.
505/// C body (c:404-407): scanhashtable callback that compares each
506/// Colorpairnode's `colorpair` short field against `cp` and stashes
507/// the matching node in the global `cpn_match`. Static-link path:
508/// the Rust port returns the matching name directly so callers
509/// don't need a side-channel global.
510/// WARNING: param names don't match C — Rust=(name, pair, cp) vs C=(hn, cp)
511pub fn zcurses_colornode(name: &str, pair: i16, cp: i16) -> Option<String> {
512    // c:402
513    if pair == cp {
514        Some(name.to_string())
515    } else {
516        None
517    } // c:410
518}
519
520/// Direct port of `zcurses_colorget_reverse(cp)` from `Src/Modules/
521/// curses.c:410`. C body (c:412-419): scanhashtable over
522/// `zcurses_colorpairs`, calling zcurses_colornode per node; returns
523/// `cpn_match`. Static-link path: walk the colorpairs table and
524/// return the first name whose pair matches.
525pub fn zcurses_colorget_reverse(cp: i16) -> Option<String> {
526    // c:410
527    let g = colorpairs_lock().lock().ok()?; // c:410
528    if g.is_empty() {
529        return None;
530    } // c:413
531    for (name, pair) in g.iter() {
532        // c:417 scanhashtable
533        if let Some(matched) = zcurses_colornode(name, *pair, cp) {
534            // c:417
535            return Some(matched); // c:418 cpn_match
536        }
537    }
538    None
539}
540
541/// Direct port of `freecolorpairnode(HashNode hn)` from `Src/Modules/curses.c:422`.
542/// C body (c:424-427): `zsfree(hn->nam); zfree(hn, sizeof(struct
543/// colorpairnode));`. Rust drop handles both via Box ownership.
544pub fn freecolorpairnode(hn: &str) { // c:422
545                                     // c:422-426 — frees handled by Rust drop; nothing to do.
546}
547
548// =====================================================================
549// Port of `zccmd_init(UNUSED(const char *nam), UNUSED(char **args))` from `Src/Modules/curses.c:434`.
550// =====================================================================
551
552/// Port of `zccmd_init(UNUSED(const char *nam), UNUSED(char **args))` from `Src/Modules/curses.c:434`.
553/// WARNING: param names don't match C — Rust=(_args) vs C=(nam, args)
554pub(crate) fn zccmd_init(_nam: &str, _args: &[String]) -> i32 {
555    if zcurses_getwindowbyname("stdscr") {
556        // c:495-496 — `} else { settyinfo(&curses_tty_state); }` —
557        // re-entry restores the CURSES-mode termios captured at c:494
558        // (post-cbreak/noecho), re-ENTERING curses mode. The prior
559        // port restored the pre-init cooked state here — exactly
560        // inverted: `zcurses init` after an `end` dropped the
561        // terminal back to cooked mode instead of re-arming cbreak.
562        if let Ok(saved) = curses_tty_state.lock() {
563            if let Some(ti) = saved.as_ref() {
564                unsafe {
565                    libc::tcsetattr(0, libc::TCSANOW, ti as *const _);
566                }
567            }
568        }
569        return 0;
570    }
571    // c:443 — `gettyinfo(&saved_tty_state);` — snapshot the
572    // PRE-curses termios; zccmd_endwin's c:830 settyinfo restores
573    // this to hand the terminal back to the shell.
574    {
575        let mut saved = saved_tty_state.lock().unwrap();
576        if saved.is_none() {
577            let mut ti: libc::termios = unsafe { std::mem::zeroed() };
578            if unsafe { libc::tcgetattr(0, &mut ti) } == 0 {
579                *saved = Some(ti);
580            }
581        }
582    }
583    let mut stdout = io::stdout();
584    let _ = write!(stdout, "\x1b[?1049h\x1b[2J\x1b[H");
585    let _ = stdout.flush();
586    let _ = cbreak(); // c:492-493 cbreak(); noecho();
587                      // c:494 — `gettyinfo(&curses_tty_state);` — snapshot the termios
588                      // AFTER cbreak/noecho; this is what re-entry restores.
589    {
590        let mut cur = curses_tty_state.lock().unwrap();
591        let mut ti: libc::termios = unsafe { std::mem::zeroed() };
592        if unsafe { libc::tcgetattr(0, &mut ti) } == 0 {
593            *cur = Some(ti);
594        }
595    }
596    let (rows, cols) = terminal_size().unwrap_or((24, 80));
597    let mut stdscr = zc_win::new("stdscr", rows, cols, 0, 0);
598    stdscr.flags = ZCWF_PERMANENT;
599    windows_lock()
600        .lock()
601        .unwrap()
602        .insert("stdscr".into(), stdscr);
603    order_lock().lock().unwrap().push("stdscr".into());
604    *next_cp_lock().lock().unwrap() = 0;
605    // C: addhashnode(zcurses_colorpairs, ztrdup("default/default"), …)
606    // for the default pair.
607    colorpairs_lock()
608        .lock()
609        .unwrap()
610        .insert("default/default".into(), 0);
611    0
612}
613
614// =====================================================================
615// Port of `zccmd_addwin(const char *nam, char **args)` from `Src/Modules/curses.c:503`.
616// =====================================================================
617
618/// Port of `zccmd_addwin(const char *nam, char **args)` from `Src/Modules/curses.c:503`.
619///
620/// `addwin NAME ROWS COLS Y X [PARENT]`. Validates that NAME is
621/// not already in use via `zcurses_validate_window(_, ZCURSES_UNUSED)`,
622/// and (when PARENT is given) that PARENT exists via
623/// `zcurses_validate_window(_, ZCURSES_USED)`.
624/// WARNING: param names don't match C — Rust=(args) vs C=(nam, args)
625pub(crate) fn zccmd_addwin(nam: &str, args: &[String]) -> i32 {
626    if !zcurses_validate_window(args[0].as_str(), ZCURSES_UNUSED) && zc_errno_get() != 0 {
627        zerrnam(
628            nam,
629            &format!("{}: {}", zcurses_strerror(zc_errno_get()), args[0]),
630        );
631        return 1;
632    }
633    // c:514-517 — `nlines = atoi(args[1]); ncols = atoi(args[2]);
634    //              begin_y = atoi(args[3]); begin_x = atoi(args[4]);`
635    // atoi keeps the sign; the geometry contract is then newwin(3)'s:
636    //   - nlines/ncols == 0 → extend to the screen's bottom/right
637    //     edge (NOT an empty window — prior port created a 0x0
638    //     buffer that silently "worked")
639    //   - negative dims / origin off-screen → newwin returns NULL →
640    //     c:551-555 "failed to create window `%s'" + exit 1 (prior
641    //     port clamped negatives to 0 via parse::<usize> and never
642    //     failed)
643    let (nlines_raw, _) = crate::ported::utils::zstrtol(&args[1], 10); // c:514
644    let (ncols_raw, _) = crate::ported::utils::zstrtol(&args[2], 10); // c:515
645    let (begin_y_raw, _) = crate::ported::utils::zstrtol(&args[3], 10); // c:516
646    let (begin_x_raw, _) = crate::ported::utils::zstrtol(&args[4], 10); // c:517
647    let (scr_rows, scr_cols) = {
648        let wins = windows_lock().lock().unwrap();
649        wins.get("stdscr")
650            .map(|s| (s.rows as i64, s.cols as i64))
651            .unwrap_or((24, 80))
652    };
653    let fail_create = |nam: &str| -> i32 {
654        // c:551-555 — `zwarnnam(nam, "failed to create window `%s'", w->name);`
655        zwarnnam(nam, &format!("failed to create window `{}'", args[0]));
656        1
657    };
658    if nlines_raw < 0
659        || ncols_raw < 0
660        || begin_y_raw < 0
661        || begin_x_raw < 0
662        || begin_y_raw >= scr_rows
663        || begin_x_raw >= scr_cols
664    {
665        return fail_create(nam); // c:551 newwin NULL
666    }
667    // newwin(3): zero dims default to the distance to the screen edge.
668    let nlines = if nlines_raw == 0 {
669        (scr_rows - begin_y_raw) as usize
670    } else {
671        nlines_raw as usize
672    };
673    let ncols = if ncols_raw == 0 {
674        (scr_cols - begin_x_raw) as usize
675    } else {
676        ncols_raw as usize
677    };
678    let begin_y = begin_y_raw as usize;
679    let begin_x = begin_x_raw as usize;
680    let mut w = zc_win::new(args[0].as_str(), nlines, ncols, begin_y, begin_x);
681    if let Some(parent_name) = args.get(5) {
682        // C: node = zcurses_validate_window(args[5], ZCURSES_USED);
683        if !zcurses_validate_window(parent_name.as_str(), ZCURSES_USED) {
684            zwarnnam(
685                nam,
686                &format!("{}: {}", zcurses_strerror(zc_errno_get()), args[0]),
687            );
688            return 1;
689        }
690        w.parent = Some(parent_name.clone());
691        // C: zinsertlinknode(worig->children, lastnode(worig->children),
692        //                    (void *)w);
693        let mut wins = windows_lock().lock().unwrap();
694        if let Some(parent) = wins.get_mut(parent_name.as_str()) {
695            parent.children.push(args[0].clone());
696        }
697    }
698    windows_lock().lock().unwrap().insert(args[0].clone(), w);
699    order_lock().lock().unwrap().push(args[0].clone());
700    0
701}
702
703// =====================================================================
704// Port of `zccmd_delwin(const char *nam, char **args)` from `Src/Modules/curses.c:564`.
705// =====================================================================
706
707/// Port of `zccmd_delwin(const char *nam, char **args)` from `Src/Modules/curses.c:564`.
708/// WARNING: param names don't match C — Rust=(args) vs C=(nam, args)
709pub(crate) fn zccmd_delwin(nam: &str, args: &[String]) -> i32 {
710    // C: node = zcurses_validate_window(args[0], ZCURSES_USED);
711    if !zcurses_validate_window(args[0].as_str(), ZCURSES_USED) {
712        zwarnnam(
713            nam,
714            &format!("{}: {}", zcurses_strerror(zc_errno_get()), args[0]),
715        );
716        return 1;
717    }
718    // c:582-591 — pre-delete guards:
719    //
720    //     if (w->flags & ZCWF_PERMANENT) {
721    //         zwarnnam(nam, "window `%s' can't be deleted", args[0]);
722    //         return 1;
723    //     }
724    //     if (w->children && firstnode(w->children)) {
725    //         zwarnnam(nam, "window `%s' has subwindows, delete those first",
726    //                  w->name);
727    //         return 1;
728    //     }
729    //
730    // Prior port had no subwindow guard at all (a parent could be
731    // deleted out from under its children, orphaning their parent
732    // back-references) and the PERMANENT diagnostic dropped the
733    // window name.
734    let parent_name: Option<String> = {
735        let wins = windows_lock().lock().unwrap();
736        let w = match wins.get(args[0].as_str()) {
737            Some(w) => w,
738            None => return 1,
739        };
740        if w.flags & ZCWF_PERMANENT != 0 {
741            // c:582-584
742            zwarnnam(nam, &format!("window `{}' can't be deleted", args[0]));
743            return 1;
744        }
745        if !w.children.is_empty() {
746            // c:587-590
747            zwarnnam(
748                nam,
749                &format!("window `{}' has subwindows, delete those first", args[0]),
750            );
751            return 1;
752        }
753        w.parent.clone()
754    };
755    if zcurses_free_window(args[0].as_str()) != 0 {
756        zwarnnam(nam, &format!("window `{}' can't be deleted", args[0])); // c:583
757        return 1;
758    }
759    // c:601-611 — `if (w->parent) { ...remnode(wpc, pcnode)... }` —
760    // unlink the deleted window from its parent's children list so
761    // the parent's subwindow guard above doesn't permanently wedge
762    // on a stale name.
763    if let Some(pname) = parent_name {
764        let mut wins = windows_lock().lock().unwrap();
765        if let Some(parent) = wins.get_mut(pname.as_str()) {
766            parent.children.retain(|c| c != &args[0]); // c:608 remnode
767        }
768    }
769    0 // c:627
770}
771
772// =====================================================================
773// Port of `zccmd_refresh(const char *nam, char **args)` from `Src/Modules/curses.c:632`.
774// =====================================================================
775
776/// Port of `zccmd_refresh(const char *nam, char **args)` from `Src/Modules/curses.c:632`.
777/// WARNING: param names don't match C — Rust=(args) vs C=(nam, args)
778pub(crate) fn zccmd_refresh(nam: &str, args: &[String]) -> i32 {
779    let wins = windows_lock().lock().unwrap();
780    if args.is_empty() {
781        // C: refresh stdscr if no args; but the table also walks
782        // through every window. Rust port matches C's "no args =
783        // refresh stdscr" path via wnoutrefresh+doupdate inside C.
784        if let Some(stdscr) = wins.get("stdscr") {
785            let _ = stdscr.refresh();
786        }
787        return 0;
788    }
789    drop(wins);
790    for name in args {
791        if !zcurses_validate_window(name.as_str(), ZCURSES_USED) {
792            zwarnnam(
793                nam,
794                &format!("{}: {}", zcurses_strerror(zc_errno_get()), name),
795            );
796            return 1;
797        }
798        if let Some(w) = windows_lock().lock().unwrap().get(name.as_str()) {
799            let _ = w.refresh();
800        }
801    }
802    0
803}
804
805// =====================================================================
806// Port of `zccmd_move(const char *nam, char **args)` from `Src/Modules/curses.c:669`.
807// =====================================================================
808
809/// Port of `zccmd_move(const char *nam, char **args)` from `Src/Modules/curses.c:669`.
810/// WARNING: param names don't match C — Rust=(args) vs C=(nam, args)
811pub(crate) fn zccmd_move(nam: &str, args: &[String]) -> i32 {
812    if !zcurses_validate_window(args[0].as_str(), ZCURSES_USED) {
813        zwarnnam(
814            nam,
815            &format!("{}: {}", zcurses_strerror(zc_errno_get()), args[0]),
816        );
817        return 1;
818    }
819    // c:681-687 — `y = atoi(args[1]); x = atoi(args[2]);
820    //              if (wmove(w->win, y, x) != OK) return 1;`
821    // atoi: garbage/negative input parses to whatever leading int
822    // exists (0 for pure garbage, negative for "-1"); wmove then
823    // REJECTS any (y,x) outside the window with ERR → exit 1. Prior
824    // port parsed as usize (negative → 0) and silently swallowed
825    // out-of-bounds moves, returning 0 — `zcurses move w 999 0`
826    // exited 0 without moving where zsh exits 1.
827    let (new_y, _) = crate::ported::utils::zstrtol(&args[1], 10); // c:681 atoi
828    let (new_x, _) = crate::ported::utils::zstrtol(&args[2], 10); // c:682 atoi
829    let mut wins = windows_lock().lock().unwrap();
830    if let Some(w) = wins.get_mut(args[0].as_str()) {
831        if new_y < 0 || new_x < 0 || new_y as usize >= w.rows || new_x as usize >= w.cols {
832            return 1; // c:686-687 wmove != OK
833        }
834        w.cursor_y = new_y as usize;
835        w.cursor_x = new_x as usize;
836    }
837    0 // c:689
838}
839
840// =====================================================================
841// Port of `zccmd_clear(const char *nam, char **args)` from `Src/Modules/curses.c:694`.
842// =====================================================================
843
844/// Port of `zccmd_clear(const char *nam, char **args)` from `Src/Modules/curses.c:694`.
845/// WARNING: param names don't match C — Rust=(args) vs C=(nam, args)
846pub(crate) fn zccmd_clear(nam: &str, args: &[String]) -> i32 {
847    if !zcurses_validate_window(args[0].as_str(), ZCURSES_USED) {
848        zwarnnam(
849            nam,
850            &format!("{}: {}", zcurses_strerror(zc_errno_get()), args[0]),
851        );
852        return 1;
853    }
854    // c:707-718 — submode dispatch:
855    //
856    //     if (!args[1]) {
857    //         return werase(w->win) != OK;
858    //     } else if (!strcmp(args[1], "redraw")) {
859    //         return wclear(w->win) != OK;
860    //     } else if (!strcmp(args[1], "eol")) {
861    //         return wclrtoeol(w->win) != OK;
862    //     } else if (!strcmp(args[1], "bot")) {
863    //         return wclrtobot(w->win) != OK;
864    //     } else {
865    //         zwarnnam(nam, "`clear' expects `redraw', `eol' or `bot'");
866    //         return 1;
867    //     }
868    //
869    // Prior port ignored args[1] entirely — every submode full-wiped
870    // the buffer AND homed the cursor (ncurses werase/wclear leave
871    // the cursor where it was; only the cell contents blank). `eol` /
872    // `bot` partial clears and the unknown-submode diagnostic were
873    // all missing.
874    let mode = args.get(1).map(|s| s.as_str());
875    match mode {
876        None | Some("redraw") => {} // validated below
877        Some("eol") | Some("bot") => {}
878        Some(_) => {
879            // c:716-717
880            zwarnnam(nam, "`clear' expects `redraw', `eol' or `bot'");
881            return 1;
882        }
883    }
884    let mut wins = windows_lock().lock().unwrap();
885    if let Some(w) = wins.get_mut(args[0].as_str()) {
886        match mode {
887            None | Some("redraw") => {
888                // c:708 werase / c:710 wclear — blank every cell; the
889                // emulation has no clearok bit so both collapse to a
890                // full wipe. Cursor stays put per ncurses semantics.
891                for row in &mut w.buffer {
892                    for cell in row {
893                        *cell = ' ';
894                    }
895                }
896            }
897            Some("eol") => {
898                // c:712 wclrtoeol — blank from cursor to end of line.
899                if w.cursor_y < w.rows {
900                    for x in w.cursor_x..w.cols {
901                        w.buffer[w.cursor_y][x] = ' ';
902                    }
903                }
904            }
905            Some("bot") => {
906                // c:714 wclrtobot — blank from cursor to end of line,
907                // then every row below.
908                if w.cursor_y < w.rows {
909                    for x in w.cursor_x..w.cols {
910                        w.buffer[w.cursor_y][x] = ' ';
911                    }
912                    for y in (w.cursor_y + 1)..w.rows {
913                        for x in 0..w.cols {
914                            w.buffer[y][x] = ' ';
915                        }
916                    }
917                }
918            }
919            Some(_) => unreachable!(),
920        }
921    }
922    0
923}
924
925// =====================================================================
926// Port of `zccmd_char(const char *nam, char **args)` from `Src/Modules/curses.c:723`.
927// =====================================================================
928
929/// Port of `zccmd_char(const char *nam, char **args)` from `Src/Modules/curses.c:723`. Writes
930/// one character into the window buffer at the current cursor.
931/// WARNING: param names don't match C — Rust=(args) vs C=(nam, args)
932pub(crate) fn zccmd_char(nam: &str, args: &[String]) -> i32 {
933    if !zcurses_validate_window(args[0].as_str(), ZCURSES_USED) {
934        zwarnnam(
935            nam,
936            &format!("{}: {}", zcurses_strerror(zc_errno_get()), args[0]),
937        );
938        return 1;
939    }
940    let ch = match args[1].chars().next() {
941        Some(c) => c,
942        None => return 1,
943    };
944    let mut wins = windows_lock().lock().unwrap();
945    if let Some(w) = wins.get_mut(args[0].as_str()) {
946        if w.cursor_y < w.rows && w.cursor_x < w.cols {
947            w.buffer[w.cursor_y][w.cursor_x] = ch;
948            // C: wadd_wch advances the cursor.
949            w.cursor_x += 1;
950            if w.cursor_x >= w.cols {
951                w.cursor_x = 0;
952                if w.cursor_y + 1 < w.rows {
953                    w.cursor_y += 1;
954                }
955            }
956        }
957    }
958    0
959}
960
961// =====================================================================
962// Port of `zccmd_string(const char *nam, char **args)` from `Src/Modules/curses.c:759`.
963// =====================================================================
964
965/// Port of `zccmd_string(const char *nam, char **args)` from `Src/Modules/curses.c:759`.
966/// WARNING: param names don't match C — Rust=(args) vs C=(nam, args)
967pub(crate) fn zccmd_string(nam: &str, args: &[String]) -> i32 {
968    if !zcurses_validate_window(args[0].as_str(), ZCURSES_USED) {
969        zwarnnam(
970            nam,
971            &format!("{}: {}", zcurses_strerror(zc_errno_get()), args[0]),
972        );
973        return 1;
974    }
975    // c:780-788 — `mb_charinit();
976    //              while (*str && (clen = mb_metacharlenconv(str, &wc))) {
977    //                  str += clen;
978    //                  if (wc == WEOF) continue;
979    //                  *wptr++ = wc;
980    //              }`
981    // mb_metacharlenconv is the META-AWARE wide decoder: it strips
982    // zsh's Meta+(byte^32) encoding while assembling each wchar. The
983    // shell-side arg arrives metafied; walking chars() on it directly
984    // fed the raw Meta escape pairs into the window buffer for any
985    // high-byte text. Decode first, then per-char walk.
986    let raw = crate::ported::utils::unmeta(&args[1]); // c:783 Meta strip
987    let mut wins = windows_lock().lock().unwrap();
988    if let Some(w) = wins.get_mut(args[0].as_str()) {
989        for ch in raw.chars() {
990            // c:783 wide-char walk
991            if w.cursor_y < w.rows && w.cursor_x < w.cols {
992                w.buffer[w.cursor_y][w.cursor_x] = ch;
993                w.cursor_x += 1;
994                if w.cursor_x >= w.cols {
995                    w.cursor_x = 0;
996                    if w.cursor_y + 1 < w.rows {
997                        w.cursor_y += 1;
998                    }
999                }
1000            }
1001        }
1002    }
1003    0
1004}
1005
1006// =====================================================================
1007// Port of `zccmd_border(const char *nam, char **args)` from `Src/Modules/curses.c:802`.
1008// =====================================================================
1009
1010/// Port of `zccmd_border(const char *nam, char **args)` from `Src/Modules/curses.c:802`.
1011///
1012/// C calls libncurses's `wborder(w, 0, 0, 0, 0, 0, 0, 0, 0)` which
1013/// draws default ACS line-drawing chars around the window. The
1014/// Rust port writes Unicode box-drawing equivalents to the buffer
1015/// perimeter — the closest faithful match without libncurses ACS
1016/// codepoint dispatch.
1017/// WARNING: param names don't match C — Rust=(args) vs C=(nam, args)
1018pub(crate) fn zccmd_border(nam: &str, args: &[String]) -> i32 {
1019    if !zcurses_validate_window(args[0].as_str(), ZCURSES_USED) {
1020        zwarnnam(
1021            nam,
1022            &format!("{}: {}", zcurses_strerror(zc_errno_get()), args[0]),
1023        );
1024        return 1;
1025    }
1026    let mut wins = windows_lock().lock().unwrap();
1027    if let Some(w) = wins.get_mut(args[0].as_str()) {
1028        if w.rows < 2 || w.cols < 2 {
1029            return 0;
1030        }
1031        // Top + bottom horizontal runs.
1032        for x in 1..w.cols.saturating_sub(1) {
1033            w.buffer[0][x] = '─';
1034            w.buffer[w.rows - 1][x] = '─';
1035        }
1036        // Left + right vertical runs.
1037        for y in 1..w.rows.saturating_sub(1) {
1038            w.buffer[y][0] = '│';
1039            w.buffer[y][w.cols - 1] = '│';
1040        }
1041        // Corners.
1042        w.buffer[0][0] = '┌';
1043        w.buffer[0][w.cols - 1] = '┐';
1044        w.buffer[w.rows - 1][0] = '└';
1045        w.buffer[w.rows - 1][w.cols - 1] = '┘';
1046    }
1047    0
1048}
1049
1050// =====================================================================
1051// Port of `zccmd_endwin(UNUSED(const char *nam), UNUSED(char **args))` from `Src/Modules/curses.c:823`.
1052// =====================================================================
1053
1054/// Port of `zccmd_endwin(UNUSED(const char *nam), UNUSED(char **args))` from `Src/Modules/curses.c:823`.
1055/// WARNING: param names don't match C — Rust=(_args) vs C=(nam, args)
1056pub(crate) fn zccmd_endwin(_nam: &str, _args: &[String]) -> i32 {
1057    // c:825-838:
1058    //
1059    //     if (stdscr_win) {
1060    //         endwin();
1061    //         /* Restore TTY as it was before zcurses -i */
1062    //         settyinfo(&saved_tty_state);
1063    //         gettyinfo(&shttyinfo);
1064    //     }
1065    //     return 0;
1066    if !zcurses_getwindowbyname("stdscr") {
1067        return 0;
1068    }
1069    let mut stdout = io::stdout();
1070    let _ = write!(stdout, "\x1b[?1049l\x1b[0m"); // c:828 endwin()
1071    let _ = stdout.flush();
1072    // c:830 — `settyinfo(&saved_tty_state);` — hand the terminal back
1073    // to the shell in its pre-`zcurses init` mode. Prior port skipped
1074    // this entirely, leaving the tty in cbreak/noecho after
1075    // `zcurses end` — a cooked-mode shell with no echo and no canonical
1076    // line editing until the user blind-typed `stty sane`.
1077    if let Ok(saved) = saved_tty_state.lock() {
1078        if let Some(ti) = saved.as_ref() {
1079            unsafe {
1080                libc::tcsetattr(0, libc::TCSANOW, ti as *const _);
1081            }
1082        }
1083    }
1084    // C endwin does NOT free the window list — windows survive so a
1085    // later `zcurses init` re-enters curses mode with state intact
1086    // (c:495-496 settyinfo path); only module cleanup_ frees them.
1087    // Prior port cleared windows/order/colorpairs here, destroying
1088    // every window across an end/init bounce.
1089    0 // c:838
1090}
1091
1092// =====================================================================
1093// Port of `zccmd_attr(const char *nam, char **args)` from `Src/Modules/curses.c:843`.
1094// =====================================================================
1095
1096/// Port of `zccmd_attr(const char *nam, char **args)` from `Src/Modules/curses.c:843`.
1097///
1098/// `attr WINDOW [+ATTR|-ATTR]...`: turn each named attribute on
1099/// (`+`) or off (`-`). C handles ZCURSES_ATTRON / ZCURSES_ATTROFF
1100/// dispatch internally; Rust port mirrors via the `+`/`-` prefix.
1101/// WARNING: param names don't match C — Rust=(args) vs C=(nam, args)
1102pub(crate) fn zccmd_attr(nam: &str, args: &[String]) -> i32 {
1103    if !zcurses_validate_window(args[0].as_str(), ZCURSES_USED) {
1104        zwarnnam(
1105            nam,
1106            &format!("{}: {}", zcurses_strerror(zc_errno_get()), args[0]),
1107        );
1108        return 1;
1109    }
1110    let mut ret = 0; // c:848
1111    let mut wins = windows_lock().lock().unwrap();
1112    let w = match wins.get_mut(args[0].as_str()) {
1113        Some(w) => w,
1114        None => return 1,
1115    };
1116    for spec in &args[1..] {
1117        // c:861
1118        if spec.contains('/') {
1119            // c:862-866 — `if (strchr(*attrs, '/')) {
1120            //                  if ((cpn = zcurses_colorget(nam, *attrs)) == NULL ||
1121            //                      wcolor_set(w->win, cpn->colorpair, NULL) == ERR)
1122            //                      ret = 1;
1123            //              }`
1124            // Color-pair spec "fg/bg". Prior port had NO `/` arm —
1125            // `zcurses attr win red/black` fell into the attribute
1126            // lookup and errored "attribute `red/black' not known".
1127            // The pair index lands in the chtype-convention high bits
1128            // of w.attrs (same COLOR_PAIR(n)<<16 encoding zccmd_bg
1129            // uses) for the refresh-time SGR emitter.
1130            let cp = colorpair_get_or_alloc(spec);
1131            if cp < 0 {
1132                ret = 1; // c:866
1133            } else {
1134                w.attrs = (w.attrs & 0xffff) | ((cp as u32) << 16); // c:865 wcolor_set
1135            }
1136            continue;
1137        }
1138        // c:872-885 — leading +/- selects ATTRON/ATTROFF; bare name
1139        // defaults to ATTRON.
1140        let (mode, attr_name) = match spec.as_bytes().first() {
1141            Some(b'+') => (ZCURSES_ATTRON, &spec[1..]),  // c:877-879
1142            Some(b'-') => (ZCURSES_ATTROFF, &spec[1..]), // c:873-875
1143            _ => (ZCURSES_ATTRON, spec.as_str()),        // c:881-883
1144        };
1145        match zcurses_attrget(w, attr_name) {
1146            Some(entry) => match mode {
1147                ZCURSES_ATTRON => w.attrs |= entry.number as u32, // c:892 wattron
1148                ZCURSES_ATTROFF => w.attrs &= !(entry.number as u32), // c:896 wattroff
1149                _ => {}
1150            },
1151            None => {
1152                // c:886-888 — `zwarnnam(nam, "attribute `%s' not
1153                // known", ptr); ret = 1;` — C CONTINUES the loop so
1154                // later specs still apply; prior port returned 1
1155                // immediately, dropping every spec after the bad one.
1156                zwarnnam(nam, &format!("attribute `{}' not known", attr_name));
1157                ret = 1; // c:888
1158            }
1159        }
1160    }
1161    ret // c:903
1162}
1163
1164// =====================================================================
1165// Port of `zccmd_bg(const char *nam, char **args)` from `Src/Modules/curses.c:908`.
1166// =====================================================================
1167
1168/// Port of `zccmd_bg(const char *nam, char **args)` from `Src/Modules/curses.c:908`.
1169///
1170/// `bg WINDOW [+ATTR|-ATTR|@CHAR|FG/BG]...`: build a `chtype` mask
1171/// of attributes / a fill character / a color-pair, then apply it
1172/// as the window's background via `wbkgd()`. The Rust port stores
1173/// the resolved mask on `w.bg_chtype` since the ANSI-escape pipeline
1174/// emits the equivalent SGR at refresh time.
1175/// WARNING: param names don't match C — Rust=(args) vs C=(nam, args)
1176pub(crate) fn zccmd_bg(nam: &str, args: &[String]) -> i32 {
1177    if !zcurses_validate_window(args[0].as_str(), ZCURSES_USED) {
1178        zwarnnam(
1179            nam,
1180            &format!("{}: {}", zcurses_strerror(zc_errno_get()), args[0]),
1181        );
1182        return 1;
1183    }
1184    let mut ret = 0;
1185    let mut ch: u32 = 0;
1186    let mut wins = windows_lock().lock().unwrap();
1187    let w = match wins.get_mut(args[0].as_str()) {
1188        Some(w) => w,
1189        None => return 1,
1190    };
1191    for spec in &args[1..] {
1192        if spec.contains('/') {
1193            // Color pair "fg/bg" — resolve via colorpairs, OR pair
1194            // index into ch's high bits per `COLOR_PAIR(n)`. Since
1195            // libncurses isn't linked we just record the pair index
1196            // for later SGR emission.
1197            let cp = colorpair_get_or_alloc(spec);
1198            if cp < 0 {
1199                ret = 1;
1200            } else {
1201                // C: ch |= COLOR_PAIR(cp); — high bits encode pair.
1202                ch |= (cp as u32) << 16;
1203            }
1204        } else if let Some(rest) = spec.strip_prefix('@') {
1205            // C: ch |= the character itself. Use first char.
1206            if let Some(c) = rest.chars().next() {
1207                ch |= c as u32;
1208            }
1209        } else {
1210            let (mode, attr_name) = match spec.as_bytes().first() {
1211                Some(b'+') => (ZCURSES_ATTRON, &spec[1..]),
1212                Some(b'-') => (ZCURSES_ATTROFF, &spec[1..]),
1213                _ => (ZCURSES_ATTRON, spec.as_str()),
1214            };
1215            match zcurses_attrget(w, attr_name) {
1216                Some(p) => {
1217                    if mode == ZCURSES_ATTRON {
1218                        ch |= p.number as u32; // c:967 wattron
1219                    } else {
1220                        ch &= !(p.number as u32); // c:971 wattroff
1221                    }
1222                }
1223                None => {
1224                    // c:961-963 — `zwarnnam(...); ret = 1;` — C
1225                    // continues the spec loop; prior port returned 1
1226                    // immediately, dropping later specs AND skipping
1227                    // the c:979 wbkgd gate below.
1228                    zwarnnam(nam, &format!("attribute `{}' not known", attr_name));
1229                    ret = 1; // c:963
1230                }
1231            }
1232        }
1233    }
1234    if ret == 0 {
1235        // C: return wbkgd(w->win, ch) != OK;
1236        w.bg_chtype = ch;
1237    }
1238    ret
1239}
1240
1241// =====================================================================
1242// Port of `zccmd_scroll(const char *nam, char **args)` from `Src/Modules/curses.c:986`.
1243// =====================================================================
1244
1245/// Port of `zccmd_scroll(const char *nam, char **args)` from `Src/Modules/curses.c:986`.
1246///
1247/// `scroll WINDOW (on|off|N)`: enable/disable scroll for the
1248/// window, or scroll by N lines.
1249/// WARNING: param names don't match C — Rust=(args) vs C=(nam, args)
1250pub(crate) fn zccmd_scroll(nam: &str, args: &[String]) -> i32 {
1251    if !zcurses_validate_window(args[0].as_str(), ZCURSES_USED) {
1252        zwarnnam(
1253            nam,
1254            &format!("{}: {}", zcurses_strerror(zc_errno_get()), args[0]),
1255        );
1256        return 1;
1257    }
1258    let mut wins = windows_lock().lock().unwrap();
1259    let w = match wins.get_mut(args[0].as_str()) {
1260        Some(w) => w,
1261        None => return 1,
1262    };
1263    match args[1].as_str() {
1264        "on" => {
1265            w.flags |= ZCWF_SCROLL;
1266        }
1267        "off" => {
1268            w.flags &= !ZCWF_SCROLL;
1269        }
1270        s => {
1271            let n: i32 = match s.parse() {
1272                Ok(n) => n,
1273                Err(_) => {
1274                    drop(wins);
1275                    zwarnnam(
1276                        nam,
1277                        &format!("scroll requires `on', `off' or integer: {}", s),
1278                    );
1279                    return 1;
1280                }
1281            };
1282            // C: wscrl(w->win, sl) — scroll buffer up by N lines
1283            // (negative = down).
1284            if n > 0 {
1285                for _ in 0..n {
1286                    if !w.buffer.is_empty() {
1287                        w.buffer.remove(0);
1288                        w.buffer.push(vec![' '; w.cols]);
1289                    }
1290                }
1291            } else {
1292                for _ in 0..(-n) {
1293                    if !w.buffer.is_empty() {
1294                        w.buffer.pop();
1295                        w.buffer.insert(0, vec![' '; w.cols]);
1296                    }
1297                }
1298            }
1299        }
1300    }
1301    0
1302}
1303
1304// =====================================================================
1305// Port of `zccmd_input(const char *nam, char **args)` from `Src/Modules/curses.c:1029`.
1306// =====================================================================
1307
1308/// Port of `zccmd_input(const char *nam, char **args)` from `Src/Modules/curses.c:1029`.
1309///
1310/// `input WINDOW [VAR [KEYVAR [MOUSEVAR]]]`: read one character
1311/// from stdin into VAR. With KEYVAR, also recognise keypad / arrow /
1312/// function keys — the Rust port parses the common CSI sequences
1313/// (`\e[A`/`\e[B`/`\e[C`/`\e[D` for arrows, `\e[1~`/`\e[2~`/etc.).
1314/// With a fourth MOUSEVAR arg, `read_key_sequence` decodes the xterm
1315/// SGR-1006 mouse report (`\e[<b;x;yM|m`) — and the legacy X10 form
1316/// (`\e[Mbxy`) — into a synthesized `KEY_MOUSE` code + a pending
1317/// `Mevent`, which this branch maps to the MOUSEVAR array exactly as
1318/// C does via `getmouse` / `zcurses_mouse_map` (c:1209-1263).
1319/// WARNING: param names don't match C — Rust=(args) vs C=(nam, args)
1320pub(crate) fn zccmd_input(nam: &str, args: &[String]) -> i32 {
1321    if !zcurses_validate_window(args[0].as_str(), ZCURSES_USED) {
1322        zwarnnam(
1323            nam,
1324            &format!("{}: {}", zcurses_strerror(zc_errno_get()), args[0]),
1325        );
1326        return 1;
1327    }
1328    // C: keypad(w->win, args.len() >= 3 ? TRUE : FALSE);
1329    let want_keypad = args.len() >= 3;
1330
1331    // c:1108-1131 — mousemask() setup. Pure-Rust curses has no
1332    // libncurses, so "enabling mouse mode" means (a) flipping the
1333    // ZCF_MOUSE_ACTIVE state read_key_sequence's decoder gates on and
1334    // (b) asking the terminal to emit xterm mouse reports via DECSET
1335    // 1000 (X10) + 1006 (SGR) — the native analog of what ncurses'
1336    // mousemask() drives through terminfo. `nargs >= 4` == a MOUSEVAR
1337    // was supplied.
1338    if args.len() >= 4 {
1339        let mut flags = flags_lock().lock().unwrap();
1340        // c:1110-1111 — (re)arm when inactive or the mask changed.
1341        if *flags & ZCF_MOUSE_ACTIVE == 0 || *flags & ZCF_MOUSE_MASK_CHANGED != 0 {
1342            *flags = (*flags & !ZCF_MOUSE_MASK_CHANGED) | ZCF_MOUSE_ACTIVE; // c:1116-1117
1343            drop(flags);
1344            let mut out = io::stdout();
1345            let _ = write!(out, "\x1b[?1000h\x1b[?1006h");
1346            let _ = out.flush();
1347        }
1348    } else {
1349        // c:1125-1130 — no MOUSEVAR: disable mouse mode if it was on.
1350        let mut flags = flags_lock().lock().unwrap();
1351        if *flags & ZCF_MOUSE_ACTIVE != 0 {
1352            *flags &= !ZCF_MOUSE_ACTIVE; // c:1128
1353            drop(flags);
1354            let mut out = io::stdout();
1355            let _ = write!(out, "\x1b[?1006l\x1b[?1000l");
1356            let _ = out.flush();
1357        }
1358    }
1359
1360    let timeout = windows_lock()
1361        .lock()
1362        .unwrap()
1363        .get(args[0].as_str())
1364        .map(|w| w.timeout_ms)
1365        .unwrap_or(-1);
1366    let (key_str, key_code) = match read_key_sequence(want_keypad, timeout) {
1367        Some(pair) => pair,
1368        None => return 1,
1369    };
1370    // c:1200-1204 — store the character string into VAR (default REPLY).
1371    let var = args.get(1).map(|v| v.as_str()).unwrap_or("REPLY");
1372    setsparam(var, &key_str);
1373
1374    if want_keypad {
1375        // c:1206 — nargs >= 3.
1376        if key_code > 0 {
1377            // c:1207 — a keypad / mouse code, not a plain character.
1378            if args.len() >= 4 && key_code == KEY_MOUSE {
1379                // c:1209-1263 — KEY_MOUSE branch.
1380                if let Some(kv) = args.get(2) {
1381                    setsparam(kv, "MOUSE"); // c:1215
1382                }
1383                let mvar = &args[3];
1384                // c:1217 — getmouse(): pull the pending decoded event.
1385                match PENDING_MOUSE.lock().unwrap().take() {
1386                    None => {
1387                        // c:1224 — nothing queued: empty array, success.
1388                        setaparam(mvar, Vec::<String>::new());
1389                    }
1390                    Some(m) => {
1391                        // c:1226-1234 — id, x, y, z leading fields.
1392                        let mut margs: Vec<String> = vec![
1393                            m.id.to_string(),
1394                            m.x.to_string(),
1395                            m.y.to_string(),
1396                            m.z.to_string(),
1397                        ];
1398                        // c:1240-1255 — one "<EVENT><button>" token per
1399                        // matching bstate bit.
1400                        for me in ZCURSES_MOUSE_MAP {
1401                            if m.bstate & me.event != 0 {
1402                                if let Some(&(name, _)) = ZCURSES_MOUSE_EVENT_LIST
1403                                    .iter()
1404                                    .find(|&&(_, num)| num == me.what)
1405                                {
1406                                    margs.push(format!("{}{}", name, me.button));
1407                                }
1408                            }
1409                        }
1410                        // c:1256-1261 — modifier tokens.
1411                        if m.bstate & BUTTON_SHIFT != 0 {
1412                            margs.push("SHIFT".to_string());
1413                        }
1414                        if m.bstate & BUTTON_CTRL != 0 {
1415                            margs.push("CTRL".to_string());
1416                        }
1417                        if m.bstate & BUTTON_ALT != 0 {
1418                            margs.push("ALT".to_string());
1419                        }
1420                        setaparam(mvar, margs); // c:1262
1421                    }
1422                }
1423            } else if let Some(kv) = args.get(2) {
1424                // c:1264-1284 — non-mouse keypad code.
1425                if key_code >= KEY_F0 && key_code < KEY_F0 + 64 {
1426                    // c:1276-1281 — function keys aren't in keypad_names;
1427                    // F1.. print "Fn", KEY_F0 itself prints its raw number.
1428                    let fbuf = if key_code > KEY_F0 {
1429                        format!("F{}", key_code - KEY_F0)
1430                    } else {
1431                        key_code.to_string()
1432                    };
1433                    setsparam(kv, &fbuf); // c:1283
1434                } else if let Some(name) = keypad_name(key_code) {
1435                    // c:1269-1274 — named key: set KEYVAR and return
1436                    // WITHOUT clearing MOUSEVAR (C's early `return 0`).
1437                    setsparam(kv, &name);
1438                    return 0;
1439                } else {
1440                    // c:1280 — unrecognized number: print it raw.
1441                    setsparam(kv, &key_code.to_string()); // c:1283
1442                }
1443            }
1444        } else if let Some(kv) = args.get(2) {
1445            // c:1288-1290 — plain character: KEYVAR := "".
1446            setsparam(kv, "");
1447        }
1448    }
1449    // c:1294-1295 — a non-mouse read with a MOUSEVAR clears it to an
1450    // EMPTY ARRAY (mkarray(NULL)). Named keys already returned above.
1451    if key_code != KEY_MOUSE && args.len() >= 4 {
1452        setaparam(&args[3], Vec::<String>::new());
1453    }
1454    0 // c:1297
1455}
1456
1457// =====================================================================
1458// Port of `zccmd_timeout(const char *nam, char **args)` from `Src/Modules/curses.c:1255`.
1459// =====================================================================
1460
1461/// Port of `zccmd_timeout(const char *nam, char **args)` from `Src/Modules/curses.c:1255`.
1462/// WARNING: param names don't match C — Rust=(args) vs C=(nam, args)
1463pub(crate) fn zccmd_timeout(nam: &str, args: &[String]) -> i32 {
1464    if !zcurses_validate_window(args[0].as_str(), ZCURSES_USED) {
1465        zwarnnam(
1466            nam,
1467            &format!("{}: {}", zcurses_strerror(zc_errno_get()), args[0]),
1468        );
1469        return 1;
1470    }
1471    let to: i32 = match args[1].parse() {
1472        Ok(n) => n,
1473        Err(_) => {
1474            zwarnnam(nam, &format!("timeout requires an integer: {}", args[1]));
1475            return 1;
1476        }
1477    };
1478    let mut wins = windows_lock().lock().unwrap();
1479    if let Some(w) = wins.get_mut(args[0].as_str()) {
1480        w.timeout_ms = to;
1481    }
1482    0
1483}
1484
1485/// Port of `zccmd_mouse(const char *nam, char **args)` from `Src/Modules/curses.c:1294`.
1486///
1487/// `mouse [+motion|-motion|delay N]...`: toggle the mouse mode.
1488/// Without libncurses calling `mouseinterval()` / `mousemask()`, the
1489/// Rust port records the mask + delay into module statics; the
1490/// ZCF_MOUSE_MASK_CHANGED flag it sets makes the next `zcurses input`
1491/// re-arm the terminal, and `read_key_sequence` consults the mask when
1492/// decoding xterm SGR / X10 mouse reports.
1493/// WARNING: param names don't match C — Rust=(args) vs C=(nam, args)
1494pub(crate) fn zccmd_mouse(nam: &str, args: &[String]) -> i32 {
1495    let mut idx = 0usize;
1496    while idx < args.len() {
1497        let arg = args[idx].as_str();
1498        if arg == "delay" {
1499            idx += 1;
1500            let v: i32 = match args.get(idx).and_then(|a| a.parse().ok()) {
1501                Some(v) => v,
1502                None => {
1503                    zwarnnam(nam, "mouse delay requires an integer argument");
1504                    return 1;
1505                }
1506            };
1507            // C: mouseinterval(delay).
1508            // WARNING: NOT IN CURSES.C — stored locally; libncurses's
1509            // `mouseinterval()` would be called when ncurses is linked.
1510            let _ = v;
1511            idx += 1;
1512        } else {
1513            let (onoff, name) = match arg.as_bytes().first() {
1514                Some(b'+') => (true, &arg[1..]),
1515                Some(b'-') => (false, &arg[1..]),
1516                _ => (true, arg),
1517            };
1518            if name == "motion" {
1519                let mut mask = mouse_mask_lock().lock().unwrap();
1520                let old = *mask;
1521                if onoff {
1522                    *mask |= REPORT_MOUSE_POSITION;
1523                } else {
1524                    *mask &= !REPORT_MOUSE_POSITION;
1525                }
1526                if old != *mask {
1527                    *flags_lock().lock().unwrap() |= ZCF_MOUSE_MASK_CHANGED;
1528                }
1529            } else {
1530                zwarnnam(nam, &format!("unrecognised mouse command: {}", name));
1531                return 1;
1532            }
1533            idx += 1;
1534        }
1535    }
1536    0
1537}
1538
1539// =====================================================================
1540// Port of `zccmd_position(const char *nam, char **args)` from `Src/Modules/curses.c:1343`.
1541// =====================================================================
1542
1543/// Port of `zccmd_position(const char *nam, char **args)` from `Src/Modules/curses.c:1343`.
1544///
1545/// `position WINDOW VAR`: write `[cy, cx, y, x, rows, cols]` into
1546/// the named array param. C uses `getyx`/`getbegyx`/`getmaxyx`
1547/// macros; Rust port reads the cached fields on `zc_win`.
1548/// WARNING: param names don't match C — Rust=(args) vs C=(nam, args)
1549pub(crate) fn zccmd_position(nam: &str, args: &[String]) -> i32 {
1550    if !zcurses_validate_window(args[0].as_str(), ZCURSES_USED) {
1551        zwarnnam(
1552            nam,
1553            &format!("{}: {}", zcurses_strerror(zc_errno_get()), args[0]),
1554        );
1555        return 1;
1556    }
1557    let wins = windows_lock().lock().unwrap();
1558    let w = match wins.get(args[0].as_str()) {
1559        Some(w) => w,
1560        None => return 1,
1561    };
1562    let arr = vec![
1563        w.cursor_y.to_string(),
1564        w.cursor_x.to_string(),
1565        w.y.to_string(),
1566        w.x.to_string(),
1567        w.rows.to_string(),
1568        w.cols.to_string(),
1569    ];
1570    drop(wins);
1571    // c:1376 — `setaparam(args[1], array);` — REAL array of six
1572    // numbers (cury curx begy begx maxy maxx). Prior port colon-
1573    // joined into a scalar, so `$pos[1]` indexed the first CHARACTER
1574    // of "3:5:0:0:24:80" instead of the cursor row.
1575    setaparam(&args[1], arr); // c:1376
1576    0 // c:1377
1577}
1578
1579// =====================================================================
1580// Port of `zccmd_querychar(const char *nam, char **args)` from `Src/Modules/curses.c:1382`.
1581// =====================================================================
1582
1583/// Port of `zccmd_querychar(const char *nam, char **args)` from `Src/Modules/curses.c:1382`.
1584///
1585/// `querychar WINDOW [VAR]`: write `[char, color, attr...]` into
1586/// the named array (or `reply`). C uses `winch` to read the
1587/// character + color + attrs at the cursor; Rust port reads from
1588/// the in-memory buffer.
1589/// WARNING: param names don't match C — Rust=(args) vs C=(nam, args)
1590pub(crate) fn zccmd_querychar(nam: &str, args: &[String]) -> i32 {
1591    if !zcurses_validate_window(args[0].as_str(), ZCURSES_USED) {
1592        zwarnnam(
1593            nam,
1594            &format!("{}: {}", zcurses_strerror(zc_errno_get()), args[0]),
1595        );
1596        return 1;
1597    }
1598    let var = args.get(1).cloned().unwrap_or_else(|| "reply".to_string());
1599    let wins = windows_lock().lock().unwrap();
1600    let w = match wins.get(args[0].as_str()) {
1601        Some(w) => w,
1602        None => return 1,
1603    };
1604    let ch = if w.cursor_y < w.rows && w.cursor_x < w.cols {
1605        w.buffer[w.cursor_y][w.cursor_x]
1606    } else {
1607        ' '
1608    };
1609    let mut clist: Vec<String> = Vec::new();
1610    clist.push(ch.to_string());
1611    // C reads the color pair via `PAIR_NUMBER(winch(...))` and
1612    // resolves to the registered name via `zcurses_colorget_reverse`.
1613    // Without libncurses linked we always report "default".
1614    clist.push("default".into());
1615    for entry in zcurses_attributes {
1616        if w.attrs & entry.number as u32 != 0 {
1617            clist.push(entry.name.to_string());
1618        }
1619    }
1620    drop(wins);
1621    // c:1467 — `return !setaparam(args[1] ? args[1] : "reply",
1622    //                             zlinklist2array(clist, 1));`
1623    // REAL array: [char, color-name, attr...]. Prior port colon-
1624    // joined into a scalar — `$reply[1]` returned the first char of
1625    // the join instead of the queried character, and a literal `:`
1626    // at the cursor corrupted the field split entirely.
1627    setaparam(&var, clist); // c:1467
1628    0
1629}
1630
1631// =====================================================================
1632// Port of `zccmd_touch(const char *nam, char **args)` from `Src/Modules/curses.c:1472`.
1633// =====================================================================
1634
1635/// Port of `zccmd_touch(const char *nam, char **args)` from `Src/Modules/curses.c:1472`.
1636///
1637/// `touch WINDOW...`: mark each window for full redraw. The Rust
1638/// port's refresh always rewrites the whole buffer, so this just
1639/// validates each name and is otherwise a no-op.
1640/// WARNING: param names don't match C — Rust=(args) vs C=(nam, args)
1641pub(crate) fn zccmd_touch(nam: &str, args: &[String]) -> i32 {
1642    for name in args {
1643        if !zcurses_validate_window(name.as_str(), ZCURSES_USED) {
1644            zwarnnam(
1645                nam,
1646                &format!("{}: {}", zcurses_strerror(zc_errno_get()), name),
1647            );
1648            return 1;
1649        }
1650    }
1651    0
1652}
1653
1654// =====================================================================
1655// Port of `zccmd_resize(const char *nam, char **args)` from `Src/Modules/curses.c:1494`.
1656// =====================================================================
1657
1658/// Port of `zccmd_resize(const char *nam, char **args)` from `Src/Modules/curses.c:1494`.
1659///
1660/// `resize ROWS COLS [endwin|nosave|endwin_nosave]`: resize stdscr
1661/// (and via parent-child chain, sub-windows) to the new geometry.
1662/// The third arg gates whether to call `endwin()` first / save tty
1663/// state — both are no-ops in the ANSI-escape backend.
1664/// WARNING: param names don't match C — Rust=(args) vs C=(nam, args)
1665pub(crate) fn zccmd_resize(nam: &str, args: &[String]) -> i32 {
1666    if !zcurses_getwindowbyname("stdscr") {
1667        return 1;
1668    }
1669    let y: usize = args[0].parse().unwrap_or(0);
1670    let x: usize = args[1].parse().unwrap_or(0);
1671    if let Some(third) = args.get(2) {
1672        match third.as_str() {
1673            "endwin" | "nosave" | "endwin_nosave" => {}
1674            other => {
1675                zwarnnam(
1676                    nam,
1677                    &format!(
1678                        "`resize' expects `endwin', `nosave' or `endwin_nosave' for third argument, if given: {}",
1679                        other
1680                    ),
1681                );
1682                return 1;
1683            }
1684        }
1685    }
1686    if y == 0 && x == 0 && args.get(2).is_none() {
1687        // C special-case: just probe whether resize_term is available.
1688        return 0;
1689    }
1690    let mut wins = windows_lock().lock().unwrap();
1691    if let Some(w) = wins.get_mut("stdscr") {
1692        w.rows = y;
1693        w.cols = x;
1694        w.buffer = vec![vec![' '; x]; y];
1695        w.cursor_y = w.cursor_y.min(y.saturating_sub(1));
1696        w.cursor_x = w.cursor_x.min(x.saturating_sub(1));
1697    }
1698    0
1699}
1700
1701// =====================================================================
1702// Port of `bin_zcurses(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/curses.c:1568`.
1703// =====================================================================
1704
1705/// Port of `bin_zcurses(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/curses.c:1568`.
1706///
1707/// Subcommand dispatcher. Walks `SCS[]` for a matching name,
1708/// validates arg-count against `minargs`/`maxargs`, then enforces
1709/// the C source's "command can't be used before `zcurses init`"
1710/// invariant before delegating to the matched `zccmd_*`.
1711/// WARNING: param names don't match C — Rust=(args, _ops, _func) vs C=(nam, args, ops, func)
1712pub(crate) fn bin_zcurses(nam: &str, args: &[String], _ops: &options, _func: i32) -> i32 {
1713    if args.is_empty() {
1714        zwarnnam(nam, "subcommand required");
1715        return 1;
1716    }
1717    // C: for(zcsc = scs; zcsc->name; zcsc++) {
1718    //         if(!strcmp(args[0], zcsc->name)) break;
1719    //     }
1720    let cmd_name = args[0].as_str();
1721    let entry = match SCS.iter().find(|sc| sc.name == cmd_name) {
1722        Some(e) => e,
1723        None => {
1724            zwarnnam(nam, &format!("unknown subcommand: {}", cmd_name));
1725            return 1;
1726        }
1727    };
1728    // C: num_args = saargs - (args + 2);  — args after subcommand.
1729    let sub_args = &args[1..];
1730    let num_args = sub_args.len() as i32;
1731    if num_args < entry.minargs {
1732        zwarnnam(
1733            nam,
1734            &format!("too few arguments for subcommand: {}", cmd_name),
1735        );
1736        return 1;
1737    }
1738    if entry.maxargs >= 0 && num_args > entry.maxargs {
1739        zwarnnam(
1740            nam,
1741            &format!("too many arguments for subcommand: {}", cmd_name),
1742        );
1743        return 1;
1744    }
1745    // C: if (zcsc->cmd != zccmd_init && zcsc->cmd != zccmd_endwin &&
1746    //         !zcurses_getwindowbyname("stdscr")) { … return 1; }
1747    let is_init = matches!(cmd_name, "init");
1748    let is_end = matches!(cmd_name, "end");
1749    if !is_init && !is_end && !zcurses_getwindowbyname("stdscr") {
1750        zwarnnam(
1751            nam,
1752            &format!("command `{}' can't be used before `zcurses init'", cmd_name),
1753        );
1754        return 1;
1755    }
1756    (entry.cmd)(nam, sub_args)
1757}
1758
1759/// Direct port of `zcurses_colorsarrgetfn()` from `Src/Modules/
1760/// curses.c:1641`. C body: `return zcurses_pairs_to_array(zcurses_colors);`
1761pub fn zcurses_colorsarrgetfn() -> Vec<String> {
1762    // c:1641
1763    zcurses_pairs_to_array(zcurses_colors) // c:1651
1764}
1765
1766/// Direct port of `zcurses_attrgetfn()` from `Src/Modules/
1767/// curses.c:1651`. C body: `return zcurses_pairs_to_array(zcurses_attributes);`
1768pub fn zcurses_attrgetfn() -> Vec<String> {
1769    // c:1651
1770    zcurses_pairs_to_array(zcurses_attributes) // c:1661
1771}
1772
1773/// Direct port of `zcurses_keycodesgetfn()` from `Src/Modules/
1774/// curses.c:1661`. C body: `return zcurses_pairs_to_array(keypad_names);`
1775/// Static-link path: walk the canonical KEY_* set the Rust port
1776/// already enumerates via keypad_name.
1777pub fn zcurses_keycodesgetfn() -> Vec<String> {
1778    // c:1661
1779    // Walk the full canonical KEY_* range mirroring C's
1780    // `zcurses_pairs_to_array(keypad_names)` over the awk-generated
1781    // table. keypad_name returns None for unrecognized codes so the
1782    // filter_map prunes any gaps (KEY_F0+1..KEY_F0+63 are F-keys per
1783    // the inline range in keypad_name; codes outside the named
1784    // ranges are silently dropped).
1785    let base = (0o401..=0o407).chain(0o510..=0o633);
1786    let fkeys = (0..64).map(|n| KEY_F0 + n);
1787    base.chain(fkeys).filter_map(keypad_name).collect()
1788}
1789
1790/// Direct port of `zcurses_windowsgetfn()` from `Src/Modules/
1791/// curses.c:1671`. C body (c:1675-1683): walk the `zcurses_windows`
1792/// LinkList collecting each ZCWin's `name`. Rust port reads from
1793/// the `order_lock` insertion-order tracker so the result preserves
1794/// creation order matching C's LinkList walk.
1795pub fn zcurses_windowsgetfn() -> Vec<String> {
1796    // c:1671
1797    order_lock().lock().map(|g| g.clone()).unwrap_or_default() // c:1691
1798}
1799
1800/// Direct port of `zcurses_colorsintgetfn()` from `Src/Modules/
1801/// curses.c:1691`. C body: `return COLORS;` (the ncurses global).
1802/// Static-link path: probe the live ncurses COLORS via tigetnum.
1803pub fn zcurses_colorsintgetfn() -> i64 {
1804    // c:1691
1805    let n = crate::terminfo_db::tigetnum("colors"); // c:1701 COLORS
1806    if n < 0 {
1807        0
1808    } else {
1809        n as i64
1810    }
1811}
1812
1813/// Direct port of `zcurses_colorpairsintgetfn()` from `Src/Modules/
1814/// curses.c:1701`. C body: `return COLOR_PAIRS;`. Same probe as above
1815/// against the `pairs` capability.
1816pub fn zcurses_colorpairsintgetfn() -> i64 {
1817    // c:1701
1818    let n = crate::terminfo_db::tigetnum("pairs"); // c:1703 COLOR_PAIRS
1819    if n < 0 {
1820        0
1821    } else {
1822        n as i64
1823    }
1824}
1825
1826// =====================================================================
1827// Module entry points (curses.c:1744+).
1828// =====================================================================
1829
1830/// Port of `setup_(UNUSED(Module m))` from `Src/Modules/curses.c:1744`. C body:
1831/// `return 0;`.
1832#[allow(unused_variables)]
1833pub fn setup_(m: *const module) -> i32 {
1834    // c:1744
1835    0
1836}
1837
1838/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/Modules/curses.c:1751`.
1839pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
1840    // c:1751
1841    *features = featuresarray(m, module_features());
1842    0
1843}
1844
1845/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/Modules/curses.c`.
1846pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
1847    // c:1759
1848    handlefeatures(m, module_features(), enables)
1849}
1850
1851/// Port of `boot_(UNUSED(Module m))` from `Src/Modules/curses.c`. C body: `return 0;`.
1852/// WARNING: param names don't match C — Rust=(_m) vs C=(m)
1853pub fn boot_(_m: *const module) -> i32 {
1854    // c:1766
1855    0
1856}
1857
1858/// Port of `cleanup_(UNUSED(Module m))` from `Src/Modules/curses.c`.
1859pub fn cleanup_(m: *const module) -> i32 {
1860    // c:1775
1861    setfeatureenables(m, module_features(), None)
1862}
1863
1864/// Port of `finish_(UNUSED(Module m))` from `Src/Modules/curses.c`. C body: `return 0;`.
1865/// WARNING: param names don't match C — Rust=(_m) vs C=(m)
1866pub fn finish_(_m: *const module) -> i32 {
1867    // c:1785
1868    0
1869}
1870
1871// =====================================================================
1872// Port of `enum zc_win_flags` from `Src/Modules/curses.c:54`.
1873// =====================================================================
1874
1875// Window is permanent (probably "stdscr")                                  // c:55
1876/// Window is permanent (probably "stdscr"). C: `curses.c:56`.
1877pub const ZCWF_PERMANENT: u32 = 0x0001; // c:56
1878                                        // Scrolling enabled                                                        // c:57
1879/// Scrolling enabled. C: `curses.c:58`.
1880pub const ZCWF_SCROLL: u32 = 0x0002; // c:58
1881
1882/// Port of `enum zcurses_mouse_event_types` from `Src/Modules/curses.c:146`.
1883/// Mouse-event discriminator bits returned by `zcurses input` mouse
1884/// reads.
1885pub const ZCME_PRESSED: i32 = 0; // c:147
1886/// `ZCME_RELEASED` constant.
1887pub const ZCME_RELEASED: i32 = 1; // c:148
1888/// `ZCME_CLICKED` constant.
1889pub const ZCME_CLICKED: i32 = 2; // c:149
1890/// `ZCME_DOUBLE_CLICKED` constant.
1891pub const ZCME_DOUBLE_CLICKED: i32 = 3; // c:150
1892/// `ZCME_TRIPLE_CLICKED` constant.
1893pub const ZCME_TRIPLE_CLICKED: i32 = 4; // c:151
1894
1895/// `KEY_MOUSE` libncurses keycode (`curses.h`: `#define KEY_MOUSE 0631`).
1896/// Synthesized by `read_key_sequence` when it decodes an xterm mouse
1897/// report, then consumed by `zccmd_input`'s mouse branch (c:1209).
1898pub const KEY_MOUSE: i32 = 0o631;
1899
1900// ncurses mouse-event bit masks (`curses.h` BUTTONn_* /
1901// BUTTON_SHIFT/CTRL/ALT). Layout follows NCURSES_MOUSE_VERSION 2:
1902//   NCURSES_MOUSE_MASK(b, m) = m << ((b - 1) * 5)
1903// with event bases NCURSES_BUTTON_RELEASED=001, _PRESSED=002,
1904// _CLICKED=004, _DOUBLE_CLICKED=010, _TRIPLE_CLICKED=020 and the
1905// SHIFT/CTRL/ALT/REPORT modifiers in the button-6 slot (shift 25).
1906// This is consistent with the already-defined
1907// `REPORT_MOUSE_POSITION = 010 << 25 = 1 << 28`. The names + bit
1908// positions match curses.h so the `ZCURSES_MOUSE_MAP` table below is
1909// reviewable side-by-side against `zcurses_mouse_map[]` (c:169).
1910/// `BUTTON1_RELEASED` (curses.h).
1911pub const BUTTON1_RELEASED: u64 = 0o1 << 0;
1912/// `BUTTON1_PRESSED` (curses.h).
1913pub const BUTTON1_PRESSED: u64 = 0o2 << 0;
1914/// `BUTTON1_CLICKED` (curses.h).
1915pub const BUTTON1_CLICKED: u64 = 0o4 << 0;
1916/// `BUTTON1_DOUBLE_CLICKED` (curses.h).
1917pub const BUTTON1_DOUBLE_CLICKED: u64 = 0o10 << 0;
1918/// `BUTTON1_TRIPLE_CLICKED` (curses.h).
1919pub const BUTTON1_TRIPLE_CLICKED: u64 = 0o20 << 0;
1920/// `BUTTON2_RELEASED` (curses.h).
1921pub const BUTTON2_RELEASED: u64 = 0o1 << 5;
1922/// `BUTTON2_PRESSED` (curses.h).
1923pub const BUTTON2_PRESSED: u64 = 0o2 << 5;
1924/// `BUTTON2_CLICKED` (curses.h).
1925pub const BUTTON2_CLICKED: u64 = 0o4 << 5;
1926/// `BUTTON2_DOUBLE_CLICKED` (curses.h).
1927pub const BUTTON2_DOUBLE_CLICKED: u64 = 0o10 << 5;
1928/// `BUTTON2_TRIPLE_CLICKED` (curses.h).
1929pub const BUTTON2_TRIPLE_CLICKED: u64 = 0o20 << 5;
1930/// `BUTTON3_RELEASED` (curses.h).
1931pub const BUTTON3_RELEASED: u64 = 0o1 << 10;
1932/// `BUTTON3_PRESSED` (curses.h).
1933pub const BUTTON3_PRESSED: u64 = 0o2 << 10;
1934/// `BUTTON3_CLICKED` (curses.h).
1935pub const BUTTON3_CLICKED: u64 = 0o4 << 10;
1936/// `BUTTON3_DOUBLE_CLICKED` (curses.h).
1937pub const BUTTON3_DOUBLE_CLICKED: u64 = 0o10 << 10;
1938/// `BUTTON3_TRIPLE_CLICKED` (curses.h).
1939pub const BUTTON3_TRIPLE_CLICKED: u64 = 0o20 << 10;
1940/// `BUTTON4_RELEASED` (curses.h).
1941pub const BUTTON4_RELEASED: u64 = 0o1 << 15;
1942/// `BUTTON4_PRESSED` (curses.h).
1943pub const BUTTON4_PRESSED: u64 = 0o2 << 15;
1944/// `BUTTON4_CLICKED` (curses.h).
1945pub const BUTTON4_CLICKED: u64 = 0o4 << 15;
1946/// `BUTTON4_DOUBLE_CLICKED` (curses.h).
1947pub const BUTTON4_DOUBLE_CLICKED: u64 = 0o10 << 15;
1948/// `BUTTON4_TRIPLE_CLICKED` (curses.h).
1949pub const BUTTON4_TRIPLE_CLICKED: u64 = 0o20 << 15;
1950/// `BUTTON5_RELEASED` (curses.h).
1951pub const BUTTON5_RELEASED: u64 = 0o1 << 20;
1952/// `BUTTON5_PRESSED` (curses.h).
1953pub const BUTTON5_PRESSED: u64 = 0o2 << 20;
1954/// `BUTTON5_CLICKED` (curses.h).
1955pub const BUTTON5_CLICKED: u64 = 0o4 << 20;
1956/// `BUTTON5_DOUBLE_CLICKED` (curses.h).
1957pub const BUTTON5_DOUBLE_CLICKED: u64 = 0o10 << 20;
1958/// `BUTTON5_TRIPLE_CLICKED` (curses.h).
1959pub const BUTTON5_TRIPLE_CLICKED: u64 = 0o20 << 20;
1960/// `BUTTON_CTRL` (curses.h) — control modifier bit.
1961pub const BUTTON_CTRL: u64 = 0o1 << 25;
1962/// `BUTTON_SHIFT` (curses.h) — shift modifier bit.
1963pub const BUTTON_SHIFT: u64 = 0o2 << 25;
1964/// `BUTTON_ALT` (curses.h) — alt/meta modifier bit.
1965pub const BUTTON_ALT: u64 = 0o4 << 25;
1966
1967/// Port of `static const struct zcurses_mouse_event zcurses_mouse_map[]`
1968/// from `Src/Modules/curses.c:169`. Maps each ncurses button bit to a
1969/// `(button-number, event-kind)` pair for the decode loop at c:1240.
1970static ZCURSES_MOUSE_MAP: &[zcurses_mouse_event] = &[
1971    zcurses_mouse_event {
1972        button: 1,
1973        what: ZCME_PRESSED,
1974        event: BUTTON1_PRESSED,
1975    }, // c:170
1976    zcurses_mouse_event {
1977        button: 1,
1978        what: ZCME_RELEASED,
1979        event: BUTTON1_RELEASED,
1980    }, // c:171
1981    zcurses_mouse_event {
1982        button: 1,
1983        what: ZCME_CLICKED,
1984        event: BUTTON1_CLICKED,
1985    }, // c:172
1986    zcurses_mouse_event {
1987        button: 1,
1988        what: ZCME_DOUBLE_CLICKED,
1989        event: BUTTON1_DOUBLE_CLICKED,
1990    }, // c:173
1991    zcurses_mouse_event {
1992        button: 1,
1993        what: ZCME_TRIPLE_CLICKED,
1994        event: BUTTON1_TRIPLE_CLICKED,
1995    }, // c:174
1996    zcurses_mouse_event {
1997        button: 2,
1998        what: ZCME_PRESSED,
1999        event: BUTTON2_PRESSED,
2000    }, // c:176
2001    zcurses_mouse_event {
2002        button: 2,
2003        what: ZCME_RELEASED,
2004        event: BUTTON2_RELEASED,
2005    }, // c:177
2006    zcurses_mouse_event {
2007        button: 2,
2008        what: ZCME_CLICKED,
2009        event: BUTTON2_CLICKED,
2010    }, // c:178
2011    zcurses_mouse_event {
2012        button: 2,
2013        what: ZCME_DOUBLE_CLICKED,
2014        event: BUTTON2_DOUBLE_CLICKED,
2015    }, // c:179
2016    zcurses_mouse_event {
2017        button: 2,
2018        what: ZCME_TRIPLE_CLICKED,
2019        event: BUTTON2_TRIPLE_CLICKED,
2020    }, // c:180
2021    zcurses_mouse_event {
2022        button: 3,
2023        what: ZCME_PRESSED,
2024        event: BUTTON3_PRESSED,
2025    }, // c:182
2026    zcurses_mouse_event {
2027        button: 3,
2028        what: ZCME_RELEASED,
2029        event: BUTTON3_RELEASED,
2030    }, // c:183
2031    zcurses_mouse_event {
2032        button: 3,
2033        what: ZCME_CLICKED,
2034        event: BUTTON3_CLICKED,
2035    }, // c:184
2036    zcurses_mouse_event {
2037        button: 3,
2038        what: ZCME_DOUBLE_CLICKED,
2039        event: BUTTON3_DOUBLE_CLICKED,
2040    }, // c:185
2041    zcurses_mouse_event {
2042        button: 3,
2043        what: ZCME_TRIPLE_CLICKED,
2044        event: BUTTON3_TRIPLE_CLICKED,
2045    }, // c:186
2046    zcurses_mouse_event {
2047        button: 4,
2048        what: ZCME_PRESSED,
2049        event: BUTTON4_PRESSED,
2050    }, // c:188
2051    zcurses_mouse_event {
2052        button: 4,
2053        what: ZCME_RELEASED,
2054        event: BUTTON4_RELEASED,
2055    }, // c:189
2056    zcurses_mouse_event {
2057        button: 4,
2058        what: ZCME_CLICKED,
2059        event: BUTTON4_CLICKED,
2060    }, // c:190
2061    zcurses_mouse_event {
2062        button: 4,
2063        what: ZCME_DOUBLE_CLICKED,
2064        event: BUTTON4_DOUBLE_CLICKED,
2065    }, // c:191
2066    zcurses_mouse_event {
2067        button: 4,
2068        what: ZCME_TRIPLE_CLICKED,
2069        event: BUTTON4_TRIPLE_CLICKED,
2070    }, // c:192
2071    // c:194-201 — button 5 (guarded by `#ifdef BUTTON5_PRESSED` in C;
2072    // present here since the version-2 layout always defines it).
2073    zcurses_mouse_event {
2074        button: 5,
2075        what: ZCME_PRESSED,
2076        event: BUTTON5_PRESSED,
2077    }, // c:196
2078    zcurses_mouse_event {
2079        button: 5,
2080        what: ZCME_RELEASED,
2081        event: BUTTON5_RELEASED,
2082    }, // c:197
2083    zcurses_mouse_event {
2084        button: 5,
2085        what: ZCME_CLICKED,
2086        event: BUTTON5_CLICKED,
2087    }, // c:198
2088    zcurses_mouse_event {
2089        button: 5,
2090        what: ZCME_DOUBLE_CLICKED,
2091        event: BUTTON5_DOUBLE_CLICKED,
2092    }, // c:199
2093    zcurses_mouse_event {
2094        button: 5,
2095        what: ZCME_TRIPLE_CLICKED,
2096        event: BUTTON5_TRIPLE_CLICKED,
2097    }, // c:200
2098];
2099
2100/// Port of `static const struct zcurses_namenumberpair
2101/// zcurses_mouse_event_list[]` from `Src/Modules/curses.c:154`.
2102static ZCURSES_MOUSE_EVENT_LIST: &[(&str, i32)] = &[
2103    ("PRESSED", ZCME_PRESSED),               // c:155
2104    ("RELEASED", ZCME_RELEASED),             // c:156
2105    ("CLICKED", ZCME_CLICKED),               // c:157
2106    ("DOUBLE_CLICKED", ZCME_DOUBLE_CLICKED), // c:158
2107    ("TRIPLE_CLICKED", ZCME_TRIPLE_CLICKED), // c:159
2108];
2109
2110/// ncurses `MEVENT` (`curses.h`) — one decoded mouse report, holding
2111/// the fields `zccmd_input` reads at c:1227-1234 and the `bstate` it
2112/// tests at c:1240-1261. `id`/`z` are unused by the terminal decoder
2113/// (kept for MEVENT parity); `x`/`y` are 0-based screen coordinates,
2114/// as ncurses' `getmouse()` returns them.
2115#[derive(Debug, Clone, Copy)]
2116pub struct Mevent {
2117    /// Device id (`MEVENT.id`); always 0 for the terminal decoder.
2118    pub id: i32,
2119    /// 0-based column (`MEVENT.x`).
2120    pub x: i32,
2121    /// 0-based row (`MEVENT.y`).
2122    pub y: i32,
2123    /// Unused scroll axis (`MEVENT.z`).
2124    pub z: i32,
2125    /// Button + modifier state bits (`MEVENT.bstate`).
2126    pub bstate: u64,
2127}
2128
2129/// Pending decoded mouse event — the terminal-decoder analog of
2130/// ncurses' internal `getmouse()` FIFO. `read_key_sequence` stores an
2131/// event here when it synthesizes `KEY_MOUSE`; `zccmd_input`'s mouse
2132/// branch (c:1217 `getmouse`) takes it back out. Single slot: "we only
2133/// expect one event" (c:1236). Accessed inline (no accessor fn) to keep
2134/// the port a data extension, not a new function.
2135#[allow(non_upper_case_globals)]
2136static PENDING_MOUSE: Mutex<Option<Mevent>> = Mutex::new(None);
2137
2138// =====================================================================
2139// Port of `static const struct zcurses_namenumberpair
2140// zcurses_attributes[]` from `curses.c:120`. ncurses constant
2141// values mirrored byte-for-byte (A_BOLD = 1<<8, etc.).
2142// =====================================================================
2143
2144/// libncurses `A_BLINK` from `<curses.h>`.
2145pub const A_BLINK: i32 = 1 << 11;
2146/// libncurses `A_BOLD`.
2147pub const A_BOLD: i32 = 1 << 13;
2148/// libncurses `A_DIM`.
2149pub const A_DIM: i32 = 1 << 12;
2150/// libncurses `A_REVERSE`.
2151pub const A_REVERSE: i32 = 1 << 10;
2152/// libncurses `A_STANDOUT`.
2153pub const A_STANDOUT: i32 = 1 << 8;
2154/// libncurses `A_UNDERLINE`.
2155pub const A_UNDERLINE: i32 = 1 << 9;
2156
2157/// Port of `zcurses_attributes[]` from `Src/Modules/curses.c:120`.
2158pub static zcurses_attributes: &[zcurses_namenumberpair] = &[
2159    zcurses_namenumberpair {
2160        name: "blink",
2161        number: A_BLINK,
2162    },
2163    zcurses_namenumberpair {
2164        name: "bold",
2165        number: A_BOLD,
2166    },
2167    zcurses_namenumberpair {
2168        name: "dim",
2169        number: A_DIM,
2170    },
2171    zcurses_namenumberpair {
2172        name: "reverse",
2173        number: A_REVERSE,
2174    },
2175    zcurses_namenumberpair {
2176        name: "standout",
2177        number: A_STANDOUT,
2178    },
2179    zcurses_namenumberpair {
2180        name: "underline",
2181        number: A_UNDERLINE,
2182    },
2183];
2184
2185// =====================================================================
2186// Port of `zcurses_colors[]` from `Src/Modules/curses.c:130`.
2187// =====================================================================
2188
2189/// libncurses `COLOR_BLACK` from `<curses.h>`.
2190pub const COLOR_BLACK: i32 = 0;
2191/// libncurses `COLOR_RED`.
2192pub const COLOR_RED: i32 = 1;
2193/// libncurses `COLOR_GREEN`.
2194pub const COLOR_GREEN: i32 = 2;
2195/// libncurses `COLOR_YELLOW`.
2196pub const COLOR_YELLOW: i32 = 3;
2197/// libncurses `COLOR_BLUE`.
2198pub const COLOR_BLUE: i32 = 4;
2199/// libncurses `COLOR_MAGENTA`.
2200pub const COLOR_MAGENTA: i32 = 5;
2201/// libncurses `COLOR_CYAN`.
2202pub const COLOR_CYAN: i32 = 6;
2203/// libncurses `COLOR_WHITE`.
2204pub const COLOR_WHITE: i32 = 7;
2205
2206/// Port of `zcurses_colors[]` from `Src/Modules/curses.c:130`.
2207pub static zcurses_colors: &[zcurses_namenumberpair] = &[
2208    zcurses_namenumberpair {
2209        name: "black",
2210        number: COLOR_BLACK,
2211    },
2212    zcurses_namenumberpair {
2213        name: "red",
2214        number: COLOR_RED,
2215    },
2216    zcurses_namenumberpair {
2217        name: "green",
2218        number: COLOR_GREEN,
2219    },
2220    zcurses_namenumberpair {
2221        name: "yellow",
2222        number: COLOR_YELLOW,
2223    },
2224    zcurses_namenumberpair {
2225        name: "blue",
2226        number: COLOR_BLUE,
2227    },
2228    zcurses_namenumberpair {
2229        name: "magenta",
2230        number: COLOR_MAGENTA,
2231    },
2232    zcurses_namenumberpair {
2233        name: "cyan",
2234        number: COLOR_CYAN,
2235    },
2236    zcurses_namenumberpair {
2237        name: "white",
2238        number: COLOR_WHITE,
2239    },
2240    zcurses_namenumberpair {
2241        name: "default",
2242        number: -1,
2243    },
2244];
2245
2246/// Insertion order tracker for `zcurses_windows`. C's LinkList
2247/// gives this for free; Rust port uses a parallel Vec.
2248static WINDOW_ORDER: OnceLock<Mutex<Vec<String>>> = OnceLock::new();
2249
2250/// Port of `static int zc_errno` from `curses.c:112`. Last
2251/// validation error code set by `zcurses_validate_window`.
2252static zc_errno_cell: OnceLock<Mutex<i32>> = OnceLock::new();
2253
2254/// `curses_tty_state` — file-static `struct ttyinfo curses_tty_state;`
2255/// from Src/Modules/curses.c:75. Snapshotted by gettyinfo at first
2256/// init (c:443), restored by settyinfo on re-entry (c:438).
2257#[allow(non_upper_case_globals)]
2258static curses_tty_state: Mutex<Option<libc::termios>> = Mutex::new(None); // c:75
2259
2260/// `saved_tty_state` — file-static `struct ttyinfo saved_tty_state;`
2261/// from `Src/Modules/curses.c:74`. The PRE-`zcurses init` termios
2262/// snapshot (c:443 gettyinfo); zccmd_endwin's c:830 settyinfo
2263/// restores it to hand the terminal back to the shell.
2264#[allow(non_upper_case_globals)]
2265static saved_tty_state: Mutex<Option<libc::termios>> = Mutex::new(None); // c:74
2266
2267/// `ZCF_MOUSE_ACTIVE` flag (curses.c:116).
2268pub const ZCF_MOUSE_ACTIVE: u32 = 1 << 0;
2269/// `ZCF_MOUSE_MASK_CHANGED` flag (curses.c:117).
2270pub const ZCF_MOUSE_MASK_CHANGED: u32 = 1 << 1;
2271
2272/// `REPORT_MOUSE_POSITION` libncurses constant. Port of the bit
2273/// `zcurses_mouse_mask` toggles for `mouse motion`.
2274pub const REPORT_MOUSE_POSITION: u32 = 1 << 28;
2275
2276/// Port of the `struct zcurses_subcommand scs[]` array inside
2277/// `bin_zcurses` (curses.c:1574). Order + min/max args match C.
2278static SCS: &[zcurses_subcommand] = &[
2279    zcurses_subcommand {
2280        name: "init",
2281        cmd: zccmd_init,
2282        minargs: 0,
2283        maxargs: 0,
2284    },
2285    zcurses_subcommand {
2286        name: "addwin",
2287        cmd: zccmd_addwin,
2288        minargs: 5,
2289        maxargs: 6,
2290    },
2291    zcurses_subcommand {
2292        name: "delwin",
2293        cmd: zccmd_delwin,
2294        minargs: 1,
2295        maxargs: 1,
2296    },
2297    zcurses_subcommand {
2298        name: "refresh",
2299        cmd: zccmd_refresh,
2300        minargs: 0,
2301        maxargs: -1,
2302    },
2303    zcurses_subcommand {
2304        name: "move",
2305        cmd: zccmd_move,
2306        minargs: 3,
2307        maxargs: 3,
2308    },
2309    zcurses_subcommand {
2310        name: "clear",
2311        cmd: zccmd_clear,
2312        minargs: 1,
2313        maxargs: 2,
2314    },
2315    zcurses_subcommand {
2316        name: "position",
2317        cmd: zccmd_position,
2318        minargs: 2,
2319        maxargs: 2,
2320    },
2321    zcurses_subcommand {
2322        name: "char",
2323        cmd: zccmd_char,
2324        minargs: 2,
2325        maxargs: 2,
2326    },
2327    zcurses_subcommand {
2328        name: "string",
2329        cmd: zccmd_string,
2330        minargs: 2,
2331        maxargs: 2,
2332    },
2333    zcurses_subcommand {
2334        name: "border",
2335        cmd: zccmd_border,
2336        minargs: 1,
2337        maxargs: 1,
2338    },
2339    zcurses_subcommand {
2340        name: "end",
2341        cmd: zccmd_endwin,
2342        minargs: 0,
2343        maxargs: 0,
2344    },
2345    zcurses_subcommand {
2346        name: "attr",
2347        cmd: zccmd_attr,
2348        minargs: 2,
2349        maxargs: -1,
2350    },
2351    zcurses_subcommand {
2352        name: "bg",
2353        cmd: zccmd_bg,
2354        minargs: 2,
2355        maxargs: -1,
2356    },
2357    zcurses_subcommand {
2358        name: "scroll",
2359        cmd: zccmd_scroll,
2360        minargs: 2,
2361        maxargs: 2,
2362    },
2363    zcurses_subcommand {
2364        name: "input",
2365        cmd: zccmd_input,
2366        minargs: 1,
2367        maxargs: 4,
2368    },
2369    zcurses_subcommand {
2370        name: "timeout",
2371        cmd: zccmd_timeout,
2372        minargs: 2,
2373        maxargs: 2,
2374    },
2375    zcurses_subcommand {
2376        name: "mouse",
2377        cmd: zccmd_mouse,
2378        minargs: 0,
2379        maxargs: -1,
2380    },
2381    zcurses_subcommand {
2382        name: "querychar",
2383        cmd: zccmd_querychar,
2384        minargs: 1,
2385        maxargs: 2,
2386    },
2387    zcurses_subcommand {
2388        name: "touch",
2389        cmd: zccmd_touch,
2390        minargs: 1,
2391        maxargs: -1,
2392    },
2393    zcurses_subcommand {
2394        name: "resize",
2395        cmd: zccmd_resize,
2396        minargs: 2,
2397        maxargs: 3,
2398    },
2399];
2400
2401/// libncurses keypad code constants. Subset of the
2402/// autogenerated `curses_keys.h` table the C source includes.
2403pub const KEY_DOWN: i32 = 0o402;
2404/// `KEY_UP` constant.
2405pub const KEY_UP: i32 = 0o403;
2406/// `KEY_LEFT` constant.
2407pub const KEY_LEFT: i32 = 0o404;
2408/// `KEY_RIGHT` constant.
2409pub const KEY_RIGHT: i32 = 0o405;
2410/// `KEY_HOME` constant.
2411pub const KEY_HOME: i32 = 0o406;
2412/// `KEY_DC` constant.
2413pub const KEY_DC: i32 = 0o512;
2414/// `KEY_IC` constant.
2415pub const KEY_IC: i32 = 0o513;
2416/// `KEY_NPAGE` constant.
2417pub const KEY_NPAGE: i32 = 0o522;
2418/// `KEY_PPAGE` constant.
2419pub const KEY_PPAGE: i32 = 0o523;
2420/// `KEY_END` constant.
2421pub const KEY_END: i32 = 0o550;
2422/// `KEY_F0` constant.
2423pub const KEY_F0: i32 = 0o410;
2424
2425// =====================================================================
2426// Module paraphernalia (curses.c:1631-).
2427// =====================================================================
2428
2429// Port of `static struct builtin bintab[]` from `curses.c:1631`:
2430//   BUILTIN("zcurses", 0, bin_zcurses, 1, -1, 0, "", NULL)
2431// The Rust port keeps the descriptor inline in the local
2432// `featuresarray` stub below — the static-link path doesn't
2433// need a `static [builtin]` array since `bin_zcurses` is
2434// invoked through the canonical builtin dispatcher, not the
2435// per-module bintab pointer C uses for dlopen.
2436
2437/// Port of `static struct features module_features` from
2438/// `Src/Modules/curses.c:1638`. C builds it as a stack literal;
2439/// Rust port keeps the OnceLock<Mutex> shape so callers consume
2440/// the same `*const module, &Mutex<crate::ported::zsh_h::features>` handle other
2441/// modules use.
2442static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
2443
2444// WARNING: NOT IN CURSES.C — Rust-only OnceLock get-or-init for
2445// each module static. C dereferences globals directly.
2446fn windows_lock() -> &'static Mutex<HashMap<String, zc_win>> {
2447    zcurses_windows.get_or_init(|| Mutex::new(HashMap::new()))
2448}
2449
2450// WARNING: NOT IN CURSES.C — see windows_lock above.
2451fn errno_lock() -> &'static Mutex<i32> {
2452    zc_errno_cell.get_or_init(|| Mutex::new(0))
2453}
2454
2455// WARNING: NOT IN CURSES.C — see windows_lock above.
2456fn colorpairs_lock() -> &'static Mutex<HashMap<String, i16>> {
2457    zcurses_colorpairs.get_or_init(|| Mutex::new(HashMap::new()))
2458}
2459
2460// WARNING: NOT IN CURSES.C — see windows_lock above.
2461fn next_cp_lock() -> &'static Mutex<i16> {
2462    next_cp.get_or_init(|| Mutex::new(0))
2463}
2464
2465/// WARNING: NOT IN CURSES.C — Mutex-based errno accessor; C reads `zc_errno` directly
2466/// (equivalent C logic at Src/Modules/curses.c:107).
2467/// Read the current `zc_errno` value. C dereferences the global
2468/// directly; Rust port wraps the lock acquire.
2469fn zc_errno_get() -> i32 {
2470    *errno_lock().lock().unwrap()
2471}
2472
2473/// WARNING: NOT IN CURSES.C — Mutex-based errno setter; C writes `zc_errno` directly
2474/// (equivalent C logic at Src/Modules/curses.c:107).
2475/// Set `zc_errno`. Used by `zcurses_validate_window`.
2476fn zc_errno_set(v: i32) {
2477    *errno_lock().lock().unwrap() = v;
2478}
2479
2480// WARNING: NOT IN CURSES.C — OnceLock get-or-init for `zcurses_flags`
2481// and `zcurses_mouse_mask` (C dereferences globals directly).
2482fn flags_lock() -> &'static Mutex<u32> {
2483    zcurses_flags.get_or_init(|| Mutex::new(0))
2484}
2485
2486// `COLORS` / `COLOR_PAIRS` come from `crate::terminfo_db`, which reads the
2487// compiled terminfo database directly. This was the whole of `zsh/curses`'s
2488// dependency on a C terminal library — the rest of the module is already a
2489// pure-Rust port — so removing it left the binary with no `libtinfo` /
2490// `libncurses` link at all.
2491// WARNING: NOT IN CURSES.C — see flags_lock above.
2492/// Port of `zccmd_input(const char *nam, char **args)` from `Src/Modules/curses.c:1029`.
2493fn mouse_mask_lock() -> &'static Mutex<u32> {
2494    // ALL_MOUSE_EVENTS in ncurses ≈ 0x07ffffff for the standard
2495    // event set. Default to that until a `mouse delay`/`mouse motion`
2496    // command flips it.
2497    zcurses_mouse_mask.get_or_init(|| Mutex::new(0x07ffffff))
2498}
2499
2500// =====================================================================
2501// Port of `zcurses_colorget(const char *nam, char *colorpair)` from `Src/Modules/curses.c:331`.
2502// =====================================================================
2503
2504/// Port of `zcurses_colorget(const char *nam, char *colorpair)` from `Src/Modules/curses.c:331`.
2505///
2506/// Resolve `"fg/bg"` to a color-pair index, allocating a new pair
2507/// via `init_pair()` semantics (incrementing `next_cp`) on first
2508/// use. C uses libncurses's `init_pair`; Rust port stores the
2509/// mapping in the `zcurses_colorpairs` HashMap for later SGR.
2510pub(crate) fn colorpair_get_or_alloc(spec: &str) -> i32 {
2511    let mut pairs = colorpairs_lock().lock().unwrap();
2512    if let Some(&n) = pairs.get(spec) {
2513        return n as i32;
2514    }
2515    let slash = match spec.find('/') {
2516        Some(p) => p,
2517        None => return -1,
2518    };
2519    let fg = &spec[..slash];
2520    let bg = &spec[slash + 1..];
2521    let _fg_n = zcurses_color(fg);
2522    let _bg_n = zcurses_color(bg);
2523    if _fg_n == -2 || _bg_n == -2 {
2524        return -1;
2525    }
2526    let mut np = next_cp_lock().lock().unwrap();
2527    *np = np.saturating_add(1);
2528    let cp = *np;
2529    pairs.insert(spec.to_string(), cp);
2530    cp as i32
2531}
2532
2533// =====================================================================
2534// Keypad / keyboard input helpers — substitutes for libncurses's
2535// `wgetch` / `wget_wch` + `keypad()` table from `curses_keys.h`.
2536// =====================================================================
2537
2538// WARNING: NOT IN CURSES.C — Rust-only termios+read shim. C uses
2539// libncurses's `wget_wch(w->win, &wi)` / `wgetch(w->win, ...)`.
2540// Without libncurses linked, the Rust port reads raw bytes from
2541// stdin (cbreak set by `zccmd_init`) and parses CSI / SS3 sequences
2542// for arrow + function-key codes, plus xterm SGR-1006 / legacy X10
2543// mouse reports (decoded into a `KEY_MOUSE` code + pending `Mevent`
2544// when ZCF_MOUSE_ACTIVE is armed). Returns `(string, keycode)`.
2545// `keycode == 0` means a regular character; non-zero means a
2546// keypad code.
2547/// WARNING: NOT IN CURSES.C — Rust-side TTY key-sequence reader; C uses `wgetch()` from libncurses
2548/// (equivalent C logic at Src/Modules/curses.c:1473).
2549fn read_key_sequence(want_keypad: bool, _timeout_ms: i32) -> Option<(String, i32)> {
2550    #[cfg(unix)]
2551    {
2552        let mut buf = [0u8; 1];
2553        let n = io::stdin().read(&mut buf).ok()?;
2554        if n == 0 {
2555            return None;
2556        }
2557        let c = buf[0];
2558        if c == 0x1b && want_keypad {
2559            // ESC introduces a CSI / SS3 / mouse sequence. Read the
2560            // introducer byte, then dispatch.
2561            let mut seq = vec![c];
2562            let mut nb = [0u8; 1];
2563            if io::stdin().read(&mut nb).ok()? == 0 {
2564                return Some(((c as char).to_string(), 0));
2565            }
2566            seq.push(nb[0]);
2567            // c:1108-1131 mousemask gate: only decode mouse reports when
2568            // `zcurses input` armed mouse mode (ZCF_MOUSE_ACTIVE).
2569            let mouse_active = *flags_lock().lock().unwrap() & ZCF_MOUSE_ACTIVE != 0;
2570            if seq[1] == b'[' {
2571                if io::stdin().read(&mut nb).ok()? == 0 {
2572                    return Some(((c as char).to_string(), 0));
2573                }
2574                seq.push(nb[0]);
2575                if mouse_active && seq[2] == b'<' {
2576                    // xterm SGR-1006 mouse: ESC [ < b ; x ; y (M|m).
2577                    // The read_key_sequence half of the KEY_MOUSE decode
2578                    // (c:1162-1216): synthesize KEY_MOUSE + pending Mevent.
2579                    let mut params: Vec<u8> = Vec::new();
2580                    let mut final_ch = 0u8;
2581                    for _ in 0..32 {
2582                        if io::stdin().read(&mut nb).ok()? == 0 {
2583                            break;
2584                        }
2585                        if nb[0] == b'M' || nb[0] == b'm' {
2586                            final_ch = nb[0];
2587                            break;
2588                        }
2589                        params.push(nb[0]);
2590                    }
2591                    let text = String::from_utf8_lossy(&params);
2592                    let mut fields = text.split(';');
2593                    let b: i64 = fields.next().and_then(|v| v.parse().ok()).unwrap_or(-1);
2594                    let sx: i64 = fields.next().and_then(|v| v.parse().ok()).unwrap_or(0);
2595                    let sy: i64 = fields.next().and_then(|v| v.parse().ok()).unwrap_or(0);
2596                    if b >= 0 && (final_ch == b'M' || final_ch == b'm') {
2597                        // xterm button field: low 2 bits = button, +4 shift,
2598                        // +8 alt/meta, +16 ctrl, +32 motion, +64 wheel.
2599                        // Final 'M' = press, 'm' = release.
2600                        let released = final_ch == b'm';
2601                        let low = (b & 3) as i32;
2602                        let wheel = b & 64 != 0;
2603                        let motion = b & 32 != 0;
2604                        let button = if wheel {
2605                            4 + low
2606                        } else if low == 3 {
2607                            0 // motion with no button held
2608                        } else {
2609                            low + 1
2610                        };
2611                        let mut bstate: u64 = 0;
2612                        if button >= 1 && button <= 5 {
2613                            // NCURSES_MOUSE_MASK(button, base) = base << ((button-1)*5)
2614                            let base: u64 = if released { 0o1 } else { 0o2 };
2615                            bstate |= base << (((button - 1) * 5) as u64);
2616                        }
2617                        if motion {
2618                            bstate |= REPORT_MOUSE_POSITION as u64;
2619                        }
2620                        if b & 4 != 0 {
2621                            bstate |= BUTTON_SHIFT;
2622                        }
2623                        if b & 8 != 0 {
2624                            bstate |= BUTTON_ALT;
2625                        }
2626                        if b & 16 != 0 {
2627                            bstate |= BUTTON_CTRL;
2628                        }
2629                        // ncurses getmouse() reports 0-based coords.
2630                        *PENDING_MOUSE.lock().unwrap() = Some(Mevent {
2631                            id: 0,
2632                            x: (sx - 1) as i32,
2633                            y: (sy - 1) as i32,
2634                            z: 0,
2635                            bstate,
2636                        });
2637                        return Some((String::new(), KEY_MOUSE));
2638                    }
2639                    return Some(((c as char).to_string(), 0));
2640                } else if mouse_active && seq[2] == b'M' {
2641                    // Legacy X10 mouse: ESC [ M cb cx cy (three bytes,
2642                    // each offset by +32).
2643                    let mut raw = [0u8; 3];
2644                    let mut got = 0usize;
2645                    while got < 3 {
2646                        if io::stdin().read(&mut nb).ok()? == 0 {
2647                            break;
2648                        }
2649                        raw[got] = nb[0];
2650                        got += 1;
2651                    }
2652                    if got == 3 {
2653                        let b = raw[0] as i64 - 32;
2654                        let sx = raw[1] as i64 - 32;
2655                        let sy = raw[2] as i64 - 32;
2656                        let low = (b & 3) as i32;
2657                        let wheel = b & 64 != 0;
2658                        let (button, released) = if wheel {
2659                            (4 + low, false)
2660                        } else if low == 3 {
2661                            (1, true) // X10 release: button not reported
2662                        } else {
2663                            (low + 1, false)
2664                        };
2665                        let mut bstate: u64 = 0;
2666                        if button >= 1 && button <= 5 {
2667                            let base: u64 = if released { 0o1 } else { 0o2 };
2668                            bstate |= base << (((button - 1) * 5) as u64);
2669                        }
2670                        if b & 4 != 0 {
2671                            bstate |= BUTTON_SHIFT;
2672                        }
2673                        if b & 8 != 0 {
2674                            bstate |= BUTTON_ALT;
2675                        }
2676                        if b & 16 != 0 {
2677                            bstate |= BUTTON_CTRL;
2678                        }
2679                        *PENDING_MOUSE.lock().unwrap() = Some(Mevent {
2680                            id: 0,
2681                            x: (sx - 1) as i32,
2682                            y: (sy - 1) as i32,
2683                            z: 0,
2684                            bstate,
2685                        });
2686                        return Some((String::new(), KEY_MOUSE));
2687                    }
2688                    return Some(((c as char).to_string(), 0));
2689                } else {
2690                    // Ordinary CSI: read remaining bytes up to a final
2691                    // byte in 0x40..=0x7e, then map via csi_to_keypad.
2692                    if !(seq[2] >= 0x40 && seq[2] <= 0x7e) {
2693                        for _ in 0..6 {
2694                            if io::stdin().read(&mut nb).ok()? == 0 {
2695                                break;
2696                            }
2697                            seq.push(nb[0]);
2698                            if nb[0] >= 0x40 && nb[0] <= 0x7e {
2699                                break;
2700                            }
2701                        }
2702                    }
2703                    if let Some(code) = csi_to_keypad(&seq) {
2704                        return Some((String::new(), code));
2705                    }
2706                }
2707            } else if seq[1] == b'O' {
2708                // SS3: ESC O <final> (arrows / F1-F4).
2709                if io::stdin().read(&mut nb).ok()? == 0 {
2710                    return Some(((c as char).to_string(), 0));
2711                }
2712                seq.push(nb[0]);
2713                if let Some(code) = csi_to_keypad(&seq) {
2714                    return Some((String::new(), code));
2715                }
2716            }
2717            return Some(((c as char).to_string(), 0));
2718        }
2719        Some(((c as char).to_string(), 0))
2720    }
2721    #[cfg(not(unix))]
2722    {
2723        let _ = (want_keypad, _timeout_ms);
2724        None
2725    }
2726}
2727
2728// WARNING: NOT IN CURSES.C — table-driven CSI/SS3 → libncurses
2729// keypad-code mapping. Substitutes for the `curses_keys.h`
2730// autogenerated table that ncurses' `keypad()` uses internally.
2731// Values match libncurses constants (KEY_UP = 0o403, etc.).
2732fn csi_to_keypad(seq: &[u8]) -> Option<i32> {
2733    if seq.len() < 3 || seq[0] != 0x1b {
2734        return None;
2735    }
2736    if seq[1] == b'[' {
2737        match seq[2] {
2738            b'A' => Some(KEY_UP),
2739            b'B' => Some(KEY_DOWN),
2740            b'C' => Some(KEY_RIGHT),
2741            b'D' => Some(KEY_LEFT),
2742            b'H' => Some(KEY_HOME),
2743            b'F' => Some(KEY_END),
2744            b'1' if seq.len() >= 4 && seq[3] == b'~' => Some(KEY_HOME),
2745            b'2' if seq.len() >= 4 && seq[3] == b'~' => Some(KEY_IC),
2746            b'3' if seq.len() >= 4 && seq[3] == b'~' => Some(KEY_DC),
2747            b'4' if seq.len() >= 4 && seq[3] == b'~' => Some(KEY_END),
2748            b'5' if seq.len() >= 4 && seq[3] == b'~' => Some(KEY_PPAGE),
2749            b'6' if seq.len() >= 4 && seq[3] == b'~' => Some(KEY_NPAGE),
2750            _ => None,
2751        }
2752    } else if seq[1] == b'O' {
2753        match seq[2] {
2754            b'A' => Some(KEY_UP),
2755            b'B' => Some(KEY_DOWN),
2756            b'C' => Some(KEY_RIGHT),
2757            b'D' => Some(KEY_LEFT),
2758            b'P' => Some(KEY_F0 + 1),
2759            b'Q' => Some(KEY_F0 + 2),
2760            b'R' => Some(KEY_F0 + 3),
2761            b'S' => Some(KEY_F0 + 4),
2762            _ => None,
2763        }
2764    } else {
2765        None
2766    }
2767}
2768
2769// WARNING: NOT IN CURSES.C — Rust-only reverse lookup of the
2770// keypad-code → name table. C uses `keypad_names[]` from
2771// `curses_keys.h`; Rust port hardcodes the most common keys to
2772// keep the table compact.
2773fn keypad_name(code: i32) -> Option<String> {
2774    // Direct value-to-name table mirroring what C's awk-generated
2775    // keypad_names[] in curses_keys.h carries — every KEY_* define
2776    // from the system curses.h except MIN, MAX, CODE_YES (per the
2777    // awk filter at curses_keys.awk:8). Values match the canonical
2778    // ncurses octal-coded ranges:
2779    //   0o401-0o407: base set (BREAK..BACKSPACE)
2780    //   0o410-0o467: F-keys (F0..F63, handled inline below)
2781    //   0o510+:      extended set (DL..MOUSE)
2782    // Octal literals match the curses.h #defines exactly so a side-
2783    // by-side review against the original macros stays trivially
2784    // verifiable.
2785    let name = match code {
2786        // c:base set
2787        0o401 => "BREAK",
2788        KEY_DOWN => "DOWN",   // 0o402
2789        KEY_UP => "UP",       // 0o403
2790        KEY_LEFT => "LEFT",   // 0o404
2791        KEY_RIGHT => "RIGHT", // 0o405
2792        KEY_HOME => "HOME",   // 0o406
2793        0o407 => "BACKSPACE",
2794        // c:extended set
2795        0o510 => "DL",
2796        0o511 => "IL",
2797        KEY_DC => "DC", // 0o512
2798        KEY_IC => "IC", // 0o513
2799        0o514 => "EIC",
2800        0o515 => "CLEAR",
2801        0o516 => "EOS",
2802        0o517 => "EOL",
2803        0o520 => "SF",
2804        0o521 => "SR",
2805        KEY_NPAGE => "NPAGE", // 0o522
2806        KEY_PPAGE => "PPAGE", // 0o523
2807        0o524 => "STAB",
2808        0o525 => "CTAB",
2809        0o526 => "CATAB",
2810        0o527 => "ENTER",
2811        0o530 => "SRESET",
2812        0o531 => "RESET",
2813        0o532 => "PRINT",
2814        0o533 => "LL",
2815        0o534 => "A1",
2816        0o535 => "A3",
2817        0o536 => "B2",
2818        0o537 => "C1",
2819        0o540 => "C3",
2820        0o541 => "BTAB",
2821        0o542 => "BEG",
2822        0o543 => "CANCEL",
2823        0o544 => "CLOSE",
2824        0o545 => "COMMAND",
2825        0o546 => "COPY",
2826        0o547 => "CREATE",
2827        KEY_END => "END", // 0o550
2828        0o551 => "EXIT",
2829        0o552 => "FIND",
2830        0o553 => "HELP",
2831        0o554 => "MARK",
2832        0o555 => "MESSAGE",
2833        0o556 => "MOVE",
2834        0o557 => "NEXT",
2835        0o560 => "OPEN",
2836        0o561 => "OPTIONS",
2837        0o562 => "PREVIOUS",
2838        0o563 => "REDO",
2839        0o564 => "REFERENCE",
2840        0o565 => "REFRESH",
2841        0o566 => "REPLACE",
2842        0o567 => "RESTART",
2843        0o570 => "RESUME",
2844        0o571 => "SAVE",
2845        0o572 => "SBEG",
2846        0o573 => "SCANCEL",
2847        0o574 => "SCOMMAND",
2848        0o575 => "SCOPY",
2849        0o576 => "SCREATE",
2850        0o577 => "SDC",
2851        0o600 => "SDL",
2852        0o601 => "SELECT",
2853        0o602 => "SEND",
2854        0o603 => "SEOL",
2855        0o604 => "SEXIT",
2856        0o605 => "SFIND",
2857        0o606 => "SHELP",
2858        0o607 => "SHOME",
2859        0o610 => "SIC",
2860        0o611 => "SLEFT",
2861        0o612 => "SMESSAGE",
2862        0o613 => "SMOVE",
2863        0o614 => "SNEXT",
2864        0o615 => "SOPTIONS",
2865        0o616 => "SPREVIOUS",
2866        0o617 => "SPRINT",
2867        0o620 => "SREDO",
2868        0o621 => "SREPLACE",
2869        0o622 => "SRIGHT",
2870        0o623 => "SRSUME",
2871        0o624 => "SSAVE",
2872        0o625 => "SSUSPEND",
2873        0o626 => "SUNDO",
2874        0o627 => "SUSPEND",
2875        0o630 => "UNDO",
2876        0o631 => "MOUSE",
2877        0o632 => "RESIZE",
2878        0o633 => "EVENT", // KEY_EVENT — may not be defined on all platforms
2879        // F-keys: F0..F63 at KEY_F0 + n.
2880        c if c >= KEY_F0 && c < KEY_F0 + 64 => {
2881            return Some(format!("F{}", c - KEY_F0));
2882        }
2883        _ => return None,
2884    };
2885    Some(name.to_string())
2886}
2887
2888// Local stubs mirroring the C `featuresarray` / `handlefeatures` /
2889// `setfeatureenables` signatures (Src/module.c:3388/3370/3445). The
2890// canonical free ported operate on `Module *` and `Features *`; the
2891// Rust port collapses the bintab into one constant since curses
2892// ships exactly one builtin (`zcurses`).
2893// WARNING: NOT IN CURSES.C — Rust-only module-framework shim.
2894// C uses generic featuresarray/handlefeatures/setfeatureenables from
2895// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
2896// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
2897fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
2898    vec!["b:zcurses".to_string()]
2899}
2900
2901// WARNING: NOT IN CURSES.C — Rust-only module-framework shim.
2902// C uses generic featuresarray/handlefeatures/setfeatureenables from
2903// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
2904// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
2905fn handlefeatures(m: *const module, f: &Mutex<features>, enables: &mut Option<Vec<i32>>) -> i32 {
2906    // c:3392 — the name-keyed variant in src/ported/module.rs; this
2907    // module ships no `Features` descriptor tables for the per-feature
2908    // ADDED bit to live on (see MODULE_FEATURE_ENABLES there).
2909    crate::ported::module::handlefeatures("zsh/curses", &featuresarray(m, f), enables)
2910}
2911
2912// WARNING: NOT IN CURSES.C — Rust-only module-framework shim.
2913// C uses generic featuresarray/handlefeatures/setfeatureenables from
2914// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
2915// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
2916fn setfeatureenables(_m: *const module, _f: &Mutex<features>, _e: Option<&[i32]>) -> i32 {
2917    0
2918}
2919
2920// =====================================================================
2921// Rust-only helpers (libncurses substitutes).
2922// =====================================================================
2923
2924// WARNING: NOT IN CURSES.C — Rust-only TIOCGWINSZ probe. C uses
2925// libncurses's `LINES` / `COLS` globals which `initscr()` populates
2926// from terminfo; Rust port queries the kernel directly because
2927// libncurses isn't linked.
2928fn terminal_size() -> Option<(usize, usize)> {
2929    #[cfg(unix)]
2930    {
2931        let mut ws: libc::winsize = unsafe { std::mem::zeroed() };
2932        let result = unsafe { libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, &mut ws) };
2933        if result == 0 && ws.ws_row > 0 && ws.ws_col > 0 {
2934            return Some((ws.ws_row as usize, ws.ws_col as usize));
2935        }
2936    }
2937    // C falls back to `getiparam("LINES")` / `getiparam("COLUMNS")`
2938    // when TIOCGWINSZ fails. Read paramtab; convert i64→usize.
2939    let lines = getiparam("LINES");
2940    let cols = getiparam("COLUMNS");
2941    if lines > 0 && cols > 0 {
2942        Some((lines as usize, cols as usize))
2943    } else {
2944        None
2945    }
2946}
2947
2948// WARNING: NOT IN CURSES.C — termios shim replacing libncurses's
2949// `cbreak()` call inside `zccmd_init` (curses.c:492).
2950#[cfg(unix)]
2951fn cbreak() -> io::Result<()> {
2952    let mut termios: libc::termios = unsafe { std::mem::zeroed() };
2953    unsafe {
2954        if libc::tcgetattr(libc::STDIN_FILENO, &mut termios) < 0 {
2955            return Err(io::Error::last_os_error());
2956        }
2957        termios.c_lflag &= !(libc::ICANON | libc::ECHO);
2958        termios.c_cc[libc::VMIN] = 1;
2959        termios.c_cc[libc::VTIME] = 0;
2960        if libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &termios) < 0 {
2961            return Err(io::Error::last_os_error());
2962        }
2963    }
2964    Ok(())
2965}
2966
2967/// WARNING: NOT IN CURSES.C — Rust-side terminal mode helper; C calls libncurses `cbreak()` directly
2968/// (equivalent C logic at Src/Modules/curses.c:1920).
2969#[cfg(not(unix))]
2970fn cbreak() -> io::Result<()> {
2971    Ok(())
2972}
2973
2974// WARNING: NOT IN CURSES.C — Rust-only ANSI SGR encoder. C calls
2975// libncurses's `wattrset(win, attrs)` which the terminal handles;
2976// Rust port emits the matching SGR sequences since libncurses
2977// isn't linked.
2978fn sgr_for_attrs(attrs: u32) -> String {
2979    let mut s = "\x1b[0".to_string();
2980    if attrs & A_BOLD as u32 != 0 {
2981        s.push_str(";1");
2982    }
2983    if attrs & A_DIM as u32 != 0 {
2984        s.push_str(";2");
2985    }
2986    if attrs & A_UNDERLINE as u32 != 0 {
2987        s.push_str(";4");
2988    }
2989    if attrs & A_BLINK as u32 != 0 {
2990        s.push_str(";5");
2991    }
2992    if attrs & A_REVERSE as u32 != 0 || attrs & A_STANDOUT as u32 != 0 {
2993        s.push_str(";7");
2994    }
2995    s.push('m');
2996    s
2997}
2998
2999// =====================================================================
3000// Tests
3001// =====================================================================
3002
3003// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3004// ─── RUST-ONLY ACCESSORS ───
3005//
3006// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
3007// RwLock<T>>` globals declared above. C zsh uses direct global
3008// access; Rust needs these wrappers because `OnceLock::get_or_init`
3009// is the only way to lazily construct shared state. These ported sit
3010// here so the body of this file reads in C source order without
3011// the accessor wrappers interleaved between real port ported.
3012// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3013
3014// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3015// ─── RUST-ONLY ACCESSORS ───
3016//
3017// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
3018// RwLock<T>>` globals declared above. C zsh uses direct global
3019// access; Rust needs these wrappers because `OnceLock::get_or_init`
3020// is the only way to lazily construct shared state. These ported sit
3021// here so the body of this file reads in C source order without
3022// the accessor wrappers interleaved between real port ported.
3023// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3024
3025// WARNING: NOT IN CURSES.C — see windows_lock above.
3026fn order_lock() -> &'static Mutex<Vec<String>> {
3027    WINDOW_ORDER.get_or_init(|| Mutex::new(Vec::new()))
3028}
3029
3030// WARNING: NOT IN CURSES.C — Rust-only module-framework shim.
3031// C uses generic featuresarray/handlefeatures/setfeatureenables from
3032// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
3033// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
3034fn module_features() -> &'static Mutex<features> {
3035    MODULE_FEATURES.get_or_init(|| {
3036        Mutex::new(features {
3037            bn_list: None, // c:1639 bintab
3038            bn_size: 1,    // c:1639 sizeof(bintab)/sizeof(*bintab)
3039            cd_list: None, // c:1640
3040            cd_size: 0,
3041            mf_list: None, // c:1641
3042            mf_size: 0,
3043            pd_list: None, // c:1642
3044            pd_size: 0,
3045            n_abstract: 0, // c:1643
3046        })
3047    })
3048}
3049
3050#[cfg(test)]
3051mod tests {
3052    use super::*;
3053    use crate::ported::zsh_h::MAX_OPS;
3054
3055    static TEST_SERIAL: std::sync::Mutex<()> = std::sync::Mutex::new(());
3056
3057    /// Empty Options struct for test invocations of bin_zcurses (whose
3058    /// canonical sig per Src/Modules/curses.c:1568 takes Options).
3059    /// All zccmd_* subcommands parse their own opts inline, so the
3060    /// dispatcher passes through a no-op ops bag.
3061    #[allow(non_upper_case_globals)]
3062    const _test_ops_: options = options {
3063        ind: [0u8; MAX_OPS],
3064        args: Vec::new(),
3065        argscount: 0,
3066        argsalloc: 0,
3067    };
3068
3069    fn reset() -> std::sync::MutexGuard<'static, ()> {
3070        let guard = TEST_SERIAL.lock().unwrap_or_else(|e| {
3071            TEST_SERIAL.clear_poison();
3072            e.into_inner()
3073        });
3074        windows_lock()
3075            .lock()
3076            .unwrap_or_else(|e| {
3077                windows_lock().clear_poison();
3078                e.into_inner()
3079            })
3080            .clear();
3081        order_lock()
3082            .lock()
3083            .unwrap_or_else(|e| {
3084                order_lock().clear_poison();
3085                e.into_inner()
3086            })
3087            .clear();
3088        colorpairs_lock()
3089            .lock()
3090            .unwrap_or_else(|e| {
3091                colorpairs_lock().clear_poison();
3092                e.into_inner()
3093            })
3094            .clear();
3095        zc_errno_set(0);
3096        guard
3097    }
3098
3099    /// Port of `zcurses_strerror(int err)` from `Src/Modules/curses.c:233`.
3100    #[test]
3101    fn test_zcurses_strerror_table() {
3102        let _g = crate::test_util::global_state_lock();
3103        assert_eq!(zcurses_strerror(0), "unknown error");
3104        assert_eq!(zcurses_strerror(1), "window name invalid");
3105        assert_eq!(zcurses_strerror(2), "window already defined");
3106        assert_eq!(zcurses_strerror(3), "window undefined");
3107        assert_eq!(zcurses_strerror(99), "unknown error");
3108    }
3109
3110    #[test]
3111    fn test_zcurses_color_lookup() {
3112        let _g = crate::test_util::global_state_lock();
3113        assert_eq!(zcurses_color("red"), COLOR_RED);
3114        assert_eq!(zcurses_color("default"), -1);
3115        assert_eq!(zcurses_color("magenta"), COLOR_MAGENTA);
3116        assert_eq!(zcurses_color("unknown"), -2);
3117    }
3118
3119    #[test]
3120    fn test_zcurses_validate_window_invalid_name() {
3121        let _g = crate::test_util::global_state_lock();
3122        let _g = reset();
3123        assert!(!zcurses_validate_window("", ZCURSES_UNUSED));
3124        assert_eq!(zc_errno_get(), ZCURSES_EINVALID);
3125    }
3126
3127    #[test]
3128    fn test_zcurses_validate_window_unused_when_already_defined() {
3129        let _g = crate::test_util::global_state_lock();
3130        let _g = reset();
3131        windows_lock()
3132            .lock()
3133            .unwrap()
3134            .insert("win1".into(), zc_win::new("win1", 5, 10, 0, 0));
3135        assert!(!zcurses_validate_window("win1", ZCURSES_UNUSED));
3136        assert_eq!(zc_errno_get(), ZCURSES_EDEFINED);
3137    }
3138
3139    #[test]
3140    fn test_zcurses_validate_window_used_when_undefined() {
3141        let _g = crate::test_util::global_state_lock();
3142        let _g = reset();
3143        assert!(!zcurses_validate_window("nope", ZCURSES_USED));
3144        assert_eq!(zc_errno_get(), ZCURSES_EUNDEFINED);
3145    }
3146
3147    /// Port of `zccmd_delwin(const char *nam, char **args)` from `Src/Modules/curses.c:564`.
3148    #[test]
3149    fn test_zcurses_validate_window_success_used() {
3150        let _g = crate::test_util::global_state_lock();
3151        let _g = reset();
3152        windows_lock()
3153            .lock()
3154            .unwrap()
3155            .insert("stdscr".into(), zc_win::new("stdscr", 24, 80, 0, 0));
3156        assert!(zcurses_validate_window("stdscr", ZCURSES_USED));
3157        assert_eq!(zc_errno_get(), 0);
3158    }
3159
3160    #[test]
3161    fn test_zcurses_attrget_lookup() {
3162        let _g = crate::test_util::global_state_lock();
3163        let w = zc_win::new("test", 5, 10, 0, 0);
3164        let bold = zcurses_attrget(&w, "bold").expect("bold should resolve");
3165        assert_eq!(bold.number, A_BOLD);
3166        assert!(zcurses_attrget(&w, "unknown_attr").is_none());
3167    }
3168
3169    #[test]
3170    fn test_features_returns_bintab_names() {
3171        let _g = crate::test_util::global_state_lock();
3172        let mut features: Vec<String> = Vec::new();
3173        let rc = features_(std::ptr::null(), &mut features);
3174        assert_eq!(rc, 0);
3175        assert_eq!(features, vec!["b:zcurses"]);
3176    }
3177
3178    #[test]
3179    fn test_cleanup_returns_zero() {
3180        let _g = crate::test_util::global_state_lock();
3181        assert_eq!(cleanup_(std::ptr::null()), 0);
3182    }
3183
3184    /// Port of `bin_zcurses(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/curses.c:1568`.
3185    #[test]
3186    fn test_bin_zcurses_init_then_addwin_then_delwin() {
3187        let _g = crate::test_util::global_state_lock();
3188        let _g = reset();
3189        let init_args: Vec<String> = vec!["init".into()];
3190        assert_eq!(bin_zcurses("zcurses", &init_args, &_test_ops_, 0), 0);
3191        assert!(zcurses_getwindowbyname("stdscr"));
3192        let add_args: Vec<String> = vec![
3193            "addwin".into(),
3194            "win1".into(),
3195            "5".into(),
3196            "10".into(),
3197            "0".into(),
3198            "0".into(),
3199        ];
3200        assert_eq!(bin_zcurses("zcurses", &add_args, &_test_ops_, 0), 0);
3201        assert!(zcurses_getwindowbyname("win1"));
3202        let del_args: Vec<String> = vec!["delwin".into(), "win1".into()];
3203        assert_eq!(bin_zcurses("zcurses", &del_args, &_test_ops_, 0), 0);
3204        assert!(!zcurses_getwindowbyname("win1"));
3205    }
3206
3207    #[test]
3208    fn test_bin_zcurses_addwin_before_init_rejected() {
3209        let _g = crate::test_util::global_state_lock();
3210        let _g = reset();
3211        let add_args: Vec<String> = vec![
3212            "addwin".into(),
3213            "win1".into(),
3214            "5".into(),
3215            "10".into(),
3216            "0".into(),
3217            "0".into(),
3218        ];
3219        assert_eq!(bin_zcurses("zcurses", &add_args, &_test_ops_, 0), 1);
3220    }
3221
3222    #[test]
3223    fn test_bin_zcurses_addwin_duplicate_rejected() {
3224        let _g = crate::test_util::global_state_lock();
3225        let _g = reset();
3226        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3227        let add_args: Vec<String> = vec![
3228            "addwin".into(),
3229            "win1".into(),
3230            "5".into(),
3231            "10".into(),
3232            "0".into(),
3233            "0".into(),
3234        ];
3235        assert_eq!(bin_zcurses("zcurses", &add_args, &_test_ops_, 0), 0);
3236        // Second add of same name fails — zcurses_validate_window
3237        // sets ZCURSES_EDEFINED.
3238        assert_eq!(bin_zcurses("zcurses", &add_args, &_test_ops_, 0), 1);
3239        assert_eq!(zc_errno_get(), ZCURSES_EDEFINED);
3240    }
3241
3242    #[test]
3243    fn test_bin_zcurses_delwin_undefined_rejected() {
3244        let _g = crate::test_util::global_state_lock();
3245        let _g = reset();
3246        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3247        let del_args: Vec<String> = vec!["delwin".into(), "ghost".into()];
3248        assert_eq!(bin_zcurses("zcurses", &del_args, &_test_ops_, 0), 1);
3249        assert_eq!(zc_errno_get(), ZCURSES_EUNDEFINED);
3250    }
3251
3252    /// Port of `bin_zcurses(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/curses.c:1568`.
3253    #[test]
3254    fn test_bin_zcurses_delwin_stdscr_rejected() {
3255        let _g = crate::test_util::global_state_lock();
3256        let _g = reset();
3257        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3258        let del_args: Vec<String> = vec!["delwin".into(), "stdscr".into()];
3259        assert_eq!(bin_zcurses("zcurses", &del_args, &_test_ops_, 0), 1);
3260    }
3261
3262    #[test]
3263    fn test_bin_zcurses_too_few_args() {
3264        let _g = crate::test_util::global_state_lock();
3265        let _g = reset();
3266        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3267        // addwin needs 5 args but we only give 2.
3268        let bad_args: Vec<String> = vec!["addwin".into(), "win1".into()];
3269        assert_eq!(bin_zcurses("zcurses", &bad_args, &_test_ops_, 0), 1);
3270    }
3271
3272    #[test]
3273    fn test_bin_zcurses_unknown_subcommand() {
3274        let _g = crate::test_util::global_state_lock();
3275        let _g = reset();
3276        assert_eq!(bin_zcurses("zcurses", &["nope".into()], &_test_ops_, 0), 1);
3277    }
3278
3279    #[test]
3280    fn test_bin_zcurses_no_args() {
3281        let _g = crate::test_util::global_state_lock();
3282        let _g = reset();
3283        assert_eq!(bin_zcurses("zcurses", &[], &_test_ops_, 0), 1);
3284    }
3285
3286    #[test]
3287    fn test_zccmd_char_writes_into_buffer() {
3288        let _g = crate::test_util::global_state_lock();
3289        let _g = reset();
3290        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3291        let add: Vec<String> = vec![
3292            "addwin".into(),
3293            "w".into(),
3294            "5".into(),
3295            "10".into(),
3296            "0".into(),
3297            "0".into(),
3298        ];
3299        bin_zcurses("zcurses", &add, &_test_ops_, 0);
3300        let ch: Vec<String> = vec!["char".into(), "w".into(), "X".into()];
3301        assert_eq!(bin_zcurses("zcurses", &ch, &_test_ops_, 0), 0);
3302        let wins = windows_lock().lock().unwrap();
3303        assert_eq!(wins.get("w").unwrap().buffer[0][0], 'X');
3304        assert_eq!(wins.get("w").unwrap().cursor_x, 1);
3305    }
3306
3307    /// Port of `bin_zcurses(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/curses.c:1568`.
3308    #[test]
3309    fn test_zccmd_border_draws_box() {
3310        let _g = crate::test_util::global_state_lock();
3311        let _g = reset();
3312        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3313        let add: Vec<String> = vec![
3314            "addwin".into(),
3315            "box".into(),
3316            "3".into(),
3317            "5".into(),
3318            "0".into(),
3319            "0".into(),
3320        ];
3321        bin_zcurses("zcurses", &add, &_test_ops_, 0);
3322        assert_eq!(
3323            bin_zcurses("zcurses", &["border".into(), "box".into()], &_test_ops_, 0),
3324            0
3325        );
3326        let wins = windows_lock().lock().unwrap();
3327        let b = &wins.get("box").unwrap().buffer;
3328        assert_eq!(b[0][0], '┌');
3329        assert_eq!(b[0][4], '┐');
3330        assert_eq!(b[2][0], '└');
3331        assert_eq!(b[2][4], '┘');
3332        assert_eq!(b[1][0], '│');
3333        assert_eq!(b[0][2], '─');
3334    }
3335
3336    /// Port of `zccmd_scroll(const char *nam, char **args)` from `Src/Modules/curses.c:986`.
3337    #[test]
3338    fn test_zccmd_scroll_on_off() {
3339        let _g = crate::test_util::global_state_lock();
3340        let _g = reset();
3341        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3342        let add: Vec<String> = vec![
3343            "addwin".into(),
3344            "sw".into(),
3345            "5".into(),
3346            "10".into(),
3347            "0".into(),
3348            "0".into(),
3349        ];
3350        bin_zcurses("zcurses", &add, &_test_ops_, 0);
3351        let on: Vec<String> = vec!["scroll".into(), "sw".into(), "on".into()];
3352        assert_eq!(bin_zcurses("zcurses", &on, &_test_ops_, 0), 0);
3353        assert_eq!(
3354            windows_lock().lock().unwrap().get("sw").unwrap().flags & ZCWF_SCROLL,
3355            ZCWF_SCROLL
3356        );
3357        let off: Vec<String> = vec!["scroll".into(), "sw".into(), "off".into()];
3358        assert_eq!(bin_zcurses("zcurses", &off, &_test_ops_, 0), 0);
3359        assert_eq!(
3360            windows_lock().lock().unwrap().get("sw").unwrap().flags & ZCWF_SCROLL,
3361            0
3362        );
3363    }
3364
3365    /// Port of `zccmd_resize(const char *nam, char **args)` from `Src/Modules/curses.c:1494`.
3366    #[test]
3367    fn test_zccmd_scroll_integer_advances_buffer() {
3368        let _g = crate::test_util::global_state_lock();
3369        let _g = reset();
3370        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3371        let add: Vec<String> = vec![
3372            "addwin".into(),
3373            "sw".into(),
3374            "3".into(),
3375            "5".into(),
3376            "0".into(),
3377            "0".into(),
3378        ];
3379        bin_zcurses("zcurses", &add, &_test_ops_, 0);
3380        // Pre-fill row 0 with 'A' to detect the shift.
3381        windows_lock().lock().unwrap().get_mut("sw").unwrap().buffer[0][0] = 'A';
3382        let scr: Vec<String> = vec!["scroll".into(), "sw".into(), "1".into()];
3383        assert_eq!(bin_zcurses("zcurses", &scr, &_test_ops_, 0), 0);
3384        // Row 0 'A' shifted out — bottom row is fresh spaces.
3385        let wins = windows_lock().lock().unwrap();
3386        assert_eq!(wins.get("sw").unwrap().buffer[0][0], ' ');
3387    }
3388
3389    /// Port of `bin_zcurses(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/curses.c:1568`.
3390    #[test]
3391    fn test_zccmd_timeout_stores_value() {
3392        let _g = crate::test_util::global_state_lock();
3393        let _g = reset();
3394        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3395        let add: Vec<String> = vec![
3396            "addwin".into(),
3397            "tw".into(),
3398            "5".into(),
3399            "10".into(),
3400            "0".into(),
3401            "0".into(),
3402        ];
3403        bin_zcurses("zcurses", &add, &_test_ops_, 0);
3404        let to: Vec<String> = vec!["timeout".into(), "tw".into(), "100".into()];
3405        assert_eq!(bin_zcurses("zcurses", &to, &_test_ops_, 0), 0);
3406        assert_eq!(
3407            windows_lock().lock().unwrap().get("tw").unwrap().timeout_ms,
3408            100
3409        );
3410    }
3411
3412    /// Port of `bin_zcurses(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/curses.c:1568`.
3413    #[test]
3414    fn test_zccmd_touch_validates_each() {
3415        let _g = crate::test_util::global_state_lock();
3416        let _g = reset();
3417        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3418        let touch_ok: Vec<String> = vec!["touch".into(), "stdscr".into()];
3419        assert_eq!(bin_zcurses("zcurses", &touch_ok, &_test_ops_, 0), 0);
3420        let touch_bad: Vec<String> = vec!["touch".into(), "ghost".into()];
3421        assert_eq!(bin_zcurses("zcurses", &touch_bad, &_test_ops_, 0), 1);
3422        assert_eq!(zc_errno_get(), ZCURSES_EUNDEFINED);
3423    }
3424
3425    /// Port of `bin_zcurses(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/curses.c:1568`.
3426    #[test]
3427    fn test_zccmd_resize_changes_stdscr() {
3428        let _g = crate::test_util::global_state_lock();
3429        let _g = reset();
3430        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3431        let rs: Vec<String> = vec!["resize".into(), "30".into(), "100".into()];
3432        assert_eq!(bin_zcurses("zcurses", &rs, &_test_ops_, 0), 0);
3433        let wins = windows_lock().lock().unwrap();
3434        let stdscr = wins.get("stdscr").unwrap();
3435        assert_eq!(stdscr.rows, 30);
3436        assert_eq!(stdscr.cols, 100);
3437        assert_eq!(stdscr.buffer.len(), 30);
3438        assert_eq!(stdscr.buffer[0].len(), 100);
3439    }
3440
3441    #[test]
3442    fn test_zccmd_resize_bad_third_arg() {
3443        let _g = crate::test_util::global_state_lock();
3444        let _g = reset();
3445        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3446        let rs: Vec<String> = vec!["resize".into(), "30".into(), "100".into(), "junk".into()];
3447        assert_eq!(bin_zcurses("zcurses", &rs, &_test_ops_, 0), 1);
3448    }
3449
3450    /// Port of `zccmd_input(const char *nam, char **args)` from `Src/Modules/curses.c:1029`.
3451    #[test]
3452    fn test_zccmd_mouse_motion_toggle() {
3453        let _g = crate::test_util::global_state_lock();
3454        let _g = reset();
3455        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3456        // Default mask should not have REPORT_MOUSE_POSITION.
3457        let mouse_on: Vec<String> = vec!["mouse".into(), "+motion".into()];
3458        assert_eq!(bin_zcurses("zcurses", &mouse_on, &_test_ops_, 0), 0);
3459        assert_ne!(
3460            *mouse_mask_lock().lock().unwrap() & REPORT_MOUSE_POSITION,
3461            0
3462        );
3463        let mouse_off: Vec<String> = vec!["mouse".into(), "-motion".into()];
3464        assert_eq!(bin_zcurses("zcurses", &mouse_off, &_test_ops_, 0), 0);
3465        assert_eq!(
3466            *mouse_mask_lock().lock().unwrap() & REPORT_MOUSE_POSITION,
3467            0
3468        );
3469    }
3470
3471    #[test]
3472    fn test_zccmd_bg_with_color_pair() {
3473        let _g = crate::test_util::global_state_lock();
3474        let _g = reset();
3475        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3476        let add: Vec<String> = vec![
3477            "addwin".into(),
3478            "bgw".into(),
3479            "5".into(),
3480            "10".into(),
3481            "0".into(),
3482            "0".into(),
3483        ];
3484        bin_zcurses("zcurses", &add, &_test_ops_, 0);
3485        let bg: Vec<String> = vec!["bg".into(), "bgw".into(), "red/black".into()];
3486        assert_eq!(bin_zcurses("zcurses", &bg, &_test_ops_, 0), 0);
3487        // Pair was allocated; bg_chtype encodes the pair in the high bits.
3488        assert_ne!(
3489            windows_lock().lock().unwrap().get("bgw").unwrap().bg_chtype,
3490            0
3491        );
3492    }
3493
3494    /// Port of `zccmd_delwin(const char *nam, char **args)` from `Src/Modules/curses.c:564`.
3495    #[test]
3496    fn test_addwin_with_parent() {
3497        let _g = crate::test_util::global_state_lock();
3498        let _g = reset();
3499        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3500        let parent_args: Vec<String> = vec![
3501            "addwin".into(),
3502            "parent".into(),
3503            "10".into(),
3504            "20".into(),
3505            "0".into(),
3506            "0".into(),
3507        ];
3508        assert_eq!(bin_zcurses("zcurses", &parent_args, &_test_ops_, 0), 0);
3509        let child_args: Vec<String> = vec![
3510            "addwin".into(),
3511            "child".into(),
3512            "5".into(),
3513            "10".into(),
3514            "1".into(),
3515            "1".into(),
3516            "parent".into(),
3517        ];
3518        assert_eq!(bin_zcurses("zcurses", &child_args, &_test_ops_, 0), 0);
3519        let wins = windows_lock().lock().unwrap();
3520        assert_eq!(wins.get("child").unwrap().parent.as_deref(), Some("parent"));
3521        assert_eq!(
3522            wins.get("parent").unwrap().children,
3523            vec!["child".to_string()]
3524        );
3525    }
3526
3527    // ─── zsh-corpus pins for zcurses_strerror ──────────────────────
3528
3529    /// `zcurses_strerror(0)` returns "unknown error" (out-of-range maps to idx 0).
3530    #[test]
3531    fn curses_corpus_strerror_zero_unknown() {
3532        let _g = crate::test_util::global_state_lock();
3533        assert_eq!(zcurses_strerror(0), "unknown error");
3534    }
3535
3536    /// `zcurses_strerror(1)` returns "window name invalid".
3537    #[test]
3538    fn curses_corpus_strerror_one_invalid_name() {
3539        let _g = crate::test_util::global_state_lock();
3540        assert_eq!(zcurses_strerror(1), "window name invalid");
3541    }
3542
3543    /// `zcurses_strerror(2)` returns "window already defined".
3544    #[test]
3545    fn curses_corpus_strerror_two_already_defined() {
3546        let _g = crate::test_util::global_state_lock();
3547        assert_eq!(zcurses_strerror(2), "window already defined");
3548    }
3549
3550    /// `zcurses_strerror(3)` returns "window undefined".
3551    #[test]
3552    fn curses_corpus_strerror_three_undefined() {
3553        let _g = crate::test_util::global_state_lock();
3554        assert_eq!(zcurses_strerror(3), "window undefined");
3555    }
3556
3557    /// `zcurses_strerror(99)` clamps to "unknown error" (out of range).
3558    #[test]
3559    fn curses_corpus_strerror_out_of_range_clamps_to_unknown() {
3560        let _g = crate::test_util::global_state_lock();
3561        assert_eq!(zcurses_strerror(99), "unknown error");
3562        assert_eq!(zcurses_strerror(-1), "unknown error");
3563        assert_eq!(zcurses_strerror(i32::MAX), "unknown error");
3564    }
3565
3566    /// `zcurses_pairs_to_array` returns names from a zcurses_namenumberpair slice.
3567    #[test]
3568    fn curses_corpus_pairs_to_array_extracts_names() {
3569        let pairs = [
3570            zcurses_namenumberpair {
3571                name: "alpha",
3572                number: 1,
3573            },
3574            zcurses_namenumberpair {
3575                name: "beta",
3576                number: 2,
3577            },
3578            zcurses_namenumberpair {
3579                name: "gamma",
3580                number: 3,
3581            },
3582        ];
3583        let v = zcurses_pairs_to_array(&pairs);
3584        assert_eq!(v, vec!["alpha", "beta", "gamma"]);
3585    }
3586
3587    /// `zcurses_pairs_to_array` on empty input returns empty Vec.
3588    #[test]
3589    fn curses_corpus_pairs_to_array_empty_input() {
3590        let v = zcurses_pairs_to_array(&[]);
3591        assert!(v.is_empty());
3592    }
3593
3594    // ═══════════════════════════════════════════════════════════════════
3595    // Additional C-parity tests for Src/Modules/curses.c.
3596    // ═══════════════════════════════════════════════════════════════════
3597
3598    /// c:233 — `zcurses_strerror(0)` returns 'unknown error'.
3599    #[test]
3600    fn zcurses_strerror_zero_returns_unknown() {
3601        assert_eq!(zcurses_strerror(0), "unknown error");
3602    }
3603
3604    /// c:233 — `zcurses_strerror(1)` returns 'window name invalid'.
3605    #[test]
3606    fn zcurses_strerror_one_returns_name_invalid() {
3607        assert_eq!(zcurses_strerror(1), "window name invalid");
3608    }
3609
3610    /// c:233 — `zcurses_strerror(2)` returns 'window already defined'.
3611    #[test]
3612    fn zcurses_strerror_two_returns_already_defined() {
3613        assert_eq!(zcurses_strerror(2), "window already defined");
3614    }
3615
3616    /// c:233 — `zcurses_strerror(3)` returns 'window undefined'.
3617    #[test]
3618    fn zcurses_strerror_three_returns_undefined() {
3619        assert_eq!(zcurses_strerror(3), "window undefined");
3620    }
3621
3622    /// c:233 — `zcurses_strerror` out-of-range returns 'unknown error'.
3623    #[test]
3624    fn zcurses_strerror_out_of_range_returns_unknown() {
3625        assert_eq!(zcurses_strerror(-1), "unknown error");
3626        assert_eq!(zcurses_strerror(4), "unknown error");
3627        assert_eq!(zcurses_strerror(999), "unknown error");
3628    }
3629
3630    /// c:233 — `zcurses_strerror` is deterministic for known errnos.
3631    #[test]
3632    fn zcurses_strerror_is_deterministic() {
3633        for e in 0..=3 {
3634            let first = zcurses_strerror(e);
3635            for _ in 0..5 {
3636                assert_eq!(zcurses_strerror(e), first);
3637            }
3638        }
3639    }
3640
3641    /// c:1420 — `zcurses_colorsarrgetfn` returns vec (color names).
3642    #[test]
3643    fn zcurses_colorsarrgetfn_returns_vec() {
3644        let _g = crate::test_util::global_state_lock();
3645        let v = zcurses_colorsarrgetfn();
3646        // Either empty (no curses init) or has color names — pin no panic.
3647        let _ = v;
3648    }
3649
3650    /// c:1427 — `zcurses_attrgetfn` returns vec (attribute names).
3651    #[test]
3652    fn zcurses_attrgetfn_returns_vec() {
3653        let _g = crate::test_util::global_state_lock();
3654        let v = zcurses_attrgetfn();
3655        let _ = v;
3656    }
3657
3658    /// c:1436 — `zcurses_keycodesgetfn` returns vec.
3659    #[test]
3660    fn zcurses_keycodesgetfn_returns_vec() {
3661        let _g = crate::test_util::global_state_lock();
3662        let v = zcurses_keycodesgetfn();
3663        let _ = v;
3664    }
3665
3666    /// c:1453 — `zcurses_windowsgetfn` returns vec.
3667    #[test]
3668    fn zcurses_windowsgetfn_returns_vec() {
3669        let _g = crate::test_util::global_state_lock();
3670        let v = zcurses_windowsgetfn();
3671        let _ = v;
3672    }
3673
3674    /// c:1461 — `zcurses_colorsintgetfn` returns non-negative.
3675    #[test]
3676    fn zcurses_colorsintgetfn_returns_non_negative() {
3677        let _g = crate::test_util::global_state_lock();
3678        let n = zcurses_colorsintgetfn();
3679        assert!(n >= 0, "color count must be ≥ 0, got {}", n);
3680    }
3681
3682    /// c:1475 — `zcurses_colorpairsintgetfn` returns non-negative.
3683    #[test]
3684    fn zcurses_colorpairsintgetfn_returns_non_negative() {
3685        let _g = crate::test_util::global_state_lock();
3686        let n = zcurses_colorpairsintgetfn();
3687        assert!(n >= 0, "color pair count must be ≥ 0, got {}", n);
3688    }
3689
3690    /// Lifecycle (c:1493/1513/1519/1526) split per-hook.
3691    #[test]
3692    fn curses_setup_returns_zero_pin() {
3693        let _g = crate::test_util::global_state_lock();
3694        assert_eq!(setup_(std::ptr::null()), 0);
3695    }
3696
3697    /// c:1513 — boot_(NULL) = 0.
3698    #[test]
3699    fn curses_boot_returns_zero_pin() {
3700        let _g = crate::test_util::global_state_lock();
3701        assert_eq!(boot_(std::ptr::null()), 0);
3702    }
3703
3704    // ═══════════════════════════════════════════════════════════════════
3705    // Additional C-parity tests for Src/Modules/curses.c
3706    // c:293 zcurses_pairs_to_array / c:437 zcurses_colorget /
3707    // c:525 zcurses_colorget_reverse / c:544 freecolorpairnode /
3708    // c:1420/1427/1436/1453 *getfn determinism + lifecycle types
3709    // ═══════════════════════════════════════════════════════════════════
3710
3711    /// c:293 — `zcurses_pairs_to_array(empty)` returns empty Vec.
3712    #[test]
3713    fn zcurses_pairs_to_array_empty_returns_empty() {
3714        let r = zcurses_pairs_to_array(&[]);
3715        assert!(r.is_empty(), "empty input → empty Vec");
3716    }
3717
3718    /// c:293 — `zcurses_pairs_to_array` returns Vec<String> (type pin).
3719    #[test]
3720    fn zcurses_pairs_to_array_returns_vec_string_type() {
3721        let _: Vec<String> = zcurses_pairs_to_array(&[]);
3722    }
3723
3724    /// c:437 — `zcurses_colorget("", "")` returns Option<i16>.
3725    #[test]
3726    fn zcurses_colorget_returns_option_i16_type() {
3727        let _g = crate::test_util::global_state_lock();
3728        let _: Option<i16> = zcurses_colorget("", "");
3729    }
3730
3731    /// c:525 — `zcurses_colorget_reverse(N)` returns Option<String>.
3732    #[test]
3733    fn zcurses_colorget_reverse_returns_option_string_type() {
3734        let _g = crate::test_util::global_state_lock();
3735        let _: Option<String> = zcurses_colorget_reverse(0);
3736    }
3737
3738    /// c:525 — `zcurses_colorget_reverse(invalid)` returns None
3739    /// (i16::MAX is never a valid color pair index).
3740    #[test]
3741    fn zcurses_colorget_reverse_invalid_returns_none() {
3742        let _g = crate::test_util::global_state_lock();
3743        assert!(
3744            zcurses_colorget_reverse(i16::MAX).is_none(),
3745            "invalid color pair → None"
3746        );
3747    }
3748
3749    /// c:544 — `freecolorpairnode("")` empty name is safe.
3750    #[test]
3751    fn freecolorpairnode_empty_name_safe() {
3752        let _g = crate::test_util::global_state_lock();
3753        freecolorpairnode("");
3754    }
3755
3756    /// c:1420 — `zcurses_colorsarrgetfn` is deterministic.
3757    #[test]
3758    fn zcurses_colorsarrgetfn_is_deterministic() {
3759        let _g = crate::test_util::global_state_lock();
3760        let first = zcurses_colorsarrgetfn();
3761        for _ in 0..3 {
3762            assert_eq!(
3763                zcurses_colorsarrgetfn(),
3764                first,
3765                "zcurses_colorsarrgetfn must be deterministic"
3766            );
3767        }
3768    }
3769
3770    /// c:1427 — `zcurses_attrgetfn` is deterministic.
3771    #[test]
3772    fn zcurses_attrgetfn_is_deterministic() {
3773        let _g = crate::test_util::global_state_lock();
3774        let first = zcurses_attrgetfn();
3775        for _ in 0..3 {
3776            assert_eq!(
3777                zcurses_attrgetfn(),
3778                first,
3779                "zcurses_attrgetfn must be deterministic"
3780            );
3781        }
3782    }
3783
3784    /// c:1436 — `zcurses_keycodesgetfn` is deterministic.
3785    #[test]
3786    fn zcurses_keycodesgetfn_is_deterministic() {
3787        let _g = crate::test_util::global_state_lock();
3788        let first = zcurses_keycodesgetfn();
3789        for _ in 0..3 {
3790            assert_eq!(
3791                zcurses_keycodesgetfn(),
3792                first,
3793                "zcurses_keycodesgetfn must be deterministic"
3794            );
3795        }
3796    }
3797
3798    /// c:1461 — `zcurses_colorsintgetfn` returns i64 (type pin).
3799    #[test]
3800    fn zcurses_colorsintgetfn_returns_i64_type() {
3801        let _g = crate::test_util::global_state_lock();
3802        let _: i64 = zcurses_colorsintgetfn();
3803    }
3804
3805    /// c:1475 — `zcurses_colorpairsintgetfn` returns i64 (type pin).
3806    #[test]
3807    fn zcurses_colorpairsintgetfn_returns_i64_type() {
3808        let _g = crate::test_util::global_state_lock();
3809        let _: i64 = zcurses_colorpairsintgetfn();
3810    }
3811
3812    /// c:1493 — `setup_` returns i32 (compile-time type pin).
3813    #[test]
3814    fn curses_setup_returns_i32_type() {
3815        let _g = crate::test_util::global_state_lock();
3816        let _: i32 = setup_(std::ptr::null());
3817    }
3818
3819    /// c:1499 — features list non-empty.
3820    #[test]
3821    fn curses_features_nonempty() {
3822        let _g = crate::test_util::global_state_lock();
3823        let mut feats = Vec::new();
3824        features_(std::ptr::null(), &mut feats);
3825        assert!(!feats.is_empty(), "curses must advertise ≥1 feature");
3826    }
3827
3828    /// c:1519 + c:1526 — cleanup/finish idempotent.
3829    #[test]
3830    fn curses_cleanup_finish_idempotent() {
3831        let _g = crate::test_util::global_state_lock();
3832        for _ in 0..5 {
3833            assert_eq!(cleanup_(std::ptr::null()), 0);
3834            assert_eq!(finish_(std::ptr::null()), 0);
3835        }
3836    }
3837
3838    // ═══════════════════════════════════════════════════════════════════
3839    // Additional C-parity tests for Src/Modules/curses.c
3840    // c:293 zcurses_pairs_to_array / c:307 zcurses_strerror /
3841    // c:332 zcurses_getwindowbyname / c:379 zcurses_free_window /
3842    // c:418 zcurses_color / c:437 zcurses_colorget
3843    // ═══════════════════════════════════════════════════════════════════
3844
3845    /// c:293 — `zcurses_pairs_to_array(&[])` returns empty Vec (alt).
3846    #[test]
3847    fn zcurses_pairs_to_array_empty_returns_empty_alt() {
3848        let v = zcurses_pairs_to_array(&[]);
3849        assert!(v.is_empty(), "empty input → empty Vec");
3850    }
3851
3852    /// c:293 — `zcurses_pairs_to_array` returns Vec<String> (alt pin).
3853    #[test]
3854    fn zcurses_pairs_to_array_returns_vec_string_pin_alt() {
3855        let _: Vec<String> = zcurses_pairs_to_array(&[]);
3856    }
3857
3858    /// c:307 — `zcurses_strerror(0)` returns non-empty static str.
3859    #[test]
3860    fn zcurses_strerror_zero_returns_non_empty() {
3861        let s = zcurses_strerror(0);
3862        assert!(!s.is_empty(), "zcurses_strerror(0) must be non-empty");
3863    }
3864
3865    /// c:307 — `zcurses_strerror` returns `&'static str` (compile-time pin).
3866    #[test]
3867    fn zcurses_strerror_returns_static_str_type() {
3868        let _: &'static str = zcurses_strerror(-1);
3869    }
3870
3871    /// c:307 — `zcurses_strerror` deterministic for any input.
3872    #[test]
3873    fn zcurses_strerror_deterministic() {
3874        for err in [0, -1, 1, 42, i32::MIN, i32::MAX] {
3875            let a = zcurses_strerror(err);
3876            let b = zcurses_strerror(err);
3877            assert_eq!(a, b, "zcurses_strerror({}) must be pure", err);
3878        }
3879    }
3880
3881    /// c:332 — `zcurses_getwindowbyname("")` returns bool false (empty name).
3882    #[test]
3883    fn zcurses_getwindowbyname_empty_returns_false() {
3884        let _g = crate::test_util::global_state_lock();
3885        assert!(!zcurses_getwindowbyname(""), "empty name has no window");
3886    }
3887
3888    /// c:332 — `zcurses_getwindowbyname` returns bool (compile-time pin).
3889    #[test]
3890    fn zcurses_getwindowbyname_returns_bool_type() {
3891        let _g = crate::test_util::global_state_lock();
3892        let _: bool = zcurses_getwindowbyname("__never_window__");
3893    }
3894
3895    /// c:379 — `zcurses_free_window` returns i32 (compile-time pin).
3896    #[test]
3897    fn zcurses_free_window_returns_i32_type() {
3898        let _g = crate::test_util::global_state_lock();
3899        let _: i32 = zcurses_free_window("__never_window__");
3900    }
3901
3902    /// c:418 — `zcurses_color("")` returns i32 sentinel for unknown.
3903    #[test]
3904    fn zcurses_color_empty_string_returns_sentinel() {
3905        let _g = crate::test_util::global_state_lock();
3906        let r = zcurses_color("");
3907        assert!(
3908            r < 0,
3909            "empty color name must return negative sentinel; got {}",
3910            r
3911        );
3912    }
3913
3914    /// c:418 — `zcurses_color` is deterministic.
3915    #[test]
3916    fn zcurses_color_deterministic_for_unknown() {
3917        let _g = crate::test_util::global_state_lock();
3918        for name in ["__bogus_color__", "", "xyz"] {
3919            let first = zcurses_color(name);
3920            for _ in 0..3 {
3921                assert_eq!(
3922                    zcurses_color(name),
3923                    first,
3924                    "zcurses_color({:?}) must be pure",
3925                    name
3926                );
3927            }
3928        }
3929    }
3930
3931    /// c:437 — `zcurses_colorget` returns Option<i16> (compile-time pin, alt).
3932    #[test]
3933    fn zcurses_colorget_returns_option_i16_pin_alt() {
3934        let _g = crate::test_util::global_state_lock();
3935        let _: Option<i16> = zcurses_colorget("__never__", "red/black");
3936    }
3937}