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(¶ms);
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}