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