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 cap = std::ffi::CString::new("colors").unwrap();
1806    let n = unsafe { libc_tigetnum(cap.as_ptr()) }; // c:1701 COLORS
1807    if n < 0 {
1808        0
1809    } else {
1810        n as i64
1811    }
1812}
1813
1814/// Direct port of `zcurses_colorpairsintgetfn()` from `Src/Modules/
1815/// curses.c:1701`. C body: `return COLOR_PAIRS;`. Same probe as above
1816/// against the `pairs` capability.
1817pub fn zcurses_colorpairsintgetfn() -> i64 {
1818    // c:1701
1819    let cap = std::ffi::CString::new("pairs").unwrap();
1820    let n = unsafe { libc_tigetnum(cap.as_ptr()) }; // c:1703 COLOR_PAIRS
1821    if n < 0 {
1822        0
1823    } else {
1824        n as i64
1825    }
1826}
1827
1828// =====================================================================
1829// Module entry points (curses.c:1744+).
1830// =====================================================================
1831
1832/// Port of `setup_(UNUSED(Module m))` from `Src/Modules/curses.c:1744`. C body:
1833/// `return 0;`.
1834#[allow(unused_variables)]
1835pub fn setup_(m: *const module) -> i32 {
1836    // c:1744
1837    0
1838}
1839
1840/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/Modules/curses.c:1751`.
1841pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
1842    // c:1751
1843    *features = featuresarray(m, module_features());
1844    0
1845}
1846
1847/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/Modules/curses.c`.
1848pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
1849    // c:1759
1850    handlefeatures(m, module_features(), enables)
1851}
1852
1853/// Port of `boot_(UNUSED(Module m))` from `Src/Modules/curses.c`. C body: `return 0;`.
1854/// WARNING: param names don't match C — Rust=(_m) vs C=(m)
1855pub fn boot_(_m: *const module) -> i32 {
1856    // c:1766
1857    0
1858}
1859
1860/// Port of `cleanup_(UNUSED(Module m))` from `Src/Modules/curses.c`.
1861pub fn cleanup_(m: *const module) -> i32 {
1862    // c:1775
1863    setfeatureenables(m, module_features(), None)
1864}
1865
1866/// Port of `finish_(UNUSED(Module m))` from `Src/Modules/curses.c`. C body: `return 0;`.
1867/// WARNING: param names don't match C — Rust=(_m) vs C=(m)
1868pub fn finish_(_m: *const module) -> i32 {
1869    // c:1785
1870    0
1871}
1872
1873// =====================================================================
1874// Port of `enum zc_win_flags` from `Src/Modules/curses.c:54`.
1875// =====================================================================
1876
1877// Window is permanent (probably "stdscr")                                  // c:55
1878/// Window is permanent (probably "stdscr"). C: `curses.c:56`.
1879pub const ZCWF_PERMANENT: u32 = 0x0001; // c:56
1880                                        // Scrolling enabled                                                        // c:57
1881/// Scrolling enabled. C: `curses.c:58`.
1882pub const ZCWF_SCROLL: u32 = 0x0002; // c:58
1883
1884/// Port of `enum zcurses_mouse_event_types` from `Src/Modules/curses.c:146`.
1885/// Mouse-event discriminator bits returned by `zcurses input` mouse
1886/// reads.
1887pub const ZCME_PRESSED: i32 = 0; // c:147
1888/// `ZCME_RELEASED` constant.
1889pub const ZCME_RELEASED: i32 = 1; // c:148
1890/// `ZCME_CLICKED` constant.
1891pub const ZCME_CLICKED: i32 = 2; // c:149
1892/// `ZCME_DOUBLE_CLICKED` constant.
1893pub const ZCME_DOUBLE_CLICKED: i32 = 3; // c:150
1894/// `ZCME_TRIPLE_CLICKED` constant.
1895pub const ZCME_TRIPLE_CLICKED: i32 = 4; // c:151
1896
1897/// `KEY_MOUSE` libncurses keycode (`curses.h`: `#define KEY_MOUSE 0631`).
1898/// Synthesized by `read_key_sequence` when it decodes an xterm mouse
1899/// report, then consumed by `zccmd_input`'s mouse branch (c:1209).
1900pub const KEY_MOUSE: i32 = 0o631;
1901
1902// ncurses mouse-event bit masks (`curses.h` BUTTONn_* /
1903// BUTTON_SHIFT/CTRL/ALT). Layout follows NCURSES_MOUSE_VERSION 2:
1904//   NCURSES_MOUSE_MASK(b, m) = m << ((b - 1) * 5)
1905// with event bases NCURSES_BUTTON_RELEASED=001, _PRESSED=002,
1906// _CLICKED=004, _DOUBLE_CLICKED=010, _TRIPLE_CLICKED=020 and the
1907// SHIFT/CTRL/ALT/REPORT modifiers in the button-6 slot (shift 25).
1908// This is consistent with the already-defined
1909// `REPORT_MOUSE_POSITION = 010 << 25 = 1 << 28`. The names + bit
1910// positions match curses.h so the `ZCURSES_MOUSE_MAP` table below is
1911// reviewable side-by-side against `zcurses_mouse_map[]` (c:169).
1912/// `BUTTON1_RELEASED` (curses.h).
1913pub const BUTTON1_RELEASED: u64 = 0o1 << 0;
1914/// `BUTTON1_PRESSED` (curses.h).
1915pub const BUTTON1_PRESSED: u64 = 0o2 << 0;
1916/// `BUTTON1_CLICKED` (curses.h).
1917pub const BUTTON1_CLICKED: u64 = 0o4 << 0;
1918/// `BUTTON1_DOUBLE_CLICKED` (curses.h).
1919pub const BUTTON1_DOUBLE_CLICKED: u64 = 0o10 << 0;
1920/// `BUTTON1_TRIPLE_CLICKED` (curses.h).
1921pub const BUTTON1_TRIPLE_CLICKED: u64 = 0o20 << 0;
1922/// `BUTTON2_RELEASED` (curses.h).
1923pub const BUTTON2_RELEASED: u64 = 0o1 << 5;
1924/// `BUTTON2_PRESSED` (curses.h).
1925pub const BUTTON2_PRESSED: u64 = 0o2 << 5;
1926/// `BUTTON2_CLICKED` (curses.h).
1927pub const BUTTON2_CLICKED: u64 = 0o4 << 5;
1928/// `BUTTON2_DOUBLE_CLICKED` (curses.h).
1929pub const BUTTON2_DOUBLE_CLICKED: u64 = 0o10 << 5;
1930/// `BUTTON2_TRIPLE_CLICKED` (curses.h).
1931pub const BUTTON2_TRIPLE_CLICKED: u64 = 0o20 << 5;
1932/// `BUTTON3_RELEASED` (curses.h).
1933pub const BUTTON3_RELEASED: u64 = 0o1 << 10;
1934/// `BUTTON3_PRESSED` (curses.h).
1935pub const BUTTON3_PRESSED: u64 = 0o2 << 10;
1936/// `BUTTON3_CLICKED` (curses.h).
1937pub const BUTTON3_CLICKED: u64 = 0o4 << 10;
1938/// `BUTTON3_DOUBLE_CLICKED` (curses.h).
1939pub const BUTTON3_DOUBLE_CLICKED: u64 = 0o10 << 10;
1940/// `BUTTON3_TRIPLE_CLICKED` (curses.h).
1941pub const BUTTON3_TRIPLE_CLICKED: u64 = 0o20 << 10;
1942/// `BUTTON4_RELEASED` (curses.h).
1943pub const BUTTON4_RELEASED: u64 = 0o1 << 15;
1944/// `BUTTON4_PRESSED` (curses.h).
1945pub const BUTTON4_PRESSED: u64 = 0o2 << 15;
1946/// `BUTTON4_CLICKED` (curses.h).
1947pub const BUTTON4_CLICKED: u64 = 0o4 << 15;
1948/// `BUTTON4_DOUBLE_CLICKED` (curses.h).
1949pub const BUTTON4_DOUBLE_CLICKED: u64 = 0o10 << 15;
1950/// `BUTTON4_TRIPLE_CLICKED` (curses.h).
1951pub const BUTTON4_TRIPLE_CLICKED: u64 = 0o20 << 15;
1952/// `BUTTON5_RELEASED` (curses.h).
1953pub const BUTTON5_RELEASED: u64 = 0o1 << 20;
1954/// `BUTTON5_PRESSED` (curses.h).
1955pub const BUTTON5_PRESSED: u64 = 0o2 << 20;
1956/// `BUTTON5_CLICKED` (curses.h).
1957pub const BUTTON5_CLICKED: u64 = 0o4 << 20;
1958/// `BUTTON5_DOUBLE_CLICKED` (curses.h).
1959pub const BUTTON5_DOUBLE_CLICKED: u64 = 0o10 << 20;
1960/// `BUTTON5_TRIPLE_CLICKED` (curses.h).
1961pub const BUTTON5_TRIPLE_CLICKED: u64 = 0o20 << 20;
1962/// `BUTTON_CTRL` (curses.h) — control modifier bit.
1963pub const BUTTON_CTRL: u64 = 0o1 << 25;
1964/// `BUTTON_SHIFT` (curses.h) — shift modifier bit.
1965pub const BUTTON_SHIFT: u64 = 0o2 << 25;
1966/// `BUTTON_ALT` (curses.h) — alt/meta modifier bit.
1967pub const BUTTON_ALT: u64 = 0o4 << 25;
1968
1969/// Port of `static const struct zcurses_mouse_event zcurses_mouse_map[]`
1970/// from `Src/Modules/curses.c:169`. Maps each ncurses button bit to a
1971/// `(button-number, event-kind)` pair for the decode loop at c:1240.
1972static ZCURSES_MOUSE_MAP: &[zcurses_mouse_event] = &[
1973    zcurses_mouse_event { button: 1, what: ZCME_PRESSED, event: BUTTON1_PRESSED }, // c:170
1974    zcurses_mouse_event { button: 1, what: ZCME_RELEASED, event: BUTTON1_RELEASED }, // c:171
1975    zcurses_mouse_event { button: 1, what: ZCME_CLICKED, event: BUTTON1_CLICKED }, // c:172
1976    zcurses_mouse_event { button: 1, what: ZCME_DOUBLE_CLICKED, event: BUTTON1_DOUBLE_CLICKED }, // c:173
1977    zcurses_mouse_event { button: 1, what: ZCME_TRIPLE_CLICKED, event: BUTTON1_TRIPLE_CLICKED }, // c:174
1978    zcurses_mouse_event { button: 2, what: ZCME_PRESSED, event: BUTTON2_PRESSED }, // c:176
1979    zcurses_mouse_event { button: 2, what: ZCME_RELEASED, event: BUTTON2_RELEASED }, // c:177
1980    zcurses_mouse_event { button: 2, what: ZCME_CLICKED, event: BUTTON2_CLICKED }, // c:178
1981    zcurses_mouse_event { button: 2, what: ZCME_DOUBLE_CLICKED, event: BUTTON2_DOUBLE_CLICKED }, // c:179
1982    zcurses_mouse_event { button: 2, what: ZCME_TRIPLE_CLICKED, event: BUTTON2_TRIPLE_CLICKED }, // c:180
1983    zcurses_mouse_event { button: 3, what: ZCME_PRESSED, event: BUTTON3_PRESSED }, // c:182
1984    zcurses_mouse_event { button: 3, what: ZCME_RELEASED, event: BUTTON3_RELEASED }, // c:183
1985    zcurses_mouse_event { button: 3, what: ZCME_CLICKED, event: BUTTON3_CLICKED }, // c:184
1986    zcurses_mouse_event { button: 3, what: ZCME_DOUBLE_CLICKED, event: BUTTON3_DOUBLE_CLICKED }, // c:185
1987    zcurses_mouse_event { button: 3, what: ZCME_TRIPLE_CLICKED, event: BUTTON3_TRIPLE_CLICKED }, // c:186
1988    zcurses_mouse_event { button: 4, what: ZCME_PRESSED, event: BUTTON4_PRESSED }, // c:188
1989    zcurses_mouse_event { button: 4, what: ZCME_RELEASED, event: BUTTON4_RELEASED }, // c:189
1990    zcurses_mouse_event { button: 4, what: ZCME_CLICKED, event: BUTTON4_CLICKED }, // c:190
1991    zcurses_mouse_event { button: 4, what: ZCME_DOUBLE_CLICKED, event: BUTTON4_DOUBLE_CLICKED }, // c:191
1992    zcurses_mouse_event { button: 4, what: ZCME_TRIPLE_CLICKED, event: BUTTON4_TRIPLE_CLICKED }, // c:192
1993    // c:194-201 — button 5 (guarded by `#ifdef BUTTON5_PRESSED` in C;
1994    // present here since the version-2 layout always defines it).
1995    zcurses_mouse_event { button: 5, what: ZCME_PRESSED, event: BUTTON5_PRESSED }, // c:196
1996    zcurses_mouse_event { button: 5, what: ZCME_RELEASED, event: BUTTON5_RELEASED }, // c:197
1997    zcurses_mouse_event { button: 5, what: ZCME_CLICKED, event: BUTTON5_CLICKED }, // c:198
1998    zcurses_mouse_event { button: 5, what: ZCME_DOUBLE_CLICKED, event: BUTTON5_DOUBLE_CLICKED }, // c:199
1999    zcurses_mouse_event { button: 5, what: ZCME_TRIPLE_CLICKED, event: BUTTON5_TRIPLE_CLICKED }, // c:200
2000];
2001
2002/// Port of `static const struct zcurses_namenumberpair
2003/// zcurses_mouse_event_list[]` from `Src/Modules/curses.c:154`.
2004static ZCURSES_MOUSE_EVENT_LIST: &[(&str, i32)] = &[
2005    ("PRESSED", ZCME_PRESSED),               // c:155
2006    ("RELEASED", ZCME_RELEASED),             // c:156
2007    ("CLICKED", ZCME_CLICKED),               // c:157
2008    ("DOUBLE_CLICKED", ZCME_DOUBLE_CLICKED), // c:158
2009    ("TRIPLE_CLICKED", ZCME_TRIPLE_CLICKED), // c:159
2010];
2011
2012/// ncurses `MEVENT` (`curses.h`) — one decoded mouse report, holding
2013/// the fields `zccmd_input` reads at c:1227-1234 and the `bstate` it
2014/// tests at c:1240-1261. `id`/`z` are unused by the terminal decoder
2015/// (kept for MEVENT parity); `x`/`y` are 0-based screen coordinates,
2016/// as ncurses' `getmouse()` returns them.
2017#[derive(Debug, Clone, Copy)]
2018pub struct Mevent {
2019    /// Device id (`MEVENT.id`); always 0 for the terminal decoder.
2020    pub id: i32,
2021    /// 0-based column (`MEVENT.x`).
2022    pub x: i32,
2023    /// 0-based row (`MEVENT.y`).
2024    pub y: i32,
2025    /// Unused scroll axis (`MEVENT.z`).
2026    pub z: i32,
2027    /// Button + modifier state bits (`MEVENT.bstate`).
2028    pub bstate: u64,
2029}
2030
2031/// Pending decoded mouse event — the terminal-decoder analog of
2032/// ncurses' internal `getmouse()` FIFO. `read_key_sequence` stores an
2033/// event here when it synthesizes `KEY_MOUSE`; `zccmd_input`'s mouse
2034/// branch (c:1217 `getmouse`) takes it back out. Single slot: "we only
2035/// expect one event" (c:1236). Accessed inline (no accessor fn) to keep
2036/// the port a data extension, not a new function.
2037#[allow(non_upper_case_globals)]
2038static PENDING_MOUSE: Mutex<Option<Mevent>> = Mutex::new(None);
2039
2040// =====================================================================
2041// Port of `static const struct zcurses_namenumberpair
2042// zcurses_attributes[]` from `curses.c:120`. ncurses constant
2043// values mirrored byte-for-byte (A_BOLD = 1<<8, etc.).
2044// =====================================================================
2045
2046/// libncurses `A_BLINK` from `<curses.h>`.
2047pub const A_BLINK: i32 = 1 << 11;
2048/// libncurses `A_BOLD`.
2049pub const A_BOLD: i32 = 1 << 13;
2050/// libncurses `A_DIM`.
2051pub const A_DIM: i32 = 1 << 12;
2052/// libncurses `A_REVERSE`.
2053pub const A_REVERSE: i32 = 1 << 10;
2054/// libncurses `A_STANDOUT`.
2055pub const A_STANDOUT: i32 = 1 << 8;
2056/// libncurses `A_UNDERLINE`.
2057pub const A_UNDERLINE: i32 = 1 << 9;
2058
2059/// Port of `zcurses_attributes[]` from `Src/Modules/curses.c:120`.
2060pub static zcurses_attributes: &[zcurses_namenumberpair] = &[
2061    zcurses_namenumberpair {
2062        name: "blink",
2063        number: A_BLINK,
2064    },
2065    zcurses_namenumberpair {
2066        name: "bold",
2067        number: A_BOLD,
2068    },
2069    zcurses_namenumberpair {
2070        name: "dim",
2071        number: A_DIM,
2072    },
2073    zcurses_namenumberpair {
2074        name: "reverse",
2075        number: A_REVERSE,
2076    },
2077    zcurses_namenumberpair {
2078        name: "standout",
2079        number: A_STANDOUT,
2080    },
2081    zcurses_namenumberpair {
2082        name: "underline",
2083        number: A_UNDERLINE,
2084    },
2085];
2086
2087// =====================================================================
2088// Port of `zcurses_colors[]` from `Src/Modules/curses.c:130`.
2089// =====================================================================
2090
2091/// libncurses `COLOR_BLACK` from `<curses.h>`.
2092pub const COLOR_BLACK: i32 = 0;
2093/// libncurses `COLOR_RED`.
2094pub const COLOR_RED: i32 = 1;
2095/// libncurses `COLOR_GREEN`.
2096pub const COLOR_GREEN: i32 = 2;
2097/// libncurses `COLOR_YELLOW`.
2098pub const COLOR_YELLOW: i32 = 3;
2099/// libncurses `COLOR_BLUE`.
2100pub const COLOR_BLUE: i32 = 4;
2101/// libncurses `COLOR_MAGENTA`.
2102pub const COLOR_MAGENTA: i32 = 5;
2103/// libncurses `COLOR_CYAN`.
2104pub const COLOR_CYAN: i32 = 6;
2105/// libncurses `COLOR_WHITE`.
2106pub const COLOR_WHITE: i32 = 7;
2107
2108/// Port of `zcurses_colors[]` from `Src/Modules/curses.c:130`.
2109pub static zcurses_colors: &[zcurses_namenumberpair] = &[
2110    zcurses_namenumberpair {
2111        name: "black",
2112        number: COLOR_BLACK,
2113    },
2114    zcurses_namenumberpair {
2115        name: "red",
2116        number: COLOR_RED,
2117    },
2118    zcurses_namenumberpair {
2119        name: "green",
2120        number: COLOR_GREEN,
2121    },
2122    zcurses_namenumberpair {
2123        name: "yellow",
2124        number: COLOR_YELLOW,
2125    },
2126    zcurses_namenumberpair {
2127        name: "blue",
2128        number: COLOR_BLUE,
2129    },
2130    zcurses_namenumberpair {
2131        name: "magenta",
2132        number: COLOR_MAGENTA,
2133    },
2134    zcurses_namenumberpair {
2135        name: "cyan",
2136        number: COLOR_CYAN,
2137    },
2138    zcurses_namenumberpair {
2139        name: "white",
2140        number: COLOR_WHITE,
2141    },
2142    zcurses_namenumberpair {
2143        name: "default",
2144        number: -1,
2145    },
2146];
2147
2148/// Insertion order tracker for `zcurses_windows`. C's LinkList
2149/// gives this for free; Rust port uses a parallel Vec.
2150static WINDOW_ORDER: OnceLock<Mutex<Vec<String>>> = OnceLock::new();
2151
2152/// Port of `static int zc_errno` from `curses.c:112`. Last
2153/// validation error code set by `zcurses_validate_window`.
2154static zc_errno_cell: OnceLock<Mutex<i32>> = OnceLock::new();
2155
2156/// `curses_tty_state` — file-static `struct ttyinfo curses_tty_state;`
2157/// from Src/Modules/curses.c:75. Snapshotted by gettyinfo at first
2158/// init (c:443), restored by settyinfo on re-entry (c:438).
2159#[allow(non_upper_case_globals)]
2160static curses_tty_state: Mutex<Option<libc::termios>> = Mutex::new(None); // c:75
2161
2162/// `saved_tty_state` — file-static `struct ttyinfo saved_tty_state;`
2163/// from `Src/Modules/curses.c:74`. The PRE-`zcurses init` termios
2164/// snapshot (c:443 gettyinfo); zccmd_endwin's c:830 settyinfo
2165/// restores it to hand the terminal back to the shell.
2166#[allow(non_upper_case_globals)]
2167static saved_tty_state: Mutex<Option<libc::termios>> = Mutex::new(None); // c:74
2168
2169/// `ZCF_MOUSE_ACTIVE` flag (curses.c:116).
2170pub const ZCF_MOUSE_ACTIVE: u32 = 1 << 0;
2171/// `ZCF_MOUSE_MASK_CHANGED` flag (curses.c:117).
2172pub const ZCF_MOUSE_MASK_CHANGED: u32 = 1 << 1;
2173
2174/// `REPORT_MOUSE_POSITION` libncurses constant. Port of the bit
2175/// `zcurses_mouse_mask` toggles for `mouse motion`.
2176pub const REPORT_MOUSE_POSITION: u32 = 1 << 28;
2177
2178/// Port of the `struct zcurses_subcommand scs[]` array inside
2179/// `bin_zcurses` (curses.c:1574). Order + min/max args match C.
2180static SCS: &[zcurses_subcommand] = &[
2181    zcurses_subcommand {
2182        name: "init",
2183        cmd: zccmd_init,
2184        minargs: 0,
2185        maxargs: 0,
2186    },
2187    zcurses_subcommand {
2188        name: "addwin",
2189        cmd: zccmd_addwin,
2190        minargs: 5,
2191        maxargs: 6,
2192    },
2193    zcurses_subcommand {
2194        name: "delwin",
2195        cmd: zccmd_delwin,
2196        minargs: 1,
2197        maxargs: 1,
2198    },
2199    zcurses_subcommand {
2200        name: "refresh",
2201        cmd: zccmd_refresh,
2202        minargs: 0,
2203        maxargs: -1,
2204    },
2205    zcurses_subcommand {
2206        name: "move",
2207        cmd: zccmd_move,
2208        minargs: 3,
2209        maxargs: 3,
2210    },
2211    zcurses_subcommand {
2212        name: "clear",
2213        cmd: zccmd_clear,
2214        minargs: 1,
2215        maxargs: 2,
2216    },
2217    zcurses_subcommand {
2218        name: "position",
2219        cmd: zccmd_position,
2220        minargs: 2,
2221        maxargs: 2,
2222    },
2223    zcurses_subcommand {
2224        name: "char",
2225        cmd: zccmd_char,
2226        minargs: 2,
2227        maxargs: 2,
2228    },
2229    zcurses_subcommand {
2230        name: "string",
2231        cmd: zccmd_string,
2232        minargs: 2,
2233        maxargs: 2,
2234    },
2235    zcurses_subcommand {
2236        name: "border",
2237        cmd: zccmd_border,
2238        minargs: 1,
2239        maxargs: 1,
2240    },
2241    zcurses_subcommand {
2242        name: "end",
2243        cmd: zccmd_endwin,
2244        minargs: 0,
2245        maxargs: 0,
2246    },
2247    zcurses_subcommand {
2248        name: "attr",
2249        cmd: zccmd_attr,
2250        minargs: 2,
2251        maxargs: -1,
2252    },
2253    zcurses_subcommand {
2254        name: "bg",
2255        cmd: zccmd_bg,
2256        minargs: 2,
2257        maxargs: -1,
2258    },
2259    zcurses_subcommand {
2260        name: "scroll",
2261        cmd: zccmd_scroll,
2262        minargs: 2,
2263        maxargs: 2,
2264    },
2265    zcurses_subcommand {
2266        name: "input",
2267        cmd: zccmd_input,
2268        minargs: 1,
2269        maxargs: 4,
2270    },
2271    zcurses_subcommand {
2272        name: "timeout",
2273        cmd: zccmd_timeout,
2274        minargs: 2,
2275        maxargs: 2,
2276    },
2277    zcurses_subcommand {
2278        name: "mouse",
2279        cmd: zccmd_mouse,
2280        minargs: 0,
2281        maxargs: -1,
2282    },
2283    zcurses_subcommand {
2284        name: "querychar",
2285        cmd: zccmd_querychar,
2286        minargs: 1,
2287        maxargs: 2,
2288    },
2289    zcurses_subcommand {
2290        name: "touch",
2291        cmd: zccmd_touch,
2292        minargs: 1,
2293        maxargs: -1,
2294    },
2295    zcurses_subcommand {
2296        name: "resize",
2297        cmd: zccmd_resize,
2298        minargs: 2,
2299        maxargs: 3,
2300    },
2301];
2302
2303/// libncurses keypad code constants. Subset of the
2304/// autogenerated `curses_keys.h` table the C source includes.
2305pub const KEY_DOWN: i32 = 0o402;
2306/// `KEY_UP` constant.
2307pub const KEY_UP: i32 = 0o403;
2308/// `KEY_LEFT` constant.
2309pub const KEY_LEFT: i32 = 0o404;
2310/// `KEY_RIGHT` constant.
2311pub const KEY_RIGHT: i32 = 0o405;
2312/// `KEY_HOME` constant.
2313pub const KEY_HOME: i32 = 0o406;
2314/// `KEY_DC` constant.
2315pub const KEY_DC: i32 = 0o512;
2316/// `KEY_IC` constant.
2317pub const KEY_IC: i32 = 0o513;
2318/// `KEY_NPAGE` constant.
2319pub const KEY_NPAGE: i32 = 0o522;
2320/// `KEY_PPAGE` constant.
2321pub const KEY_PPAGE: i32 = 0o523;
2322/// `KEY_END` constant.
2323pub const KEY_END: i32 = 0o550;
2324/// `KEY_F0` constant.
2325pub const KEY_F0: i32 = 0o410;
2326
2327// =====================================================================
2328// Module paraphernalia (curses.c:1631-).
2329// =====================================================================
2330
2331// Port of `static struct builtin bintab[]` from `curses.c:1631`:
2332//   BUILTIN("zcurses", 0, bin_zcurses, 1, -1, 0, "", NULL)
2333// The Rust port keeps the descriptor inline in the local
2334// `featuresarray` stub below — the static-link path doesn't
2335// need a `static [builtin]` array since `bin_zcurses` is
2336// invoked through the canonical builtin dispatcher, not the
2337// per-module bintab pointer C uses for dlopen.
2338
2339/// Port of `static struct features module_features` from
2340/// `Src/Modules/curses.c:1638`. C builds it as a stack literal;
2341/// Rust port keeps the OnceLock<Mutex> shape so callers consume
2342/// the same `*const module, &Mutex<crate::ported::zsh_h::features>` handle other
2343/// modules use.
2344static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
2345
2346// WARNING: NOT IN CURSES.C — Rust-only OnceLock get-or-init for
2347// each module static. C dereferences globals directly.
2348fn windows_lock() -> &'static Mutex<HashMap<String, zc_win>> {
2349    zcurses_windows.get_or_init(|| Mutex::new(HashMap::new()))
2350}
2351
2352// WARNING: NOT IN CURSES.C — see windows_lock above.
2353fn errno_lock() -> &'static Mutex<i32> {
2354    zc_errno_cell.get_or_init(|| Mutex::new(0))
2355}
2356
2357// WARNING: NOT IN CURSES.C — see windows_lock above.
2358fn colorpairs_lock() -> &'static Mutex<HashMap<String, i16>> {
2359    zcurses_colorpairs.get_or_init(|| Mutex::new(HashMap::new()))
2360}
2361
2362// WARNING: NOT IN CURSES.C — see windows_lock above.
2363fn next_cp_lock() -> &'static Mutex<i16> {
2364    next_cp.get_or_init(|| Mutex::new(0))
2365}
2366
2367/// WARNING: NOT IN CURSES.C — Mutex-based errno accessor; C reads `zc_errno` directly
2368/// (equivalent C logic at Src/Modules/curses.c:107).
2369/// Read the current `zc_errno` value. C dereferences the global
2370/// directly; Rust port wraps the lock acquire.
2371fn zc_errno_get() -> i32 {
2372    *errno_lock().lock().unwrap()
2373}
2374
2375/// WARNING: NOT IN CURSES.C — Mutex-based errno setter; C writes `zc_errno` directly
2376/// (equivalent C logic at Src/Modules/curses.c:107).
2377/// Set `zc_errno`. Used by `zcurses_validate_window`.
2378fn zc_errno_set(v: i32) {
2379    *errno_lock().lock().unwrap() = v;
2380}
2381
2382// WARNING: NOT IN CURSES.C — OnceLock get-or-init for `zcurses_flags`
2383// and `zcurses_mouse_mask` (C dereferences globals directly).
2384fn flags_lock() -> &'static Mutex<u32> {
2385    zcurses_flags.get_or_init(|| Mutex::new(0))
2386}
2387
2388#[link(name = "ncurses")]
2389extern "C" {
2390    #[link_name = "tigetnum"]
2391    fn libc_tigetnum(name: *const libc::c_char) -> libc::c_int;
2392}
2393// WARNING: NOT IN CURSES.C — see flags_lock above.
2394/// Port of `zccmd_input(const char *nam, char **args)` from `Src/Modules/curses.c:1029`.
2395fn mouse_mask_lock() -> &'static Mutex<u32> {
2396    // ALL_MOUSE_EVENTS in ncurses ≈ 0x07ffffff for the standard
2397    // event set. Default to that until a `mouse delay`/`mouse motion`
2398    // command flips it.
2399    zcurses_mouse_mask.get_or_init(|| Mutex::new(0x07ffffff))
2400}
2401
2402// =====================================================================
2403// Port of `zcurses_colorget(const char *nam, char *colorpair)` from `Src/Modules/curses.c:331`.
2404// =====================================================================
2405
2406/// Port of `zcurses_colorget(const char *nam, char *colorpair)` from `Src/Modules/curses.c:331`.
2407///
2408/// Resolve `"fg/bg"` to a color-pair index, allocating a new pair
2409/// via `init_pair()` semantics (incrementing `next_cp`) on first
2410/// use. C uses libncurses's `init_pair`; Rust port stores the
2411/// mapping in the `zcurses_colorpairs` HashMap for later SGR.
2412pub(crate) fn colorpair_get_or_alloc(spec: &str) -> i32 {
2413    let mut pairs = colorpairs_lock().lock().unwrap();
2414    if let Some(&n) = pairs.get(spec) {
2415        return n as i32;
2416    }
2417    let slash = match spec.find('/') {
2418        Some(p) => p,
2419        None => return -1,
2420    };
2421    let fg = &spec[..slash];
2422    let bg = &spec[slash + 1..];
2423    let _fg_n = zcurses_color(fg);
2424    let _bg_n = zcurses_color(bg);
2425    if _fg_n == -2 || _bg_n == -2 {
2426        return -1;
2427    }
2428    let mut np = next_cp_lock().lock().unwrap();
2429    *np = np.saturating_add(1);
2430    let cp = *np;
2431    pairs.insert(spec.to_string(), cp);
2432    cp as i32
2433}
2434
2435// =====================================================================
2436// Keypad / keyboard input helpers — substitutes for libncurses's
2437// `wgetch` / `wget_wch` + `keypad()` table from `curses_keys.h`.
2438// =====================================================================
2439
2440// WARNING: NOT IN CURSES.C — Rust-only termios+read shim. C uses
2441// libncurses's `wget_wch(w->win, &wi)` / `wgetch(w->win, ...)`.
2442// Without libncurses linked, the Rust port reads raw bytes from
2443// stdin (cbreak set by `zccmd_init`) and parses CSI / SS3 sequences
2444// for arrow + function-key codes, plus xterm SGR-1006 / legacy X10
2445// mouse reports (decoded into a `KEY_MOUSE` code + pending `Mevent`
2446// when ZCF_MOUSE_ACTIVE is armed). Returns `(string, keycode)`.
2447// `keycode == 0` means a regular character; non-zero means a
2448// keypad code.
2449/// WARNING: NOT IN CURSES.C — Rust-side TTY key-sequence reader; C uses `wgetch()` from libncurses
2450/// (equivalent C logic at Src/Modules/curses.c:1473).
2451fn read_key_sequence(want_keypad: bool, _timeout_ms: i32) -> Option<(String, i32)> {
2452    #[cfg(unix)]
2453    {
2454        let mut buf = [0u8; 1];
2455        let n = io::stdin().read(&mut buf).ok()?;
2456        if n == 0 {
2457            return None;
2458        }
2459        let c = buf[0];
2460        if c == 0x1b && want_keypad {
2461            // ESC introduces a CSI / SS3 / mouse sequence. Read the
2462            // introducer byte, then dispatch.
2463            let mut seq = vec![c];
2464            let mut nb = [0u8; 1];
2465            if io::stdin().read(&mut nb).ok()? == 0 {
2466                return Some(((c as char).to_string(), 0));
2467            }
2468            seq.push(nb[0]);
2469            // c:1108-1131 mousemask gate: only decode mouse reports when
2470            // `zcurses input` armed mouse mode (ZCF_MOUSE_ACTIVE).
2471            let mouse_active = *flags_lock().lock().unwrap() & ZCF_MOUSE_ACTIVE != 0;
2472            if seq[1] == b'[' {
2473                if io::stdin().read(&mut nb).ok()? == 0 {
2474                    return Some(((c as char).to_string(), 0));
2475                }
2476                seq.push(nb[0]);
2477                if mouse_active && seq[2] == b'<' {
2478                    // xterm SGR-1006 mouse: ESC [ < b ; x ; y (M|m).
2479                    // The read_key_sequence half of the KEY_MOUSE decode
2480                    // (c:1162-1216): synthesize KEY_MOUSE + pending Mevent.
2481                    let mut params: Vec<u8> = Vec::new();
2482                    let mut final_ch = 0u8;
2483                    for _ in 0..32 {
2484                        if io::stdin().read(&mut nb).ok()? == 0 {
2485                            break;
2486                        }
2487                        if nb[0] == b'M' || nb[0] == b'm' {
2488                            final_ch = nb[0];
2489                            break;
2490                        }
2491                        params.push(nb[0]);
2492                    }
2493                    let text = String::from_utf8_lossy(&params);
2494                    let mut fields = text.split(';');
2495                    let b: i64 = fields.next().and_then(|v| v.parse().ok()).unwrap_or(-1);
2496                    let sx: i64 = fields.next().and_then(|v| v.parse().ok()).unwrap_or(0);
2497                    let sy: i64 = fields.next().and_then(|v| v.parse().ok()).unwrap_or(0);
2498                    if b >= 0 && (final_ch == b'M' || final_ch == b'm') {
2499                        // xterm button field: low 2 bits = button, +4 shift,
2500                        // +8 alt/meta, +16 ctrl, +32 motion, +64 wheel.
2501                        // Final 'M' = press, 'm' = release.
2502                        let released = final_ch == b'm';
2503                        let low = (b & 3) as i32;
2504                        let wheel = b & 64 != 0;
2505                        let motion = b & 32 != 0;
2506                        let button = if wheel {
2507                            4 + low
2508                        } else if low == 3 {
2509                            0 // motion with no button held
2510                        } else {
2511                            low + 1
2512                        };
2513                        let mut bstate: u64 = 0;
2514                        if button >= 1 && button <= 5 {
2515                            // NCURSES_MOUSE_MASK(button, base) = base << ((button-1)*5)
2516                            let base: u64 = if released { 0o1 } else { 0o2 };
2517                            bstate |= base << (((button - 1) * 5) as u64);
2518                        }
2519                        if motion {
2520                            bstate |= REPORT_MOUSE_POSITION as u64;
2521                        }
2522                        if b & 4 != 0 {
2523                            bstate |= BUTTON_SHIFT;
2524                        }
2525                        if b & 8 != 0 {
2526                            bstate |= BUTTON_ALT;
2527                        }
2528                        if b & 16 != 0 {
2529                            bstate |= BUTTON_CTRL;
2530                        }
2531                        // ncurses getmouse() reports 0-based coords.
2532                        *PENDING_MOUSE.lock().unwrap() = Some(Mevent {
2533                            id: 0,
2534                            x: (sx - 1) as i32,
2535                            y: (sy - 1) as i32,
2536                            z: 0,
2537                            bstate,
2538                        });
2539                        return Some((String::new(), KEY_MOUSE));
2540                    }
2541                    return Some(((c as char).to_string(), 0));
2542                } else if mouse_active && seq[2] == b'M' {
2543                    // Legacy X10 mouse: ESC [ M cb cx cy (three bytes,
2544                    // each offset by +32).
2545                    let mut raw = [0u8; 3];
2546                    let mut got = 0usize;
2547                    while got < 3 {
2548                        if io::stdin().read(&mut nb).ok()? == 0 {
2549                            break;
2550                        }
2551                        raw[got] = nb[0];
2552                        got += 1;
2553                    }
2554                    if got == 3 {
2555                        let b = raw[0] as i64 - 32;
2556                        let sx = raw[1] as i64 - 32;
2557                        let sy = raw[2] as i64 - 32;
2558                        let low = (b & 3) as i32;
2559                        let wheel = b & 64 != 0;
2560                        let (button, released) = if wheel {
2561                            (4 + low, false)
2562                        } else if low == 3 {
2563                            (1, true) // X10 release: button not reported
2564                        } else {
2565                            (low + 1, false)
2566                        };
2567                        let mut bstate: u64 = 0;
2568                        if button >= 1 && button <= 5 {
2569                            let base: u64 = if released { 0o1 } else { 0o2 };
2570                            bstate |= base << (((button - 1) * 5) as u64);
2571                        }
2572                        if b & 4 != 0 {
2573                            bstate |= BUTTON_SHIFT;
2574                        }
2575                        if b & 8 != 0 {
2576                            bstate |= BUTTON_ALT;
2577                        }
2578                        if b & 16 != 0 {
2579                            bstate |= BUTTON_CTRL;
2580                        }
2581                        *PENDING_MOUSE.lock().unwrap() = Some(Mevent {
2582                            id: 0,
2583                            x: (sx - 1) as i32,
2584                            y: (sy - 1) as i32,
2585                            z: 0,
2586                            bstate,
2587                        });
2588                        return Some((String::new(), KEY_MOUSE));
2589                    }
2590                    return Some(((c as char).to_string(), 0));
2591                } else {
2592                    // Ordinary CSI: read remaining bytes up to a final
2593                    // byte in 0x40..=0x7e, then map via csi_to_keypad.
2594                    if !(seq[2] >= 0x40 && seq[2] <= 0x7e) {
2595                        for _ in 0..6 {
2596                            if io::stdin().read(&mut nb).ok()? == 0 {
2597                                break;
2598                            }
2599                            seq.push(nb[0]);
2600                            if nb[0] >= 0x40 && nb[0] <= 0x7e {
2601                                break;
2602                            }
2603                        }
2604                    }
2605                    if let Some(code) = csi_to_keypad(&seq) {
2606                        return Some((String::new(), code));
2607                    }
2608                }
2609            } else if seq[1] == b'O' {
2610                // SS3: ESC O <final> (arrows / F1-F4).
2611                if io::stdin().read(&mut nb).ok()? == 0 {
2612                    return Some(((c as char).to_string(), 0));
2613                }
2614                seq.push(nb[0]);
2615                if let Some(code) = csi_to_keypad(&seq) {
2616                    return Some((String::new(), code));
2617                }
2618            }
2619            return Some(((c as char).to_string(), 0));
2620        }
2621        Some(((c as char).to_string(), 0))
2622    }
2623    #[cfg(not(unix))]
2624    {
2625        let _ = (want_keypad, _timeout_ms);
2626        None
2627    }
2628}
2629
2630// WARNING: NOT IN CURSES.C — table-driven CSI/SS3 → libncurses
2631// keypad-code mapping. Substitutes for the `curses_keys.h`
2632// autogenerated table that ncurses' `keypad()` uses internally.
2633// Values match libncurses constants (KEY_UP = 0o403, etc.).
2634fn csi_to_keypad(seq: &[u8]) -> Option<i32> {
2635    if seq.len() < 3 || seq[0] != 0x1b {
2636        return None;
2637    }
2638    if seq[1] == b'[' {
2639        match seq[2] {
2640            b'A' => Some(KEY_UP),
2641            b'B' => Some(KEY_DOWN),
2642            b'C' => Some(KEY_RIGHT),
2643            b'D' => Some(KEY_LEFT),
2644            b'H' => Some(KEY_HOME),
2645            b'F' => Some(KEY_END),
2646            b'1' if seq.len() >= 4 && seq[3] == b'~' => Some(KEY_HOME),
2647            b'2' if seq.len() >= 4 && seq[3] == b'~' => Some(KEY_IC),
2648            b'3' if seq.len() >= 4 && seq[3] == b'~' => Some(KEY_DC),
2649            b'4' if seq.len() >= 4 && seq[3] == b'~' => Some(KEY_END),
2650            b'5' if seq.len() >= 4 && seq[3] == b'~' => Some(KEY_PPAGE),
2651            b'6' if seq.len() >= 4 && seq[3] == b'~' => Some(KEY_NPAGE),
2652            _ => None,
2653        }
2654    } else if seq[1] == b'O' {
2655        match seq[2] {
2656            b'A' => Some(KEY_UP),
2657            b'B' => Some(KEY_DOWN),
2658            b'C' => Some(KEY_RIGHT),
2659            b'D' => Some(KEY_LEFT),
2660            b'P' => Some(KEY_F0 + 1),
2661            b'Q' => Some(KEY_F0 + 2),
2662            b'R' => Some(KEY_F0 + 3),
2663            b'S' => Some(KEY_F0 + 4),
2664            _ => None,
2665        }
2666    } else {
2667        None
2668    }
2669}
2670
2671// WARNING: NOT IN CURSES.C — Rust-only reverse lookup of the
2672// keypad-code → name table. C uses `keypad_names[]` from
2673// `curses_keys.h`; Rust port hardcodes the most common keys to
2674// keep the table compact.
2675fn keypad_name(code: i32) -> Option<String> {
2676    // Direct value-to-name table mirroring what C's awk-generated
2677    // keypad_names[] in curses_keys.h carries — every KEY_* define
2678    // from the system curses.h except MIN, MAX, CODE_YES (per the
2679    // awk filter at curses_keys.awk:8). Values match the canonical
2680    // ncurses octal-coded ranges:
2681    //   0o401-0o407: base set (BREAK..BACKSPACE)
2682    //   0o410-0o467: F-keys (F0..F63, handled inline below)
2683    //   0o510+:      extended set (DL..MOUSE)
2684    // Octal literals match the curses.h #defines exactly so a side-
2685    // by-side review against the original macros stays trivially
2686    // verifiable.
2687    let name = match code {
2688        // c:base set
2689        0o401 => "BREAK",
2690        KEY_DOWN => "DOWN",   // 0o402
2691        KEY_UP => "UP",       // 0o403
2692        KEY_LEFT => "LEFT",   // 0o404
2693        KEY_RIGHT => "RIGHT", // 0o405
2694        KEY_HOME => "HOME",   // 0o406
2695        0o407 => "BACKSPACE",
2696        // c:extended set
2697        0o510 => "DL",
2698        0o511 => "IL",
2699        KEY_DC => "DC", // 0o512
2700        KEY_IC => "IC", // 0o513
2701        0o514 => "EIC",
2702        0o515 => "CLEAR",
2703        0o516 => "EOS",
2704        0o517 => "EOL",
2705        0o520 => "SF",
2706        0o521 => "SR",
2707        KEY_NPAGE => "NPAGE", // 0o522
2708        KEY_PPAGE => "PPAGE", // 0o523
2709        0o524 => "STAB",
2710        0o525 => "CTAB",
2711        0o526 => "CATAB",
2712        0o527 => "ENTER",
2713        0o530 => "SRESET",
2714        0o531 => "RESET",
2715        0o532 => "PRINT",
2716        0o533 => "LL",
2717        0o534 => "A1",
2718        0o535 => "A3",
2719        0o536 => "B2",
2720        0o537 => "C1",
2721        0o540 => "C3",
2722        0o541 => "BTAB",
2723        0o542 => "BEG",
2724        0o543 => "CANCEL",
2725        0o544 => "CLOSE",
2726        0o545 => "COMMAND",
2727        0o546 => "COPY",
2728        0o547 => "CREATE",
2729        KEY_END => "END", // 0o550
2730        0o551 => "EXIT",
2731        0o552 => "FIND",
2732        0o553 => "HELP",
2733        0o554 => "MARK",
2734        0o555 => "MESSAGE",
2735        0o556 => "MOVE",
2736        0o557 => "NEXT",
2737        0o560 => "OPEN",
2738        0o561 => "OPTIONS",
2739        0o562 => "PREVIOUS",
2740        0o563 => "REDO",
2741        0o564 => "REFERENCE",
2742        0o565 => "REFRESH",
2743        0o566 => "REPLACE",
2744        0o567 => "RESTART",
2745        0o570 => "RESUME",
2746        0o571 => "SAVE",
2747        0o572 => "SBEG",
2748        0o573 => "SCANCEL",
2749        0o574 => "SCOMMAND",
2750        0o575 => "SCOPY",
2751        0o576 => "SCREATE",
2752        0o577 => "SDC",
2753        0o600 => "SDL",
2754        0o601 => "SELECT",
2755        0o602 => "SEND",
2756        0o603 => "SEOL",
2757        0o604 => "SEXIT",
2758        0o605 => "SFIND",
2759        0o606 => "SHELP",
2760        0o607 => "SHOME",
2761        0o610 => "SIC",
2762        0o611 => "SLEFT",
2763        0o612 => "SMESSAGE",
2764        0o613 => "SMOVE",
2765        0o614 => "SNEXT",
2766        0o615 => "SOPTIONS",
2767        0o616 => "SPREVIOUS",
2768        0o617 => "SPRINT",
2769        0o620 => "SREDO",
2770        0o621 => "SREPLACE",
2771        0o622 => "SRIGHT",
2772        0o623 => "SRSUME",
2773        0o624 => "SSAVE",
2774        0o625 => "SSUSPEND",
2775        0o626 => "SUNDO",
2776        0o627 => "SUSPEND",
2777        0o630 => "UNDO",
2778        0o631 => "MOUSE",
2779        0o632 => "RESIZE",
2780        0o633 => "EVENT", // KEY_EVENT — may not be defined on all platforms
2781        // F-keys: F0..F63 at KEY_F0 + n.
2782        c if c >= KEY_F0 && c < KEY_F0 + 64 => {
2783            return Some(format!("F{}", c - KEY_F0));
2784        }
2785        _ => return None,
2786    };
2787    Some(name.to_string())
2788}
2789
2790// Local stubs mirroring the C `featuresarray` / `handlefeatures` /
2791// `setfeatureenables` signatures (Src/module.c:3388/3370/3445). The
2792// canonical free ported operate on `Module *` and `Features *`; the
2793// Rust port collapses the bintab into one constant since curses
2794// ships exactly one builtin (`zcurses`).
2795// WARNING: NOT IN CURSES.C — Rust-only module-framework shim.
2796// C uses generic featuresarray/handlefeatures/setfeatureenables from
2797// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
2798// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
2799fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
2800    vec!["b:zcurses".to_string()]
2801}
2802
2803// WARNING: NOT IN CURSES.C — Rust-only module-framework shim.
2804// C uses generic featuresarray/handlefeatures/setfeatureenables from
2805// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
2806// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
2807fn handlefeatures(_m: *const module, _f: &Mutex<features>, enables: &mut Option<Vec<i32>>) -> i32 {
2808    if enables.is_none() {
2809        *enables = Some(vec![1]);
2810    }
2811    0
2812}
2813
2814// WARNING: NOT IN CURSES.C — Rust-only module-framework shim.
2815// C uses generic featuresarray/handlefeatures/setfeatureenables from
2816// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
2817// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
2818fn setfeatureenables(_m: *const module, _f: &Mutex<features>, _e: Option<&[i32]>) -> i32 {
2819    0
2820}
2821
2822// =====================================================================
2823// Rust-only helpers (libncurses substitutes).
2824// =====================================================================
2825
2826// WARNING: NOT IN CURSES.C — Rust-only TIOCGWINSZ probe. C uses
2827// libncurses's `LINES` / `COLS` globals which `initscr()` populates
2828// from terminfo; Rust port queries the kernel directly because
2829// libncurses isn't linked.
2830fn terminal_size() -> Option<(usize, usize)> {
2831    #[cfg(unix)]
2832    {
2833        let mut ws: libc::winsize = unsafe { std::mem::zeroed() };
2834        let result = unsafe { libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, &mut ws) };
2835        if result == 0 && ws.ws_row > 0 && ws.ws_col > 0 {
2836            return Some((ws.ws_row as usize, ws.ws_col as usize));
2837        }
2838    }
2839    // C falls back to `getiparam("LINES")` / `getiparam("COLUMNS")`
2840    // when TIOCGWINSZ fails. Read paramtab; convert i64→usize.
2841    let lines = getiparam("LINES");
2842    let cols = getiparam("COLUMNS");
2843    if lines > 0 && cols > 0 {
2844        Some((lines as usize, cols as usize))
2845    } else {
2846        None
2847    }
2848}
2849
2850// WARNING: NOT IN CURSES.C — termios shim replacing libncurses's
2851// `cbreak()` call inside `zccmd_init` (curses.c:492).
2852#[cfg(unix)]
2853fn cbreak() -> io::Result<()> {
2854    let mut termios: libc::termios = unsafe { std::mem::zeroed() };
2855    unsafe {
2856        if libc::tcgetattr(libc::STDIN_FILENO, &mut termios) < 0 {
2857            return Err(io::Error::last_os_error());
2858        }
2859        termios.c_lflag &= !(libc::ICANON | libc::ECHO);
2860        termios.c_cc[libc::VMIN] = 1;
2861        termios.c_cc[libc::VTIME] = 0;
2862        if libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &termios) < 0 {
2863            return Err(io::Error::last_os_error());
2864        }
2865    }
2866    Ok(())
2867}
2868
2869/// WARNING: NOT IN CURSES.C — Rust-side terminal mode helper; C calls libncurses `cbreak()` directly
2870/// (equivalent C logic at Src/Modules/curses.c:1920).
2871#[cfg(not(unix))]
2872fn cbreak() -> io::Result<()> {
2873    Ok(())
2874}
2875
2876// WARNING: NOT IN CURSES.C — Rust-only ANSI SGR encoder. C calls
2877// libncurses's `wattrset(win, attrs)` which the terminal handles;
2878// Rust port emits the matching SGR sequences since libncurses
2879// isn't linked.
2880fn sgr_for_attrs(attrs: u32) -> String {
2881    let mut s = "\x1b[0".to_string();
2882    if attrs & A_BOLD as u32 != 0 {
2883        s.push_str(";1");
2884    }
2885    if attrs & A_DIM as u32 != 0 {
2886        s.push_str(";2");
2887    }
2888    if attrs & A_UNDERLINE as u32 != 0 {
2889        s.push_str(";4");
2890    }
2891    if attrs & A_BLINK as u32 != 0 {
2892        s.push_str(";5");
2893    }
2894    if attrs & A_REVERSE as u32 != 0 || attrs & A_STANDOUT as u32 != 0 {
2895        s.push_str(";7");
2896    }
2897    s.push('m');
2898    s
2899}
2900
2901// =====================================================================
2902// Tests
2903// =====================================================================
2904
2905// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2906// ─── RUST-ONLY ACCESSORS ───
2907//
2908// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
2909// RwLock<T>>` globals declared above. C zsh uses direct global
2910// access; Rust needs these wrappers because `OnceLock::get_or_init`
2911// is the only way to lazily construct shared state. These ported sit
2912// here so the body of this file reads in C source order without
2913// the accessor wrappers interleaved between real port ported.
2914// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2915
2916// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2917// ─── RUST-ONLY ACCESSORS ───
2918//
2919// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
2920// RwLock<T>>` globals declared above. C zsh uses direct global
2921// access; Rust needs these wrappers because `OnceLock::get_or_init`
2922// is the only way to lazily construct shared state. These ported sit
2923// here so the body of this file reads in C source order without
2924// the accessor wrappers interleaved between real port ported.
2925// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2926
2927// WARNING: NOT IN CURSES.C — see windows_lock above.
2928fn order_lock() -> &'static Mutex<Vec<String>> {
2929    WINDOW_ORDER.get_or_init(|| Mutex::new(Vec::new()))
2930}
2931
2932// WARNING: NOT IN CURSES.C — Rust-only module-framework shim.
2933// C uses generic featuresarray/handlefeatures/setfeatureenables from
2934// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
2935// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
2936fn module_features() -> &'static Mutex<features> {
2937    MODULE_FEATURES.get_or_init(|| {
2938        Mutex::new(features {
2939            bn_list: None, // c:1639 bintab
2940            bn_size: 1,    // c:1639 sizeof(bintab)/sizeof(*bintab)
2941            cd_list: None, // c:1640
2942            cd_size: 0,
2943            mf_list: None, // c:1641
2944            mf_size: 0,
2945            pd_list: None, // c:1642
2946            pd_size: 0,
2947            n_abstract: 0, // c:1643
2948        })
2949    })
2950}
2951
2952#[cfg(test)]
2953mod tests {
2954    use super::*;
2955    use crate::ported::zsh_h::MAX_OPS;
2956
2957    static TEST_SERIAL: std::sync::Mutex<()> = std::sync::Mutex::new(());
2958
2959    /// Empty Options struct for test invocations of bin_zcurses (whose
2960    /// canonical sig per Src/Modules/curses.c:1568 takes Options).
2961    /// All zccmd_* subcommands parse their own opts inline, so the
2962    /// dispatcher passes through a no-op ops bag.
2963    #[allow(non_upper_case_globals)]
2964    const _test_ops_: options = options {
2965        ind: [0u8; MAX_OPS],
2966        args: Vec::new(),
2967        argscount: 0,
2968        argsalloc: 0,
2969    };
2970
2971    fn reset() -> std::sync::MutexGuard<'static, ()> {
2972        let guard = TEST_SERIAL.lock().unwrap_or_else(|e| {
2973            TEST_SERIAL.clear_poison();
2974            e.into_inner()
2975        });
2976        windows_lock()
2977            .lock()
2978            .unwrap_or_else(|e| {
2979                windows_lock().clear_poison();
2980                e.into_inner()
2981            })
2982            .clear();
2983        order_lock()
2984            .lock()
2985            .unwrap_or_else(|e| {
2986                order_lock().clear_poison();
2987                e.into_inner()
2988            })
2989            .clear();
2990        colorpairs_lock()
2991            .lock()
2992            .unwrap_or_else(|e| {
2993                colorpairs_lock().clear_poison();
2994                e.into_inner()
2995            })
2996            .clear();
2997        zc_errno_set(0);
2998        guard
2999    }
3000
3001    /// Port of `zcurses_strerror(int err)` from `Src/Modules/curses.c:233`.
3002    #[test]
3003    fn test_zcurses_strerror_table() {
3004        let _g = crate::test_util::global_state_lock();
3005        assert_eq!(zcurses_strerror(0), "unknown error");
3006        assert_eq!(zcurses_strerror(1), "window name invalid");
3007        assert_eq!(zcurses_strerror(2), "window already defined");
3008        assert_eq!(zcurses_strerror(3), "window undefined");
3009        assert_eq!(zcurses_strerror(99), "unknown error");
3010    }
3011
3012    #[test]
3013    fn test_zcurses_color_lookup() {
3014        let _g = crate::test_util::global_state_lock();
3015        assert_eq!(zcurses_color("red"), COLOR_RED);
3016        assert_eq!(zcurses_color("default"), -1);
3017        assert_eq!(zcurses_color("magenta"), COLOR_MAGENTA);
3018        assert_eq!(zcurses_color("unknown"), -2);
3019    }
3020
3021    #[test]
3022    fn test_zcurses_validate_window_invalid_name() {
3023        let _g = crate::test_util::global_state_lock();
3024        let _g = reset();
3025        assert!(!zcurses_validate_window("", ZCURSES_UNUSED));
3026        assert_eq!(zc_errno_get(), ZCURSES_EINVALID);
3027    }
3028
3029    #[test]
3030    fn test_zcurses_validate_window_unused_when_already_defined() {
3031        let _g = crate::test_util::global_state_lock();
3032        let _g = reset();
3033        windows_lock()
3034            .lock()
3035            .unwrap()
3036            .insert("win1".into(), zc_win::new("win1", 5, 10, 0, 0));
3037        assert!(!zcurses_validate_window("win1", ZCURSES_UNUSED));
3038        assert_eq!(zc_errno_get(), ZCURSES_EDEFINED);
3039    }
3040
3041    #[test]
3042    fn test_zcurses_validate_window_used_when_undefined() {
3043        let _g = crate::test_util::global_state_lock();
3044        let _g = reset();
3045        assert!(!zcurses_validate_window("nope", ZCURSES_USED));
3046        assert_eq!(zc_errno_get(), ZCURSES_EUNDEFINED);
3047    }
3048
3049    /// Port of `zccmd_delwin(const char *nam, char **args)` from `Src/Modules/curses.c:564`.
3050    #[test]
3051    fn test_zcurses_validate_window_success_used() {
3052        let _g = crate::test_util::global_state_lock();
3053        let _g = reset();
3054        windows_lock()
3055            .lock()
3056            .unwrap()
3057            .insert("stdscr".into(), zc_win::new("stdscr", 24, 80, 0, 0));
3058        assert!(zcurses_validate_window("stdscr", ZCURSES_USED));
3059        assert_eq!(zc_errno_get(), 0);
3060    }
3061
3062    #[test]
3063    fn test_zcurses_attrget_lookup() {
3064        let _g = crate::test_util::global_state_lock();
3065        let w = zc_win::new("test", 5, 10, 0, 0);
3066        let bold = zcurses_attrget(&w, "bold").expect("bold should resolve");
3067        assert_eq!(bold.number, A_BOLD);
3068        assert!(zcurses_attrget(&w, "unknown_attr").is_none());
3069    }
3070
3071    #[test]
3072    fn test_features_returns_bintab_names() {
3073        let _g = crate::test_util::global_state_lock();
3074        let mut features: Vec<String> = Vec::new();
3075        let rc = features_(std::ptr::null(), &mut features);
3076        assert_eq!(rc, 0);
3077        assert_eq!(features, vec!["b:zcurses"]);
3078    }
3079
3080    #[test]
3081    fn test_cleanup_returns_zero() {
3082        let _g = crate::test_util::global_state_lock();
3083        assert_eq!(cleanup_(std::ptr::null()), 0);
3084    }
3085
3086    /// Port of `bin_zcurses(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/curses.c:1568`.
3087    #[test]
3088    fn test_bin_zcurses_init_then_addwin_then_delwin() {
3089        let _g = crate::test_util::global_state_lock();
3090        let _g = reset();
3091        let init_args: Vec<String> = vec!["init".into()];
3092        assert_eq!(bin_zcurses("zcurses", &init_args, &_test_ops_, 0), 0);
3093        assert!(zcurses_getwindowbyname("stdscr"));
3094        let add_args: Vec<String> = vec![
3095            "addwin".into(),
3096            "win1".into(),
3097            "5".into(),
3098            "10".into(),
3099            "0".into(),
3100            "0".into(),
3101        ];
3102        assert_eq!(bin_zcurses("zcurses", &add_args, &_test_ops_, 0), 0);
3103        assert!(zcurses_getwindowbyname("win1"));
3104        let del_args: Vec<String> = vec!["delwin".into(), "win1".into()];
3105        assert_eq!(bin_zcurses("zcurses", &del_args, &_test_ops_, 0), 0);
3106        assert!(!zcurses_getwindowbyname("win1"));
3107    }
3108
3109    #[test]
3110    fn test_bin_zcurses_addwin_before_init_rejected() {
3111        let _g = crate::test_util::global_state_lock();
3112        let _g = reset();
3113        let add_args: Vec<String> = vec![
3114            "addwin".into(),
3115            "win1".into(),
3116            "5".into(),
3117            "10".into(),
3118            "0".into(),
3119            "0".into(),
3120        ];
3121        assert_eq!(bin_zcurses("zcurses", &add_args, &_test_ops_, 0), 1);
3122    }
3123
3124    #[test]
3125    fn test_bin_zcurses_addwin_duplicate_rejected() {
3126        let _g = crate::test_util::global_state_lock();
3127        let _g = reset();
3128        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3129        let add_args: Vec<String> = vec![
3130            "addwin".into(),
3131            "win1".into(),
3132            "5".into(),
3133            "10".into(),
3134            "0".into(),
3135            "0".into(),
3136        ];
3137        assert_eq!(bin_zcurses("zcurses", &add_args, &_test_ops_, 0), 0);
3138        // Second add of same name fails — zcurses_validate_window
3139        // sets ZCURSES_EDEFINED.
3140        assert_eq!(bin_zcurses("zcurses", &add_args, &_test_ops_, 0), 1);
3141        assert_eq!(zc_errno_get(), ZCURSES_EDEFINED);
3142    }
3143
3144    #[test]
3145    fn test_bin_zcurses_delwin_undefined_rejected() {
3146        let _g = crate::test_util::global_state_lock();
3147        let _g = reset();
3148        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3149        let del_args: Vec<String> = vec!["delwin".into(), "ghost".into()];
3150        assert_eq!(bin_zcurses("zcurses", &del_args, &_test_ops_, 0), 1);
3151        assert_eq!(zc_errno_get(), ZCURSES_EUNDEFINED);
3152    }
3153
3154    /// Port of `bin_zcurses(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/curses.c:1568`.
3155    #[test]
3156    fn test_bin_zcurses_delwin_stdscr_rejected() {
3157        let _g = crate::test_util::global_state_lock();
3158        let _g = reset();
3159        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3160        let del_args: Vec<String> = vec!["delwin".into(), "stdscr".into()];
3161        assert_eq!(bin_zcurses("zcurses", &del_args, &_test_ops_, 0), 1);
3162    }
3163
3164    #[test]
3165    fn test_bin_zcurses_too_few_args() {
3166        let _g = crate::test_util::global_state_lock();
3167        let _g = reset();
3168        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3169        // addwin needs 5 args but we only give 2.
3170        let bad_args: Vec<String> = vec!["addwin".into(), "win1".into()];
3171        assert_eq!(bin_zcurses("zcurses", &bad_args, &_test_ops_, 0), 1);
3172    }
3173
3174    #[test]
3175    fn test_bin_zcurses_unknown_subcommand() {
3176        let _g = crate::test_util::global_state_lock();
3177        let _g = reset();
3178        assert_eq!(bin_zcurses("zcurses", &["nope".into()], &_test_ops_, 0), 1);
3179    }
3180
3181    #[test]
3182    fn test_bin_zcurses_no_args() {
3183        let _g = crate::test_util::global_state_lock();
3184        let _g = reset();
3185        assert_eq!(bin_zcurses("zcurses", &[], &_test_ops_, 0), 1);
3186    }
3187
3188    #[test]
3189    fn test_zccmd_char_writes_into_buffer() {
3190        let _g = crate::test_util::global_state_lock();
3191        let _g = reset();
3192        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3193        let add: Vec<String> = vec![
3194            "addwin".into(),
3195            "w".into(),
3196            "5".into(),
3197            "10".into(),
3198            "0".into(),
3199            "0".into(),
3200        ];
3201        bin_zcurses("zcurses", &add, &_test_ops_, 0);
3202        let ch: Vec<String> = vec!["char".into(), "w".into(), "X".into()];
3203        assert_eq!(bin_zcurses("zcurses", &ch, &_test_ops_, 0), 0);
3204        let wins = windows_lock().lock().unwrap();
3205        assert_eq!(wins.get("w").unwrap().buffer[0][0], 'X');
3206        assert_eq!(wins.get("w").unwrap().cursor_x, 1);
3207    }
3208
3209    /// Port of `bin_zcurses(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/curses.c:1568`.
3210    #[test]
3211    fn test_zccmd_border_draws_box() {
3212        let _g = crate::test_util::global_state_lock();
3213        let _g = reset();
3214        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3215        let add: Vec<String> = vec![
3216            "addwin".into(),
3217            "box".into(),
3218            "3".into(),
3219            "5".into(),
3220            "0".into(),
3221            "0".into(),
3222        ];
3223        bin_zcurses("zcurses", &add, &_test_ops_, 0);
3224        assert_eq!(
3225            bin_zcurses("zcurses", &["border".into(), "box".into()], &_test_ops_, 0),
3226            0
3227        );
3228        let wins = windows_lock().lock().unwrap();
3229        let b = &wins.get("box").unwrap().buffer;
3230        assert_eq!(b[0][0], '┌');
3231        assert_eq!(b[0][4], '┐');
3232        assert_eq!(b[2][0], '└');
3233        assert_eq!(b[2][4], '┘');
3234        assert_eq!(b[1][0], '│');
3235        assert_eq!(b[0][2], '─');
3236    }
3237
3238    /// Port of `zccmd_scroll(const char *nam, char **args)` from `Src/Modules/curses.c:986`.
3239    #[test]
3240    fn test_zccmd_scroll_on_off() {
3241        let _g = crate::test_util::global_state_lock();
3242        let _g = reset();
3243        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3244        let add: Vec<String> = vec![
3245            "addwin".into(),
3246            "sw".into(),
3247            "5".into(),
3248            "10".into(),
3249            "0".into(),
3250            "0".into(),
3251        ];
3252        bin_zcurses("zcurses", &add, &_test_ops_, 0);
3253        let on: Vec<String> = vec!["scroll".into(), "sw".into(), "on".into()];
3254        assert_eq!(bin_zcurses("zcurses", &on, &_test_ops_, 0), 0);
3255        assert_eq!(
3256            windows_lock().lock().unwrap().get("sw").unwrap().flags & ZCWF_SCROLL,
3257            ZCWF_SCROLL
3258        );
3259        let off: Vec<String> = vec!["scroll".into(), "sw".into(), "off".into()];
3260        assert_eq!(bin_zcurses("zcurses", &off, &_test_ops_, 0), 0);
3261        assert_eq!(
3262            windows_lock().lock().unwrap().get("sw").unwrap().flags & ZCWF_SCROLL,
3263            0
3264        );
3265    }
3266
3267    /// Port of `zccmd_resize(const char *nam, char **args)` from `Src/Modules/curses.c:1494`.
3268    #[test]
3269    fn test_zccmd_scroll_integer_advances_buffer() {
3270        let _g = crate::test_util::global_state_lock();
3271        let _g = reset();
3272        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3273        let add: Vec<String> = vec![
3274            "addwin".into(),
3275            "sw".into(),
3276            "3".into(),
3277            "5".into(),
3278            "0".into(),
3279            "0".into(),
3280        ];
3281        bin_zcurses("zcurses", &add, &_test_ops_, 0);
3282        // Pre-fill row 0 with 'A' to detect the shift.
3283        windows_lock().lock().unwrap().get_mut("sw").unwrap().buffer[0][0] = 'A';
3284        let scr: Vec<String> = vec!["scroll".into(), "sw".into(), "1".into()];
3285        assert_eq!(bin_zcurses("zcurses", &scr, &_test_ops_, 0), 0);
3286        // Row 0 'A' shifted out — bottom row is fresh spaces.
3287        let wins = windows_lock().lock().unwrap();
3288        assert_eq!(wins.get("sw").unwrap().buffer[0][0], ' ');
3289    }
3290
3291    /// Port of `bin_zcurses(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/curses.c:1568`.
3292    #[test]
3293    fn test_zccmd_timeout_stores_value() {
3294        let _g = crate::test_util::global_state_lock();
3295        let _g = reset();
3296        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3297        let add: Vec<String> = vec![
3298            "addwin".into(),
3299            "tw".into(),
3300            "5".into(),
3301            "10".into(),
3302            "0".into(),
3303            "0".into(),
3304        ];
3305        bin_zcurses("zcurses", &add, &_test_ops_, 0);
3306        let to: Vec<String> = vec!["timeout".into(), "tw".into(), "100".into()];
3307        assert_eq!(bin_zcurses("zcurses", &to, &_test_ops_, 0), 0);
3308        assert_eq!(
3309            windows_lock().lock().unwrap().get("tw").unwrap().timeout_ms,
3310            100
3311        );
3312    }
3313
3314    /// Port of `bin_zcurses(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/curses.c:1568`.
3315    #[test]
3316    fn test_zccmd_touch_validates_each() {
3317        let _g = crate::test_util::global_state_lock();
3318        let _g = reset();
3319        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3320        let touch_ok: Vec<String> = vec!["touch".into(), "stdscr".into()];
3321        assert_eq!(bin_zcurses("zcurses", &touch_ok, &_test_ops_, 0), 0);
3322        let touch_bad: Vec<String> = vec!["touch".into(), "ghost".into()];
3323        assert_eq!(bin_zcurses("zcurses", &touch_bad, &_test_ops_, 0), 1);
3324        assert_eq!(zc_errno_get(), ZCURSES_EUNDEFINED);
3325    }
3326
3327    /// Port of `bin_zcurses(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/curses.c:1568`.
3328    #[test]
3329    fn test_zccmd_resize_changes_stdscr() {
3330        let _g = crate::test_util::global_state_lock();
3331        let _g = reset();
3332        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3333        let rs: Vec<String> = vec!["resize".into(), "30".into(), "100".into()];
3334        assert_eq!(bin_zcurses("zcurses", &rs, &_test_ops_, 0), 0);
3335        let wins = windows_lock().lock().unwrap();
3336        let stdscr = wins.get("stdscr").unwrap();
3337        assert_eq!(stdscr.rows, 30);
3338        assert_eq!(stdscr.cols, 100);
3339        assert_eq!(stdscr.buffer.len(), 30);
3340        assert_eq!(stdscr.buffer[0].len(), 100);
3341    }
3342
3343    #[test]
3344    fn test_zccmd_resize_bad_third_arg() {
3345        let _g = crate::test_util::global_state_lock();
3346        let _g = reset();
3347        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3348        let rs: Vec<String> = vec!["resize".into(), "30".into(), "100".into(), "junk".into()];
3349        assert_eq!(bin_zcurses("zcurses", &rs, &_test_ops_, 0), 1);
3350    }
3351
3352    /// Port of `zccmd_input(const char *nam, char **args)` from `Src/Modules/curses.c:1029`.
3353    #[test]
3354    fn test_zccmd_mouse_motion_toggle() {
3355        let _g = crate::test_util::global_state_lock();
3356        let _g = reset();
3357        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3358        // Default mask should not have REPORT_MOUSE_POSITION.
3359        let mouse_on: Vec<String> = vec!["mouse".into(), "+motion".into()];
3360        assert_eq!(bin_zcurses("zcurses", &mouse_on, &_test_ops_, 0), 0);
3361        assert_ne!(
3362            *mouse_mask_lock().lock().unwrap() & REPORT_MOUSE_POSITION,
3363            0
3364        );
3365        let mouse_off: Vec<String> = vec!["mouse".into(), "-motion".into()];
3366        assert_eq!(bin_zcurses("zcurses", &mouse_off, &_test_ops_, 0), 0);
3367        assert_eq!(
3368            *mouse_mask_lock().lock().unwrap() & REPORT_MOUSE_POSITION,
3369            0
3370        );
3371    }
3372
3373    #[test]
3374    fn test_zccmd_bg_with_color_pair() {
3375        let _g = crate::test_util::global_state_lock();
3376        let _g = reset();
3377        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3378        let add: Vec<String> = vec![
3379            "addwin".into(),
3380            "bgw".into(),
3381            "5".into(),
3382            "10".into(),
3383            "0".into(),
3384            "0".into(),
3385        ];
3386        bin_zcurses("zcurses", &add, &_test_ops_, 0);
3387        let bg: Vec<String> = vec!["bg".into(), "bgw".into(), "red/black".into()];
3388        assert_eq!(bin_zcurses("zcurses", &bg, &_test_ops_, 0), 0);
3389        // Pair was allocated; bg_chtype encodes the pair in the high bits.
3390        assert_ne!(
3391            windows_lock().lock().unwrap().get("bgw").unwrap().bg_chtype,
3392            0
3393        );
3394    }
3395
3396    /// Port of `zccmd_delwin(const char *nam, char **args)` from `Src/Modules/curses.c:564`.
3397    #[test]
3398    fn test_addwin_with_parent() {
3399        let _g = crate::test_util::global_state_lock();
3400        let _g = reset();
3401        bin_zcurses("zcurses", &["init".into()], &_test_ops_, 0);
3402        let parent_args: Vec<String> = vec![
3403            "addwin".into(),
3404            "parent".into(),
3405            "10".into(),
3406            "20".into(),
3407            "0".into(),
3408            "0".into(),
3409        ];
3410        assert_eq!(bin_zcurses("zcurses", &parent_args, &_test_ops_, 0), 0);
3411        let child_args: Vec<String> = vec![
3412            "addwin".into(),
3413            "child".into(),
3414            "5".into(),
3415            "10".into(),
3416            "1".into(),
3417            "1".into(),
3418            "parent".into(),
3419        ];
3420        assert_eq!(bin_zcurses("zcurses", &child_args, &_test_ops_, 0), 0);
3421        let wins = windows_lock().lock().unwrap();
3422        assert_eq!(wins.get("child").unwrap().parent.as_deref(), Some("parent"));
3423        assert_eq!(
3424            wins.get("parent").unwrap().children,
3425            vec!["child".to_string()]
3426        );
3427    }
3428
3429    // ─── zsh-corpus pins for zcurses_strerror ──────────────────────
3430
3431    /// `zcurses_strerror(0)` returns "unknown error" (out-of-range maps to idx 0).
3432    #[test]
3433    fn curses_corpus_strerror_zero_unknown() {
3434        let _g = crate::test_util::global_state_lock();
3435        assert_eq!(zcurses_strerror(0), "unknown error");
3436    }
3437
3438    /// `zcurses_strerror(1)` returns "window name invalid".
3439    #[test]
3440    fn curses_corpus_strerror_one_invalid_name() {
3441        let _g = crate::test_util::global_state_lock();
3442        assert_eq!(zcurses_strerror(1), "window name invalid");
3443    }
3444
3445    /// `zcurses_strerror(2)` returns "window already defined".
3446    #[test]
3447    fn curses_corpus_strerror_two_already_defined() {
3448        let _g = crate::test_util::global_state_lock();
3449        assert_eq!(zcurses_strerror(2), "window already defined");
3450    }
3451
3452    /// `zcurses_strerror(3)` returns "window undefined".
3453    #[test]
3454    fn curses_corpus_strerror_three_undefined() {
3455        let _g = crate::test_util::global_state_lock();
3456        assert_eq!(zcurses_strerror(3), "window undefined");
3457    }
3458
3459    /// `zcurses_strerror(99)` clamps to "unknown error" (out of range).
3460    #[test]
3461    fn curses_corpus_strerror_out_of_range_clamps_to_unknown() {
3462        let _g = crate::test_util::global_state_lock();
3463        assert_eq!(zcurses_strerror(99), "unknown error");
3464        assert_eq!(zcurses_strerror(-1), "unknown error");
3465        assert_eq!(zcurses_strerror(i32::MAX), "unknown error");
3466    }
3467
3468    /// `zcurses_pairs_to_array` returns names from a zcurses_namenumberpair slice.
3469    #[test]
3470    fn curses_corpus_pairs_to_array_extracts_names() {
3471        let pairs = [
3472            zcurses_namenumberpair {
3473                name: "alpha",
3474                number: 1,
3475            },
3476            zcurses_namenumberpair {
3477                name: "beta",
3478                number: 2,
3479            },
3480            zcurses_namenumberpair {
3481                name: "gamma",
3482                number: 3,
3483            },
3484        ];
3485        let v = zcurses_pairs_to_array(&pairs);
3486        assert_eq!(v, vec!["alpha", "beta", "gamma"]);
3487    }
3488
3489    /// `zcurses_pairs_to_array` on empty input returns empty Vec.
3490    #[test]
3491    fn curses_corpus_pairs_to_array_empty_input() {
3492        let v = zcurses_pairs_to_array(&[]);
3493        assert!(v.is_empty());
3494    }
3495
3496    // ═══════════════════════════════════════════════════════════════════
3497    // Additional C-parity tests for Src/Modules/curses.c.
3498    // ═══════════════════════════════════════════════════════════════════
3499
3500    /// c:233 — `zcurses_strerror(0)` returns 'unknown error'.
3501    #[test]
3502    fn zcurses_strerror_zero_returns_unknown() {
3503        assert_eq!(zcurses_strerror(0), "unknown error");
3504    }
3505
3506    /// c:233 — `zcurses_strerror(1)` returns 'window name invalid'.
3507    #[test]
3508    fn zcurses_strerror_one_returns_name_invalid() {
3509        assert_eq!(zcurses_strerror(1), "window name invalid");
3510    }
3511
3512    /// c:233 — `zcurses_strerror(2)` returns 'window already defined'.
3513    #[test]
3514    fn zcurses_strerror_two_returns_already_defined() {
3515        assert_eq!(zcurses_strerror(2), "window already defined");
3516    }
3517
3518    /// c:233 — `zcurses_strerror(3)` returns 'window undefined'.
3519    #[test]
3520    fn zcurses_strerror_three_returns_undefined() {
3521        assert_eq!(zcurses_strerror(3), "window undefined");
3522    }
3523
3524    /// c:233 — `zcurses_strerror` out-of-range returns 'unknown error'.
3525    #[test]
3526    fn zcurses_strerror_out_of_range_returns_unknown() {
3527        assert_eq!(zcurses_strerror(-1), "unknown error");
3528        assert_eq!(zcurses_strerror(4), "unknown error");
3529        assert_eq!(zcurses_strerror(999), "unknown error");
3530    }
3531
3532    /// c:233 — `zcurses_strerror` is deterministic for known errnos.
3533    #[test]
3534    fn zcurses_strerror_is_deterministic() {
3535        for e in 0..=3 {
3536            let first = zcurses_strerror(e);
3537            for _ in 0..5 {
3538                assert_eq!(zcurses_strerror(e), first);
3539            }
3540        }
3541    }
3542
3543    /// c:1420 — `zcurses_colorsarrgetfn` returns vec (color names).
3544    #[test]
3545    fn zcurses_colorsarrgetfn_returns_vec() {
3546        let _g = crate::test_util::global_state_lock();
3547        let v = zcurses_colorsarrgetfn();
3548        // Either empty (no curses init) or has color names — pin no panic.
3549        let _ = v;
3550    }
3551
3552    /// c:1427 — `zcurses_attrgetfn` returns vec (attribute names).
3553    #[test]
3554    fn zcurses_attrgetfn_returns_vec() {
3555        let _g = crate::test_util::global_state_lock();
3556        let v = zcurses_attrgetfn();
3557        let _ = v;
3558    }
3559
3560    /// c:1436 — `zcurses_keycodesgetfn` returns vec.
3561    #[test]
3562    fn zcurses_keycodesgetfn_returns_vec() {
3563        let _g = crate::test_util::global_state_lock();
3564        let v = zcurses_keycodesgetfn();
3565        let _ = v;
3566    }
3567
3568    /// c:1453 — `zcurses_windowsgetfn` returns vec.
3569    #[test]
3570    fn zcurses_windowsgetfn_returns_vec() {
3571        let _g = crate::test_util::global_state_lock();
3572        let v = zcurses_windowsgetfn();
3573        let _ = v;
3574    }
3575
3576    /// c:1461 — `zcurses_colorsintgetfn` returns non-negative.
3577    #[test]
3578    fn zcurses_colorsintgetfn_returns_non_negative() {
3579        let _g = crate::test_util::global_state_lock();
3580        let n = zcurses_colorsintgetfn();
3581        assert!(n >= 0, "color count must be ≥ 0, got {}", n);
3582    }
3583
3584    /// c:1475 — `zcurses_colorpairsintgetfn` returns non-negative.
3585    #[test]
3586    fn zcurses_colorpairsintgetfn_returns_non_negative() {
3587        let _g = crate::test_util::global_state_lock();
3588        let n = zcurses_colorpairsintgetfn();
3589        assert!(n >= 0, "color pair count must be ≥ 0, got {}", n);
3590    }
3591
3592    /// Lifecycle (c:1493/1513/1519/1526) split per-hook.
3593    #[test]
3594    fn curses_setup_returns_zero_pin() {
3595        let _g = crate::test_util::global_state_lock();
3596        assert_eq!(setup_(std::ptr::null()), 0);
3597    }
3598
3599    /// c:1513 — boot_(NULL) = 0.
3600    #[test]
3601    fn curses_boot_returns_zero_pin() {
3602        let _g = crate::test_util::global_state_lock();
3603        assert_eq!(boot_(std::ptr::null()), 0);
3604    }
3605
3606    // ═══════════════════════════════════════════════════════════════════
3607    // Additional C-parity tests for Src/Modules/curses.c
3608    // c:293 zcurses_pairs_to_array / c:437 zcurses_colorget /
3609    // c:525 zcurses_colorget_reverse / c:544 freecolorpairnode /
3610    // c:1420/1427/1436/1453 *getfn determinism + lifecycle types
3611    // ═══════════════════════════════════════════════════════════════════
3612
3613    /// c:293 — `zcurses_pairs_to_array(empty)` returns empty Vec.
3614    #[test]
3615    fn zcurses_pairs_to_array_empty_returns_empty() {
3616        let r = zcurses_pairs_to_array(&[]);
3617        assert!(r.is_empty(), "empty input → empty Vec");
3618    }
3619
3620    /// c:293 — `zcurses_pairs_to_array` returns Vec<String> (type pin).
3621    #[test]
3622    fn zcurses_pairs_to_array_returns_vec_string_type() {
3623        let _: Vec<String> = zcurses_pairs_to_array(&[]);
3624    }
3625
3626    /// c:437 — `zcurses_colorget("", "")` returns Option<i16>.
3627    #[test]
3628    fn zcurses_colorget_returns_option_i16_type() {
3629        let _g = crate::test_util::global_state_lock();
3630        let _: Option<i16> = zcurses_colorget("", "");
3631    }
3632
3633    /// c:525 — `zcurses_colorget_reverse(N)` returns Option<String>.
3634    #[test]
3635    fn zcurses_colorget_reverse_returns_option_string_type() {
3636        let _g = crate::test_util::global_state_lock();
3637        let _: Option<String> = zcurses_colorget_reverse(0);
3638    }
3639
3640    /// c:525 — `zcurses_colorget_reverse(invalid)` returns None
3641    /// (i16::MAX is never a valid color pair index).
3642    #[test]
3643    fn zcurses_colorget_reverse_invalid_returns_none() {
3644        let _g = crate::test_util::global_state_lock();
3645        assert!(
3646            zcurses_colorget_reverse(i16::MAX).is_none(),
3647            "invalid color pair → None"
3648        );
3649    }
3650
3651    /// c:544 — `freecolorpairnode("")` empty name is safe.
3652    #[test]
3653    fn freecolorpairnode_empty_name_safe() {
3654        let _g = crate::test_util::global_state_lock();
3655        freecolorpairnode("");
3656    }
3657
3658    /// c:1420 — `zcurses_colorsarrgetfn` is deterministic.
3659    #[test]
3660    fn zcurses_colorsarrgetfn_is_deterministic() {
3661        let _g = crate::test_util::global_state_lock();
3662        let first = zcurses_colorsarrgetfn();
3663        for _ in 0..3 {
3664            assert_eq!(
3665                zcurses_colorsarrgetfn(),
3666                first,
3667                "zcurses_colorsarrgetfn must be deterministic"
3668            );
3669        }
3670    }
3671
3672    /// c:1427 — `zcurses_attrgetfn` is deterministic.
3673    #[test]
3674    fn zcurses_attrgetfn_is_deterministic() {
3675        let _g = crate::test_util::global_state_lock();
3676        let first = zcurses_attrgetfn();
3677        for _ in 0..3 {
3678            assert_eq!(
3679                zcurses_attrgetfn(),
3680                first,
3681                "zcurses_attrgetfn must be deterministic"
3682            );
3683        }
3684    }
3685
3686    /// c:1436 — `zcurses_keycodesgetfn` is deterministic.
3687    #[test]
3688    fn zcurses_keycodesgetfn_is_deterministic() {
3689        let _g = crate::test_util::global_state_lock();
3690        let first = zcurses_keycodesgetfn();
3691        for _ in 0..3 {
3692            assert_eq!(
3693                zcurses_keycodesgetfn(),
3694                first,
3695                "zcurses_keycodesgetfn must be deterministic"
3696            );
3697        }
3698    }
3699
3700    /// c:1461 — `zcurses_colorsintgetfn` returns i64 (type pin).
3701    #[test]
3702    fn zcurses_colorsintgetfn_returns_i64_type() {
3703        let _g = crate::test_util::global_state_lock();
3704        let _: i64 = zcurses_colorsintgetfn();
3705    }
3706
3707    /// c:1475 — `zcurses_colorpairsintgetfn` returns i64 (type pin).
3708    #[test]
3709    fn zcurses_colorpairsintgetfn_returns_i64_type() {
3710        let _g = crate::test_util::global_state_lock();
3711        let _: i64 = zcurses_colorpairsintgetfn();
3712    }
3713
3714    /// c:1493 — `setup_` returns i32 (compile-time type pin).
3715    #[test]
3716    fn curses_setup_returns_i32_type() {
3717        let _g = crate::test_util::global_state_lock();
3718        let _: i32 = setup_(std::ptr::null());
3719    }
3720
3721    /// c:1499 — features list non-empty.
3722    #[test]
3723    fn curses_features_nonempty() {
3724        let _g = crate::test_util::global_state_lock();
3725        let mut feats = Vec::new();
3726        features_(std::ptr::null(), &mut feats);
3727        assert!(!feats.is_empty(), "curses must advertise ≥1 feature");
3728    }
3729
3730    /// c:1519 + c:1526 — cleanup/finish idempotent.
3731    #[test]
3732    fn curses_cleanup_finish_idempotent() {
3733        let _g = crate::test_util::global_state_lock();
3734        for _ in 0..5 {
3735            assert_eq!(cleanup_(std::ptr::null()), 0);
3736            assert_eq!(finish_(std::ptr::null()), 0);
3737        }
3738    }
3739
3740    // ═══════════════════════════════════════════════════════════════════
3741    // Additional C-parity tests for Src/Modules/curses.c
3742    // c:293 zcurses_pairs_to_array / c:307 zcurses_strerror /
3743    // c:332 zcurses_getwindowbyname / c:379 zcurses_free_window /
3744    // c:418 zcurses_color / c:437 zcurses_colorget
3745    // ═══════════════════════════════════════════════════════════════════
3746
3747    /// c:293 — `zcurses_pairs_to_array(&[])` returns empty Vec (alt).
3748    #[test]
3749    fn zcurses_pairs_to_array_empty_returns_empty_alt() {
3750        let v = zcurses_pairs_to_array(&[]);
3751        assert!(v.is_empty(), "empty input → empty Vec");
3752    }
3753
3754    /// c:293 — `zcurses_pairs_to_array` returns Vec<String> (alt pin).
3755    #[test]
3756    fn zcurses_pairs_to_array_returns_vec_string_pin_alt() {
3757        let _: Vec<String> = zcurses_pairs_to_array(&[]);
3758    }
3759
3760    /// c:307 — `zcurses_strerror(0)` returns non-empty static str.
3761    #[test]
3762    fn zcurses_strerror_zero_returns_non_empty() {
3763        let s = zcurses_strerror(0);
3764        assert!(!s.is_empty(), "zcurses_strerror(0) must be non-empty");
3765    }
3766
3767    /// c:307 — `zcurses_strerror` returns `&'static str` (compile-time pin).
3768    #[test]
3769    fn zcurses_strerror_returns_static_str_type() {
3770        let _: &'static str = zcurses_strerror(-1);
3771    }
3772
3773    /// c:307 — `zcurses_strerror` deterministic for any input.
3774    #[test]
3775    fn zcurses_strerror_deterministic() {
3776        for err in [0, -1, 1, 42, i32::MIN, i32::MAX] {
3777            let a = zcurses_strerror(err);
3778            let b = zcurses_strerror(err);
3779            assert_eq!(a, b, "zcurses_strerror({}) must be pure", err);
3780        }
3781    }
3782
3783    /// c:332 — `zcurses_getwindowbyname("")` returns bool false (empty name).
3784    #[test]
3785    fn zcurses_getwindowbyname_empty_returns_false() {
3786        let _g = crate::test_util::global_state_lock();
3787        assert!(!zcurses_getwindowbyname(""), "empty name has no window");
3788    }
3789
3790    /// c:332 — `zcurses_getwindowbyname` returns bool (compile-time pin).
3791    #[test]
3792    fn zcurses_getwindowbyname_returns_bool_type() {
3793        let _g = crate::test_util::global_state_lock();
3794        let _: bool = zcurses_getwindowbyname("__never_window__");
3795    }
3796
3797    /// c:379 — `zcurses_free_window` returns i32 (compile-time pin).
3798    #[test]
3799    fn zcurses_free_window_returns_i32_type() {
3800        let _g = crate::test_util::global_state_lock();
3801        let _: i32 = zcurses_free_window("__never_window__");
3802    }
3803
3804    /// c:418 — `zcurses_color("")` returns i32 sentinel for unknown.
3805    #[test]
3806    fn zcurses_color_empty_string_returns_sentinel() {
3807        let _g = crate::test_util::global_state_lock();
3808        let r = zcurses_color("");
3809        assert!(
3810            r < 0,
3811            "empty color name must return negative sentinel; got {}",
3812            r
3813        );
3814    }
3815
3816    /// c:418 — `zcurses_color` is deterministic.
3817    #[test]
3818    fn zcurses_color_deterministic_for_unknown() {
3819        let _g = crate::test_util::global_state_lock();
3820        for name in ["__bogus_color__", "", "xyz"] {
3821            let first = zcurses_color(name);
3822            for _ in 0..3 {
3823                assert_eq!(
3824                    zcurses_color(name),
3825                    first,
3826                    "zcurses_color({:?}) must be pure",
3827                    name
3828                );
3829            }
3830        }
3831    }
3832
3833    /// c:437 — `zcurses_colorget` returns Option<i16> (compile-time pin, alt).
3834    #[test]
3835    fn zcurses_colorget_returns_option_i16_pin_alt() {
3836        let _g = crate::test_util::global_state_lock();
3837        let _: Option<i16> = zcurses_colorget("__never__", "red/black");
3838    }
3839}