Skip to main content

zsh/ported/zle/
zle_refresh.rs

1//! ZLE refresh - screen redraw routines
2//!
3//! Direct port from zsh/Src/Zle/zle_refresh.c
4
5use super::zle_h::{REFRESH_ELEMENT, REFRESH_STRING};
6use crate::ported::init::{tclen, SHTTY};
7use crate::ported::utils::{adjustcolumns, adjustlines, write_loop};
8#[allow(unused_imports)]
9use crate::ported::zle::{
10    deltochar::*, textobjects::*, zle_hist::*, zle_main::*, zle_misc::*, zle_move::*,
11    zle_params::*, zle_tricky::*, zle_utils::*, zle_vi::*, zle_word::*,
12};
13use crate::ported::zsh_h::{
14    isset, COMBININGCHARS, TCCLEAREOL, TCDEL, TCINS, TXT_ERROR, TXT_MULTIWORD_MASK,
15};
16use std::fmt::Write;
17use std::io;
18use std::sync::atomic::Ordering;
19
20/// Port of `ZR_memset(REFRESH_ELEMENT *dst, REFRESH_ELEMENT rc, int len)` from `Src/Zle/zle_refresh.c:86`.
21/// ```c
22/// static void
23/// ZR_memset(REFRESH_ELEMENT *dst, REFRESH_ELEMENT rc, int len)
24/// {
25///     while (len--)
26///         *dst++ = rc;
27/// }
28/// ```
29/// Fill `dst[0..len]` with copies of `rc`. Equivalent to
30/// `memset` for REFRESH_ELEMENT slices.
31#[allow(non_snake_case)]
32/// WARNING: param names don't match C — Rust=(rc, len) vs C=(dst, rc, len)
33pub fn ZR_memset(
34    // c:86
35    dst: &mut [REFRESH_ELEMENT],
36    rc: REFRESH_ELEMENT,
37    len: usize,
38) {
39    let n = len.min(dst.len());
40    for slot in dst.iter_mut().take(n) {
41        // c:88-89 while (len--) *dst++ = rc
42        *slot = rc;
43    }
44}
45
46impl TextAttr {
47    /// Render this attribute set as the corresponding ANSI SGR
48    /// escape. Loose equivalent of `tsetcap()` from
49    /// Src/Zle/zle_refresh.c (which emits termcap-derived sequences
50    /// from per-cell attr changes during the diff/paint cycle).
51    pub fn to_ansi(&self) -> String {
52        let mut codes = Vec::new();
53        if self.bold {
54            codes.push("1".to_string());
55        }
56        if self.underline {
57            codes.push("4".to_string());
58        }
59        if self.standout {
60            codes.push("7".to_string());
61        }
62        if self.blink {
63            codes.push("5".to_string());
64        }
65        if let Some(fg) = self.fg_color {
66            codes.push(format!("38;5;{}", fg));
67        }
68        if let Some(bg) = self.bg_color {
69            codes.push(format!("48;5;{}", bg));
70        }
71        if codes.is_empty() {
72            String::new()
73        } else {
74            format!("\x1b[{}m", codes.join(";"))
75        }
76    }
77}
78
79/// Sentinel for C's `WEOF` in a cell's `chr`. C's `REFRESH_CHAR` is a
80/// `wchar_t`/`wint_t`, so `WEOF` ((wint_t)-1) is a distinct value from the
81/// NUL terminator `ZWC('\0')`. Rust's `chr` is a `char`, which cannot hold
82/// `(wint_t)-1`, so we use the Unicode noncharacter `U+FFFF` — permanently
83/// reserved, never valid in interchange — as the WEOF marker. WEOF cells are
84/// the trailing column placeholders of a wide (CJK) glyph: counted by
85/// `ZR_strlen` (they occupy columns) but NOT emitted by `zwcputc` (the leading
86/// cell already drew the full-width glyph). Using `'\0'` for this — as the
87/// earlier port did — truncated `ZR_strlen`/`ZR_strcpy` at the first wide
88/// char, dropping the rest of the line.
89pub const ZWC_WEOF: char = '\u{FFFF}'; // c: WEOF == (wint_t)-1
90
91/// Port of `ZR_strcpy(REFRESH_ELEMENT *dst, const REFRESH_ELEMENT *src)` from `Src/Zle/zle_refresh.c:95`.
92/// ```c
93/// static void
94/// ZR_strcpy(REFRESH_ELEMENT *dst, const REFRESH_ELEMENT *src)
95/// {
96///     while ((*dst++ = *src++).chr != ZWC('\0'))
97///         ;
98/// }
99/// ```
100/// Copy a NUL-terminated REFRESH_ELEMENT string from `src` to
101/// `dst`. The terminator is INCLUDED in the copy.
102/// C body (Src/Zle/zle_refresh.c:95) is a single while-loop:
103///     `while ((*dst++ = *src++).chr != ZWC('\\0'));`
104/// Rust uses a take-up-to-NUL iterator chain to express the same
105/// pointer-walking copy as a single statement.
106#[allow(non_snake_case)]
107/// WARNING: param names don't match C — Rust=(src) vs C=(dst, src)
108pub fn ZR_strcpy(
109    // c:95
110    dst: &mut [REFRESH_ELEMENT],
111    src: &[REFRESH_ELEMENT],
112) {
113    let n = src.iter().take_while(|e| e.chr != '\0').count() + 1; // c:97 incl trailing NUL
114    let n = n.min(src.len()).min(dst.len());
115    dst[..n].copy_from_slice(&src[..n]);
116}
117
118impl RefreshElement {
119    /// Construct a refresh cell holding a single character with
120    /// default attributes. Equivalent shape to a freshly-zeroed
121    /// `REFRESH_ELEMENT` from Src/Zle/zle_refresh.h.
122    pub fn new(chr: char) -> Self {
123        let width = unicode_width::UnicodeWidthChar::width(chr).unwrap_or(1) as u8;
124        RefreshElement {
125            chr,
126            atr: TextAttr::default(),
127            width,
128        }
129    }
130
131    /// Construct a refresh cell with explicit text attributes.
132    /// Used by callers painting attributed regions (visual-mode
133    /// standout, isearch underline, etc.) directly into a
134    /// `VideoBuffer`.
135    pub fn with_attr(chr: char, atr: TextAttr) -> Self {
136        let width = unicode_width::UnicodeWidthChar::width(chr).unwrap_or(1) as u8;
137        RefreshElement { chr, atr, width }
138    }
139}
140
141/// Port of `ZR_strlen(const REFRESH_ELEMENT *wstr)` from `Src/Zle/zle_refresh.c:102`.
142/// ```c
143/// static size_t
144/// ZR_strlen(const REFRESH_ELEMENT *wstr)
145/// {
146///     int len = 0;
147///     while (wstr++->chr != ZWC('\0'))
148///         len++;
149///     return len;
150/// }
151/// ```
152/// Length of a NUL-terminated REFRESH_ELEMENT string.
153#[allow(non_snake_case)]
154/// Port of `ZR_strlen(const REFRESH_ELEMENT *wstr)` from `Src/Zle/zle_refresh.c:102`.
155pub fn ZR_strlen(wstr: &[REFRESH_ELEMENT]) -> usize {
156    // c:102
157    let mut len = 0; // c:102 int len = 0
158    while len < wstr.len() && wstr[len].chr != '\0' {
159        // c:106 while (wstr++->chr != ZWC('\0'))
160        len += 1; // c:107 len++
161    }
162    len // c:109 return len
163}
164
165impl VideoBuffer {
166    /// Allocate a fresh video buffer of `cols × rows` filled with
167    /// blank cells. Equivalent to `resetvideo()` at
168    /// Src/Zle/zle_refresh.c:725 which allocates `nlnct * winw`
169    /// cells for `nbuf` each refresh.
170    pub fn new(cols: usize, rows: usize) -> Self {
171        let lines = vec![vec![RefreshElement::new(' '); cols]; rows];
172        VideoBuffer { lines, cols, rows }
173    }
174
175    /// Reset every cell to a blank-attribute space. Used by
176    /// `zrefresh()` between frames to wipe the working buffer
177    /// before the new paint pass — see `freevideo()` at
178    /// zle_refresh.c:700 for the equivalent role.
179    pub fn clear(&mut self) {
180        for line in &mut self.lines {
181            for elem in line.iter_mut() {
182                *elem = RefreshElement::new(' ');
183            }
184        }
185    }
186
187    /// Reshape the buffer for a new terminal size. Equivalent to
188    /// the cols/lines update + `nbuf`/`obuf` reallocation chain in
189    /// zle_refresh.c that fires on SIGWINCH (see the `winw`/`winh`
190    /// re-read in `zrefresh()` at zle_refresh.c:975).
191    pub fn resize(&mut self, cols: usize, rows: usize) {
192        self.cols = cols;
193        self.rows = rows;
194        self.lines
195            .resize(rows, vec![RefreshElement::new(' '); cols]);
196        for line in &mut self.lines {
197            line.resize(cols, RefreshElement::new(' '));
198        }
199    }
200
201    /// Write a single cell into the buffer; out-of-range writes are
202    /// silently dropped (matches the C source's bounds check before
203    /// `nbuf[row][col] = ...` in zle_refresh.c).
204    pub fn set(&mut self, row: usize, col: usize, elem: RefreshElement) {
205        if row < self.rows && col < self.cols {
206            self.lines[row][col] = elem;
207        }
208    }
209
210    /// Read a single cell. Returns None for out-of-range coords —
211    /// the C source's index path is unchecked (uses `winw`/`nlnct`
212    /// invariants).
213    pub fn get(&self, row: usize, col: usize) -> Option<&RefreshElement> {
214        self.lines.get(row).and_then(|line| line.get(col))
215    }
216}
217
218/// Port of `ZR_strncmp(const REFRESH_ELEMENT *oldwstr, const REFRESH_ELEMENT *newwstr, int len)` from `Src/Zle/zle_refresh.c:119`.
219/// ```c
220/// static int
221/// ZR_strncmp(const REFRESH_ELEMENT *oldwstr, const REFRESH_ELEMENT *newwstr,
222///            int len)
223/// {
224///     while (len--) {
225///         if ((!(oldwstr->atr & TXT_MULTIWORD_MASK) && !oldwstr->chr) ||
226///             (!(newwstr->atr & TXT_MULTIWORD_MASK) && !newwstr->chr))
227///             return !ZR_equal(*oldwstr, *newwstr);
228///         if (!ZR_equal(*oldwstr, *newwstr))
229///             return 1;
230///         oldwstr++;
231///         newwstr++;
232///     }
233///     return 0;
234/// }
235/// ```
236/// Simplified strcmp: returns 0 if first `len` elements match
237/// (chr+atr pair-equal), 1 otherwise. Stops early at NUL in
238/// either string (treating it as the shorter-string boundary).
239#[allow(non_snake_case)]
240/// Port of `ZR_strncmp(const REFRESH_ELEMENT *oldwstr, const REFRESH_ELEMENT *newwstr, int len)` from `Src/Zle/zle_refresh.c:120`.
241/// WARNING: param names don't match C — Rust=(newwstr, len) vs C=(oldwstr, newwstr, len)
242pub fn ZR_strncmp(
243    // c:120
244    oldwstr: &[REFRESH_ELEMENT],
245    newwstr: &[REFRESH_ELEMENT],
246    len: usize,
247) -> i32 {
248    let mut i = 0;
249    while i < len {
250        // c:123 while (len--)
251        if i >= oldwstr.len() || i >= newwstr.len() {
252            // C reads past end via pointer; we bound it.
253            return if oldwstr.get(i) == newwstr.get(i) {
254                0
255            } else {
256                1
257            };
258        }
259        let o = oldwstr[i];
260        let n = newwstr[i];
261        // c:124-126 — `if early-NUL → return !equal`.
262        let old_is_nul = (o.atr & TXT_MULTIWORD_MASK) == 0 && o.chr == '\0';
263        let new_is_nul = (n.atr & TXT_MULTIWORD_MASK) == 0 && n.chr == '\0';
264        if old_is_nul || new_is_nul {
265            return if o == n { 0 } else { 1 }; // c:126 !ZR_equal
266        }
267        if o != n {
268            // c:127 if (!ZR_equal(...)) return 1
269            return 1;
270        }
271        i += 1; // c:129-130 oldwstr++; newwstr++
272    }
273    0 // c:133 return 0
274}
275
276impl RefreshState {
277    /// Build the initial refresh state at zleread() entry.
278    /// Equivalent to the global `nbuf`/`obuf`/`vln`/`vcs`
279    /// allocation + reset performed by `resetvideo()` at
280    /// Src/Zle/zle_refresh.c:725 — terminal size queried once,
281    /// both video buffers allocated, `need_full_redraw` set so the
282    /// first paint touches every cell.
283    pub fn new() -> Self {
284        let (cols, rows) = (adjustcolumns(), adjustlines());
285        RefreshState {
286            columns: cols,
287            lines: rows,
288            old_video: Some(VideoBuffer::new(cols, rows)),
289            new_video: Some(VideoBuffer::new(cols, rows)),
290            need_full_redraw: true,
291            ..Default::default()
292        }
293    }
294
295    /// Reallocate the video buffers for the current terminal size
296    /// and arm a full redraw on the next paint. Equivalent to
297    /// `resetvideo()` from Src/Zle/zle_refresh.c:725 invoked after
298    /// SIGWINCH (the C source calls it from `adjustwinsize()` in
299    /// Src/init.c).
300    pub fn reset_video(&mut self) {
301        let (cols, rows) = (adjustcolumns(), adjustlines());
302        self.columns = cols;
303        self.lines = rows;
304        self.old_video = Some(VideoBuffer::new(cols, rows));
305        self.new_video = Some(VideoBuffer::new(cols, rows));
306        self.need_full_redraw = true;
307    }
308
309    /// Drop both video buffers — used at ZLE shutdown. Equivalent
310    /// to `freevideo()` from Src/Zle/zle_refresh.c:700.
311    pub fn free_video(&mut self) {
312        self.old_video = None;
313        self.new_video = None;
314    }
315
316    /// Promote the freshly-painted buffer to "previously displayed"
317    /// and clear the new-buffer slate for the next frame.
318    /// Equivalent to `bufswap()` from Src/Zle/zle_refresh.c:946 —
319    /// the C source swaps `nbuf` and `obuf` pointers and zeroes the
320    /// new `nbuf` so the diff loop has a clean target.
321    pub fn swap_buffers(&mut self) {
322        std::mem::swap(&mut self.old_video, &mut self.new_video);
323        if let Some(ref mut new) = self.new_video {
324            new.clear();
325        }
326    }
327}
328
329/// Main refresh function — redraws the line.
330/// Port of `zrefresh()` from Src/Zle/zle_refresh.c. The C source paints
331/// a full virtual-screen diff against the previous frame; this Rust
332/// port renders the single line each call but adds three behaviors
333/// the previous bare-buffer version was missing:
334///   * region-attribute overlay (zle_refresh.c `region_highlights[]`),
335///   * vi visual-mode auto-region (mirrors zle_refresh.c's check of
336///     `region_active` to paint mark..zlecs in standout),
337///   * RPS1 / right-prompt rendering at the right margin
338///     (zle_refresh.c `put_rpromptbuf` path).
339
340// --- AUTO: cross-zle hoisted-fn use glob ---
341#[allow(unused_imports)]
342#[allow(unused_imports)]
343
344/// Port of `ZR_END_ELLIPSIS_SIZE` macro from `zle_refresh.c:284`.
345pub const ZR_END_ELLIPSIS_SIZE: usize = ZR_END_ELLIPSIS.len(); // c:284
346
347/// Port of `ZR_MID_ELLIPSIS1_SIZE` macro from `zle_refresh.c:295`.
348pub const ZR_MID_ELLIPSIS1_SIZE: usize = ZR_MID_ELLIPSIS1.len(); // c:295
349
350/// Port of `ZR_MID_ELLIPSIS2_SIZE` macro from `zle_refresh.c:302`.
351pub const ZR_MID_ELLIPSIS2_SIZE: usize = ZR_MID_ELLIPSIS2.len(); // c:302
352
353/// Port of `ZR_START_ELLIPSIS_SIZE` macro from `zle_refresh.c:312`.
354pub const ZR_START_ELLIPSIS_SIZE: usize = ZR_START_ELLIPSIS.len(); // c:312
355
356/// Apply a `$zle_highlight` array to the manager.
357/// Port of `zle_set_highlight()` from Src/Zle/zle_refresh.c:322. Walks
358/// each `category:spec` entry, parses the spec via `match_highlight`,
359/// and stores it in `category_attrs`. Categories not mentioned keep the
360/// zsh defaults, applied here on first call: `region` and `special`
361/// default to `standout`, `isearch` to `underline`, `suffix` to `bold`
362/// — direct ports of zle_refresh.c:395-402.
363/// WARNING: param names don't match C — Rust=(manager, atrs) vs C=()
364pub fn zle_set_highlight(manager: &mut HighlightManager, atrs: &[&str]) {
365    let mut seen = std::collections::HashSet::new();
366    for entry in atrs {
367        if entry.is_empty() {
368            continue;
369        }
370        if *entry == "none" {
371            // zle_refresh.c:355-360 — `none` clears every category.
372            for cat in [
373                HighlightCategory::Region,
374                HighlightCategory::Isearch,
375                HighlightCategory::Suffix,
376                HighlightCategory::Paste,
377                HighlightCategory::Default,
378                HighlightCategory::Special,
379                HighlightCategory::Ellipsis,
380            ] {
381                manager.category_attrs.insert(cat, TextAttr::default());
382                seen.insert(cat);
383            }
384            continue;
385        }
386        let (prefix, rest) = match entry.split_once(':') {
387            Some(t) => t,
388            None => continue,
389        };
390        let cat = match prefix {
391            "region" => HighlightCategory::Region,
392            "isearch" => HighlightCategory::Isearch,
393            "suffix" => HighlightCategory::Suffix,
394            "paste" => HighlightCategory::Paste,
395            "default" => HighlightCategory::Default,
396            "special" => HighlightCategory::Special,
397            "ellipsis" => HighlightCategory::Ellipsis,
398            _ => continue,
399        };
400        manager.category_attrs.insert(cat, match_highlight(rest));
401        seen.insert(cat);
402    }
403
404    // Defaults for unset slots — zle_refresh.c:395-402.
405    let default_standout = TextAttr {
406        standout: true,
407        ..TextAttr::default()
408    };
409    let default_underline = TextAttr {
410        underline: true,
411        ..TextAttr::default()
412    };
413    let default_bold = TextAttr {
414        bold: true,
415        ..TextAttr::default()
416    };
417    if !seen.contains(&HighlightCategory::Region) {
418        manager
419            .category_attrs
420            .insert(HighlightCategory::Region, default_standout);
421    }
422    if !seen.contains(&HighlightCategory::Isearch) {
423        manager
424            .category_attrs
425            .insert(HighlightCategory::Isearch, default_underline);
426    }
427    if !seen.contains(&HighlightCategory::Suffix) {
428        manager
429            .category_attrs
430            .insert(HighlightCategory::Suffix, default_bold);
431    }
432    if !seen.contains(&HighlightCategory::Special) {
433        manager
434            .category_attrs
435            .insert(HighlightCategory::Special, default_standout);
436    }
437}
438
439/// Port of `zle_free_highlight()` from `Src/Zle/zle_refresh.c:415`.
440/// ```c
441/// void
442/// zle_free_highlight(void) {
443///     free_colour_buffer();
444/// }
445/// ```
446/// Direct port of `void zle_free_highlight(void)` from
447/// `Src/Zle/zle_refresh.c:415-420`.
448/// ```c
449/// free_colour_buffer();
450/// ```
451///
452/// C's `free_colour_buffer` frees the per-cell colour-attribute
453/// storage used by `region_highlight`. In the Rust port that
454/// storage is a `Vec<HighlightSpan>` inside the file-scope
455/// `HIGHLIGHT` static, dropped automatically by Vec::clear at the
456/// same invalidate points that fire the C free. No-op here is the
457/// correct cross-language equivalent for this fn shape (the
458/// caller doesn't reach into the highlight buffer from this entry
459/// point; the live tick clears its buffer directly).
460pub fn zle_free_highlight() { // c:415
461                              // Rust ownership handles the equivalent free; explicit clear
462                              // happens against the file-scope HIGHLIGHT static when
463                              // invalidate fires.
464}
465
466/// Direct port of `void tcoutclear(int cap)` from
467/// `Src/Zle/zle_refresh.c:607`.
468/// ```c
469/// void tcoutclear(int cap) {
470///     treplaceattrs((cap == TCCLEAREOL) ? prompt_attr : 0);
471///     applytextattributes(0);
472///     tcout(cap);
473/// }
474/// ```
475/// Emit a clear capability (`cap` is the termcap index TCCLEAREOL /
476/// TCCLEAREOD / TCCLEARSCREEN), after making the cleared region carry the
477/// right attributes — the prompt's for clear-to-end-of-line (so a
478/// coloured prompt's background fills correctly), else the default.
479/// The previous port took a `bool` and hardcoded CSI J for both, which
480/// wrongly cleared to end of *display* for the clear-to-end-of-*line*
481/// case (TCCLEAREOL → CSI K) and dropped the attribute setup. Every C
482/// caller guards on `tccan(cap)`, so `tcstr[cap]` is always loaded here.
483pub fn tcoutclear(cap: i32) {
484    // c:607
485    use crate::ported::zsh_h::TCCLEAREOL;
486    // c:609 — `treplaceattrs((cap == TCCLEAREOL) ? prompt_attr : 0);`
487    let attr = if cap == TCCLEAREOL {
488        PROMPT_ATTR.load(Ordering::SeqCst)
489    } else {
490        0
491    };
492    crate::ported::prompt::treplaceattrs(attr);
493    // c:610 — `applytextattributes(0);` emit the SGR change.
494    let sgr = crate::ported::prompt::applytextattributes(0);
495    let fd = SHTTY.load(Ordering::Relaxed);
496    let out_fd = if fd >= 0 { fd } else { 1 };
497    if !sgr.is_empty() {
498        let _ = write_loop(out_fd, sgr.as_bytes());
499    }
500    tcout(cap); // c:611
501}
502
503/// Port of `void zwcputc(const REFRESH_ELEMENT *c)` from
504/// `Src/Zle/zle_refresh.c:622`. Sets the pending attributes to the
505/// cell's (c:630), emits the SGR attribute-change diff (c:631 — empty
506/// when the attr is unchanged, so output stays minimal), then writes
507/// the character (c:644-651). The multiword/`nmwbuf` glyph path
508/// (c:634-643) reads the combining cluster `addmultiword` stored in the
509/// thread-local `NMWBUF` (length at `nmwbuf[c.chr]`, codepoints following)
510/// and emits each — the matching consumer for the already-ported producer.
511pub fn zwcputc(c: &REFRESH_ELEMENT) {
512    use std::sync::atomic::Ordering;
513    // c:630-631 — make the cell's attrs pending, emit the SGR diff.
514    crate::ported::prompt::treplaceattrs(c.atr);
515    let mut out = crate::ported::prompt::applytextattributes(0);
516    if c.atr & TXT_MULTIWORD_MASK != 0 {
517        // c:633-643 — multiword glyph: the cell's chr is an index into
518        // nmwbuf (set by addmultiword c:934); read the cluster length at
519        // that slot then its codepoints and emit each in turn.
520        let base = c.chr as u32 as usize; // c:634 — `nmwbuf[c->chr]`
521        NMWBUF.with(|buf| {
522            let b = buf.borrow();
523            if let Some(&nchars) = b.get(base) {
524                let mut p = base + 1; // c:635 — `nmwbuf + c->chr + 1`
525                for _ in 0..nchars {
526                    // c:639-641 — `*wcptr++`, wcrtomb → fwrite each char.
527                    if let Some(ch) = b.get(p).copied().and_then(char::from_u32) {
528                        let mut tmp = [0u8; 4];
529                        out.push_str(ch.encode_utf8(&mut tmp));
530                    }
531                    p += 1;
532                }
533            }
534        });
535    } else if c.chr != '\0' && c.chr != ZWC_WEOF {
536        // c:644-651 — `else if (c->chr != WEOF)`: emit the single codepoint. A
537        // NUL chr is the empty/terminator cell; ZWC_WEOF is the trailing
538        // column placeholder of a wide glyph — both are skipped (the leading
539        // cell already drew the full-width character).
540        let mut buf = [0u8; 4];
541        out.push_str(c.chr.encode_utf8(&mut buf));
542    }
543    if !out.is_empty() {
544        let f = SHTTY.load(Ordering::Relaxed);
545        let _ = write_loop(if f >= 0 { f } else { 1 }, out.as_bytes());
546    }
547}
548
549/// Port of `int zwcwrite(const REFRESH_STRING s, size_t i)` from
550/// `Src/Zle/zle_refresh.c:655`. Writes the first `i` cells of the
551/// video-buffer string `s`, each via `zwcputc` (c:659). Returns the
552/// number of cells written. The `zwrite(a,b)` macro (c:255/260) is just
553/// `zwcwrite(a, b)`. (Cell attributes are deferred to `zwcputc`'s
554/// colour path; the cell `chr` is faithful.)
555pub fn zwcwrite(s: &[REFRESH_ELEMENT], i: usize) -> usize {
556    // c:657-659 — `for (j = 0; j < i; j++) zwcputc(s + j);`
557    let n = i.min(s.len());
558    for cell in &s[..n] {
559        zwcputc(cell); // c:659
560    }
561    n // c:660 `return i;`
562}
563
564// =====================================================================
565// `DEF_MWBUF_ALLOC` + `zr_*_ellipsis` tables — `Src/Zle/zle_refresh.c:697`
566// + c:269-313. Pre-built REFRESH_ELEMENT sequences for line-truncation
567// markers.
568// =====================================================================
569
570/// Port of `DEF_MWBUF_ALLOC` from `Src/Zle/zle_refresh.c:697`.
571/// Number of words to allocate in one go for the multiword buffers.
572pub const DEF_MWBUF_ALLOC: usize = 32; // c:697
573
574// =====================================================================
575// Multi-codepoint cluster buffers — `Src/Zle/zle_refresh.c:53-55, 688-691`.
576// Layout per the c:39-51 doc-comment: each entry is `[count, char0,
577// char1, …, char(count-1)]` (count + count chars = count+1 elements).
578// `nmw_ind` starts at 1 so the index stored in `base.chr` is never 0
579// (a zero-index slot would compare-equal to a NUL chr cell). C uses
580// REFRESH_CHAR (wint_t) entries; Rust uses u32 to match the wint_t
581// shape without round-tripping through `char`'s validity constraints.
582//
583// Bucket-1 thread_locals per PORT_PLAN.md: each ZLE evaluator walks
584// its own per-keystroke refresh, so per-thread state preserves the
585// per-evaluator semantic without serialising across workers.
586// =====================================================================
587
588thread_local! {
589    /// Port of `static REFRESH_CHAR *nmwbuf` from `Src/Zle/zle_refresh.c:55`.
590    pub static NMWBUF: std::cell::RefCell<Vec<u32>> = const {
591        std::cell::RefCell::new(Vec::new())                                  // c:55
592    };
593    /// Port of `static REFRESH_CHAR *omwbuf` from `Src/Zle/zle_refresh.c:54`.
594    pub static OMWBUF: std::cell::RefCell<Vec<u32>> = const {
595        std::cell::RefCell::new(Vec::new())                                  // c:54
596    };
597    /// Port of `static int nmw_ind` from `Src/Zle/zle_refresh.c:691`.
598    /// Init 1 per c:43 — "We initialise nmw_ind to 1 to avoid the
599    /// index stored in the character looking like a NULL."
600    pub static NMW_IND: std::cell::Cell<usize> = const {
601        std::cell::Cell::new(1)                                              // c:691
602    };
603    /// Port of `static int nmw_size` from `Src/Zle/zle_refresh.c:690`.
604    pub static NMW_SIZE: std::cell::Cell<usize> = const {
605        std::cell::Cell::new(0)                                              // c:690
606    };
607    /// Port of `static int omw_size` from `Src/Zle/zle_refresh.c:689`.
608    pub static OMW_SIZE: std::cell::Cell<usize> = const {
609        std::cell::Cell::new(0)                                              // c:689
610    };
611}
612
613/// Direct port of `void freevideo(void)` from
614/// `Src/Zle/zle_refresh.c:700`.
615///
616/// Drops the new/old video buffers and (under MULTIBYTE_SUPPORT) the
617/// multi-codepoint cluster pools (the NMWBUF/OMWBUF thread_locals). C
618/// body's per-row zfree loop collapses to Rust's Vec drop cascade;
619/// clearing the NMWBUF/OMWBUF Vecs is the cluster-pool free.
620/// The empty Vecs are the NULL state so the next [`resetvideo`] pass
621/// reallocates fresh buffers.
622/// WARNING: param names don't match C — Rust=(state) vs C=()
623pub fn freevideo() {
624    // c:700
625    // c:702-709 — C walks nbuf/obuf rows calling zfree on each
626    //             REFRESH_STRING, then frees the row arrays. The Rust Vec
627    //             drop cascade subsumes both. Clear the GLOBAL NBUF/OBUF
628    //             (the lifecycle pair of resetvideo, which now allocates
629    //             them), not the RefreshState buffers.
630    *NBUF.lock().unwrap() = Vec::new();
631    *OBUF.lock().unwrap() = Vec::new();
632    // c:716-719 — `nbuf = obuf = NULL; winw_alloc = winh_alloc = -1;`. The
633    //             empty Vecs ARE the NULL state; resetvideo always
634    //             reallocates (no realloc-on-change optimisation), so no
635    //             separate `*_alloc` dimensions are tracked.
636    // c:710-714 — MULTIBYTE_SUPPORT: free the multiword cluster stores and
637    //             reset their sizes/index. The empty Vecs are the freed
638    //             (NULL) state; nmw_ind restarts at 1 (never 0, c:43) so the
639    //             next addmultiword's stored index can't alias a NUL cell.
640    NMWBUF.with(|b| b.borrow_mut().clear()); // c:710
641    OMWBUF.with(|b| b.borrow_mut().clear()); // c:711
642    NMW_SIZE.with(|c| c.set(0)); // c:712 — omw_size = nmw_size = 0
643    OMW_SIZE.with(|c| c.set(0)); // c:712
644    NMW_IND.with(|c| c.set(1)); // c:713 — nmw_ind = 1
645}
646
647/// Port of `resetvideo()` from Src/Zle/zle_refresh.c:725.
648/// Rust idiom replacement: `VideoBuffer::new(cols, rows)` covers
649/// the C `zrealloc(nbuf/obuf, (winh+1) * sizeof(...))` + memset-zero
650/// pair; geometry pulled from the canonical adjustcolumns/lines.
651/// Direct port of `void resetvideo(void)` from
652/// `Src/Zle/zle_refresh.c:725`.
653///
654/// Re-initialises the new/old video buffers + cursor + lprompt/
655/// rprompt widths at the start of every refresh cycle. Mirrors the
656/// C body's TERM_SHORT branch (winh=1 on dumb terms), lazy
657/// reallocation gated on winw_alloc/winh_alloc change, per-row
658/// zero-fill, then countprompt for both prompts and the
659/// lprompth wraparound bump when lpromptwof crosses winw.
660///
661/// **Signature note:** takes the RefreshState (which owns the
662/// VideoBuffers + the prompt cache) rather than the C file-statics.
663/// WARNING: param names don't match C — Rust=(state) vs C=()
664pub fn resetvideo(state: &mut RefreshState) {
665    // c:725
666    use crate::ported::params::TERMFLAGS;
667    use crate::ported::prompt::countprompt;
668    use crate::ported::zsh_h::TERM_SHORT;
669
670    // c:729 — `winw = zterm_columns;`
671    let cols = adjustcolumns();
672    state.columns = cols;
673    WINW.store(cols as i32, Ordering::Relaxed);
674
675    // c:730-733 — TERM_SHORT clamps to 1 row, else clamp ≥ 24.
676    let real_lines = adjustlines();
677    let rows = if TERMFLAGS.load(Ordering::Relaxed) & TERM_SHORT != 0 {
678        1
679    } else if real_lines < 2 {
680        24
681    } else {
682        real_lines
683    };
684    state.lines = rows;
685    WINH.store(rows as i32, Ordering::Relaxed);
686    RWINH.store(real_lines as i32, Ordering::Relaxed); // c:734
687
688    // c:735 — `vln = vmaxln = winprompt = 0;`
689    VLN.store(0, Ordering::Relaxed);
690    VMAXLN.store(0, Ordering::Relaxed);
691    WINPROMPT.store(0, Ordering::Relaxed);
692    // c:736 — `winpos = -1;`
693    WINPOS.store(-1, Ordering::Relaxed);
694
695    // c:737-755 — re-alloc the video buffers (winw/winh changed). C
696    //              allocates the global nbuf/obuf: (winh+1) row slots,
697    //              each row (winw+2) cells. Allocate the global NBUF/OBUF
698    //              the same way (eagerly sized; nextline's alloc-if-missing
699    //              still covers the lazy-row case). This replaces the prior
700    //              RefreshState VideoBuffer allocation so the buffer
701    //              lifecycle matches nextline/snextline/scrollwindow, which
702    //              all operate on the global NBUF.
703    let nrows = (rows + 1) as usize;
704    let rowlen = (cols + 2) as usize;
705    let fresh = || -> Vec<REFRESH_STRING> {
706        (0..nrows)
707            .map(|_| vec![REFRESH_ELEMENT::default(); rowlen])
708            .collect()
709    };
710    // c:757-767 — the per-row `nbuf[ln][0]=zr_zr` init is subsumed by the
711    //              REFRESH_ELEMENT::default() zero-fill above.
712    *NBUF.lock().unwrap() = fresh();
713    *OBUF.lock().unwrap() = fresh();
714
715    // c:770-774 — `countprompt(lpromptbuf, &lpromptwof, &lprompth, 1);
716    //              countprompt(rpromptbuf, &rpromptw, &rprompth, 0);`
717    let mut lpromptwof_v = 0i32;
718    let mut lprompth_v = 0i32;
719    let mut rpromptw_v = 0i32;
720    let mut rprompth_v = 0i32;
721    countprompt(&state.lpromptbuf, &mut lpromptwof_v, &mut lprompth_v, 1);
722    countprompt(&state.rpromptbuf, &mut rpromptw_v, &mut rprompth_v, 0);
723
724    // c:775-779 — `if (lpromptwof != winw) lpromptw = lpromptwof;
725    //              else { lpromptw = 0; lprompth++; }`
726    let lpromptw_v = if lpromptwof_v != cols as i32 {
727        lpromptwof_v
728    } else {
729        lprompth_v += 1;
730        0
731    };
732    state.lpromptw = lpromptw_v as usize;
733    state.rpromptw = rpromptw_v as usize;
734    LPROMPTW.store(lpromptw_v, Ordering::Relaxed);
735    LPROMPTH.store(lprompth_v, Ordering::Relaxed);
736    LPROMPTWOF.store(lpromptwof_v, Ordering::Relaxed);
737    RPROMPTW.store(rpromptw_v, Ordering::Relaxed);
738    RPROMPTH.store(rprompth_v, Ordering::Relaxed);
739
740    // c:782-787 — pre-fill nbuf[0]/obuf[0] with `lpromptw` spaces so the
741    //              first row's prompt area is reserved.
742    if lpromptw_v > 0 {
743        let spaces = lpromptw_v as usize;
744        if let Some(v) = state.new_video.as_mut() {
745            if let Some(row) = v.lines.get_mut(0) {
746                for cell in row.iter_mut().take(spaces) {
747                    cell.chr = ' ';
748                }
749            }
750        }
751        if let Some(v) = state.old_video.as_mut() {
752            if let Some(row) = v.lines.get_mut(0) {
753                for cell in row.iter_mut().take(spaces) {
754                    cell.chr = ' ';
755                }
756            }
757        }
758    }
759
760    // c:790-794 — `vcs = lpromptw; olnct = nlnct = 0;
761    //              if (showinglist > 0) showinglist = -2;
762    //              trashedzle = 0;`
763    state.vcs = lpromptw_v as usize;
764    VCS.store(lpromptw_v, Ordering::Relaxed);
765    OLNCT.store(0, Ordering::Relaxed);
766    NLNCT.store(0, Ordering::Relaxed);
767    if SHOWINGLIST.load(Ordering::Relaxed) > 0 {
768        SHOWINGLIST.store(-2, Ordering::Relaxed);
769    }
770    TRASHEDZLE.store(0, Ordering::Relaxed);
771
772    state.need_full_redraw = true;
773}
774
775/// Direct port of `void scrollwindow(int tline)` from
776/// `Src/Zle/zle_refresh.c:798`.
777/// ```c
778/// void scrollwindow(int tline) {
779///     int t0;
780///     REFRESH_STRING s = nbuf[tline];
781///     for (t0 = tline; t0 < winh - 1; t0++)
782///         nbuf[t0] = nbuf[t0 + 1];
783///     nbuf[winh - 1] = s;
784///     if (!tline) more_start = 1;
785/// }
786/// ```
787/// Rotate the video buffer: line `tline` is lifted out, the lines below
788/// it shift up one row, and the lifted line wraps to the bottom
789/// (`winh - 1`). When scrolling from the very top, set `more_start` so the
790/// first line can show the "more text above" indicator. The previous port
791/// was a fake — it emitted a terminal scroll escape (CSI S/T) and took a
792/// line *count*, neither of which is what C does.
793pub fn scrollwindow(tline: i32) {
794    // c:798
795    let winh = WINH.load(Ordering::SeqCst);
796    if tline >= 0 {
797        let t = tline as usize;
798        let mut nbuf = NBUF.lock().unwrap();
799        // C operates on the full winh grid; the Vec may be shorter, so
800        // clamp the rotated range to what's allocated.
801        let end = (winh as usize).min(nbuf.len());
802        // c:803-806 — `s = nbuf[tline]; shift [tline..winh-1] up;
803        //              nbuf[winh-1] = s;` is a left-rotation by one over
804        //              the `[tline, winh)` window.
805        if t < end {
806            nbuf[t..end].rotate_left(1);
807        }
808    }
809    // c:807-808 — `if (!tline) more_start = 1;`
810    if tline == 0 {
811        MORE_START.store(1, Ordering::SeqCst);
812    }
813}
814
815/// Direct port of `int nextline(Rparams rpms, int wrapped)` from
816/// `Src/Zle/zle_refresh.c:842`.
817///
818/// Advances `rpms->ln` to the next video row inside the new-video
819/// buffer. When already at the last row, either declines further
820/// scroll (`canscroll==0` and constraints fail → return 1, caller
821/// stops emitting) or scrolls the window by one line and decrements
822/// `nvln` so cursor tracking stays consistent. Always ensures the
823/// fresh row exists, then resets `rpms->s`/`sen` to the start/end
824/// of that row.
825///
826/// **Signature note (faithful to C):** `nextline(Rparams rpms, int wrapped)`
827/// operates on the global `nbuf`/`winw`/`winh`/`numscrolls` file-statics —
828/// here the global `NBUF` and `WINW`/`WINH`/`NUMSCROLLS` atomics, with
829/// `REFRESH_ELEMENT` cells (consistent with `scrollwindow`, which it calls
830/// at c:863). The earlier port threaded a `RefreshState`/`new_video` (the
831/// `RefreshElement` cell type) — divergent from C and inconsistent with the
832/// now-global `scrollwindow`; this is the first consolidation step toward
833/// driving the live build's vertical scroll through `nextline`.
834pub fn nextline(rpms: &mut rparams, wrapped: i32) -> i32 {
835    // c:841
836    let winw = WINW.load(Ordering::SeqCst);
837    let winh = WINH.load(Ordering::SeqCst);
838
839    // c:844-845 — `nbuf[ln][winw+1] = wrapped ? zr_nl : zr_zr; *s = zr_zr;`
840    {
841        let mut nbuf = NBUF.lock().unwrap();
842        if let Some(row) = nbuf.get_mut(rpms.ln as usize) {
843            let end_idx = (winw + 1) as usize;
844            if end_idx < row.len() {
845                row[end_idx] = if wrapped != 0 {
846                    REFRESH_ELEMENT { chr: '\n', atr: 0 }
847                } else {
848                    REFRESH_ELEMENT::default()
849                };
850            }
851            if rpms.pos < row.len() {
852                row[rpms.pos] = REFRESH_ELEMENT::default();
853            }
854        }
855    }
856
857    if rpms.ln != winh - 1 {
858        // c:849 — `rpms->ln++;`
859        rpms.ln += 1;
860    } else {
861        // c:851-860 — scroll-or-bail branch.
862        if rpms.canscroll == 0 {
863            let onumscrolls = ONUMSCROLLS.load(Ordering::Relaxed);
864            let numscrolls = NUMSCROLLS.load(Ordering::Relaxed);
865            // c:853-855 — bail when we shouldn't scroll yet.
866            if rpms.nvln != -1
867                && rpms.nvln != winh - 1
868                && (numscrolls != onumscrolls - 1 || rpms.nvln <= winh / 2)
869            {
870                return 1;
871            }
872            NUMSCROLLS.fetch_add(1, Ordering::Relaxed); // c:858
873            rpms.canscroll = winh / 2; // c:859
874        }
875        rpms.canscroll -= 1; // c:862
876        scrollwindow(0); // c:863 — same global NBUF as this function
877        if rpms.nvln != -1 {
878            rpms.nvln -= 1; // c:865
879        }
880    }
881
882    // c:867-869 — allocate the row if missing.
883    {
884        let mut nbuf = NBUF.lock().unwrap();
885        if rpms.ln as usize >= nbuf.len() {
886            nbuf.resize(
887                rpms.ln as usize + 1,
888                vec![REFRESH_ELEMENT::default(); (winw + 2) as usize],
889            );
890        }
891    }
892    // c:871-872 — `rpms->s = nbuf[ln]; rpms->sen = s + winw;`
893    rpms.pos = 0;
894    rpms.end = winw as usize;
895    0 // c:873
896}
897
898/// Direct port of `void snextline(Rparams rpms)` from
899/// `Src/Zle/zle_refresh.c:875` — "go to the next line in the status area".
900/// ```c
901/// void snextline(Rparams rpms) {
902///     *rpms->s = zr_zr;
903///     if (rpms->ln != winh - 1) rpms->ln++;
904///     else if (rpms->tosln > rpms->ln) {
905///         rpms->tosln--;
906///         if (rpms->nvln > 1) { scrollwindow(0); rpms->nvln--; }
907///         else more_end = 1;
908///     } else if (rpms->tosln > 2 && rpms->nvln > 1) {
909///         rpms->tosln--;
910///         if (rpms->tosln <= rpms->nvln) { scrollwindow(0); rpms->nvln--; }
911///         else { scrollwindow(rpms->tosln); more_end = 1; }
912///     } else { rpms->more_status = 1; scrollwindow(rpms->tosln + 1); }
913///     if (!nbuf[rpms->ln]) nbuf[rpms->ln] = zalloc(...);
914///     rpms->s = nbuf[rpms->ln]; rpms->sen = rpms->s + winw;
915/// }
916/// ```
917/// The previous Rust body was a fake: a made-up `more_status && tosln != ln`
918/// guard, no scroll logic, `RefreshState`/`new_video`, and an `int` return.
919/// Ported faithfully on the global `NBUF` (`REFRESH_ELEMENT`) with the real
920/// status-pane scroll cascade (tosln/nvln/more_end + the three scrollwindow
921/// calls).
922pub fn snextline(rpms: &mut rparams) {
923    // c:875
924    let winw = WINW.load(Ordering::SeqCst);
925    let winh = WINH.load(Ordering::SeqCst);
926
927    // c:877 — `*rpms->s = zr_zr;` terminate the current row at pos.
928    {
929        let mut nbuf = NBUF.lock().unwrap();
930        if let Some(row) = nbuf.get_mut(rpms.ln as usize) {
931            if rpms.pos < row.len() {
932                row[rpms.pos] = REFRESH_ELEMENT::default();
933            }
934        }
935    }
936
937    if rpms.ln != winh - 1 {
938        rpms.ln += 1; // c:879
939    } else if rpms.tosln > rpms.ln {
940        // c:881-887
941        rpms.tosln -= 1;
942        if rpms.nvln > 1 {
943            scrollwindow(0); // c:884
944            rpms.nvln -= 1; // c:885
945        } else {
946            MORE_END.store(1, Ordering::SeqCst); // c:887
947        }
948    } else if rpms.tosln > 2 && rpms.nvln > 1 {
949        // c:888-896
950        rpms.tosln -= 1;
951        if rpms.tosln <= rpms.nvln {
952            scrollwindow(0); // c:891
953            rpms.nvln -= 1; // c:892
954        } else {
955            scrollwindow(rpms.tosln); // c:894
956            MORE_END.store(1, Ordering::SeqCst); // c:895
957        }
958    } else {
959        // c:897-899
960        rpms.more_status = 1; // c:898
961        scrollwindow(rpms.tosln + 1); // c:899
962    }
963
964    // c:901-904 — alloc the row if missing; reset s/sen to its start/end.
965    {
966        let mut nbuf = NBUF.lock().unwrap();
967        if rpms.ln as usize >= nbuf.len() {
968            nbuf.resize(
969                rpms.ln as usize + 1,
970                vec![REFRESH_ELEMENT::default(); (winw + 2) as usize],
971            );
972        }
973    }
974    rpms.pos = 0; // c:903
975    rpms.end = winw as usize; // c:904
976}
977
978/// Direct port of `static void addmultiword(REFRESH_ELEMENT *base,
979///                                          ZLE_STRING_T tptr, int ichars)`
980/// from `Src/Zle/zle_refresh.c:913`.
981///
982/// Push the `ichars`-codepoint cluster `tptr[0..ichars]` into the
983/// shared `nmwbuf` storage, set the cell's `TXT_MULTIWORD_MASK`
984/// flag, and store the buffer entry's start index in `base.chr`.
985/// The renderer dispatches on the flag and reads `nmwbuf[base.chr]`
986/// (count) then `nmwbuf[base.chr+1..base.chr+1+count]` (cluster
987/// codepoints) — see C c:635-636 and the COMPARE macro at c:77.
988pub fn addmultiword(
989    base: &mut REFRESH_ELEMENT, // c:913
990    tptr: &[char],
991    ichars: usize,
992) {
993    // c:917 — `int iadd = ichars + 1;` total slots needed (count + count chars).
994    let iadd = ichars + 1;
995    // c:920 — `base->atr |= TXT_MULTIWORD_MASK;`.
996    base.atr |= TXT_MULTIWORD_MASK;
997    NMWBUF.with(|buf| {
998        let ind = NMW_IND.get();
999        let size = NMW_SIZE.get();
1000        // c:921-927 — `if (nmw_ind + iadd > nmw_size) { … realloc … }`.
1001        if ind + iadd > size {
1002            let mw_more = if iadd > DEF_MWBUF_ALLOC {
1003                iadd
1004            } else {
1005                DEF_MWBUF_ALLOC
1006            }; // c:922-923
1007            let new_size = size + mw_more;
1008            buf.borrow_mut().resize(new_size, 0); // c:924-926
1009            NMW_SIZE.set(new_size); // c:925 nmw_size += mw_more
1010        }
1011        let mut b = buf.borrow_mut();
1012        // c:929-932 — `nmwptr = nmwbuf + nmw_ind; *nmwptr++ = ichars; for(…) *nmwptr++ = tptr[icnt];`
1013        b[ind] = ichars as u32; // c:930
1014        for icnt in 0..ichars {
1015            b[ind + 1 + icnt] = tptr[icnt] as u32; // c:931-932
1016        }
1017    });
1018    // c:934 — `base->chr = (wint_t)nmw_ind;`. Store the buffer index in
1019    // the chr slot; downstream readers dispatch via TXT_MULTIWORD_MASK.
1020    // char::from_u32 returns Some for any u32 ≤ 0x10FFFF excluding the
1021    // surrogate range; realistic nmw_ind values are far below that.
1022    let ind = NMW_IND.get();
1023    base.chr = char::from_u32(ind as u32).unwrap_or('\0');
1024    // c:935 — `nmw_ind += iadd;`.
1025    NMW_IND.set(ind + iadd);
1026}
1027
1028/// Direct port of `void bufswap(void)` from `Src/Zle/zle_refresh.c:946`.
1029/// Swap the new/old video buffers — "better than freeing/allocating
1030/// every time" (c:944): last frame's NBUF becomes this frame's OBUF.
1031/// Operates on the global NBUF/OBUF as C does on the global nbuf/obuf, and
1032/// (MULTIBYTE_SUPPORT) the NMWBUF/OMWBUF multiword stores + nmw_size/omw_size,
1033/// resetting nmw_ind = 1 for the next build.
1034pub fn bufswap() {
1035    // c:946
1036    // c:954-956 — `qbuf = nbuf; nbuf = obuf; obuf = qbuf;`
1037    let mut nbuf = NBUF.lock().unwrap();
1038    let mut obuf = OBUF.lock().unwrap();
1039    std::mem::swap(&mut *nbuf, &mut *obuf);
1040    // c:960-968 — MULTIBYTE_SUPPORT: likewise swap the multiword buffers so
1041    // the cluster store follows nbuf→obuf, then restart the new buffer's
1042    // insert point. Now that zwcputc reads NMWBUF live (c:633-643), this keeps
1043    // last frame's clusters in OMWBUF (the diff's old side) while the next
1044    // build refills NMWBUF from nmw_ind=1 (c:967 — 1, not 0, so a fresh cell's
1045    // index can never look like the NUL/empty-cell sentinel; see c:43).
1046    NMWBUF.with(|nmw| {
1047        OMWBUF.with(|omw| {
1048            std::mem::swap(&mut *nmw.borrow_mut(), &mut *omw.borrow_mut()); // c:961-963
1049        });
1050    });
1051    NMW_SIZE.with(|ns| {
1052        OMW_SIZE.with(|os| {
1053            let t = ns.get(); // c:965-967 — itmp = nmw_size; nmw_size = omw_size; omw_size = itmp;
1054            ns.set(os.get());
1055            os.set(t);
1056        });
1057    });
1058    NMW_IND.with(|ind| ind.set(1)); // c:967 — nmw_ind = 1
1059}
1060/// `zrefresh` — see implementation.
1061pub fn zrefresh() {
1062    // c:975
1063    // c:975 — full repaint pipeline. C writes every byte through
1064    //          `tputs(..., putshout)` / `fputs(..., shout)`. Rust
1065    //          collects the rendered escape stream into a String
1066    //          and writes it to SHTTY in one shot — matches C's
1067    //          shout destination and reduces syscall count.
1068    let mut handle = String::new();
1069
1070    let cols = adjustcolumns();
1071    // c:729-734 — sync the global video dimensions to the terminal each
1072    // frame, as C's resetvideo does (`winw = zterm_columns`, winh clamped).
1073    // The cursor primitives (moveto automargin, scrollwindow, the line-opt)
1074    // read these globals; without this they'd stay at the static default
1075    // (80×24) on any other terminal size. VLN/VMAXLN are NOT reset here —
1076    // the diff path manages them itself (unlike resetvideo, which C only
1077    // calls on a size change).
1078    {
1079        use crate::ported::params::TERMFLAGS;
1080        use crate::ported::zsh_h::TERM_SHORT;
1081        WINW.store(cols as i32, Ordering::Relaxed); // c:729
1082        let real_lines = adjustlines();
1083        let rows = if TERMFLAGS.load(Ordering::Relaxed) & TERM_SHORT != 0 {
1084            1 // c:730-731
1085        } else if real_lines < 2 {
1086            24 // c:732-733
1087        } else {
1088            real_lines
1089        };
1090        WINH.store(rows as i32, Ordering::Relaxed);
1091        RWINH.store(real_lines as i32, Ordering::Relaxed); // c:734
1092    }
1093
1094    let prompt = prompt().to_string();
1095    let rprompt = rprompt().to_string();
1096    let cursor = ZLECS.load(Ordering::SeqCst);
1097
1098    let prompt_width = countprompt(&prompt);
1099    // c:676 — `lpromptw` is the left prompt's display width. The NBUF build
1100    // emits that many prompt cells at the start of row 0, so syncing it
1101    // enables refreshline's prompt-skip (c:1862) — previously dead because
1102    // LPROMPTW stayed at 0 (resetvideo, which sets it, is never called).
1103    // The skip is clamped to the row length, so an over-wide value can't
1104    // overrun.
1105    LPROMPTW.store(prompt_width as i32, Ordering::Relaxed);
1106    let rprompt_width = countprompt(&rprompt);
1107    // ZLELINE was locked TWICE in this expression — std::sync::Mutex
1108    // isn't reentrant, so the second `.lock()` deadlocks forever
1109    // waiting for the first guard to drop. Take a single lock, derive
1110    // both the slice end AND the slice itself from the guard, then
1111    // collect into String so the guard drops at end of stmt.
1112    let buffer_before_cursor: String = {
1113        let guard = ZLELINE.lock().unwrap();
1114        let end = cursor.min(guard.len());
1115        guard[..end].iter().collect()
1116    };
1117    let cursor_col = prompt_width + countprompt(&buffer_before_cursor);
1118
1119    // Horizontal scroll if the cursor approaches the right edge.
1120    // Mirrors zle_refresh.c's `winw` clamp logic — without the full
1121    // multi-line wrap path our single-line shell uses scroll instead.
1122    let scroll_margin = 8;
1123    let effective_cols = cols.saturating_sub(1);
1124    let scroll_offset = if cursor_col >= effective_cols.saturating_sub(scroll_margin) {
1125        cursor_col.saturating_sub(effective_cols / 2)
1126    } else {
1127        0
1128    };
1129
1130    // Compose the per-buffer-char attribute overlay before paint, so
1131    // we don't have to re-walk the highlight list per char during write.
1132    let attrs = compute_render_attrs();
1133
1134    let _ = write!(handle, "\r\x1b[K");
1135
1136    // Prompt — drawn unless we've scrolled past it. Skip
1137    // `scroll_offset` visible chars from the prompt (inlined
1138    // from the deleted skip_chars helper) — ANSI escape
1139    // sequences are skipped unconditionally so they don't
1140    // count against width.
1141    if scroll_offset < prompt_width {
1142        let mut width = 0;
1143        let mut byte_idx = 0;
1144        let mut in_escape = false;
1145        for (i, c) in prompt.char_indices() {
1146            if width >= scroll_offset {
1147                byte_idx = i;
1148                break;
1149            }
1150            if in_escape {
1151                if c.is_ascii_alphabetic() {
1152                    in_escape = false;
1153                }
1154            } else if c == '\x1b' {
1155                in_escape = true;
1156            } else {
1157                width += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
1158            }
1159            byte_idx = i + c.len_utf8();
1160        }
1161        let _ = write!(handle, "{}", &prompt[byte_idx..]);
1162    }
1163
1164    // Compute the visible byte/char range of the buffer after scroll.
1165    let buffer_start = scroll_offset.saturating_sub(prompt_width);
1166    // Width budget for buffer = total cols - prompt drawn - rprompt reserve.
1167    let drawn_prompt_width = prompt_width.saturating_sub(scroll_offset);
1168    let rprompt_reserve = if rprompt_width > 0 {
1169        rprompt_width + 1
1170    } else {
1171        0
1172    };
1173    let buffer_budget = effective_cols
1174        .saturating_sub(drawn_prompt_width)
1175        .saturating_sub(rprompt_reserve);
1176
1177    // Walk the buffer chars from buffer_start, applying overlay attrs.
1178    let mut current_attr: Option<TextAttr> = None;
1179    let line_snapshot = ZLELINE.lock().unwrap().clone();
1180    for (written, (idx, ch)) in line_snapshot
1181        .iter()
1182        .enumerate()
1183        .skip(buffer_start)
1184        .enumerate()
1185    {
1186        if written >= buffer_budget {
1187            break;
1188        }
1189        let want_attr = attrs.get(idx).and_then(|a| *a);
1190        if want_attr != current_attr {
1191            let _ = write!(handle, "\x1b[0m");
1192            if let Some(a) = want_attr {
1193                let _ = write!(handle, "{}", a.to_ansi());
1194            }
1195            current_attr = want_attr;
1196        }
1197        let _ = write!(handle, "{}", ch);
1198    }
1199    // Reset SGR before the rprompt / cursor jump.
1200    if current_attr.is_some() {
1201        let _ = write!(handle, "\x1b[0m");
1202    }
1203
1204    // Right prompt — paint at the absolute right margin if there's
1205    // room. Mirrors put_rpromptbuf in zle_refresh.c which writes RPS1
1206    // at column (winw - rpromptw).
1207    if rprompt_width > 0 && rprompt_width + 2 < effective_cols {
1208        let rprompt_col = effective_cols.saturating_sub(rprompt_width);
1209        let _ = write!(handle, "\r\x1b[{}C{}\x1b[0m", rprompt_col, rprompt);
1210    }
1211
1212    // Cursor positioning (1-based column in ANSI).
1213    let display_cursor_col = cursor_col.saturating_sub(scroll_offset);
1214    let _ = write!(handle, "\r\x1b[{}C", display_cursor_col);
1215
1216    // c:1488 — `fwrite(out, ..., shout); fflush(shout);`. Single
1217    //          write_loop emits the whole frame to SHTTY (stdout
1218    //          fallback). Replaces the prior `stdout.lock()`
1219    //          fake that wrote refresh output to stdout instead
1220    //          of the controlling tty.
1221    let fd = SHTTY.load(Ordering::Relaxed);
1222    let out_fd = if fd >= 0 { fd } else { 1 };
1223    // ---- OUTPUT SWAP -----------------------------------------------------
1224    // The full-repaint string above is now SUPERSEDED by the NBUF/OBUF diff
1225    // emitted by the refreshline loop at the end of this function. The
1226    // build is kept (not written) so the swap is one revertable change:
1227    // restore this write_loop and delete the refreshline loop to revert.
1228    let _ = (out_fd, &handle); // formerly: write_loop(out_fd, handle.as_bytes())
1229
1230    // c:1739 — the build records the cursor's video position (nvln/nvcs) as it
1231    // lays cells; captured out of the build block for the final moveto. Init
1232    // nvln = -1 (cursor not yet reached) so the bail case (cursor scrolled off,
1233    // loop broke early) is detectable and falls back to the single-line
1234    // recompute below.
1235    let mut cur_nvln: i32 = -1;
1236    let mut cur_nvcs: i32 = 0;
1237
1238    // ---- Build NBUF (c:954-1400) ----------------------------------------
1239    // The full-repaint output above is the live renderer and is left
1240    // untouched. This populates the NBUF/OBUF video buffers that
1241    // `refreshline` diffs, so the minimal-update path can be developed and
1242    // verified against real frame data without risking the prompt. Once
1243    // refreshline's output sites are wired, zrefresh's output switches from
1244    // full-repaint to the NBUF/OBUF diff. `atr` is carried as default for
1245    // now (the cell `chr` is faithful; the colour-diff is wired with
1246    // refreshline's colour path) — a documented simplification, not a stub.
1247    {
1248        // c:954-956 — last frame's NBUF becomes this frame's OBUF.
1249        bufswap();
1250        OLNCT.store(NLNCT.load(Ordering::SeqCst), Ordering::SeqCst);
1251        // c:1194 — `numscrolls = 0;` reset the per-frame scroll counter before
1252        // generating the video buffers. nextline increments it as content
1253        // scrolls off the bottom; at frame end it's stored into ONUMSCROLLS
1254        // (c:1750) so the NEXT frame's nextline bail heuristic (c:851,
1255        // `numscrolls != onumscrolls - 1`) compares against last frame's count.
1256        // Without this reset the counter accumulated across every frame.
1257        NUMSCROLLS.store(0, Ordering::SeqCst);
1258        // Rust frame-prep: clear the swapped-in (stale) NBUF for rebuild.
1259        NBUF.lock().unwrap().clear();
1260        // c:1208-1400 — emit prompt + line cells, wrapping at `winw`.
1261        // c:1226-1248 — each cell carries the resolved attribute (`atr`)
1262        // so refreshline/zwcputc emit its colour. Convert the per-char
1263        // TextAttr overlay (compute_render_attrs) to the zattr bitmap.
1264        use crate::ported::zsh_h::zattr;
1265        let to_zattr = |ta: &TextAttr| -> zattr {
1266            use crate::ported::zsh_h::{
1267                TXTBGCOLOUR, TXTBOLDFACE, TXTFGCOLOUR, TXTSTANDOUT, TXTUNDERLINE,
1268                TXT_ATTR_BG_COL_SHIFT, TXT_ATTR_FG_COL_SHIFT,
1269            };
1270            let mut a: zattr = 0;
1271            if ta.bold {
1272                a |= TXTBOLDFACE;
1273            }
1274            if ta.underline {
1275                a |= TXTUNDERLINE;
1276            }
1277            if ta.standout {
1278                a |= TXTSTANDOUT;
1279            }
1280            if let Some(fg) = ta.fg_color {
1281                a |= TXTFGCOLOUR | ((fg as zattr) << TXT_ATTR_FG_COL_SHIFT);
1282            }
1283            if let Some(bg) = ta.bg_color {
1284                a |= TXTBGCOLOUR | ((bg as zattr) << TXT_ATTR_BG_COL_SHIFT);
1285            }
1286            a
1287        };
1288        let cols_n = cols.max(1);
1289        // ---- Build into the global NBUF via rparams + nextline (c:975-1400).
1290        // Pre-allocate NBUF to (winh+1) rows × (winw+2) cells (resetvideo's
1291        // layout, inline so we DON'T trigger resetvideo's VLN/VMAXLN reset or
1292        // RefreshState-prompt side effects, which would corrupt the diff path).
1293        let winw_b = WINW.load(Ordering::SeqCst);
1294        let winh_b = WINH.load(Ordering::SeqCst);
1295        {
1296            let nrows = (winh_b + 1).max(1) as usize;
1297            let rowlen = (winw_b + 2) as usize;
1298            *NBUF.lock().unwrap() = (0..nrows)
1299                .map(|_| vec![REFRESH_ELEMENT::default(); rowlen])
1300                .collect();
1301        }
1302        let mut rpms = rparams::default();
1303        rpms.nvln = -1; // c:1751 — cursor video line, set when we reach ZLECS.
1304        // c:1119 — `more_start = more_end = 0;` reset per frame BEFORE the
1305        // build. nextline/snextline set these (via scrollwindow) when content
1306        // scrolls off the top/bottom this frame; without the reset a single
1307        // scroll would leave them stuck set for every later frame.
1308        MORE_START.store(0, Ordering::SeqCst);
1309        MORE_END.store(0, Ordering::SeqCst);
1310        // emit: write one cell at NBUF[ln][pos] (brief lock, RELEASED before
1311        // nextline so its internal NBUF lock can't deadlock), advance pos, and
1312        // wrap via nextline at the right margin (c:842). Returns true when
1313        // nextline bailed (c:1257 `if (nextline(...)) break;`) — the buffer
1314        // can't grow further without scrolling the cursor off-screen, so the
1315        // caller must stop the line loop.
1316        let mut emit = |rpms: &mut rparams, chr: char, atr: zattr| -> bool {
1317            {
1318                let mut nbuf = NBUF.lock().unwrap();
1319                if let Some(row) = nbuf.get_mut(rpms.ln as usize) {
1320                    if rpms.pos < row.len() {
1321                        row[rpms.pos] = REFRESH_ELEMENT { chr, atr };
1322                    }
1323                }
1324            }
1325            rpms.pos += 1;
1326            if rpms.pos >= cols_n as usize {
1327                return nextline(rpms, 1) != 0; // c:842 — wrapped, resets pos=0
1328            }
1329            false
1330        };
1331        // emit_cell: like emit but writes a PRE-BUILT cell (used for combining
1332        // clusters whose chr is an addmultiword nmwbuf index + TXT_MULTIWORD_MASK
1333        // in atr — can't be expressed as a (chr, atr) pair through emit). Same
1334        // wrap/bail semantics (c:842).
1335        let mut emit_cell = |rpms: &mut rparams, cell: REFRESH_ELEMENT| -> bool {
1336            {
1337                let mut nbuf = NBUF.lock().unwrap();
1338                if let Some(row) = nbuf.get_mut(rpms.ln as usize) {
1339                    if rpms.pos < row.len() {
1340                        row[rpms.pos] = cell;
1341                    }
1342                }
1343            }
1344            rpms.pos += 1;
1345            if rpms.pos >= cols_n as usize {
1346                return nextline(rpms, 1) != 0;
1347            }
1348            false
1349        };
1350        // Prompt cells. The prompt is an ANSI string here (not attributed
1351        // cells as in C's putpromptchar), so parse its SGR escapes into the
1352        // cell `atr` — otherwise the prompt would render colourless when the
1353        // output switches to the NBUF diff. (A bridge: the faithful form
1354        // would have putpromptchar emit cells directly; flagged.)
1355        let apply_sgr = |mut attr: zattr, params: &str| -> zattr {
1356            use crate::ported::zsh_h::{
1357                TXTBGCOLOUR, TXTBOLDFACE, TXTFGCOLOUR, TXTSTANDOUT, TXTUNDERLINE,
1358                TXT_ATTR_BG_COL_SHIFT, TXT_ATTR_FG_COL_SHIFT,
1359            };
1360            let nums: Vec<i64> = if params.is_empty() {
1361                vec![0] // bare CSI m == reset
1362            } else {
1363                params.split(';').filter_map(|s| s.parse().ok()).collect()
1364            };
1365            let set_fg = |a: zattr, c: i64| {
1366                (a & !(TXTFGCOLOUR | (0xff << TXT_ATTR_FG_COL_SHIFT)))
1367                    | TXTFGCOLOUR
1368                    | ((c as zattr) << TXT_ATTR_FG_COL_SHIFT)
1369            };
1370            let set_bg = |a: zattr, c: i64| {
1371                (a & !(TXTBGCOLOUR | (0xff << TXT_ATTR_BG_COL_SHIFT)))
1372                    | TXTBGCOLOUR
1373                    | ((c as zattr) << TXT_ATTR_BG_COL_SHIFT)
1374            };
1375            let mut i = 0;
1376            while i < nums.len() {
1377                match nums[i] {
1378                    0 => attr = 0,
1379                    1 => attr |= TXTBOLDFACE,
1380                    4 => attr |= TXTUNDERLINE,
1381                    7 => attr |= TXTSTANDOUT,
1382                    30..=37 => attr = set_fg(attr, nums[i] - 30),
1383                    40..=47 => attr = set_bg(attr, nums[i] - 40),
1384                    90..=97 => attr = set_fg(attr, nums[i] - 90 + 8),
1385                    100..=107 => attr = set_bg(attr, nums[i] - 100 + 8),
1386                    38 if i + 2 < nums.len() && nums[i + 1] == 5 => {
1387                        attr = set_fg(attr, nums[i + 2]);
1388                        i += 2;
1389                    }
1390                    48 if i + 2 < nums.len() && nums[i + 1] == 5 => {
1391                        attr = set_bg(attr, nums[i + 2]);
1392                        i += 2;
1393                    }
1394                    _ => {}
1395                }
1396                i += 1;
1397            }
1398            attr
1399        };
1400        let mut prompt_attr: zattr = 0;
1401        let mut esc_params = String::new();
1402        let mut in_esc = false;
1403        for c in prompt.chars() {
1404            if in_esc {
1405                if c == '[' {
1406                    // CSI introducer — not a param.
1407                } else if c == 'm' {
1408                    prompt_attr = apply_sgr(prompt_attr, &esc_params); // SGR
1409                    in_esc = false;
1410                    esc_params.clear();
1411                } else if c.is_ascii_alphabetic() {
1412                    in_esc = false; // non-SGR escape (cursor/clear) — ignore
1413                    esc_params.clear();
1414                } else {
1415                    esc_params.push(c);
1416                }
1417            } else if c == '\x1b' {
1418                in_esc = true;
1419                esc_params.clear();
1420            } else if emit(&mut rpms, c, prompt_attr) {
1421                break; // c:1257 — nextline bailed
1422            }
1423        }
1424        // c:152 / zle_main.c:1280 — publish the prompt's trailing attribute
1425        // so refreshline's TCDEL attr-apply (c:2044) and tcoutclear (c:609)
1426        // make deleted/cleared cells carry the prompt's colour. C derives
1427        // prompt_attr via mixattrs(pmpt_attr, .., rpmpt_attr) from
1428        // promptexpand (blocked); this is the left-prompt SGR-parsed
1429        // approximation (rpmpt_attr folding deferred with that subsystem).
1430        PROMPT_ATTR.store(prompt_attr, Ordering::Relaxed);
1431        // Editable line with tab/control expansion (c:1248-1398). Each
1432        // line char's overlay attr is applied to the cell(s) it produces.
1433        let cursor_idx = ZLECS.load(Ordering::SeqCst);
1434        // c:1297-1320 — combining marks absorbed into the preceding base char's
1435        // cluster cell are skipped in subsequent iterations (the for-loop can't
1436        // advance `i` like C's `t += ichars`, so count them down here).
1437        let mut skip_combining = 0usize;
1438        for (i, &ch) in line_snapshot.iter().enumerate() {
1439            if skip_combining > 0 {
1440                skip_combining -= 1;
1441                continue;
1442            }
1443            // c:1247 — when the build reaches the cursor char, record its
1444            // video line AND column: `rpms.nvcs = rpms.s - nbuf[rpms.nvln =
1445            // rpms.ln]`. nvln keeps nextline's scroll-or-bail centred on the
1446            // cursor; nvcs is the final cursor column for moveto (c:1741).
1447            if i == cursor_idx {
1448                rpms.nvln = rpms.ln;
1449                rpms.nvcs = rpms.pos as i32;
1450            }
1451            let atr = attrs
1452                .get(i)
1453                .and_then(|o| o.as_ref())
1454                .map(&to_zattr)
1455                .unwrap_or(0);
1456            if ch == '\n' {
1457                // c:1251 — `if (nextline(&rpms, 0)) break;` hard newline.
1458                if nextline(&mut rpms, 0) != 0 {
1459                    break;
1460                }
1461            } else if ch == '\t' {
1462                // c:1254-1265 — spaces to the next 8-column stop, wrapping
1463                // (and possibly bailing) at the right margin.
1464                let mut bail = false;
1465                loop {
1466                    if emit(&mut rpms, ' ', atr) {
1467                        bail = true;
1468                        break;
1469                    }
1470                    if rpms.pos % 8 == 0 {
1471                        break;
1472                    }
1473                }
1474                if bail {
1475                    break;
1476                }
1477            } else if (ch as u32) < 0x20 || ch as u32 == 0x7f {
1478                // c:1340-1356 — control char as `^X` / `^?`.
1479                if emit(&mut rpms, '^', atr) {
1480                    break;
1481                }
1482                let c2 = if ((ch as u32) & !0x80u32) > 31 {
1483                    '?'
1484                } else {
1485                    char::from_u32((ch as u32) | 0x40).unwrap_or('?')
1486                };
1487                if emit(&mut rpms, c2, atr) {
1488                    break;
1489                }
1490            } else {
1491                // c:1267-1340 — printable char: lay it out at its display
1492                // width, wrapping early if it would straddle the right margin,
1493                // clustering combining marks (COMBININGCHARS), and padding the
1494                // extra columns of a wide (CJK) glyph with WEOF placeholders.
1495                // For the common ASCII case (width 1, COMBININGCHARS off) this
1496                // reduces to a single emit_cell — identical to the old emit.
1497                let cw = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0);
1498                if cw == 0 {
1499                    // c:1474-1493 — a zero-width / non-printable orphan (combining
1500                    // marks following a base are absorbed above via
1501                    // skip_combining; with COMBININGCHARS off they fall here):
1502                    // render as the hex escape `<%.04x>` / `<%.08x>`, one cell
1503                    // per character.
1504                    let hex = if (ch as u32) > 0xffff {
1505                        format!("<{:08x}>", ch as u32) // c:1480
1506                    } else {
1507                        format!("<{:04x}>", ch as u32) // c:1482
1508                    };
1509                    let mut bail = false;
1510                    for hc in hex.chars() {
1511                        if emit(&mut rpms, hc, atr) {
1512                            // c:1485-1491 — each cell, wrapping at the margin.
1513                            bail = true;
1514                            break;
1515                        }
1516                    }
1517                    if bail {
1518                        break;
1519                    }
1520                    continue; // c:1493 — char fully rendered
1521                }
1522                let width = cw;
1523                // c:1274-1287 — too wide for the columns left on this line: fill
1524                // the remainder with spaces (the last emit wraps via nextline)
1525                // so the glyph starts whole on the next line.
1526                let remaining = (cols_n as usize).saturating_sub(rpms.pos);
1527                if width > remaining {
1528                    let mut bail = false;
1529                    for _ in 0..remaining {
1530                        if emit(&mut rpms, ' ', atr) {
1531                            // c:1281 — `*s++ = zr_sp` (all_attr); last one wraps.
1532                            bail = true;
1533                            break;
1534                        }
1535                    }
1536                    if bail {
1537                        break; // c:1283 — nextline bailed
1538                    }
1539                    // c:1284-1286 — cursor was on this char: re-record after wrap.
1540                    if i == cursor_idx {
1541                        rpms.nvln = rpms.ln;
1542                        rpms.nvcs = rpms.pos as i32;
1543                    }
1544                }
1545                // c:1293-1299 — combining scan: absorb following combining marks
1546                // via the canonical IS_COMBINING (`wc != 0 && WCWIDTH(wc) == 0`).
1547                let mut ichars = 1usize;
1548                if isset(COMBININGCHARS) {
1549                    while i + ichars < line_snapshot.len()
1550                        && crate::ported::zsh_h::IS_COMBINING(line_snapshot[i + ichars])
1551                    {
1552                        ichars += 1; // c:1297-1299
1553                    }
1554                }
1555                skip_combining = ichars - 1; // c:1325 — t += ichars - 1
1556                // c:1303-1319 — emit. If the glyph still can't fit (terminal
1557                // narrower than its width even on a fresh line), render '?';
1558                // otherwise the leading cell (cluster or single char) plus
1559                // WEOF column-placeholders for the rest of its width.
1560                let remaining2 = (cols_n as usize).saturating_sub(rpms.pos);
1561                if width > remaining2 {
1562                    if emit(&mut rpms, '?', atr) {
1563                        break; // c:1304-1306
1564                    }
1565                } else {
1566                    let mut cell = REFRESH_ELEMENT { chr: ch, atr };
1567                    if ichars > 1 {
1568                        let cluster: Vec<char> =
1569                            line_snapshot[i..i + ichars].to_vec();
1570                        addmultiword(&mut cell, &cluster, ichars); // c:1311
1571                    }
1572                    if emit_cell(&mut rpms, cell) {
1573                        break; // c:1257 — nextline bailed
1574                    }
1575                    let mut w = width - 1;
1576                    let mut bail = false;
1577                    while w > 0 {
1578                        // c:1316-1318 — `while (--width>0){ s->chr=WEOF; s++; }`
1579                        if emit_cell(
1580                            &mut rpms,
1581                            REFRESH_ELEMENT { chr: ZWC_WEOF, atr },
1582                        ) {
1583                            bail = true;
1584                            break;
1585                        }
1586                        w -= 1;
1587                    }
1588                    if bail {
1589                        break;
1590                    }
1591                }
1592            }
1593        }
1594        // c:1411-1417 — cursor at end-of-buffer (`t == scs`): record its video
1595        // position, and if it sits exactly at the right margin (nvcs == winw),
1596        // wrap onto the next line so the cursor isn't faked one column past the
1597        // edge: `nextline(&rpms, 1); nvcs = 0; nvln++`.
1598        if cursor_idx >= line_snapshot.len() {
1599            rpms.nvln = rpms.ln;
1600            rpms.nvcs = rpms.pos as i32;
1601            if rpms.nvcs == WINW.load(Ordering::SeqCst) {
1602                let _ = nextline(&mut rpms, 1); // c:1414 — actually advance
1603                rpms.nvcs = 0; // c:1416
1604                rpms.nvln += 1; // c:1417
1605            }
1606        }
1607
1608        // c:1423-1554 — the status line (the `zle -M` message) rendered below
1609        // the editable line via the status-pane scroll cascade (snextline),
1610        // gated on STATUSLINE being set so the common (no-status) path is
1611        // untouched. Status runs BEFORE the nlnct computation so its rows are
1612        // counted (snextline advances rpms.ln). The MULTIBYTE_SUPPORT branches
1613        // are ported: the printable wide/combining path (c:1438-1473), the
1614        // width-0 / non-printable `<hex>` escape (c:1474-1493 and c:1512-1532,
1615        // collapsed into one branch since both emit `<%04x>`/`<%08x>` @all_attr),
1616        // and the control `^X`/`^?` path (c:1499-1510). `stringaszleline` is the
1617        // metafied→ZLE-string decode; here STATUSLINE is already a Rust `String`,
1618        // so we iterate its chars directly (the decode is the producer's job,
1619        // zle_main.rs).
1620        let statusline_present = if let Some(status) =
1621            STATUSLINE.lock().unwrap().clone()
1622        {
1623            let winw_s = WINW.load(Ordering::SeqCst);
1624            let all_attr = SPECIAL_ATTR.load(Ordering::SeqCst); // c:1430
1625            // c:1432 — `rpms.tosln = rpms.ln + 1;` top of the status pane.
1626            rpms.tosln = rpms.ln + 1;
1627            // c:1433 — `nbuf[rpms.ln][winw+1] = zr_zr;` editable line not wrapped.
1628            {
1629                let mut nbuf = NBUF.lock().unwrap();
1630                if let Some(row) = nbuf.get_mut(rpms.ln as usize) {
1631                    let end_idx = (winw_s + 1) as usize;
1632                    if end_idx < row.len() {
1633                        row[end_idx] = REFRESH_ELEMENT::default();
1634                    }
1635                }
1636            }
1637            snextline(&mut rpms); // c:1434 — advance into the status area.
1638            // semit: write one status cell at NBUF[ln][pos] (brief lock,
1639            // RELEASED before snextline so its internal NBUF lock can't
1640            // deadlock), advance pos, and wrap via snextline at the right
1641            // margin (c:1538 `if (rpms.s==rpms.sen){ nbuf[ln][winw+1]=zr_nl;
1642            // snextline(&rpms); }`). Each cell does its own wrap check, which
1643            // matches C's mid-pair check (c:1502, after `^`) plus the common
1644            // check (c:1538, after the second/verbatim cell).
1645            let mut semit = |rpms: &mut rparams, chr: char, atr: zattr| {
1646                {
1647                    let mut nbuf = NBUF.lock().unwrap();
1648                    if let Some(row) = nbuf.get_mut(rpms.ln as usize) {
1649                        if rpms.pos < row.len() {
1650                            row[rpms.pos] = REFRESH_ELEMENT { chr, atr };
1651                        }
1652                    }
1653                }
1654                rpms.pos += 1;
1655                if rpms.pos >= rpms.end {
1656                    // c:1538 — mark the row wrapped, then advance the pane.
1657                    {
1658                        let mut nbuf = NBUF.lock().unwrap();
1659                        if let Some(row) = nbuf.get_mut(rpms.ln as usize) {
1660                            let end_idx = (winw_s + 1) as usize;
1661                            if end_idx < row.len() {
1662                                row[end_idx] =
1663                                    REFRESH_ELEMENT { chr: '\n', atr: 0 };
1664                            }
1665                        }
1666                    }
1667                    snextline(rpms);
1668                }
1669            };
1670            // semit_cell: like semit but writes a PRE-BUILT cell (combining
1671            // clusters / WEOF placeholders). Same snextline-wrap as semit.
1672            let mut semit_cell = |rpms: &mut rparams, cell: REFRESH_ELEMENT| {
1673                {
1674                    let mut nbuf = NBUF.lock().unwrap();
1675                    if let Some(row) = nbuf.get_mut(rpms.ln as usize) {
1676                        if rpms.pos < row.len() {
1677                            row[rpms.pos] = cell;
1678                        }
1679                    }
1680                }
1681                rpms.pos += 1;
1682                if rpms.pos >= rpms.end {
1683                    {
1684                        let mut nbuf = NBUF.lock().unwrap();
1685                        if let Some(row) = nbuf.get_mut(rpms.ln as usize) {
1686                            let end_idx = (winw_s + 1) as usize;
1687                            if end_idx < row.len() {
1688                                row[end_idx] =
1689                                    REFRESH_ELEMENT { chr: '\n', atr: 0 };
1690                            }
1691                        }
1692                    }
1693                    snextline(rpms);
1694                }
1695            };
1696            // c:1438-1538 — status render loop. The MB printable branch (wide /
1697            // combining glyphs, c:1438-1473) mirrors the main build: combining
1698            // scan, margin wrap, leading cell + WEOF padding, '?' when too wide.
1699            // The control branch renders `^X`/`^?` (c:1499-1510); the final
1700            // `else` renders width-0 / non-printable orphans as the `<hex>`
1701            // escape (c:1474-1493 / c:1512-1532).
1702            let status_chars: Vec<char> = status.chars().collect();
1703            let mut skip_s = 0usize;
1704            for su in 0..status_chars.len() {
1705                if skip_s > 0 {
1706                    skip_s -= 1;
1707                    continue;
1708                }
1709                let u = status_chars[su];
1710                let width = unicode_width::UnicodeWidthChar::width(u).unwrap_or(0);
1711                let is_ctrl = (u as u32) < 0x20 || u as u32 == 0x7f;
1712                if width > 0 && !is_ctrl {
1713                    // c:1442-1446 — combining scan (COMBININGCHARS) via the
1714                    // canonical IS_COMBINING (`wc != 0 && WCWIDTH(wc) == 0`).
1715                    let mut ichars = 1usize;
1716                    if isset(COMBININGCHARS) {
1717                        while su + ichars < status_chars.len()
1718                            && crate::ported::zsh_h::IS_COMBINING(status_chars[su + ichars])
1719                        {
1720                            ichars += 1;
1721                        }
1722                    }
1723                    skip_s = ichars - 1;
1724                    // c:1449-1455 — too wide for the line: pad spaces (zr_sp,
1725                    // atr 0) to the margin (the last semit wraps via snextline).
1726                    let remaining = rpms.end.saturating_sub(rpms.pos);
1727                    if width as usize > remaining {
1728                        for _ in 0..remaining {
1729                            semit(&mut rpms, ' ', 0); // c:1450 zr_sp
1730                        }
1731                    }
1732                    // c:1456-1473 — emit: '?' if still too wide, else the
1733                    // leading cell (cluster/char) + WEOF column-placeholders.
1734                    let remaining2 = rpms.end.saturating_sub(rpms.pos);
1735                    if width as usize > remaining2 {
1736                        semit(&mut rpms, '?', all_attr); // c:1457-1459
1737                    } else {
1738                        let mut cell = REFRESH_ELEMENT { chr: u, atr: 0 }; // c:1462
1739                        if ichars > 1 {
1740                            let cluster: Vec<char> =
1741                                status_chars[su..su + ichars].to_vec();
1742                            addmultiword(&mut cell, &cluster, ichars); // c:1464
1743                        }
1744                        semit_cell(&mut rpms, cell);
1745                        let mut w = width - 1;
1746                        while w > 0 {
1747                            // c:1469-1471 — `while(--width>0){ s->chr=WEOF; }`
1748                            semit_cell(&mut rpms, REFRESH_ELEMENT {
1749                                chr: ZWC_WEOF,
1750                                atr: 0,
1751                            });
1752                            w -= 1;
1753                        }
1754                    }
1755                } else if is_ctrl {
1756                    // c:1499-1508 — control char as `^X` / `^?`.
1757                    semit(&mut rpms, '^', all_attr); // c:1500
1758                    let c2 = if ((u as u32) & !0x80u32) > 31 {
1759                        '?' // c:1507
1760                    } else {
1761                        char::from_u32((u as u32) | 0x40).unwrap_or('?') // c:1506
1762                    };
1763                    semit(&mut rpms, c2, all_attr); // c:1505-1508
1764                } else {
1765                    // c:1474-1493 — a width-0 / non-printable orphan in the
1766                    // status line (e.g. a combining mark with COMBININGCHARS
1767                    // off): render as the hex escape `<%.04x>` / `<%.08x>`, one
1768                    // cell per character, carrying all_attr like the C MB path.
1769                    let hex = if (u as u32) > 0xffff {
1770                        format!("<{:08x}>", u as u32) // c:1480
1771                    } else {
1772                        format!("<{:04x}>", u as u32) // c:1482
1773                    };
1774                    for hc in hex.chars() {
1775                        semit(&mut rpms, hc, all_attr); // c:1485-1487
1776                    }
1777                }
1778            }
1779            // c:1554 — `*rpms.s = zr_zr;` terminate the final status row.
1780            {
1781                let mut nbuf = NBUF.lock().unwrap();
1782                if let Some(row) = nbuf.get_mut(rpms.ln as usize) {
1783                    if rpms.pos < row.len() {
1784                        row[rpms.pos] = REFRESH_ELEMENT::default();
1785                    }
1786                }
1787            }
1788            true
1789        } else {
1790            false
1791        };
1792
1793        // c:1751 — `nlnct = rpms.ln + 1`. NBUF is already populated (the build
1794        // wrote into it directly); trim any unused pre-allocated tail rows so
1795        // refreshline/the diff loop see exactly the drawn lines.
1796        let nlnct = rpms.ln + 1;
1797        NBUF.lock().unwrap().truncate(nlnct as usize);
1798        NLNCT.store(nlnct, Ordering::SeqCst);
1799
1800        // c:1557-1596 — "...>" indicator at the end of the last visible line
1801        // when there is more text past the bottom of the screen (more_end,
1802        // set only by the status-pane snextline cascade at c:887/895). Non-MB
1803        // path; the MB extra_ellipsis back-off (c:1571-1592) is deferred. This
1804        // only modifies row tosln-1 (already within [0,nlnct)), so running it
1805        // after the nlnct/truncate is safe.
1806        if MORE_END.load(Ordering::SeqCst) != 0 {
1807            let winw_e = WINW.load(Ordering::SeqCst);
1808            let winh_e = WINH.load(Ordering::SeqCst);
1809            let prompt_attr = PROMPT_ATTR.load(Ordering::SeqCst);
1810            let ellipsis_attr = ELLIPSIS_ATTR.load(Ordering::SeqCst);
1811            // c:1561-1562 — `if (!statusline) rpms.tosln = winh;`
1812            if !statusline_present {
1813                rpms.tosln = winh_e;
1814            }
1815            let target = (rpms.tosln - 1).max(0) as usize; // c:1563
1816            let sen = (winw_e - 7).max(0) as usize; // c:1564 — `s + winw - 7`
1817            let mut nbuf = NBUF.lock().unwrap();
1818            if let Some(row) = nbuf.get_mut(target) {
1819                // c:1566-1573 — scan from the row start; at the first '\0'
1820                // cell pad with zr_pad ({' ', prompt_attr}, c:1650) up to sen,
1821                // set sen->chr='\0' (avoid the WEOF test), and stop.
1822                let lim = sen.min(row.len());
1823                for s in 0..lim {
1824                    if row[s].chr == '\0' {
1825                        for k in s..lim {
1826                            row[k] = REFRESH_ELEMENT { chr: ' ', atr: prompt_attr };
1827                        }
1828                        if sen < row.len() {
1829                            row[sen].chr = '\0'; // c:1570
1830                        }
1831                        break;
1832                    }
1833                }
1834                // c:1593-1595 — copy zr_end_ellipsis at sen; the leading cell
1835                // carries prompt_attr, the next ellipsis_attr.
1836                for (k, cell) in ZR_END_ELLIPSIS.iter().enumerate() {
1837                    let idx = sen + k;
1838                    if idx < row.len() {
1839                        row[idx] = *cell;
1840                    }
1841                }
1842                if sen < row.len() {
1843                    row[sen].atr = prompt_attr; // c:1594
1844                }
1845                if sen + 1 < row.len() {
1846                    row[sen + 1].atr = ellipsis_attr; // c:1595
1847                }
1848                // c:1596 — `nbuf[tosln-1][winw] = nbuf[tosln-1][winw+1] = zr_zr;`
1849                let wi = winw_e as usize;
1850                if wi < row.len() {
1851                    row[wi] = REFRESH_ELEMENT::default();
1852                }
1853                if wi + 1 < row.len() {
1854                    row[wi + 1] = REFRESH_ELEMENT::default();
1855                }
1856            }
1857        }
1858
1859        // c:1599-1635 — "<....>" indicator inserted into the FIRST status row
1860        // (nbuf[tosln]) when the status line itself overflows its pane
1861        // (rpms.more_status, set by snextline c:898). Non-MB path; the MB
1862        // extra_ellipsis back-off (c:1614-1631) is deferred. Only modifies row
1863        // tosln (within [0,nlnct)), so running it after nlnct/truncate is safe.
1864        if rpms.more_status != 0 {
1865            let winw_m = WINW.load(Ordering::SeqCst);
1866            let prompt_attr = PROMPT_ATTR.load(Ordering::SeqCst);
1867            let ellipsis_attr = ELLIPSIS_ATTR.load(Ordering::SeqCst);
1868            let target = rpms.tosln.max(0) as usize; // c:1602 — nbuf[tosln]
1869            let sen = (winw_m - 8).max(0) as usize; // c:1603 — `s + winw - 8`
1870            let mut nbuf = NBUF.lock().unwrap();
1871            if let Some(row) = nbuf.get_mut(target) {
1872                // c:1604-1610 — scan from the row start; at the first '\0' cell
1873                // fill zr_sp ({' ', 0}) up to sen and stop.
1874                let lim = sen.min(row.len());
1875                for s in 0..lim {
1876                    if row[s].chr == '\0' {
1877                        for k in s..lim {
1878                            row[k] = REFRESH_ELEMENT { chr: ' ', atr: 0 };
1879                        }
1880                        break;
1881                    }
1882                }
1883                // c:1622-1624 — copy zr_mid_ellipsis1 (" <....") at sen; its 2nd
1884                // cell carries ellipsis_attr; then advance sen by SIZE1.
1885                for (k, cell) in ZR_MID_ELLIPSIS1.iter().enumerate() {
1886                    let idx = sen + k;
1887                    if idx < row.len() {
1888                        row[idx] = *cell;
1889                    }
1890                }
1891                if sen + 1 < row.len() {
1892                    row[sen + 1].atr = ellipsis_attr; // c:1623
1893                }
1894                let sen2 = sen + ZR_MID_ELLIPSIS1_SIZE; // c:1624
1895                // c:1632-1633 — copy zr_mid_ellipsis2 ("> ") at sen2; its 2nd
1896                // cell carries prompt_attr.
1897                for (k, cell) in ZR_MID_ELLIPSIS2.iter().enumerate() {
1898                    let idx = sen2 + k;
1899                    if idx < row.len() {
1900                        row[idx] = *cell;
1901                    }
1902                }
1903                if sen2 + 1 < row.len() {
1904                    row[sen2 + 1].atr = prompt_attr; // c:1633
1905                }
1906                // c:1634 — `nbuf[tosln][winw] = nbuf[tosln][winw+1] = zr_zr;`
1907                let wi = winw_m as usize;
1908                if wi < row.len() {
1909                    row[wi] = REFRESH_ELEMENT::default();
1910                }
1911                if wi + 1 < row.len() {
1912                    row[wi + 1] = REFRESH_ELEMENT::default();
1913                }
1914            }
1915        }
1916
1917        // c:1741 — capture the cursor's final video coords out of the build
1918        // block (rpms is dropped at the brace below) for the closing moveto.
1919        cur_nvln = rpms.nvln;
1920        cur_nvcs = rpms.nvcs;
1921    }
1922
1923    // c:988 — `int rprompt_off = 1;` offset of the right prompt from the right
1924    // of the screen. Set by the placement below, read by the emit in the
1925    // render loop. A zrefresh local in C; here too (both sites are in zrefresh).
1926    let mut rprompt_off: i32 = 1;
1927
1928    // c:1663-1671 — if the build scrolled content off the TOP this frame
1929    // (more_start, set by nextline→scrollwindow), overwrite line 0 with the
1930    // ">..." start-ellipsis: lpromptw spaces, the (clamped) start ellipsis,
1931    // then padding, with the first ellipsis cell carrying ellipsis_attr and
1932    // the first pad cell prompt_attr. The `!more_start` branch is the
1933    // right-prompt placement (c:1643-1657).
1934    if MORE_START.load(Ordering::SeqCst) != 0 {
1935        let winw = WINW.load(Ordering::SeqCst).max(0) as usize;
1936        let lpromptw = (LPROMPTW.load(Ordering::SeqCst).max(0) as usize).min(winw);
1937        let prompt_attr = PROMPT_ATTR.load(Ordering::SeqCst);
1938        let ellipsis_attr = ELLIPSIS_ATTR.load(Ordering::SeqCst);
1939        let t0 = (winw - lpromptw).min(ZR_START_ELLIPSIS_SIZE); // c:1665-1666
1940        let mut row0: REFRESH_STRING = Vec::with_capacity(winw + 2);
1941        for _ in 0..lpromptw {
1942            row0.push(REFRESH_ELEMENT { chr: ' ', atr: 0 }); // c:1664 zr_sp
1943        }
1944        for k in 0..t0 {
1945            let mut cell = ZR_START_ELLIPSIS[k]; // c:1667
1946            if k == 0 {
1947                cell.atr = ellipsis_attr; // c:1668
1948            }
1949            row0.push(cell);
1950        }
1951        for j in 0..(winw - t0 - lpromptw) {
1952            let atr = if j == 0 { prompt_attr } else { 0 }; // c:1669-1670
1953            row0.push(REFRESH_ELEMENT { chr: ' ', atr });
1954        }
1955        // c:1671 — the winw/winw+1 zr_zr terminators: append two so the row
1956        // matches the winw+2 padded layout the rest of the build uses.
1957        row0.push(REFRESH_ELEMENT::default());
1958        row0.push(REFRESH_ELEMENT::default());
1959        let mut nbuf = NBUF.lock().unwrap();
1960        if let Some(first) = nbuf.get_mut(0) {
1961            *first = row0;
1962        }
1963    } else {
1964        // c:1643-1657 — no scroll-off-top this frame: decide whether the right
1965        // prompt is shown this frame and where (put_rpmpt / rprompt_off). The
1966        // emit happens in the render loop below. All gated on a non-empty,
1967        // single-line right prompt that fits, so the common no-RPROMPT case
1968        // leaves put_rpmpt = 0 and changes nothing.
1969        if TRASHEDZLE.load(Ordering::SeqCst) != 0
1970            && isset(crate::ported::zsh_h::TRANSIENTRPROMPT)
1971        {
1972            // c:1645-1646 — transient right prompt: drop it once the line is
1973            // "trashed" (accepted / a new command started).
1974            PUT_RPMPT.store(0, Ordering::SeqCst);
1975        } else {
1976            let rp = crate::ported::zle::zle_main::rprompt();
1977            let winw = WINW.load(Ordering::SeqCst);
1978            let rpromptw = RPROMPTW.load(Ordering::SeqCst);
1979            // c:1648-1650 — `rprompth == 1 && rpromptbuf[0] && !strchr(rpromptbuf,'\t')`.
1980            let mut put = (RPROMPTH.load(Ordering::SeqCst) == 1
1981                && !rp.is_empty()
1982                && !rp.contains('\t')) as i32;
1983            if put != 0 {
1984                // c:1651-1654 — rprompt_off = rprompt_indent, clamped >= 0.
1985                rprompt_off = *crate::ported::params::RPROMPT_INDENT.lock().unwrap();
1986                if rprompt_off < 0 {
1987                    rprompt_off = 0;
1988                }
1989                // c:1655-1656 — it fits iff strlen(nbuf[0]) + rpromptw fits.
1990                let nlen = {
1991                    let nbuf = NBUF.lock().unwrap();
1992                    nbuf.first().map(|r| ZR_strlen(r) as i32).unwrap_or(0)
1993                };
1994                put = (nlen + rpromptw < winw - rprompt_off) as i32;
1995            }
1996            PUT_RPMPT.store(put, Ordering::SeqCst);
1997        }
1998    }
1999
2000    // ---- Render via the NBUF/OBUF diff (c:1700-1739) -------------------
2001    // OBUF holds the previous frame (set by the swap inside the build);
2002    // refreshline diffs each new line against it and emits the minimal
2003    // terminal updates, then we move the cursor to its editing position.
2004    let nlnct = NLNCT.load(Ordering::SeqCst);
2005    let olnct = OLNCT.load(Ordering::SeqCst);
2006    VCS.store(0, Ordering::SeqCst); // start from the home position
2007    VLN.store(0, Ordering::SeqCst);
2008    let saved_cleareol = CLEAREOL.load(Ordering::SeqCst);
2009    // c:1174 — `clearf = clearflag`: snapshot the clear flag for the loop.
2010    let clearf = CLEARFLAG.load(Ordering::SeqCst) != 0;
2011    let winw = WINW.load(Ordering::SeqCst);
2012    let hasam_v = crate::ported::init::hasam.load(Ordering::SeqCst) != 0;
2013    enum LineOp {
2014        None,
2015        Del,
2016        Ins,
2017    }
2018    for iln in 0..nlnct {
2019        // olnct mutates as we insert/delete lines below; read it fresh.
2020        let olnct_now = OLNCT.load(Ordering::SeqCst);
2021        // c:1672-1674 — if we have more lines than last time, clear the
2022        // newly-used lines. cleareol is sticky: once set at iln==olnct it
2023        // stays 1 for the rest of the loop, so every new line is cleared.
2024        if iln >= olnct_now {
2025            CLEAREOL.store(1, Ordering::SeqCst);
2026        }
2027
2028        // c:1677-1707 — if the old and new line differ, try to insert or
2029        // delete a whole line (scrolling the terminal) instead of
2030        // rewriting every following line. Only viable when the terminal
2031        // has the insert/delete-line capability (tccan), so headless
2032        // (tclen all zero) leaves the plain per-line path untouched.
2033        if !clearf
2034            && iln > 0
2035            && iln < olnct_now - 1
2036            && !(hasam_v && VCS.load(Ordering::SeqCst) == winw)
2037        {
2038            let tcan_del =
2039                tclen.lock().unwrap()[crate::ported::zsh_h::TCDELLINE as usize] != 0;
2040            let tcan_ins =
2041                tclen.lock().unwrap()[crate::ported::zsh_h::TCINSLINE as usize] != 0;
2042            let vmaxln = VMAXLN.load(Ordering::SeqCst);
2043            let i = iln as usize;
2044            // Decide the op under a brief lock on both video buffers.
2045            let op = {
2046                let nbuf = NBUF.lock().unwrap();
2047                let obuf = OBUF.lock().unwrap();
2048                let nb_i = nbuf.get(i);
2049                let ob_i = obuf.get(i);
2050                // c:1681-1682 — nbuf[iln] && obuf[iln] && they differ in 16.
2051                let outer = match (nb_i, ob_i) {
2052                    (Some(nb), Some(ob)) => ZR_strncmp(ob, nb, 16) != 0,
2053                    _ => false,
2054                };
2055                if !outer {
2056                    LineOp::None
2057                } else if tcan_del
2058                    // c:1683-1685 — obuf[iln+1] real, its first cell set,
2059                    // and obuf[iln+1] == nbuf[iln] in 16 → deleting line iln
2060                    // realigns the rest with one TCDELLINE.
2061                    && obuf
2062                        .get(i + 1)
2063                        .and_then(|r| r.first())
2064                        .map(|c| c.chr != '\0')
2065                        .unwrap_or(false)
2066                    && nb_i.is_some()
2067                    && ZR_strncmp(obuf.get(i + 1).unwrap(), nb_i.unwrap(), 16) == 0
2068                {
2069                    LineOp::Del
2070                } else if tcan_ins
2071                    && olnct_now < vmaxln
2072                    // c:1697-1698 — nbuf[iln+1] real, obuf[iln] real, and
2073                    // obuf[iln] == nbuf[iln+1] in 16 → inserting a line at
2074                    // iln realigns with one TCINSLINE.
2075                    && nbuf.get(i + 1).is_some()
2076                    && ob_i.is_some()
2077                    && ZR_strncmp(ob_i.unwrap(), nbuf.get(i + 1).unwrap(), 16) == 0
2078                {
2079                    LineOp::Ins
2080                } else {
2081                    LineOp::None
2082                }
2083            };
2084            match op {
2085                LineOp::Del => {
2086                    moveto(i, 0); // c:1686
2087                    tcout(crate::ported::zsh_h::TCDELLINE); // c:1687
2088                    // c:1688-1691 — free obuf[iln], shift the rest down,
2089                    // olnct--. Vec::remove models the pointer shuffle.
2090                    let mut obuf = OBUF.lock().unwrap();
2091                    if i < obuf.len() {
2092                        obuf.remove(i);
2093                    }
2094                    OLNCT.store(olnct_now - 1, Ordering::SeqCst);
2095                }
2096                LineOp::Ins => {
2097                    moveto(i, 0); // c:1699
2098                    tcout(crate::ported::zsh_h::TCINSLINE); // c:1700
2099                    // c:1701-1705 — shift obuf up, NULL the new line at iln,
2100                    // olnct++. Vec::insert of an empty row models the NULL.
2101                    let mut obuf = OBUF.lock().unwrap();
2102                    let at = i.min(obuf.len());
2103                    obuf.insert(at, Vec::new());
2104                    OLNCT.store(olnct_now + 1, Ordering::SeqCst);
2105                }
2106                LineOp::None => {}
2107            }
2108        }
2109
2110        refreshline(iln); // c:1710 — update each line
2111
2112        // c:1713-1725 — emit the right prompt on the first line when it fits
2113        // (put_rpmpt) and wasn't already on screen last frame (!oput_rpmpt).
2114        if PUT_RPMPT.load(Ordering::SeqCst) != 0
2115            && iln == 0
2116            && OPUT_RPMPT.load(Ordering::SeqCst) == 0
2117        {
2118            let rpromptw = RPROMPTW.load(Ordering::SeqCst);
2119            // c:1716 — `moveto(0, winw - rprompt_off - rpromptw)`.
2120            let col = (winw - rprompt_off - rpromptw).max(0) as usize;
2121            moveto(0, col);
2122            // c:1717-1718 — `treplaceattrs(pmpt_attr); applytextattributes(0);`
2123            crate::ported::prompt::treplaceattrs(PMPT_ATTR.load(Ordering::SeqCst));
2124            let mut out = crate::ported::prompt::applytextattributes(0);
2125            // c:1719 — `zputs(rpromptbuf, shout)`: the expanded right prompt
2126            // (already carries its own SGR escapes).
2127            out.push_str(&crate::ported::zle::zle_main::rprompt());
2128            {
2129                let fd = SHTTY.load(Ordering::Relaxed);
2130                let _ = write_loop(if fd >= 0 { fd } else { 1 }, out.as_bytes());
2131            }
2132            // c:1720-1724 — fix up the video cursor column after the emit.
2133            if rprompt_off != 0 {
2134                VCS.store(winw - rprompt_off, Ordering::SeqCst); // c:1721
2135            } else {
2136                zwcputc(&REFRESH_ELEMENT { chr: '\r', atr: 0 }); // c:1723 zputc(&zr_cr)
2137                VCS.store(0, Ordering::SeqCst); // c:1723
2138            }
2139            // c:1725 — the prompt's literal escapes leave the terminal in
2140            // rpmpt_attr; publish it so the next attr-diff is correct.
2141            crate::ported::prompt::treplaceattrs(RPMPT_ATTR.load(Ordering::SeqCst));
2142        }
2143    }
2144    CLEAREOL.store(saved_cleareol, Ordering::SeqCst);
2145    // c:1738 — `oput_rpmpt = put_rpmpt`: remember whether the right prompt is
2146    // on screen so next frame's emit gate (!oput_rpmpt) won't redraw it.
2147    OPUT_RPMPT.store(PUT_RPMPT.load(Ordering::SeqCst), Ordering::SeqCst);
2148    // c:1751-1752 — `if (nlnct > vmaxln) vmaxln = nlnct`: remember the
2149    // tallest frame we've drawn so the insert-line opt never scrolls past it.
2150    if nlnct > VMAXLN.load(Ordering::SeqCst) {
2151        VMAXLN.store(nlnct, Ordering::SeqCst);
2152    }
2153    // c:1727-1732 — clear any extra lines the previous frame had.
2154    let olnct = OLNCT.load(Ordering::SeqCst);
2155    if olnct > nlnct {
2156        CLEAREOL.store(1, Ordering::SeqCst);
2157        for iln in nlnct..olnct {
2158            refreshline(iln);
2159        }
2160        CLEAREOL.store(0, Ordering::SeqCst);
2161    }
2162    // c:1741 — `moveto(rpms.nvln, rpms.nvcs)`: cursor to the edit position
2163    // using the video coords the build tracked as it laid cells. This is
2164    // exact under hard newlines, tab/control expansion, and vertical scroll —
2165    // all of which the old `cursor_col / cols` recompute mishandled (it
2166    // assumed a single line wrapping at `cols`). For the single-line common
2167    // case nvcs == lpromptw + cursor_idx == cursor_col, so behaviour is
2168    // unchanged. Falls back to the recompute only when the cursor was never
2169    // reached (nvln stayed -1 because the line loop bailed on a scroll).
2170    let cols_c = cols.max(1);
2171    if cur_nvln >= 0 {
2172        moveto(cur_nvln as usize, cur_nvcs.max(0) as usize);
2173    } else {
2174        moveto(cursor_col / cols_c, cursor_col % cols_c);
2175    }
2176    // c:1742 — `cursor_form()`: update the terminal cursor shape (block /
2177    // beam / underline) for the current ZLE state once it's repositioned.
2178    crate::ported::zle::termquery::cursor_form();
2179
2180    // c:1750 — `onumscrolls = numscrolls;` carry this frame's scroll count into
2181    // the next frame, where nextline's bail heuristic (c:851) reads it. Must
2182    // happen at frame end (after the build), before the next frame resets
2183    // NUMSCROLLS at c:1194. (C's c:1748 `ovln = rpms.nvln` is intentionally not
2184    // ported: the file-static `ovln` has no reader anywhere in the zsh source —
2185    // the only `moveto(ovln, ...)` at c:1092 uses a same-named block local —
2186    // so it is dead write-only state.)
2187    ONUMSCROLLS.store(NUMSCROLLS.load(Ordering::SeqCst), Ordering::SeqCst);
2188}
2189
2190impl HighlightManager {
2191    /// `new` — see implementation.
2192    pub fn new() -> Self {
2193        HighlightManager {
2194            regions: Vec::new(),
2195            category_attrs: std::collections::HashMap::new(),
2196        }
2197    }
2198
2199    /// HighlightManager-internal helper: append a single region.
2200    /// Not a direct C port — `set_region_highlight` proper is the
2201    /// file-scope free fn below.
2202    pub fn add_region(&mut self, start: usize, end: usize, attr: TextAttr) {
2203        self.regions.push(RegionHighlight {
2204            start,
2205            end,
2206            attr,
2207            memo: None,
2208            flags: 0,
2209        });
2210    }
2211
2212    /// Get region highlight for position. Equivalent to
2213    /// `get_region_highlight()` from zle_refresh.c.
2214    pub fn get_region_highlight(&self, pos: usize) -> Option<&RegionHighlight> {
2215        self.regions.iter().find(|r| pos >= r.start && pos < r.end)
2216    }
2217
2218    /// Unset region highlight. Equivalent to
2219    /// `unset_region_highlight()` from zle_refresh.c.
2220    pub fn unset_region_highlight(&mut self) {
2221        self.regions.clear();
2222    }
2223
2224    /// Free highlight resources. Equivalent to
2225    /// `zle_free_highlight()` from zle_refresh.c.
2226    pub fn free(&mut self) {
2227        self.regions.clear();
2228    }
2229}
2230
2231/// Port of `wpfxlen(const REFRESH_ELEMENT *olds, const REFRESH_ELEMENT *news)` from `Src/Zle/zle_refresh.c:1736`.
2232/// ```c
2233/// static int
2234/// wpfxlen(const REFRESH_ELEMENT *olds, const REFRESH_ELEMENT *news) {
2235///     int i = 0;
2236///     while (olds->chr && ZR_equal(*olds, *news))
2237///         olds++, news++, i++;
2238///     return i;
2239/// }
2240/// ```
2241/// Common-prefix length of two REFRESH_ELEMENT strings; stops at
2242/// the first NUL chr in `olds` or first cell that differs in chr+atr.
2243pub fn wpfxlen(olds: &[REFRESH_ELEMENT], news: &[REFRESH_ELEMENT]) -> usize {
2244    let mut i = 0;
2245    while i < olds.len() && i < news.len() && olds[i].chr != '\0' && olds[i] == news[i] {
2246        i += 1;
2247    }
2248    i
2249}
2250
2251/// Port of `static void refreshline(int ln)` from
2252/// `Src/Zle/zle_refresh.c:1749`. Repaints a single screen row at
2253/// `ln` from `nbuf[ln]` against `obuf[ln]`: handles `cleareol`,
2254/// auto-margin (`hasam`) edge cases, char-insert / char-delete
2255/// terminal capabilities (`TCDEL`/`TCINS`), and the
2256/// `MULTIBYTE_SUPPORT` `WEOF` width-padding cells.
2257///
2258/// ```c
2259/// static void
2260/// refreshline(int ln)
2261/// {
2262///     REFRESH_STRING nl, ol, p1;
2263///     int ccs = 0, char_ins = 0, col_cleareol, i, j;
2264///     int ins_last, nllen, ollen, rnllen;
2265///     const REFRESH_ELEMENT zr_pad = { ZWC(' '), prompt_attr };
2266/// /* 0: setup */
2267///     nl = nbuf[ln];
2268///     rnllen = nllen = nl ? ZR_strlen(nl) : 0;
2269///     if (ln < olnct && obuf[ln]) { ol = obuf[ln]; ollen = ZR_strlen(ol); }
2270///     else { static REFRESH_ELEMENT nullchr = { ZWC('\0'), 0 };
2271///            ol = &nullchr; ollen = 0; }
2272/// /* optimisation: clear-eol short-circuit */
2273///     if (cleareol && !nllen && !(hasam && ln < nlnct - 1)
2274///         && tccan(TCCLEAREOL)) { moveto(ln, 0); tcoutclear(TCCLEAREOL); return; }
2275/// /* 1: pad new buffer */
2276///     if (cleareol || (!nllen && (ln != 0 || !put_rpmpt))
2277///         || (ln == 0 && (put_rpmpt != oput_rpmpt))) { ... pad to winw ... }
2278///     else if (ollen > nllen) { ... pad to ollen ... }
2279/// /* 2: clear-to-eol calculation */
2280///     if (hasam && ln < nlnct - 1 && rnllen == winw) col_cleareol = -2;
2281///     else { ... compute col_cleareol from TCCLEAREOL cost ... }
2282/// /* 2b: automargin niceness */
2283///     if (hasam && vcs == winw) { ... advance vln/vcs ... }
2284///     ins_last = 0;
2285/// /* 2c: prompt-line head skip */
2286///     if (ln == 0 && lpromptw) { ... advance past prompt ... }
2287/// /* 3: main display loop */
2288///     for (;;) { ... skip-match / insert-delete / fall-through ... }
2289/// }
2290/// ```
2291///
2292/// Per PORT.md Rule 9 (function bodies port too) the structural
2293/// translation lives here even though the screen-output primitives
2294/// (`tccan`/`tc_inschars`/`tc_delchars`/`zputc`/`zwrite`/
2295/// `wpfxlen`/`tcinscost`/`tcdelcost`/`treplaceattrs`/
2296/// `applytextattributes`) and the `nbuf`/`obuf`/`vcs`/`vln`/
2297/// `winw`/`winh`/`zterm_lines`/`hasam`/`cleareol`/`put_rpmpt`/
2298/// `oput_rpmpt`/`olnct`/`prompt_attr` statics are not yet
2299/// surface-area in zshrs. Stubbed deps are flagged inline with
2300/// `// !!! STUB: needs <c-name> port at <Src/file.c:NNNN>`.
2301pub fn refreshline(ln: i32) {
2302    // c:1749
2303
2304    // c:1751 — REFRESH_STRING nl, ol, p1. The nbuf/obuf statics are now
2305    // exposed (NBUF/OBUF, populated by zrefresh) and read below. The diff
2306    // control flow and all output primitives are ported: zputc (zwcputc),
2307    // zwrite (zwcwrite), tc_delchars (c:1993/2048) and tc_inschars (c:2074)
2308    // all emit through SHTTY, and tclen drives the cost/capability checks.
2309    // c:1762 — `nl = nbuf[ln];` — read this frame's new line from NBUF.
2310    let mut nl: REFRESH_STRING = NBUF
2311        .lock()
2312        .unwrap()
2313        .get(ln as usize)
2314        .cloned()
2315        .unwrap_or_default();
2316    // c:1764-1766 — `if (ln < olnct && obuf[ln]) ol = obuf[ln];` else null.
2317    let mut ol: REFRESH_STRING = if ln < OLNCT.load(Ordering::SeqCst) {
2318        OBUF.lock()
2319            .unwrap()
2320            .get(ln as usize)
2321            .cloned()
2322            .unwrap_or_default()
2323    } else {
2324        Vec::new()
2325    };
2326    let _p1: REFRESH_STRING = Vec::new(); // c:1751 p1
2327    let mut ccs: i32 = 0; // c:1752 ccs = 0
2328    let mut char_ins: i32 = 0; // c:1753 char_ins = 0
2329    let mut col_cleareol: i32; // c:1754
2330    let mut i: i32; // c:1755 tmp
2331    let mut _j: i32 = 0; // c:1755 tmp
2332    let mut ins_last: i32; // c:1756
2333    let mut nllen: i32; // c:1757
2334    let ollen: i32; // c:1757
2335    let rnllen: i32; // c:1758
2336    // c:1817 — `const REFRESH_ELEMENT zr_pad = { ZWC(' '), prompt_attr };`. The
2337    // padding cell carries prompt_attr (not 0) so cells cleared/extended by the
2338    // diff match the prompt's colour — otherwise a coloured prompt's cleared
2339    // tail would render in the default attribute.
2340    let zr_pad = REFRESH_ELEMENT {
2341        chr: ' ',
2342        atr: PROMPT_ATTR.load(Ordering::SeqCst),
2343    };
2344
2345    // 0: setup                                                          // c:1761
2346    // nl = nbuf[ln]; rnllen = nllen = nl ? ZR_strlen(nl) : 0;           // c:1762-1763
2347    // ZR_strlen counts cells up to the `\0` terminator — NOT the Vec
2348    // length, which now includes the winw+2 padding the build writes. An
2349    // "empty" row is one whose first cell is `\0`, giving nllen 0 (which the
2350    // cleareol short-circuit at c:1776 relies on).
2351    rnllen = ZR_strlen(&nl) as i32;
2352    nllen = rnllen;
2353    // c:1764-1772 — `ollen = ZR_strlen(ol)`.
2354    ollen = ZR_strlen(&ol) as i32; // c:1766 / c:1771
2355
2356    // optimisation: clear-eol short-circuit                             // c:1774-1775
2357    // c:1776-1781 — `if (cleareol && !nllen && !(hasam && ln < nlnct-1)
2358    //               && tccan(TCCLEAREOL)) { moveto(ln, 0);
2359    //               tcoutclear(TCCLEAREOL); return; }`
2360    let cleareol = CLEAREOL.load(Ordering::SeqCst) != 0;
2361    let hasam_v = crate::ported::init::hasam.load(Ordering::SeqCst) != 0; // c:1776
2362    let nlnct_v = NLNCT.load(Ordering::SeqCst);
2363    if cleareol                                                          // c:1776
2364            && nllen == 0
2365            && !(hasam_v && ln < nlnct_v - 1)
2366            && (tclen.lock().unwrap()[TCCLEAREOL as usize] != 0)
2367    /* tccan(TCCLEAREOL) per zsh.h:2682 */                       // c:1777
2368    {
2369        moveto(ln as usize, 0); // c:1778
2370        tcoutclear(TCCLEAREOL); // c:1779
2371        return; // c:1780
2372    }
2373
2374    // 1: pad out new buffer with spaces                                 // c:1783-1784
2375    let put_rpmpt = PUT_RPMPT.load(Ordering::SeqCst);
2376    let oput_rpmpt = OPUT_RPMPT.load(Ordering::SeqCst);
2377    let winw = WINW.load(Ordering::SeqCst);
2378    if cleareol                                                          // c:1786
2379            || (nllen == 0 && (ln != 0 || put_rpmpt == 0))                   // c:1787
2380            || (ln == 0 && put_rpmpt != oput_rpmpt)
2381    // c:1788
2382    {
2383        // !!! STUB: zhalloc — Rust uses Vec growth instead of arena alloc.
2384        let mut padded: REFRESH_STRING =       // c:1789
2385                Vec::with_capacity((winw + 2) as usize);
2386        for el in nl.iter().take(nllen as usize) {
2387            // c:1790-1791 ZR_memcpy
2388            padded.push(*el);
2389        }
2390        for _ in nllen..winw {
2391            // c:1792 ZR_memset(.., zr_sp, ..)
2392            padded.push(REFRESH_ELEMENT { chr: ' ', atr: 0 });
2393        }
2394        padded.push(REFRESH_ELEMENT { chr: '\0', atr: 0 }); // c:1793 p1[winw] = zr_zr
2395        if nllen < winw {
2396            // c:1794
2397            padded.push(REFRESH_ELEMENT { chr: '\0', atr: 0 });
2398        // c:1795
2399        } else if let Some(extra) = nl.get((winw + 1) as usize).copied() {
2400            // c:1796-1797
2401            padded.push(extra);
2402        }
2403        // c:1798-1801 — if (ln && nbuf[ln]) memcpy back to nl, else nl = p1
2404        if ln != 0 && !nl.is_empty() {
2405            // c:1798
2406            let copy_len = ((winw + 2) as usize).min(padded.len()); // c:1799
2407            if nl.len() >= copy_len {
2408                for k in 0..copy_len {
2409                    nl[k] = padded[k];
2410                }
2411            } else {
2412                nl = padded.clone();
2413            }
2414        } else {
2415            // c:1800
2416            nl = padded; // c:1801
2417        }
2418        nllen = winw; // c:1802
2419    } else if ollen > nllen {
2420        // c:1803
2421        // c:1804-1809 — pad nl with zr_pad up to ollen.
2422        let mut padded: REFRESH_STRING =       // c:1804
2423                Vec::with_capacity((ollen + 1) as usize);
2424        for el in nl.iter().take(nllen as usize) {
2425            // c:1805
2426            padded.push(*el);
2427        }
2428        for _ in nllen..ollen {
2429            // c:1806
2430            padded.push(zr_pad);
2431        }
2432        padded.push(REFRESH_ELEMENT { chr: '\0', atr: 0 }); // c:1807
2433        nl = padded; // c:1808
2434        nllen = ollen; // c:1809
2435    }
2436
2437    // 2: clear-to-eol calculation                                       // c:1812-1815
2438    if hasam_v && ln < nlnct_v - 1 && rnllen == winw {
2439        // c:1817
2440        col_cleareol = -2; // c:1818 evil — don't
2441    } else {
2442        // c:1819
2443        col_cleareol = -1; // c:1820
2444        if (tclen.lock().unwrap()[TCCLEAREOL as usize] != 0)  /* tccan(TCCLEAREOL) per zsh.h:2682 */                       // c:1821
2445                && (nllen == winw || put_rpmpt != oput_rpmpt)
2446        {
2447            // c:1822-1832 — backward-scan to find trailing-space cutoff.
2448            let a = nl.get((nllen - 1) as usize).map(|e| e.atr).unwrap_or(0); // c:1822
2449            let mut i_loc = nllen; // c:1823
2450            while i_loc > 0
2451                && nl
2452                    .get((i_loc - 1) as usize)
2453                    .map(|e| e.chr == ' ' && e.atr == a)
2454                    .unwrap_or(false)
2455            {
2456                i_loc -= 1; // c:1823
2457            }
2458            if nllen == winw && i_loc < nllen {
2459                // c:1825
2460                col_cleareol = i_loc; // c:1826
2461            } else {
2462                // c:1827
2463                let a = ol.get((ollen - 1) as usize).map(|e| e.atr).unwrap_or(0); // c:1828
2464                let mut j_loc = ollen; // c:1829
2465                while j_loc > 0
2466                    && ol
2467                        .get((j_loc - 1) as usize)
2468                        .map(|e| e.chr == ' ' && e.atr == a)
2469                        .unwrap_or(false)
2470                {
2471                    j_loc -= 1; // c:1829
2472                }
2473                // c:1831 — `if (j > i + tclen[TCCLEAREOL])`: clearing to
2474                // end-of-line is only worth it when the trailing-blank run
2475                // exceeds the cost (capability length) of the clear-eol
2476                // escape. tclen is populated by the termcap loader
2477                // (init.rs:108/758); read the real cost instead of 1.
2478                let tclen_clear: i32 = tclen.lock().unwrap()[TCCLEAREOL as usize];
2479                if j_loc > i_loc + tclen_clear {
2480                    // c:1831
2481                    col_cleareol = i_loc; // c:1832
2482                }
2483            }
2484        }
2485    }
2486
2487    // 2b: automargin niceness                                           // c:1837
2488    // c:1751 — `vcs` is a live video-cursor column. In C it is a global
2489    // that `moveto` and every write path keep current; the Rust port must
2490    // track it the same way (resync to the moveto target, accumulate on
2491    // writes) rather than snapshot it, or the diff engine's column
2492    // accounting drifts across loop iterations.
2493    let mut vcs = VCS.load(Ordering::SeqCst);
2494    let mut vln = VLN.load(Ordering::SeqCst);
2495    if hasam_v && vcs == winw {
2496        // c:1839
2497        // c:1898 — `if (nbuf[vln] && nbuf[vln][vcs + 1].chr == ZWC('\n'))`.
2498        let next_is_nl = {
2499            let nbuf = NBUF.lock().unwrap();
2500            nbuf.get(vln as usize)
2501                .and_then(|row| row.get((vcs + 1) as usize))
2502                .map(|cell| cell.chr == '\n')
2503                .unwrap_or(false)
2504        };
2505        if next_is_nl {
2506            vln += 1; // c:1899 vln++, vcs = 1
2507            vcs = 1;
2508            VLN.store(vln, Ordering::SeqCst);
2509            VCS.store(1, Ordering::SeqCst);
2510            // c:1900-1903 — output the first cell of the next line, or a
2511            // space if that cell is blank ("I don't think this should
2512            // happen", per the C comment).
2513            let first_cell = {
2514                let nbuf = NBUF.lock().unwrap();
2515                nbuf.get(vln as usize)
2516                    .and_then(|row| row.first())
2517                    .copied()
2518                    .filter(|c| c.chr != '\0')
2519            };
2520            match first_cell {
2521                // c:1901 — `zputc(nbuf[vln])`: emit the FULL cell (chr + atr) so
2522                // the wrapped line's first glyph keeps its attribute (was emitted
2523                // with atr 0, dropping colour — same fault as the deferred-char
2524                // path at c:1967).
2525                Some(cell) => zwcputc(&cell),
2526                None => zwcputc(&REFRESH_ELEMENT { chr: ' ', atr: 0 }), // c:1903 zr_sp
2527            }
2528            if ln == vln {
2529                // c:1904 — better safe than sorry
2530                if !nl.is_empty() {
2531                    nl.remove(0); // c:1905 nl++
2532                }
2533                if !ol.is_empty() && ol[0].chr != '\0' {
2534                    ol.remove(0); // c:1906-1907 ol++
2535                }
2536                ccs = 1; // c:1908
2537            }
2538        } else {
2539            vln += 1; // c:1911 vln++, vcs = 0
2540            vcs = 0;
2541            VLN.store(vln, Ordering::SeqCst);
2542            VCS.store(0, Ordering::SeqCst);
2543            zwcputc(&REFRESH_ELEMENT { chr: '\n', atr: 0 }); // c:1912 zr_nl
2544        }
2545    }
2546    ins_last = 0; // c:1857
2547
2548    // 2c: prompt-line head skip                                         // c:1859-1860
2549    let lpromptw = LPROMPTW.load(Ordering::SeqCst);
2550    if ln == 0 && lpromptw != 0 {
2551        // c:1862
2552        i = lpromptw - ccs; // c:1863
2553        let j_loc = ol.len() as i32; // c:1864 j = ZR_strlen(ol)
2554                                     // c:1865 — nl += i (skip i cells)
2555        for _ in 0..i.min(nl.len() as i32) {
2556            nl.remove(0);
2557        }
2558        // c:1866 — ol += (i > j ? j : i)
2559        let ol_skip = if i > j_loc { j_loc } else { i };
2560        for _ in 0..ol_skip.min(ol.len() as i32) {
2561            ol.remove(0);
2562        }
2563        ccs = lpromptw; // c:1867
2564    }
2565
2566    // c:1933-1937 — realign to a real character after the prompt-head skip may
2567    // have landed nl on a wide-glyph WEOF continuation cell: skip the WEOF
2568    // cells, advancing the column counters, and advance ol in step while it
2569    // still has content. Now meaningful: ZWC_WEOF gives wide glyphs a distinct
2570    // continuation cell (previously '\0'-conflated, so this was a no-op).
2571    while nl.first().map(|c| c.chr == ZWC_WEOF).unwrap_or(false) {
2572        nl.remove(0); // c:1934 — nl++
2573        ccs += 1; // c:1934
2574        vcs += 1; // c:1934
2575        if ol.first().map(|c| c.chr != '\0').unwrap_or(false) {
2576            ol.remove(0); // c:1935-1936 — if (ol->chr) ol++
2577        }
2578    }
2579    VCS.store(vcs, Ordering::SeqCst);
2580
2581    // 3: main display loop                                              // c:1882
2582    loop {
2583        // c:1884
2584        // c:1888 — `if (nl->chr && ol->chr && ZR_equal(ol[1], nl[1]))`
2585        let nl_first = nl.first().copied();
2586        let ol_first = ol.first().copied();
2587        let nl_second = nl.get(1).copied();
2588        let ol_second = ol.get(1).copied();
2589        if nl_first.map(|e| e.chr != '\0').unwrap_or(false)              // c:1888
2590                && ol_first.map(|e| e.chr != '\0').unwrap_or(false)
2591                && nl_second == ol_second
2592        {
2593            // c:1894 — skip past matching cells.
2594            while !nl.is_empty()                                         // c:1894
2595                    && nl[0].chr != '\0'
2596                    && !ol.is_empty()
2597                    && nl[0] == ol[0]
2598            {
2599                nl.remove(0); // c:1894
2600                ol.remove(0);
2601                ccs += 1;
2602            }
2603        }
2604
2605        // c:1906 — `if (!nl->chr)`
2606        if nl.is_empty() || nl[0].chr == '\0' {
2607            // c:1906
2608            if ccs == winw && hasam_v && char_ins > 0 && ins_last != 0   // c:1964
2609                    && vcs != winw
2610            {
2611                // c:1965-1971 — write the deferred last character. C does
2612                // `nl--; moveto(ln, winw-1); zputc(nl); vcs++`. In the Vec
2613                // model `nl--` steps back to column winw-1; this automargin
2614                // case has a full line, so that cell is NBUF[ln][winw-1].
2615                let deferred = NBUF
2616                    .lock()
2617                    .unwrap()
2618                    .get(ln as usize)
2619                    .and_then(|row| row.get((winw - 1) as usize))
2620                    .copied();
2621                moveto(ln as usize, (winw - 1) as usize); // c:1966
2622                vcs = winw - 1; // moveto repositioned the cursor
2623                if let Some(cell) = deferred {
2624                    // c:1967 — `zputc(nl)`: emit the FULL cell (chr + atr), so
2625                    // the deferred automargin glyph keeps its colour. The
2626                    // earlier port read only `chr` and emitted atr 0, dropping
2627                    // the attribute.
2628                    zwcputc(&cell);
2629                }
2630                vcs += 1; // c:1968 vcs++
2631                VCS.store(vcs, Ordering::SeqCst);
2632                return; // c:1969
2633            }
2634            if char_ins <= 0 || ccs >= winw {
2635                // c:1915
2636                return; // c:1916 written everything
2637            }
2638            // c:1975 — `if (tccan(TCCLEAREOL) && char_ins >= tclen[TCCLEAREOL]
2639            //            && col_cleareol != -2)`. Read tclen[TCCLEAREOL] once
2640            // (Mutex isn't reentrant) and use it for both the tccan != 0
2641            // check and the char_ins cost comparison.
2642            let tcleareol_len = tclen.lock().unwrap()[TCCLEAREOL as usize];
2643            if tcleareol_len != 0  /* tccan(TCCLEAREOL) */
2644                    && char_ins >= tcleareol_len  // c:1975
2645                    && col_cleareol != -2
2646            {
2647                col_cleareol = 0; // c:1920
2648            }
2649        }
2650
2651        moveto(ln as usize, ccs as usize); // c:1923
2652        vcs = ccs; // c:1923 — moveto leaves the cursor (vcs) at ccs
2653
2654        // c:1925-1929 — if we can finish via clear-to-eol, do so
2655        if col_cleareol >= 0 && ccs >= col_cleareol {
2656            // c:1926
2657            tcoutclear(TCCLEAREOL); // c:1927 tcoutclear(TCCLEAREOL)
2658            return; // c:1928
2659        }
2660
2661        // c:1932-1942 — empty nl: pad with spaces or delete chars.
2662        if nl.is_empty() || nl[0].chr == '\0' {
2663            // c:1932
2664            let i_pad = if winw - ccs < char_ins {
2665                // c:1933
2666                winw - ccs
2667            } else {
2668                char_ins
2669            };
2670            // c:1934 — `tccan(TCDEL) && tcdelcost(i) <= i + 1`. Read tclen[TCDEL]
2671            // into a local first: tcdelcost re-locks tclen and std Mutex isn't
2672            // reentrant. The earlier port faked the cost gate as `i_pad <= i_pad
2673            // + 1` (always true) — that bypassed the real tcdelcost comparison,
2674            // so it would delete-chars even when padding with spaces is cheaper.
2675            let tcdel_len = tclen.lock().unwrap()[TCDEL as usize];
2676            let can_del = tcdel_len != 0  /* tccan(TCDEL) per zsh.h:2682 */
2677                && tcdelcost(i_pad) <= i_pad + 1;
2678            if can_del {
2679                // c:1993 — `tc_delchars(i)`: delete `i_pad` chars via the
2680                // terminal's delete-char capability (now ported).
2681                tc_delchars(i_pad);
2682            } else {
2683                // c:1996 — `vcs += i`.
2684                vcs += i_pad;
2685                VCS.store(vcs, Ordering::SeqCst);
2686                // c:1996-1997 — `while (i-- > 0) zputc(&zr_pad)`: pad the
2687                // overwritten run with zr_pad (space + prompt_attr), so the
2688                // cleared cells carry the prompt's colour.
2689                for _ in 0..i_pad {
2690                    zwcputc(&zr_pad); // c:1997 zputc(&zr_pad)
2691                }
2692            }
2693            return; // c:2002
2694        }
2695
2696        // c:1946 — `if (!ol->chr)`
2697        if ol.is_empty() || ol[0].chr == '\0' {
2698            // c:1946
2699            let i_remain = if col_cleareol >= 0 {
2700                col_cleareol
2701            } else {
2702                nllen
2703            }; // c:1947
2704            let i_write = i_remain - vcs; // c:1948
2705            if i_write > 0 {
2706                // c:1949 (DPUTS guard)
2707                // c:1958 — `zwrite(nl, i)`: emit the new line's first
2708                // `i_write` cells (zwcwrite loops zwcputc over them).
2709                zwcwrite(&nl, i_write as usize);
2710                vcs += i_write; // c:1959 vcs += i
2711                VCS.store(vcs, Ordering::SeqCst);
2712            }
2713            if col_cleareol >= 0 {
2714                // c:1960
2715                tcoutclear(TCCLEAREOL); // c:1961
2716            }
2717            return; // c:1962
2718        }
2719
2720        // c:1965-1970 — insert/delete eligibility
2721        let eligible = (ln != 0 || put_rpmpt == 0 || oput_rpmpt == 0)
2722            && !nl.is_empty()
2723            && nl_second.map(|e| e.chr != '\0').unwrap_or(false)
2724            && !ol.is_empty()
2725            && ol_second.map(|e| e.chr != '\0').unwrap_or(false)
2726            && ol_second != nl_second;
2727        if eligible {
2728            // c:1965
2729            // c:1976-2006 — TCDEL try-block: find a series we can delete
2730            if (tclen.lock().unwrap()[TCDEL as usize] != 0)
2731            /* tccan(TCDEL) per zsh.h:2682 */
2732            {
2733                // c:1976
2734                let mut first = true; // c:1977 — apply text-area attrs once
2735                let mut i_try = 1i32; // c:1978
2736                while (i_try as usize) < ol.len() && ol[i_try as usize].chr != '\0' {
2737                    // c:1979 — `tcdelcost(i) < wpfxlen(ol + i, nl)`
2738                    let ol_tail = &ol[i_try as usize..];
2739                    let cheap_delete = tcdelcost(i_try) < wpfxlen(ol_tail, &nl) as i32;
2740                    if cheap_delete {
2741                        // c:2042-2047 — some terminals output the current
2742                        // attributes into the cells a deletion adds at the
2743                        // end, so apply the text-area attrs once before the
2744                        // first delete: `treplaceattrs(prompt_attr);
2745                        // applytextattributes(0);` (both ported in prompt.rs).
2746                        if first {
2747                            crate::ported::prompt::treplaceattrs(
2748                                PROMPT_ATTR.load(Ordering::SeqCst),
2749                            ); // c:2045
2750                            let sgr = crate::ported::prompt::applytextattributes(0); // c:2046
2751                            if !sgr.is_empty() {
2752                                let fd = SHTTY.load(Ordering::Relaxed);
2753                                let _ = write_loop(if fd >= 0 { fd } else { 1 }, sgr.as_bytes());
2754                            }
2755                            first = false; // c:2047
2756                        }
2757                        // c:2048 — `tc_delchars(i)`: delete `i` characters.
2758                        tc_delchars(i_try);
2759                        for _ in 0..i_try {
2760                            if !ol.is_empty() {
2761                                ol.remove(0);
2762                            } // c:2049 ol += i
2763                        }
2764                        char_ins -= i_try; // c:2050
2765                        // c:2053-2056 — skip WEOF continuation cells in ol so
2766                        // the delete doesn't leave ol mid-wide-glyph (char_ins--
2767                        // per skipped column).
2768                        while ol.first().map(|c| c.chr == ZWC_WEOF).unwrap_or(false) {
2769                            ol.remove(0);
2770                            char_ins -= 1;
2771                        }
2772                        i_try = 0; // c:2004
2773                        break;
2774                    }
2775                    i_try += 1;
2776                }
2777                // c:2061-2062 — `if (!i) continue;`. i_try==0 means a cheap
2778                // delete fired (ol advanced, char_ins decreased): restart the
2779                // main loop to re-diff from the new position. A nonzero i_try
2780                // means no delete was found — fall through to the insert try.
2781                // (The earlier port inverted this to `!= 0`, which restarted
2782                // the loop with NO progress when no delete was found → an
2783                // infinite loop the moment TCDEL is available.)
2784                if i_try == 0 {
2785                    continue;
2786                }
2787            }
2788
2789            // c:2012-2060 — TCINS try-block: find chars to insert.
2790            let zterm_lines = WINH.load(Ordering::SeqCst);
2791            if (tclen.lock().unwrap()[TCINS as usize] != 0)  /* tccan(TCINS) per zsh.h:2682 */ && vln != zterm_lines - 1
2792            {
2793                // c:2012
2794                let mut i_try = 1i32; // c:2014
2795                while (i_try as usize) < nl.len() && nl[i_try as usize].chr != '\0' {
2796                    // c:2015 — `tcinscost(i) < wpfxlen(ol, nl + i)`
2797                    let nl_tail = &nl[i_try as usize..];
2798                    let cheap_insert = tcinscost(i_try) < wpfxlen(&ol, nl_tail) as i32;
2799                    if cheap_insert {
2800                        // c:2074-2076 — emit the terminal's insert-`i`-chars
2801                        // sequence, write the `i` new cells at the cursor, then
2802                        // advance nl past them. The earlier port advanced nl but
2803                        // emitted nothing (the diff math ran without producing
2804                        // output) — this wires the real TCINS+zwrite primitives.
2805                        tc_inschars(i_try); // c:2074 — tc_inschars(i)
2806                        zwcwrite(&nl, i_try as usize); // c:2075 — zwrite(nl, i)
2807                        for _ in 0..i_try {
2808                            if !nl.is_empty() {
2809                                nl.remove(0);
2810                            } // c:2076 — nl += i
2811                        }
2812                        // c:2078-2081 — skip WEOF continuation cells so the
2813                        // insert doesn't leave nl mid-wide-glyph; these count
2814                        // toward the advanced columns (i++ in C).
2815                        let mut i_eff = i_try;
2816                        while nl.first().map(|c| c.chr == ZWC_WEOF).unwrap_or(false) {
2817                            nl.remove(0);
2818                            i_eff += 1;
2819                        }
2820                        char_ins += i_eff; // c:2025 (incl WEOF skip)
2821                        vcs += i_eff;
2822                        VCS.store(vcs, Ordering::SeqCst);
2823                        ccs += i_eff; // c:2026
2824                                      // c:2031-2047 — truncate oldline if past right edge.
2825                        let mut k = 0i32;
2826                        while (k as usize) < ol.len()
2827                            && ol[k as usize].chr != '\0'
2828                            && k < winw - ccs
2829                        {
2830                            k += 1; // c:2031-2032
2831                        }
2832                        if k >= winw - ccs && (k as usize) < ol.len() {
2833                            // c:2049
2834                            ol[k as usize] = REFRESH_ELEMENT {
2835                                chr: '\0',
2836                                atr: 0, // c:2050 ol[i] = zr_zr
2837                            };
2838                            ins_last = 1; // c:2051
2839                        }
2840                        i_try = 0; // c:2054
2841                        break;
2842                    }
2843                    i_try += 1;
2844                }
2845                // c:2116-2117 — `if (!i) continue;`. Same as the delete try:
2846                // i_try==0 means a cheap insert fired (nl advanced) — restart
2847                // the main loop; nonzero means none found — fall through to the
2848                // single-char fallback. (Earlier port inverted to `!= 0`.)
2849                if i_try == 0 {
2850                    continue;
2851                }
2852            }
2853        }
2854
2855        // c:2065-2096 — fallback: dump one char and keep going
2856        if !nl.is_empty() && nl[0].chr == '\0' {
2857            // c:2072
2858            break; // c:2073
2859        }
2860        loop {
2861            // c:2074 do-while wrapper
2862            // c:2076-2084 — `treplaceattrs(nl->atr); applytextattributes(0);
2863            // zputc(nl);` — emit the cell. zwcputc does the attr-change
2864            // (treplaceattrs + applytextattributes) internally, then writes
2865            // the char. This is the main per-cell overwrite emit.
2866            if let Some(cell) = nl.first().cloned() {
2867                zwcputc(&cell); // c:2076-2084
2868            }
2869            if !nl.is_empty() {
2870                nl.remove(0);
2871            } // c:2085 nl++
2872            if !ol.is_empty() && ol[0].chr != '\0' {
2873                // c:2086-2087
2874                ol.remove(0); // c:2087 ol++
2875            }
2876            ccs += 1; // c:2088
2877            vcs += 1; // c:2089 vcs++
2878            VCS.store(vcs, Ordering::SeqCst);
2879
2880            // c:2090-2095 — "Make sure we always overwrite the complete width
2881            // of a character that was there before." Keep emitting while either
2882            // side sits on a WEOF continuation cell and the other side still has
2883            // a real char to consume, so a wide glyph is never left half-drawn.
2884            // The vecs retain their trailing zr_zr ('\0') terminator, so an
2885            // exhausted side reads as chr '\0' (C: `nl->chr`/`ol->chr` == 0).
2886            let ol_chr = ol.first().map(|c| c.chr).unwrap_or('\0');
2887            let nl_chr = nl.first().map(|c| c.chr).unwrap_or('\0');
2888            // c:2094-2095 — `(ol->chr==WEOF && nl->chr)||(nl->chr==WEOF && ol->chr)`
2889            if (ol_chr == ZWC_WEOF && nl_chr != '\0') || (nl_chr == ZWC_WEOF && ol_chr != '\0')
2890            {
2891                continue;
2892            }
2893            break;
2894        }
2895    }
2896
2897    let _ = (rnllen, ollen, ins_last, _p1, _j); // silence
2898}
2899
2900/// Direct port of `void moveto(int ln, int cl)` from
2901/// `Src/Zle/zle_refresh.c:2163`. Move the video cursor to (`ln`, `cl`)
2902/// using relative terminal movement (the previous port teleported with
2903/// an absolute CSI H, which cannot create lines that don't exist yet).
2904/// Operates on the global `vcs`/`vln` (the VCS/VLN atomics) exactly as C:
2905///   - automargin wrap when at the right margin (c:2167-2182)
2906///   - early return when already at the target (c:2184)
2907///   - up-movement via `tc_upcurs` (c:2188-2191)
2908///   - down-movement: `tc_downcurs` while on-screen, real newlines past
2909///     `vmaxln-1` to scroll/create lines (c:2195-2212)
2910///   - horizontal close via `singmoveto` (c:2214-2215)
2911pub fn moveto(row: usize, col: usize) {
2912    // c:2163
2913    let zr_cr = REFRESH_ELEMENT { chr: '\r', atr: 0 };
2914    let zr_nl = REFRESH_ELEMENT { chr: '\n', atr: 0 };
2915    let zr_sp = REFRESH_ELEMENT { chr: ' ', atr: 0 };
2916    let ln = row as i32;
2917    let cl = col as i32;
2918    let winw = WINW.load(Ordering::SeqCst);
2919    let hasam_v = crate::ported::init::hasam.load(Ordering::SeqCst) != 0;
2920    let mut vcs = VCS.load(Ordering::SeqCst);
2921    let mut vln = VLN.load(Ordering::SeqCst);
2922
2923    // c:2167 — `if (vcs == winw)`: wrap off the right margin.
2924    if vcs == winw {
2925        vln += 1; // c:2168 vln++, vcs = 0
2926        vcs = 0;
2927        if !hasam_v {
2928            // c:2170-2171 — no automargin: CR + NL.
2929            zwcputc(&zr_cr);
2930            zwcputc(&zr_nl);
2931        } else {
2932            // c:2173-2176 — rep = first cell of nbuf[vln] if real, else space.
2933            let nlnct = NLNCT.load(Ordering::SeqCst);
2934            let rep = {
2935                let nbuf = NBUF.lock().unwrap();
2936                nbuf.get(vln as usize)
2937                    .filter(|_| vln < nlnct)
2938                    .and_then(|r| r.first())
2939                    .copied()
2940                    .filter(|c| c.chr != '\0')
2941                    .unwrap_or(zr_sp)
2942            };
2943            zwcputc(&rep); // c:2177
2944            zwcputc(&zr_cr); // c:2178
2945            // c:2179-2181 — `if (vln<olnct && obuf[vln] && obuf[vln]->chr)
2946            //                  *obuf[vln] = *rep;`
2947            let olnct = OLNCT.load(Ordering::SeqCst);
2948            if vln < olnct {
2949                let mut obuf = OBUF.lock().unwrap();
2950                if let Some(orow) = obuf.get_mut(vln as usize) {
2951                    if let Some(first) = orow.first_mut() {
2952                        if first.chr != '\0' {
2953                            *first = rep;
2954                        }
2955                    }
2956                }
2957            }
2958        }
2959        VLN.store(vln, Ordering::SeqCst);
2960        VCS.store(vcs, Ordering::SeqCst);
2961    }
2962
2963    // c:2184 — `if (ln == vln && cl == vcs) return;`
2964    if ln == vln && cl == vcs {
2965        return;
2966    }
2967
2968    // c:2188-2191 — move up.
2969    if ln < vln {
2970        tc_upcurs(vln - ln); // c:2189
2971        vln = ln; // c:2190
2972        VLN.store(vln, Ordering::SeqCst);
2973    }
2974
2975    // c:2195-2212 — move down; past vmaxln-1 use newlines, not TCDOWN, so
2976    // we don't run off the end of what's been drawn.
2977    while ln > vln {
2978        let vmaxln = VMAXLN.load(Ordering::SeqCst);
2979        if vln < vmaxln - 1 {
2980            if ln > vmaxln - 1 {
2981                // c:2198-2200
2982                if tc_downcurs(vmaxln - 1 - vln) != 0 {
2983                    vcs = 0;
2984                    VCS.store(0, Ordering::SeqCst);
2985                }
2986                vln = vmaxln - 1;
2987                VLN.store(vln, Ordering::SeqCst);
2988            } else {
2989                // c:2202-2204
2990                if tc_downcurs(ln - vln) != 0 {
2991                    vcs = 0;
2992                    VCS.store(0, Ordering::SeqCst);
2993                }
2994                vln = ln;
2995                VLN.store(vln, Ordering::SeqCst);
2996                continue;
2997            }
2998        }
2999        // c:2207 — `zputc(&zr_cr), vcs = 0;` safety precaution.
3000        zwcputc(&zr_cr);
3001        vcs = 0;
3002        VCS.store(0, Ordering::SeqCst);
3003        while ln > vln {
3004            // c:2208-2211 — newline-scroll the remaining lines.
3005            zwcputc(&zr_nl);
3006            vln += 1;
3007        }
3008        VLN.store(vln, Ordering::SeqCst);
3009    }
3010
3011    // c:2214-2215 — `if (cl != vcs) singmoveto(cl);`
3012    if cl != vcs {
3013        singmoveto(cl);
3014    }
3015}
3016
3017/// Direct port of `void tcoutarg(int cap, int arg)` from
3018/// `Src/Zle/zle_refresh.c:2409`.
3019/// ```c
3020/// void tcoutarg(int cap, int arg) {
3021///     char *result = tgoto(tcstr[cap], arg, arg);
3022///     if (tcout_func_name) tcout_via_func(cap, arg, putshout);
3023///     else tputs(result, 1, putshout);
3024///     SELECT_ADD_COST(strlen(result));
3025/// }
3026/// ```
3027/// Output a parametrised termcap value, substituting `arg` into the
3028/// capability string via `tgoto` (the same termcap routine `init.rs`
3029/// already links alongside `tgetstr`). C passes `arg` as both tgoto
3030/// parameters; the capabilities used here (TCMULTRIGHT / TCHORIZPOS /
3031/// the multi-* caps) take a single `%d`, so the col/row order is moot
3032/// and the output is deterministic across platforms. The
3033/// `tcout_func_name` user-hook (c:2414) and `tputs` padding are deferred
3034/// — modern terminals need neither.
3035pub fn tcoutarg(cap: i32, arg: i32) {
3036    // c:2409
3037    use crate::ported::init::tcstr;
3038    use crate::ported::zsh_h::TC_COUNT;
3039    use std::ffi::{CStr, CString};
3040    extern "C" {
3041        fn tgoto(
3042            cap: *const libc::c_char,
3043            col: libc::c_int,
3044            row: libc::c_int,
3045        ) -> *mut libc::c_char;
3046    }
3047    let cap_idx = cap as usize;
3048    if cap_idx >= TC_COUNT as usize {
3049        return;
3050    }
3051    let cap_str = tcstr.lock().unwrap()[cap_idx].clone();
3052    if cap_str.is_empty() {
3053        return;
3054    }
3055    let c_cap = match CString::new(cap_str) {
3056        Ok(c) => c,
3057        Err(_) => return,
3058    };
3059    // c:2413 — `result = tgoto(tcstr[cap], arg, arg);`
3060    let result = unsafe { tgoto(c_cap.as_ptr(), arg as libc::c_int, arg as libc::c_int) };
3061    if result.is_null() {
3062        return;
3063    }
3064    let bytes = unsafe { CStr::from_ptr(result) }.to_bytes();
3065    // c:2416-2417 — `tputs(result, 1, putshout)` (padding dropped).
3066    let fd = SHTTY.load(Ordering::Relaxed);
3067    let out_fd = if fd >= 0 { fd } else { 1 };
3068    let _ = write_loop(out_fd, bytes);
3069    // c:2419 — SELECT_ADD_COST(strlen(result)) cost accounting (no-op).
3070}
3071
3072/// Direct port of `int tcmultout(int cap, int multcap, int ct)` from
3073/// `Src/Zle/zle_refresh.c:2221`.
3074///
3075/// Prefers the parametrised multi-arg capability when its escape is no
3076/// longer than `ct` repeats of the single cap (c:2223), falls back to
3077/// looping the single cap (c:2226-2229), otherwise returns 0 (c:2231) so
3078/// the caller chooses the fallback. Returns 1 when an escape was emitted.
3079pub fn tcmultout(cap: i32, multcap: i32, ct: i32) -> i32 {
3080    // c:2221
3081    use crate::ported::init::{tclen, tcstr};
3082    use crate::ported::zsh_h::TC_COUNT;
3083
3084    if ct <= 0 {
3085        return 0;
3086    }
3087    let cap_idx = cap as usize;
3088    let multcap_idx = multcap as usize;
3089    let count = TC_COUNT as usize;
3090
3091    let (cap_str, cap_len) = if cap_idx < count {
3092        let s = tcstr.lock().unwrap()[cap_idx].clone();
3093        let l = tclen.lock().unwrap()[cap_idx];
3094        (s, l)
3095    } else {
3096        (String::new(), 0)
3097    };
3098    let (mult_str, mult_len) = if multcap_idx < count {
3099        let s = tcstr.lock().unwrap()[multcap_idx].clone();
3100        let l = tclen.lock().unwrap()[multcap_idx];
3101        (s, l)
3102    } else {
3103        (String::new(), 0)
3104    };
3105
3106    let fd = SHTTY.load(Ordering::Relaxed);
3107    let out_fd = if fd >= 0 { fd } else { 1 };
3108    let mult_ok = mult_len > 0;
3109    let cap_ok = cap_len > 0;
3110
3111    // c:2223-2225 — `if (tccan(multcap) && (!tccan(cap) ||
3112    //                  tclen[multcap] <= tclen[cap]*ct)) { tcoutarg(multcap,ct); return 1; }`
3113    let _ = mult_str;
3114    if mult_ok && (!cap_ok || mult_len <= cap_len * ct) {
3115        tcoutarg(multcap, ct);
3116        return 1;
3117    } else if cap_ok {
3118        // c:2226-2229 — `else if (tccan(cap)) { while(ct--) tcout(cap); return 1; }`
3119        for _ in 0..ct {
3120            let _ = write_loop(out_fd, cap_str.as_bytes());
3121        }
3122        return 1;
3123    }
3124    // c:2231 — `return 0;` No capability: the caller decides the fallback
3125    // (tc_downcurs emits newlines, tc_rightcurs re-outputs cells / CSI C).
3126    // The previous port emitted \x08 / space here, which is not in C and
3127    // — for the right case — destructively overwrote cells with spaces.
3128    0
3129}
3130
3131/// Direct port of `static void tc_rightcurs(int ct)` from
3132/// `Src/Zle/zle_refresh.c:2237`. Move the cursor right `ct` columns,
3133/// trying each strategy in the C order of preference:
3134///   - `TCMULTRIGHT` parametrised right (c:2247-2250) — most reliable
3135///   - `TCHORIZPOS` absolute horizontal position to `ct + vcs` (c:2253-2256)
3136///   - non-destructive tab stops when `!oxtabs` (c:2260-2267)
3137///   - if anywhere in the prompt, step through it with `TCRIGHT` when that's
3138///     cheaper than reprinting, else reprint the whole prompt (c:2286-2306)
3139///   - carefully re-output the video-buffer cells from column `i` (c:2308-2313)
3140///   - last resort: pad with spaces (c:2314-2315) — "not my fault your
3141///     terminal can't go right"
3142///
3143/// This (multibyte) build excludes the `#ifndef MULTIBYTE_SUPPORT`
3144/// `strlen(lpromptbuf) == lpromptw` fast path (c:2287-2291): with potentially
3145/// wide characters the trick is unsafe, so the prompt is always either stepped
3146/// through with `TCRIGHT` or reprinted. `lpromptbuf` is `zle_main::prompt()`.
3147/// `tc_rightcurs` never mutates `vcs`/`vln` — the caller owns those.
3148pub fn tc_rightcurs(count: usize) {
3149    // c:2237
3150    if count == 0 {
3151        return;
3152    }
3153    use crate::ported::init::tclen;
3154    use crate::ported::params::TERMFLAGS;
3155    use crate::ported::zsh_h::{TCHORIZPOS, TCMULTRIGHT, TCNEXTTAB, TCRIGHT, TERM_SHORT};
3156
3157    let zr_cr = REFRESH_ELEMENT { chr: '\r', atr: 0 };
3158    let zr_sp = REFRESH_ELEMENT { chr: ' ', atr: 0 };
3159
3160    let mut ct = count as i32; // c:2237 — ct (reassigned by the tab/prompt paths)
3161    let vcs = VCS.load(Ordering::SeqCst);
3162    let vln = VLN.load(Ordering::SeqCst);
3163    let winw = WINW.load(Ordering::SeqCst);
3164    let mut i = vcs; // c:2240 — i = vcs: cursor position after the initial moves
3165    let cl = ct + vcs; // c:2244 — cl = ct + vcs: desired absolute column
3166    let out_fd = {
3167        let f = SHTTY.load(Ordering::Relaxed);
3168        if f >= 0 {
3169            f
3170        } else {
3171            1
3172        }
3173    };
3174
3175    // c:2247-2250 — `if (tccan(TCMULTRIGHT)) { tcoutarg(TCMULTRIGHT, ct); return; }`
3176    if tclen.lock().unwrap()[TCMULTRIGHT as usize] != 0 {
3177        tcoutarg(TCMULTRIGHT, ct);
3178        return;
3179    }
3180    // c:2253-2256 — `if (tccan(TCHORIZPOS)) { tcoutarg(TCHORIZPOS, cl); return; }`
3181    if tclen.lock().unwrap()[TCHORIZPOS as usize] != 0 {
3182        tcoutarg(TCHORIZPOS, cl);
3183        return;
3184    }
3185
3186    // c:2260-2267 — try tabs if non-destructive (`!oxtabs`) and the next tab
3187    // stop lies before the target. `(vcs | 7)` is one short of that tab stop.
3188    if OXTABS.load(Ordering::SeqCst) == 0
3189        && tclen.lock().unwrap()[TCNEXTTAB as usize] != 0
3190        && (vcs | 7) < cl
3191    {
3192        i = (vcs | 7) + 1; // c:2261
3193        tcout(TCNEXTTAB); // c:2262
3194        while i + 8 <= cl {
3195            // c:2263-2264 — `for ( ; i + 8 <= cl; i += 8) tcout(TCNEXTTAB);`
3196            tcout(TCNEXTTAB);
3197            i += 8;
3198        }
3199        ct = cl - i; // c:2265 — chars still to move across
3200        if ct == 0 {
3201            return; // c:2266
3202        }
3203    }
3204
3205    // c:2286-2306 — if we're anywhere in the prompt, deal with it: reprint the
3206    // whole prompt (going to the left column first), or step through it with
3207    // TCRIGHT when that's the cheaper sequence.
3208    let lpromptw = LPROMPTW.load(Ordering::SeqCst);
3209    if vln == 0 && i < lpromptw && (TERMFLAGS.load(Ordering::Relaxed) & TERM_SHORT) == 0 {
3210        let lpromptbuf = crate::ported::zle::zle_main::prompt();
3211        let tcright_len = tclen.lock().unwrap()[TCRIGHT as usize];
3212        // c:2292-2293 — `if (tccan(TCRIGHT) && (tclen[TCRIGHT]*ct <= ztrlen(lpromptbuf)))`
3213        if tcright_len != 0
3214            && (tcright_len * ct) as usize <= crate::ported::utils::ztrlen(&lpromptbuf)
3215        {
3216            // c:2294-2295 — cheaper to send TCRIGHT than reprint the prompt:
3217            // `for (ct = lpromptw - i; ct--; ) tcout(TCRIGHT);`
3218            let mut k = lpromptw - i;
3219            while k > 0 {
3220                tcout(TCRIGHT);
3221                k -= 1;
3222            }
3223        } else {
3224            // c:2297-2302 — reprint: CR to the left column, up over the prompt
3225            // height, dump lpromptbuf, then a newline if it filled the margin.
3226            if i != 0 {
3227                zwcputc(&zr_cr); // c:2298 — zputc(&zr_cr)
3228            }
3229            tc_upcurs(LPROMPTH.load(Ordering::SeqCst) - 1); // c:2299
3230            let _ = write_loop(out_fd, lpromptbuf.as_bytes()); // c:2300 — zputs(lpromptbuf, shout)
3231            if LPROMPTWOF.load(Ordering::SeqCst) == winw {
3232                // c:2301-2302 — works with both hasam and !hasam.
3233                let _ = write_loop(out_fd, b"\n");
3234            }
3235        }
3236        i = lpromptw; // c:2304
3237        ct = cl - i; // c:2305
3238    }
3239
3240    // c:2308-2313 — _carefully_ write the contents of the video buffer: walk
3241    // `i` cells in, then emit real cells until `ct` is spent or the line ends.
3242    // Clone the row so the lock isn't held across the zwcputc emits.
3243    let row = NBUF.lock().unwrap().get(vln as usize).cloned(); // c:2308 `if (nbuf[vln])`
3244    if let Some(t) = row {
3245        // c:2309 — `for (j = 0, t = nbuf[vln]; t->chr && (j < i); j++, t++);`
3246        let mut idx = 0usize;
3247        let mut j = 0i32;
3248        while idx < t.len() && t[idx].chr != '\0' && j < i {
3249            j += 1;
3250            idx += 1;
3251        }
3252        if j == i {
3253            // c:2311-2312 — `for ( ; t->chr && ct; ct--, t++) zputc(t);`
3254            while idx < t.len() && t[idx].chr != '\0' && ct > 0 {
3255                zwcputc(&t[idx]);
3256                ct -= 1;
3257                idx += 1;
3258            }
3259        }
3260    }
3261    // c:2314-2315 — `while (ct--) zputc(&zr_sp);`
3262    while ct > 0 {
3263        zwcputc(&zr_sp);
3264        ct -= 1;
3265    }
3266}
3267
3268/// Direct port of `int tc_downcurs(int ct)` from
3269/// `Src/Zle/zle_refresh.c:2320`.
3270/// ```c
3271/// int tc_downcurs(int ct) {
3272///     int ret = 0;
3273///     if (ct && !tcmultout(TCDOWN, TCMULTDOWN, ct)) {
3274///         while (ct--) zputc(&zr_nl);
3275///         zputc(&zr_cr), ret = -1;
3276///     }
3277///     return ret;
3278/// }
3279/// ```
3280/// Move the cursor down `ct` lines. Prefers the terminal's down
3281/// capability via `tcmultout`; when that's unavailable it emits real
3282/// newlines (which scroll/create lines past the drawn region — a plain
3283/// CSI B can't) followed by a CR, and returns -1 so the caller knows the
3284/// column was reset to 0. Returns 0 when the capability moved without
3285/// touching the column.
3286pub fn tc_downcurs(ct: i32) -> i32 {
3287    // c:2320
3288    let mut ret = 0; // c:2324
3289    // c:2326 — `if (ct && !tcmultout(TCDOWN, TCMULTDOWN, ct))`
3290    if ct != 0 && tcmultout(crate::ported::zsh_h::TCDOWN, crate::ported::zsh_h::TCMULTDOWN, ct) == 0
3291    {
3292        let mut c = ct; // c:2327 while (ct--)
3293        while c > 0 {
3294            zwcputc(&REFRESH_ELEMENT { chr: '\n', atr: 0 }); // c:2328 zputc(&zr_nl)
3295            c -= 1;
3296        }
3297        zwcputc(&REFRESH_ELEMENT { chr: '\r', atr: 0 }); // c:2329 zputc(&zr_cr)
3298        ret = -1; // c:2329
3299    }
3300    ret // c:2331
3301}
3302
3303/// Direct port of `int tcout_via_func(int cap, int arg, int (*outc)(int))`
3304/// from `Src/Zle/zle_refresh.c:2291`.
3305///
3306/// Faithful line-by-line translation of the C body (c:2293-2342).
3307/// Saves the SFC/STOPMSG/INCOMPFUNC context, looks up the user's
3308/// `$TCOUT_FUNC_NAME` (default `"tcout"`) via `getshfunc`, builds the
3309/// `[tcout_func_name, cap_name, arg?]` arg list, dispatches through
3310/// `callhookfunc` (the SFC_SUBST static-link path of `doshfunc`),
3311/// then reads `$REPLY` and writes each byte to the shell-output fd
3312/// (decoding Meta-byte pairs). Returns 0 when the function ran (caller
3313/// suppresses the raw termcap escape), 1 otherwise (caller emits raw).
3314pub fn tcout_via_func(cap: i32, arg: i32) -> i32 {
3315    use crate::ported::builtin::STOPMSG;
3316    use crate::ported::exec::sfcontext;
3317    use crate::ported::init::tccap_get_name;
3318    use crate::ported::params::getsparam;
3319    use crate::ported::utils::{callhookfunc, getshfunc, write_loop, INCOMPFUNC};
3320    use crate::ported::zsh_h::SFC_SUBST;
3321
3322    // c:2295-2297 — save sfcontext / stopmsg / incompfunc.
3323    let osc = sfcontext.load(Ordering::Relaxed);
3324    let osm = STOPMSG.load(Ordering::Relaxed);
3325    let old_incompfunc = INCOMPFUNC.load(Ordering::Relaxed);
3326    // c:2299-2300 — sfcontext = SFC_SUBST; incompfunc = 0.
3327    sfcontext.store(SFC_SUBST, Ordering::Relaxed);
3328    INCOMPFUNC.store(0, Ordering::Relaxed);
3329
3330    // c:2302 — Shfunc tcout_func; if ((tcout_func = getshfunc(tcout_func_name)))
3331    let func_name = TCOUT_FUNC_NAME
3332        .lock()
3333        .unwrap()
3334        .clone()
3335        .unwrap_or_else(|| "tcout".to_string());
3336
3337    let dispatched = if getshfunc(&func_name).is_some() {
3338        // c:2305-2309 — build linklist: [func_name, cap_name, arg?].
3339        let mut argv: Vec<String> = Vec::with_capacity(3);
3340        argv.push(func_name.clone()); // c:2306
3341        argv.push(tccap_get_name(cap as usize).to_string()); // c:2307
3342        if arg != -1 {
3343            // c:2310-2313 — `if (arg != -1) sprintf(buf, "%d", arg);
3344            //                                addlinknode(l, buf);`
3345            argv.push(arg.to_string());
3346        }
3347        // c:2316 — `(void)doshfunc(tcout_func, l, 1);`. Direct
3348        // doshfunc call mirrors C exactly.
3349        let shf_clone: Option<crate::ported::zsh_h::shfunc> =
3350            crate::ported::hashtable::shfunctab_lock()
3351                .read()
3352                .ok()
3353                .and_then(|t| t.get(&func_name).cloned());
3354        if let Some(mut shf) = shf_clone {
3355            let name_for_body = func_name.clone();
3356            let body_args = argv.clone();
3357            let body_runner = move || -> i32 {
3358                crate::ported::exec::run_function_body(&name_for_body, &body_args[1..])
3359                    .unwrap_or(0)
3360            };
3361            let _ = crate::ported::exec::doshfunc(&mut shf, argv.clone(), true, body_runner);
3362        } else {
3363            let _ = callhookfunc(&func_name, Some(&argv), 0, std::ptr::null_mut());
3364        }
3365
3366        // c:2318-2331 — pull $REPLY and emit each byte (Meta-decoded)
3367        //               via outc().
3368        if let Some(reply) = getsparam("REPLY") {
3369            let bytes = reply.as_bytes();
3370            let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
3371            let mut i = 0;
3372            while i < bytes.len() {
3373                if bytes[i] == 0x83 && i + 1 < bytes.len() {
3374                    out.push(bytes[i + 1] ^ 32); // c:2324
3375                    i += 2;
3376                } else {
3377                    out.push(bytes[i]); // c:2327
3378                    i += 1;
3379                }
3380            }
3381            let fd = SHTTY.load(Ordering::Relaxed);
3382            let out_fd = if fd >= 0 { fd } else { 1 };
3383            let _ = write_loop(out_fd, &out);
3384        }
3385        true
3386    } else {
3387        false
3388    };
3389
3390    // c:2338-2340 — restore sfcontext / stopmsg / incompfunc.
3391    sfcontext.store(osc, Ordering::Relaxed);
3392    STOPMSG.store(osm, Ordering::Relaxed);
3393    INCOMPFUNC.store(old_incompfunc, Ordering::Relaxed);
3394
3395    if dispatched {
3396        0
3397    } else {
3398        1
3399    }
3400}
3401
3402/// Direct port of `void tcout(int cap)` from `Src/Zle/zle_refresh.c:2339`.
3403/// Resolves the cap escape via `tcstr[cap]` and writes it to SHTTY
3404/// (stdout fallback when unset). Now that init_term populates tcstr
3405/// with ANSI/VT100 escapes, the index lookup actually does something.
3406pub fn tcout(cap: i32) {
3407    // c:2339
3408    use crate::ported::init::tcstr;
3409    use crate::ported::zsh_h::TC_COUNT;
3410    let cap_idx = cap as usize;
3411    if cap_idx >= TC_COUNT as usize {
3412        return;
3413    }
3414    let escape = tcstr.lock().unwrap()[cap_idx].clone();
3415    if escape.is_empty() {
3416        return;
3417    }
3418    let fd = SHTTY.load(Ordering::Relaxed);
3419    let out_fd = if fd >= 0 { fd } else { 1 };
3420    let _ = write_loop(out_fd, escape.as_bytes());
3421    // c:2346 — `SELECT_ADD_COST(tclen[cap])` cost accounting dropped
3422    //          (no scheduling consumer reads it yet).
3423}
3424
3425/// Direct port of `int clearscreen(UNUSED(char **args))` from
3426/// `Src/Zle/zle_refresh.c:2424`.
3427/// ```c
3428/// int clearscreen(UNUSED(char **args)) {
3429///     tcoutclear(TCCLEARSCREEN);
3430///     resetneeded = 1;
3431///     clearflag = 0;
3432///     reexpandprompt();
3433///     return 0;
3434/// }
3435/// ```
3436/// The `clear-screen` widget. The previous port hardcoded `CSI 2J CSI H`
3437/// instead of the terminal's real clear capability and dropped the
3438/// resetneeded/clearflag/reexpandprompt sequence.
3439pub fn clearscreen() -> i32 {
3440    // c:2424
3441    // c:2426 — `tcoutclear(TCCLEARSCREEN);` use the terminal's clear cap.
3442    tcoutclear(crate::ported::zsh_h::TCCLEARSCREEN);
3443    RESETNEEDED.store(1, Ordering::SeqCst); // c:2427 resetneeded = 1
3444    CLEARFLAG.store(0, Ordering::SeqCst); // c:2428 clearflag = 0
3445    // c:2429 — `reexpandprompt();` re-run prompt expansion against the saved
3446    // templates so the cleared screen redraws with a freshly-expanded prompt.
3447    // (Now wired — reexpandprompt is ported in zle_main; it was previously
3448    // deferred as "prompt subsystem".)
3449    crate::ported::zle::zle_main::reexpandprompt(); // c:2429
3450    // zshrs's ZLE loop doesn't consume resetneeded yet (the c:1125 block is
3451    // blocked on zsetterm/lpromptbuf/trashedzle), so drive the redraw directly
3452    // here for the same visible effect that honouring resetneeded would give.
3453    zrefresh();
3454    0 // c:2430
3455}
3456
3457/// Direct port of `int redisplay(UNUSED(char **args))` from
3458/// `Src/Zle/zle_refresh.c:2435`.
3459/// ```c
3460/// int redisplay(UNUSED(char **args)) {
3461///     moveto(0, 0);
3462///     zputc(&zr_cr);		/* extra care */
3463///     tc_upcurs(lprompth - 1);
3464///     resetneeded = 1;
3465///     clearflag = 0;
3466///     return 0;
3467/// }
3468/// ```
3469/// The `redisplay` widget. Positions the cursor at the top-left of the
3470/// prompt (home, CR for safety, up over the prompt's height) and flags a
3471/// full redraw. The previous port dropped the whole body and just called
3472/// zrefresh.
3473pub fn redisplay() -> i32 {
3474    // c:2435
3475    moveto(0, 0); // c:2437
3476    zwcputc(&REFRESH_ELEMENT { chr: '\r', atr: 0 }); // c:2438 zputc(&zr_cr)
3477    let lprompth = LPROMPTH.load(Ordering::SeqCst);
3478    tc_upcurs(lprompth - 1); // c:2439
3479    RESETNEEDED.store(1, Ordering::SeqCst); // c:2440 resetneeded = 1
3480    CLEARFLAG.store(0, Ordering::SeqCst); // c:2441 clearflag = 0
3481    // c:2442 return 0. zshrs's ZLE loop doesn't honour resetneeded yet, so
3482    // trigger the redraw directly for the same visible effect.
3483    zrefresh();
3484    0
3485}
3486
3487/// Port of `static void singlerefresh(ZLE_STRING_T tmpline,
3488/// int tmpll, int tmpcs)` from `Src/Zle/zle_refresh.c:2397`.
3489/// Builds the single-line video buffer used by `read -e`,
3490/// `vared`, and predisplay-only refresh paths: pre-allocates
3491/// `vbuf` sized for tabs / control chars / wide chars, walks
3492/// `tmpline` translating each cell into one or more
3493/// REFRESH_ELEMENTs (with region-highlight overlay), windows
3494/// the result against `winw - hasam`, then commits to
3495/// `nbuf[0]`.
3496///
3497/// ```c
3498/// static void
3499/// singlerefresh(ZLE_STRING_T tmpline, int tmpll, int tmpcs)
3500/// {
3501///     REFRESH_STRING vbuf, vp, refreshop;
3502///     int t0, vsiz, nvcs = 0, owinpos = winpos,
3503///         owinprompt = winprompt;
3504///     int width;
3505///     nlnct = 1;
3506///     for (vsiz = 1 + lpromptw, t0 = 0; t0 != tmpll; t0++) {
3507///         if (tmpline[t0] == '\t') vsiz = (vsiz | 7) + 2;
3508///         else if (WC_ISPRINT(tmpline[t0]) && (width = WCWIDTH(...)) > 0)
3509///             { vsiz += width; ...combining... }
3510///         else if (ZC_icntrl(...)) vsiz += 2;
3511///         else vsiz += 10;
3512///     }
3513///     vbuf = zalloc(vsiz * sizeof(*vbuf));
3514///     if (tmpcs < 0) tmpcs = 0;
3515///     ZR_memset(vbuf, zr_sp, lpromptw);
3516///     vp = vbuf + lpromptw;
3517///     *vp = zr_zr;
3518///     for (t0 = 0; t0 < tmpll; t0++) {
3519///         /* compute base_attr from region_highlights overlay */
3520///         /* compute all_attr = mixattrs(special_attr, special_mask, base_attr) */
3521///         if (t0 == tmpcs) nvcs = vp - vbuf;
3522///         /* emit cells per char class: \t, \n, printable wide,
3523///            control, default */
3524///     }
3525///     if (t0 == tmpcs) nvcs = vp - vbuf;
3526///     *vp = zr_zr;
3527///     /* window selection */
3528///     if (winpos == -1) winpos = 0;
3529///     if ((winpos && nvcs < winpos + 1) || (nvcs > winpos + winw - 2))
3530///         { winpos = nvcs - ((winw - hasam) / 2); if (winpos < 0) winpos = 0; }
3531///     if (winpos) vbuf[winpos] = '<';
3532///     if (ZR_strlen(vbuf + winpos) > winw - hasam)
3533///         { vbuf[winpos + winw - hasam - 1] = '>';
3534///           vbuf[winpos + winw - hasam] = zr_zr; }
3535///     ZR_strcpy(nbuf[0], vbuf + winpos);
3536///     zfree(vbuf, vsiz * sizeof(*vbuf));
3537///     nvcs -= winpos;
3538///     if (winpos < lpromptw) winprompt = lpromptw - winpos;
3539///     else winprompt = 0;
3540///     if (winpos != owinpos && winprompt)
3541///         { singmoveto(0); ...output left-prompt fragment... }
3542///     singmoveto(nvcs);
3543/// }
3544/// ```
3545///
3546/// Per PORT.md Rule 9 — body executes against stubs for
3547/// `addmultiword`/`mixattrs`/`region_highlights`/`shout`/`fputc`/
3548/// `lpromptbuf`/`MB_METACHARLENCONV` / `WCWIDTH` / `IS_BASECHAR` /
3549/// `IS_COMBINING` primitives not yet ported. Stubs flagged
3550/// inline with `// !!! STUB: …`.
3551pub fn singlerefresh(tmpline: &[char], tmpll: i32, mut tmpcs: i32) {
3552    // c:2397
3553
3554    // c:2399-2405 — declarations.
3555    let mut vbuf: REFRESH_STRING; // c:2399
3556    let mut vp: usize; // c:2399 video pointer (index)
3557    let _refreshop: REFRESH_STRING = Vec::new(); // c:2400
3558    let mut t0: i32; // c:2401
3559    let mut vsiz: i32; // c:2402
3560    let mut nvcs: i32 = 0; // c:2403
3561    // c:2462-2463 — snapshot the persistent window-position statics (WINPOS /
3562    // WINPROMPT, reset to -1/0 by resetvideo at c:735-736). These carry the
3563    // SINGLELINEZLE horizontal-scroll state across keystrokes; the
3564    // `winpos != owinpos` test below detects a scroll since the last frame.
3565    let owinpos: i32 = WINPOS.load(Ordering::SeqCst); // c:2462
3566    let owinprompt: i32 = WINPROMPT.load(Ordering::SeqCst); // c:2463
3567    let mut width: i32 = 0; // c:2407
3568
3569    NLNCT.store(1, Ordering::SeqCst); // c:2410
3570
3571    // c:2411-2437 — measure required vbuf size.
3572    let lpromptw = LPROMPTW.load(Ordering::SeqCst);
3573    vsiz = 1 + lpromptw; // c:2412
3574    t0 = 0;
3575    while t0 != tmpll {
3576        // c:2412
3577        let ch = *tmpline.get(t0 as usize).unwrap_or(&'\0');
3578        if ch == '\t' {
3579            // c:2413
3580            vsiz = (vsiz | 7) + 2; // c:2414
3581        } else if ch.is_alphanumeric() || ch.is_ascii_graphic() {
3582            // c:2416 WC_ISPRINT
3583            width = unicode_width::UnicodeWidthChar::width(ch) // c:2416 WCWIDTH
3584                .unwrap_or(1) as i32;
3585            if width > 0 {
3586                vsiz += width; // c:2417
3587                               // c:2418-2421 — combining-char absorption; skip combos.
3588                if isset(COMBININGCHARS) {
3589                    // c:2476-2477 — `while (t0 < tmpll-1 && IS_COMBINING(tmpline[t0+1])) t0++;`
3590                    while t0 < tmpll - 1 {
3591                        let next = *tmpline.get((t0 + 1) as usize).unwrap_or(&'\0');
3592                        if !crate::ported::zsh_h::IS_COMBINING(next) {
3593                            break;
3594                        }
3595                        t0 += 1; // c:2477
3596                    }
3597                }
3598            }
3599        } else if (ch as u32) < 0x20 || (ch as u32) == 0x7F {
3600            // c:2424 ZC_icntrl
3601            if (ch as u32) <= 0xff {
3602                // c:2426
3603                vsiz += 2; // c:2429
3604            }
3605        } else {
3606            // c:2430
3607            vsiz += 10; // c:2432 wide / non-printable
3608        }
3609        t0 += 1;
3610    }
3611
3612    // c:2438 — `vbuf = zalloc(vsiz * sizeof(*vbuf));`
3613    vbuf = vec![REFRESH_ELEMENT { chr: '\0', atr: 0 }; vsiz as usize]; // c:2438
3614
3615    if tmpcs < 0 {
3616        // c:2440
3617        tmpcs = 0; // c:2445
3618    }
3619
3620    // c:2449 — `ZR_memset(vbuf, zr_sp, lpromptw);`
3621    for k in 0..(lpromptw as usize).min(vbuf.len()) {
3622        // c:2449
3623        vbuf[k] = REFRESH_ELEMENT { chr: ' ', atr: 0 };
3624    }
3625    vp = lpromptw as usize; // c:2450 vp = vbuf + lpromptw
3626    if vp < vbuf.len() {
3627        vbuf[vp] = REFRESH_ELEMENT { chr: '\0', atr: 0 }; // c:2451 *vp = zr_zr
3628    }
3629
3630    // c:2453-2563 — main translation loop.
3631    t0 = 0;
3632    while t0 < tmpll {
3633        // c:2453
3634        // c:2454-2479 — region-highlight overlay.
3635        // !!! STUB: region_highlights / n_region_highlights /
3636        // default_attr / special_attr / special_mask / prompt_attr /
3637        // predisplaylen / mixattrs — Src/Zle/zle_refresh.c.
3638        let base_attr: u64 = 0; // c:2455 mixattrs
3639        let all_attr: u64 = 0; // c:2480 mixattrs
3640
3641        if t0 == tmpcs {
3642            // c:2482
3643            nvcs = vp as i32; // c:2483 nvcs = vp - vbuf
3644        }
3645        let ch = *tmpline.get(t0 as usize).unwrap_or(&'\0');
3646
3647        if ch == '\t' {
3648            // c:2484
3649            if vp < vbuf.len() {
3650                vbuf[vp] = REFRESH_ELEMENT {
3651                    chr: ' ',
3652                    atr: base_attr,
3653                };
3654                vp += 1; // c:2485 *vp++ = zr_sp
3655            }
3656            while (vp & 7) != 0 && vp < vbuf.len() {
3657                // c:2485
3658                vbuf[vp] = REFRESH_ELEMENT {
3659                    chr: ' ',
3660                    atr: base_attr,
3661                };
3662                vp += 1; // c:2486
3663            }
3664        } else if ch == '\n' {
3665            // c:2487
3666            if vp < vbuf.len() {
3667                vbuf[vp] = REFRESH_ELEMENT {
3668                    chr: '\\',
3669                    atr: all_attr,
3670                }; // c:2488-2489
3671                vp += 1; // c:2490
3672            }
3673            if vp < vbuf.len() {
3674                vbuf[vp] = REFRESH_ELEMENT {
3675                    chr: 'n',
3676                    atr: all_attr,
3677                }; // c:2491-2492
3678                vp += 1; // c:2493
3679            }
3680        } else if ch.is_ascii_graphic() || (ch.is_alphanumeric()) {
3681            // c:2495 WC_ISPRINT
3682            width = unicode_width::UnicodeWidthChar::width(ch) // c:2496 WCWIDTH
3683                .unwrap_or(1) as i32;
3684            if width > 0 {
3685                // c:2497-2507 — combining-char absorption: when COMBININGCHARS
3686                // is set and this is a base char, scan forward over the
3687                // following combining marks and cluster them into ONE cell via
3688                // addmultiword (the producer the multiword zwcputc reader
3689                // consumes). Was stubbed (ichars=1), so combining marks fell to
3690                // the non-printable branch and rendered as `<hex>` escapes —
3691                // and disagreed with the measure loop, which already absorbs
3692                // them.
3693                let mut ichars: i32 = 1; // c:2498
3694                if isset(COMBININGCHARS) {
3695                    // c:2499 — IS_BASECHAR(tmpline[t0]) is the width>0 char we
3696                    // are already in; scan the (width-0) combining marks.
3697                    while (t0 + ichars) < tmpll {
3698                        let nxt = *tmpline.get((t0 + ichars) as usize).unwrap_or(&'\0');
3699                        // c:2560-2562 — `for (ichars = 1; t0+ichars < tmpll; ichars++)
3700                        //   if (!IS_COMBINING(tmpline[t0+ichars])) break;`
3701                        if !crate::ported::zsh_h::IS_COMBINING(nxt) {
3702                            break;
3703                        }
3704                        ichars += 1; // c:2560
3705                    }
3706                }
3707                if vp < vbuf.len() {
3708                    if ichars > 1 {
3709                        // c:2509 — addmultiword(vp, tmpline+t0, ichars): store
3710                        // the base + combining cluster, flag the cell.
3711                        let cluster: Vec<char> =
3712                            tmpline[t0 as usize..(t0 + ichars) as usize].to_vec();
3713                        let mut cell = REFRESH_ELEMENT { chr: ch, atr: base_attr };
3714                        addmultiword(&mut cell, &cluster, ichars as usize);
3715                        vbuf[vp] = cell;
3716                    } else {
3717                        vbuf[vp] = REFRESH_ELEMENT {
3718                            chr: ch,
3719                            atr: base_attr,
3720                        }; // c:2512
3721                    }
3722                    vp += 1; // c:2513
3723                }
3724                // c:2514-2518 — WEOF cells for wide-char width padding: a
3725                // width-N glyph occupies N columns, so emit N-1 WEOF placeholder
3726                // cells after the leading cell. ZWC_WEOF (not '\0') so
3727                // ZR_strcpy/ZR_strlen don't treat them as the line terminator.
3728                let mut w = width - 1;
3729                while w > 0 {
3730                    // c:2514
3731                    if vp < vbuf.len() {
3732                        vbuf[vp] = REFRESH_ELEMENT {
3733                            chr: ZWC_WEOF,
3734                            atr: base_attr,
3735                        }; // c:2515 — `vp->chr = WEOF`
3736                        vp += 1; // c:2517
3737                    }
3738                    w -= 1;
3739                }
3740                t0 += ichars - 1; // c:2519
3741            }
3742        } else if (ch as u32) < 0x20 || (ch as u32) == 0x7F {
3743            // c:2521 ZC_icntrl
3744            if (ch as u32) <= 0xff {
3745                // c:2523
3746                let t = ch as u32; // c:2526
3747                if vp < vbuf.len() {
3748                    vbuf[vp] = REFRESH_ELEMENT {
3749                        chr: '^',
3750                        atr: all_attr,
3751                    }; // c:2528-2529
3752                    vp += 1; // c:2530
3753                }
3754                let display: char = if (t & !0x80) > 31 {
3755                    // c:2531-2532
3756                    '?'
3757                } else {
3758                    ((t | 0x40) as u8) as char // c:2532 t | '@'
3759                };
3760                if vp < vbuf.len() {
3761                    vbuf[vp] = REFRESH_ELEMENT {
3762                        chr: display,
3763                        atr: all_attr,
3764                    };
3765                    vp += 1; // c:2534
3766                }
3767            }
3768        } else {
3769            // c:2537 wide non-printable
3770            // c:2538-2554 — emit `<%.04x>` or `<%.08x>` hex display.
3771            let hex = if (ch as u32) > 0xFFFF {
3772                // c:2542
3773                format!("<{:08x}>", ch as u32) // c:2543
3774            } else {
3775                // c:2544
3776                format!("<{:04x}>", ch as u32) // c:2545
3777            };
3778            for c in hex.chars() {
3779                // c:2547
3780                if vp < vbuf.len() {
3781                    vbuf[vp] = REFRESH_ELEMENT {
3782                        chr: c,
3783                        atr: all_attr,
3784                    }; // c:2549-2550
3785                    vp += 1; // c:2551
3786                }
3787            }
3788        }
3789        t0 += 1;
3790    }
3791    if t0 == tmpcs {
3792        // c:2564
3793        nvcs = vp as i32; // c:2565
3794    }
3795    if vp < vbuf.len() {
3796        vbuf[vp] = REFRESH_ELEMENT { chr: '\0', atr: 0 }; // c:2566 *vp = zr_zr
3797    }
3798
3799    // c:2569-2587 — window selection.
3800    // c:2569 — winpos carries across frames (the persistent WINPOS static),
3801    // so the single-line view only re-scrolls when the cursor leaves the
3802    // visible window; == owinpos here, recomputed below if the cursor moved.
3803    let mut winpos: i32 = WINPOS.load(Ordering::SeqCst);
3804    let winw = WINW.load(Ordering::SeqCst);
3805    let hasam_v = crate::ported::init::hasam.load(Ordering::SeqCst);
3806    if winpos == -1 {
3807        // c:2569
3808        winpos = 0; // c:2570
3809    }
3810    if (winpos != 0 && nvcs < winpos + 1)                                // c:2571
3811            || (nvcs > winpos + winw - 2)
3812    {
3813        winpos = nvcs - ((winw - hasam_v) / 2); // c:2572
3814        if winpos < 0 {
3815            // c:2572
3816            winpos = 0; // c:2573
3817        }
3818    }
3819    if winpos != 0 && (winpos as usize) < vbuf.len() {
3820        // c:2575
3821        vbuf[winpos as usize] = REFRESH_ELEMENT { chr: '<', atr: 0 }; // c:2576-2577 line continues left
3822    }
3823    // c:2579-2583 — right-truncate if line continues
3824    let suffix_start = winpos as usize;
3825    let suffix_len = vbuf
3826        .iter()
3827        .skip(suffix_start)
3828        .take_while(|e| e.chr != '\0')
3829        .count();
3830    let max_visible = (winw - hasam_v) as usize;
3831    if suffix_len > max_visible {
3832        // c:2579
3833        let trunc_pos = suffix_start + max_visible - 1;
3834        if trunc_pos < vbuf.len() {
3835            vbuf[trunc_pos] = REFRESH_ELEMENT { chr: '>', atr: 0 }; // c:2580-2581 line continues right
3836        }
3837        if trunc_pos + 1 < vbuf.len() {
3838            vbuf[trunc_pos + 1] = REFRESH_ELEMENT { chr: '\0', atr: 0 }; // c:2582
3839        }
3840    }
3841
3842    // c:2584 — `ZR_strcpy(nbuf[0], vbuf + winpos);` copy the visible window of
3843    // the built line (from winpos through the terminator, including any `<`/`>`
3844    // continuation markers) into the global nbuf[0]. The render loop below
3845    // diffs this against obuf[0]. (Was stubbed "nbuf[] static — defer", so the
3846    // built line never reached nbuf[0] and nothing rendered.)
3847    {
3848        let start = winpos.max(0) as usize;
3849        let src: &[REFRESH_ELEMENT] = if start < vbuf.len() {
3850            &vbuf[start..]
3851        } else {
3852            &[]
3853        };
3854        let mut nbuf = NBUF.lock().unwrap();
3855        if nbuf.is_empty() {
3856            let w = (WINW.load(Ordering::SeqCst) + 2).max(1) as usize;
3857            nbuf.push(vec![REFRESH_ELEMENT::default(); w]);
3858        }
3859        if let Some(row) = nbuf.get_mut(0) {
3860            ZR_strcpy(row, src);
3861        }
3862    }
3863
3864    // c:2585 — zfree(vbuf, vsiz * sizeof(*vbuf)) — Rust drops on scope.
3865    drop(vbuf); // c:2585
3866    nvcs -= winpos; // c:2586
3867
3868    // c:2569 — winpos is finalized here; persist it so the next keystroke's
3869    // singlerefresh resumes from this scroll position (this is the producer
3870    // for owinpos above).
3871    WINPOS.store(winpos, Ordering::SeqCst);
3872
3873    // c:2588-2594 — winprompt = the part of lprompt still on screen; persist
3874    // it alongside winpos (the producer for owinprompt above).
3875    let winprompt: i32 = if winpos < lpromptw {
3876        // c:2588
3877        lpromptw - winpos // c:2590
3878    } else {
3879        // c:2591
3880        0 // c:2593
3881    };
3882    WINPROMPT.store(winprompt, Ordering::SeqCst); // c:2588
3883
3884    // c:2653-2697 — re-emit the visible fragment of the left prompt when the
3885    // line has scrolled (winpos != owinpos). The fragment OUTPUT itself is
3886    // still stubbed: it walks `lpromptbuf` honouring the Inpar/Outpar `%{..%}`
3887    // markers via MB_METACHARLENCONV — none of which are ported yet. We do the
3888    // singmoveto(0) (c:2661) so the cursor is homed; the per-char prompt emit
3889    // (c:2662-2696) is deferred until lpromptbuf lands.
3890    if winpos != owinpos && winprompt != 0 {
3891        // c:2661
3892        singmoveto(0);
3893        // c:2697 — `vcs = winprompt;` would be set after the fragment output.
3894        VCS.store(winprompt, Ordering::SeqCst);
3895    }
3896
3897    // c:2700-2738 — display the visible portion of the line: diff nbuf[0]
3898    // against obuf[0] from `winprompt`, emitting only changed cells via the
3899    // now-ported output primitives (the single-line analogue of refreshline).
3900    // The buffers don't change during emit, so snapshot both rows once.
3901    let nl0: REFRESH_STRING = NBUF.lock().unwrap().get(0).cloned().unwrap_or_default();
3902    let ol0: REFRESH_STRING = OBUF.lock().unwrap().get(0).cloned().unwrap_or_default();
3903    let zr_sp = REFRESH_ELEMENT { chr: ' ', atr: 0 };
3904    let mut t0c: i32 = winprompt; // c:2700 — output column
3905    let mut vp = winprompt.max(0) as usize; // c:2701 — index into nbuf[0]
3906    let mut rp = winprompt.max(0) as usize; // c:2702 — index into obuf[0]
3907    loop {
3908        // c:2709-2712 — skip past matching cells, but only once we're past the
3909        // old prompt fragment (owinprompt): earlier cells may hold prompt junk.
3910        if (vp as i32) >= owinprompt {
3911            while vp < nl0.len()
3912                && nl0[vp].chr != '\0'
3913                && rp < ol0.len()
3914                && ol0[rp] == nl0[vp]
3915            {
3916                t0c += 1; // c:2711
3917                vp += 1;
3918                rp += 1;
3919            }
3920        }
3921        let nchr = nl0.get(vp).map(|c| c.chr).unwrap_or('\0');
3922        let ochr = ol0.get(rp).map(|c| c.chr).unwrap_or('\0');
3923        // c:2714 — both lines ended: done.
3924        if nchr == '\0' && ochr == '\0' {
3925            break;
3926        }
3927        singmoveto(t0c); // c:2716 — move to where we output from
3928        if ochr == '\0' {
3929            // c:2718-2722 — old line ended: write the rest of the new line.
3930            let rest = &nl0[vp..];
3931            let len = ZR_strlen(rest); // c:2719 — t0 = ZR_strlen(vp)
3932            if len != 0 {
3933                zwcwrite(rest, len); // c:2720 — zwrite(vp, t0)
3934            }
3935            VCS.fetch_add(len as i32, Ordering::SeqCst); // c:2721 — vcs += t0
3936            break; // c:2722
3937        }
3938        if nchr == '\0' {
3939            // c:2723-2730 — new line ended: clear (or space-pad) the rest.
3940            let tcleareol = tclen.lock().unwrap()[TCCLEAREOL as usize];
3941            if tcleareol != 0 {
3942                tcoutclear(TCCLEAREOL); // c:2726
3943            } else {
3944                // c:2728-2729 — `for (; refreshop++->chr; vcs++) zputc(&zr_sp)`.
3945                let mut k = rp;
3946                while k < ol0.len() && ol0[k].chr != '\0' {
3947                    zwcputc(&zr_sp); // c:2729
3948                    VCS.fetch_add(1, Ordering::SeqCst);
3949                    k += 1;
3950                }
3951            }
3952            break; // c:2730
3953        }
3954        zwcputc(&nl0[vp]); // c:2731 — emit the one changed cell
3955        VCS.fetch_add(1, Ordering::SeqCst); // c:2732 — vcs++
3956        t0c += 1; // c:2732 — t0++
3957        vp += 1; // c:2733 — vp++
3958        rp += 1; // c:2733 — refreshop++
3959    }
3960
3961    // c:2738 — move to the new cursor position.
3962    singmoveto(nvcs);
3963    // c:2740 — swap the video buffers (singlerefresh manages its own swap; it
3964    // is not reached through zrefresh's start-of-frame bufswap).
3965    bufswap();
3966
3967    let _ = (lpromptw, width); // silence unused
3968}
3969
3970/// Direct port of `static void singmoveto(int pos)` from
3971/// `Src/Zle/zle_refresh.c:2745`. Single-line horizontal cursor
3972/// positioning to column `pos`, operating on the global video-cursor
3973/// column `vcs` (the VCS atomic) exactly as C does — shared by `moveto`
3974/// (c:2216) and `singlerefresh` (c:2661/2717/2738).
3975///   - exit early when already at `pos` (c:2750)
3976///   - if no TCMULTLEFT or target at/near BOL: emit `\r`, vcs = 0 (c:2755-2758)
3977///   - left of current: `tc_leftcurs(vcs - pos)` (c:2760)
3978///   - right of current: `tc_rightcurs(pos - vcs)` (c:2762)
3979///   - `vcs = pos` (c:2764)
3980///
3981/// The previous port threaded a `RefreshState` and its callers passed a
3982/// throwaway `RefreshState::new()` (vcs always 0), ignoring the global
3983/// VCS that `singlerefresh` actually maintains (line ~691) — fixed here.
3984pub fn singmoveto(pos: i32) {
3985    // c:2745
3986    use crate::ported::init::tclen;
3987    use crate::ported::zsh_h::TCMULTLEFT;
3988
3989    let vcs = VCS.load(Ordering::SeqCst);
3990    // c:2750 — `if (pos == vcs) return;`
3991    if pos == vcs {
3992        return;
3993    }
3994
3995    let multleft_present = tclen.lock().unwrap()[TCMULTLEFT as usize] > 0;
3996    // c:2755-2758 — `if ((!tccan(TCMULTLEFT) || pos == 0) && pos <= vcs/2)`
3997    let mut cur = vcs;
3998    if (!multleft_present || pos == 0) && pos <= cur / 2 {
3999        let fd = SHTTY.load(Ordering::Relaxed);
4000        let out_fd = if fd >= 0 { fd } else { 1 };
4001        let _ = write_loop(out_fd, b"\r"); // c:2756 zputc(&zr_cr)
4002        cur = 0;
4003        VCS.store(0, Ordering::SeqCst); // c:2757 vcs = 0
4004    }
4005
4006    if pos < cur {
4007        // c:2760 — `tc_leftcurs(vcs - pos);`
4008        tc_leftcurs(cur - pos);
4009    } else if pos > cur {
4010        // c:2762 — `tc_rightcurs(pos - vcs);`
4011        tc_rightcurs((pos - cur) as usize);
4012    }
4013    // c:2764 — `vcs = pos;`
4014    VCS.store(pos, Ordering::SeqCst);
4015}
4016
4017/// Initialize ZLE refresh subsystem
4018/// Port of zle_refresh_boot() from zle_refresh.c
4019pub fn zle_refresh_boot() -> RefreshState {
4020    RefreshState::new()
4021}
4022
4023/// Port of `void zle_refresh_finish(void)` from `Src/Zle/zle_refresh.c:2720`.
4024/// Module-unload cleanup: `freevideo()`, drop `region_highlights` if
4025/// allocated (freeing memos via `free_region_highlights_memos()`,
4026/// zeroing `n_region_highlights`), then `free_cursor_forms()`.
4027pub fn zle_refresh_finish() {
4028    // c:2720
4029    freevideo(); // c:2722 — frees the global NBUF/OBUF
4030    let mut rh = REGION_HIGHLIGHTS.lock().unwrap(); // c:2724 if (region_highlights)
4031    if !rh.is_empty() {
4032        free_region_highlights_memos(); // c:2726
4033        rh.clear(); // c:2727-2729 zfree + NULL
4034    }
4035    drop(rh);
4036    crate::ported::zle::termquery::free_cursor_forms(); // c:2733
4037}
4038
4039// TextAttr / RefreshElement / VideoBuffer / RefreshState — Rust-side
4040// aggregates over zsh's C flat-globals (`winw`/`winh`/`vcs`/`vln`/
4041// `lpromptw`/`rpromptw`/`region_highlights[]`/`nbuf`/`obuf` in
4042// `Src/Zle/zle_refresh.c`). The C side represents these as separate
4043// file-scope statics + bitmap-packed `zattr` cells; this port collects
4044// them into structs for ergonomic access. Eventual unification target
4045// (mirroring `Src/zsh.h:2685` `pub type zattr = u64`):
4046//   - `TextAttr` → `zattr` (u64 packed bitmap)
4047//   - `RefreshElement` → `zle_h::REFRESH_ELEMENT`
4048//   - `VideoBuffer` → raw `Vec<REFRESH_ELEMENT>` for `nbuf`/`obuf`
4049//   - `RefreshState` → discrete file-scope statics
4050
4051/// Unpacked-bool form of `zattr` (C's u64 packed attribute bitmap from
4052/// `Src/zsh.h:2685`, ported as `pub type zattr = u64`). C stores
4053/// attributes inline in `REFRESH_ELEMENT.atr` (a `zattr`); this port
4054/// pre-unpacks to a 6-field struct for ergonomic access.
4055#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
4056pub struct TextAttr {
4057    /// `bold` field.
4058    pub bold: bool,
4059    /// `underline` field.
4060    pub underline: bool,
4061    /// `standout` field.
4062    pub standout: bool,
4063    /// `blink` field.
4064    pub blink: bool,
4065    /// `fg_color` field.
4066    pub fg_color: Option<u8>,
4067    /// `bg_color` field.
4068    pub bg_color: Option<u8>,
4069}
4070
4071/// Display cell. Loosely equivalent to zsh's `REFRESH_ELEMENT`
4072/// (legit-ported at `zle_h.rs:688` as
4073/// `pub struct REFRESH_ELEMENT { chr: REFRESH_CHAR, atr: zattr }`).
4074/// Adds a `width: u8` field C doesn't have and uses `TextAttr` for
4075/// `atr` instead of the C `zattr` bitmap.
4076#[derive(Debug, Clone, Default)]
4077pub struct RefreshElement {
4078    /// `chr` field.
4079    pub chr: char,
4080    /// `atr` field.
4081    pub atr: TextAttr,
4082    /// `width` field.
4083    pub width: u8,
4084}
4085
4086/// 2D screen-buffer container. C uses `REFRESH_STRING nbuf[]` and
4087/// `obuf[]` flat arrays of `REFRESH_ELEMENT *` (zle_refresh.c
4088/// globals); this struct wraps a single 2D Vec for the per-frame
4089/// new/old buffer pair.
4090#[derive(Debug, Clone)]
4091pub struct VideoBuffer {
4092    /// Buffer contents — 2D array of lines.
4093    pub lines: Vec<Vec<RefreshElement>>,
4094    /// Number of columns.
4095    pub cols: usize,
4096    /// Number of rows.
4097    pub rows: usize,
4098}
4099
4100/// Composite of zle_refresh.c globals (winw/winh/vcs/vln/vmaxln,
4101/// oldmax, lastrow, lastcol, more_status, etc.) collected into one
4102/// struct. C uses separate file-statics per name
4103/// (`int winw, winh, vcs, vln, ...`).
4104#[derive(Debug, Clone, Default)]
4105pub struct RefreshState {
4106    /// Number of columns.
4107    pub columns: usize, // winw, window width                                // c:682
4108    /// Number of lines.
4109    pub lines: usize, // winh, window height                                 // c:682
4110    /// Current line on screen (cursor row).
4111    pub vln: usize, // video cursor position line                            // c:680
4112    /// Current column on screen (cursor col).
4113    pub vcs: usize, // video cursor position column                          // c:680
4114    /// Prompt width (left).
4115    pub lpromptw: usize, // prompt widths on screen                          // c:676
4116    /// Right prompt width.
4117    pub rpromptw: usize, // prompt widths on screen                          // c:676
4118    /// Scroll offset for horizontal scrolling.
4119    pub scrolloff: usize,
4120    /// Region highlight start.
4121    pub region_highlight_start: Option<usize>,
4122    /// Region highlight end.
4123    pub region_highlight_end: Option<usize>,
4124    /// Old video buffer.
4125    pub old_video: Option<VideoBuffer>,
4126    /// New video buffer.
4127    pub new_video: Option<VideoBuffer>,
4128    /// Prompt string (left).
4129    pub lpromptbuf: String,
4130    /// Right prompt string.
4131    pub rpromptbuf: String,
4132    /// Whether we need full redraw.
4133    pub need_full_redraw: bool,
4134    /// Predisplay string (before main buffer).
4135    pub predisplay: String,
4136    /// Postdisplay string (after main buffer).
4137    pub postdisplay: String,
4138}
4139
4140// RegionHighlight / HighlightCategory / HighlightManager — Rust-side
4141// aggregates over zsh's C `region_highlights[N_SPECIAL_HIGHLIGHTS]`
4142// array + per-category attr globals (`default_attr`/`special_attr`/
4143// `ellipsis_attr` from `Src/Zle/zle_refresh.c`). C uses bare integer
4144// indexing into a fixed-size array; this port uses a typed enum +
4145// HashMap. Eventual unification: collapse into discrete file-scope
4146// statics matching the C layout.
4147
4148/// Simplified region-highlight entry. Loosely equivalent to
4149/// `struct region_highlight` (legit-ported at `zle_h.rs:613` with
4150/// different fields: start/end/atr/flags/memo/layer).
4151#[derive(Debug, Clone)]
4152pub struct RegionHighlight {
4153    /// `start` field.
4154    pub start: usize,
4155    /// `end` field.
4156    pub end: usize,
4157    /// `attr` field.
4158    pub attr: TextAttr,
4159    /// `memo` field.
4160    pub memo: Option<String>,
4161    /// `flags` field — `ZRH_PREDISPLAY` etc. (Src/Zle/zle_refresh.c
4162    /// `struct region_highlight`). Read by `spaceinline`/`shiftchars`
4163    /// to decide whether `predisplaylen` is subtracted when shifting
4164    /// region offsets on a buffer edit.
4165    pub flags: i32,
4166}
4167
4168/// Identifies a fixed slot in zsh's
4169/// `region_highlights[N_SPECIAL_HIGHLIGHTS]` array (zle_refresh.c
4170/// indices 0=region, 1=isearch, 2=suffix, 3=paste) plus the
4171/// standalone default/special/ellipsis attr globals
4172/// (`default_attr`/`special_attr`/`ellipsis_attr`). C uses bare
4173/// integer indexing — no enum.
4174#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4175pub enum HighlightCategory {
4176    /// `Region` variant.
4177    Region,
4178    /// `Isearch` variant.
4179    Isearch,
4180    /// `Suffix` variant.
4181    Suffix,
4182    /// `Paste` variant.
4183    Paste,
4184    /// `Default` variant.
4185    Default,
4186    /// `Special` variant.
4187    Special,
4188    /// `Ellipsis` variant.
4189    Ellipsis,
4190}
4191
4192/// Collects C's `region_highlights[]` array + per-category attr
4193/// globals (`default_attr`/`special_attr`/`ellipsis_attr` from
4194/// zle_refresh.c) into one container.
4195#[derive(Debug, Default)]
4196pub struct HighlightManager {
4197    /// `regions` field.
4198    pub regions: Vec<RegionHighlight>,
4199    /// Per-category attrs from `$zle_highlight`. Index by
4200    /// `HighlightCategory`. Equivalent to the per-slot atr storage
4201    /// in `region_highlights[]` and the
4202    /// `default_attr`/`special_attr`/`ellipsis_attr` globals in
4203    /// Src/Zle/zle_refresh.c — populated by `zle_set_highlight()`.
4204    pub category_attrs: std::collections::HashMap<HighlightCategory, TextAttr>,
4205}
4206
4207/// Build the per-character attribute overlay used by `zrefresh`.
4208/// One slot per char in `zleline`; `None` means "default attrs",
4209/// `Some(attr)` means apply `attr` for that cell.
4210///
4211/// Port of the inner loop in `zrefresh()` (Src/Zle/zle_refresh.c) that
4212/// consults `region_highlights[]` for each visible cell. The vi
4213/// visual-mode region is synthesised from `region_active` + `mark`
4214/// here so `v` selects visibly without callers having to push a
4215/// region themselves — matching zle_refresh.c's auto-promotion of
4216/// `region_active` into a paintable highlight.
4217pub fn compute_render_attrs() -> Vec<Option<TextAttr>> {
4218    let buf_len = ZLELINE.lock().unwrap().len();
4219    let mut attrs: Vec<Option<TextAttr>> = vec![None; buf_len];
4220
4221    // Visual-region attr: prefer the user's `region:` setting from
4222    // $zle_highlight (populated by zle_set_highlight); fall back to
4223    // standout per zsh's default at zle_refresh.c:397.
4224    let visual_attr = highlight()
4225        .lock()
4226        .unwrap()
4227        .category_attrs
4228        .get(&HighlightCategory::Region)
4229        .copied()
4230        .unwrap_or(TextAttr {
4231            standout: true,
4232            ..TextAttr::default()
4233        });
4234
4235    if REGION_ACTIVE.load(Ordering::SeqCst) != 0 {
4236        let (lo, hi) = if MARK.load(Ordering::SeqCst) <= ZLECS.load(Ordering::SeqCst) {
4237            (MARK.load(Ordering::SeqCst), ZLECS.load(Ordering::SeqCst))
4238        } else {
4239            (ZLECS.load(Ordering::SeqCst), MARK.load(Ordering::SeqCst))
4240        };
4241        let lo = lo.min(buf_len);
4242        let hi = hi.min(buf_len);
4243        for slot in attrs.iter_mut().take(hi).skip(lo) {
4244            *slot = Some(visual_attr);
4245        }
4246    }
4247    for region in &highlight().lock().unwrap().regions {
4248        let start = region.start.min(buf_len);
4249        let end = region.end.min(buf_len);
4250        for slot in attrs.iter_mut().take(end).skip(start) {
4251            *slot = Some(region.attr);
4252        }
4253    }
4254    attrs
4255}
4256
4257/// Full screen refresh - clears and redraws everything.
4258pub fn full_refresh() -> io::Result<()> {
4259    let fd = SHTTY.load(Ordering::Relaxed);
4260    let out = if fd >= 0 { fd } else { 1 };
4261    let _ = write_loop(out, b"\x1b[2J\x1b[H");
4262    zrefresh();
4263    Ok(())
4264}
4265
4266/// Partial refresh (optimize for minimal updates).
4267pub fn partial_refresh() -> io::Result<()> {
4268    zrefresh();
4269    Ok(())
4270}
4271
4272/// Calculate visible width of a prompt string — port of `countprompt()`
4273/// from Src/prompt.c:1140. The C function counts cells while skipping
4274/// the `Inpar..Outpar` (zsh's `%{...%}`) invisible-region tokens; this
4275/// Rust port skips ANSI escape sequences instead, which is what the
4276/// expanded prompt buffer contains by the time the refresh path uses it.
4277/// The C variant outputs width AND height via out-pointers; this port
4278/// returns width only (the only field the refresh path consumes here).
4279fn countprompt(s: &str) -> usize {
4280    let mut chars = s.chars().peekable();
4281    let mut width: usize = 0;
4282    while let Some(c) = chars.next() {
4283        if c == '\x1b' {
4284            // ANSI escape: skip until terminating letter.
4285            if chars.peek() == Some(&'[') {
4286                chars.next();
4287                while let Some(&nxt) = chars.peek() {
4288                    chars.next();
4289                    if nxt.is_ascii_alphabetic() {
4290                        break;
4291                    }
4292                }
4293            }
4294            continue;
4295        }
4296        width += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
4297    }
4298    width
4299}
4300
4301/// Parse a highlight attribute spec (the part after the `category:` prefix)
4302/// into a `TextAttr`. Accepts a comma-separated list of:
4303///   * `bold` / `nobold`,
4304///   * `underline` / `nounderline`,
4305///   * `standout` / `nostandout`,
4306///   * `blink` / `noblink`,
4307///   * `fg=N` / `bg=N` where N is 0..=255 (256-colour palette index) or
4308///     one of the named ANSI colours below,
4309///   * `none` (clears every attr).
4310///
4311/// ZLE-region subset of `match_highlight` (Src/prompt.c:2031),
4312/// restricted to the tokens users actually set in `$zle_highlight`.
4313/// The `hl=`/`layer=`/`opacity=` clauses (prompt.c:2042-2094) are
4314/// not surfaced here — those are prompt-system hooks that don't
4315/// apply to ZLE region paint.
4316pub fn match_highlight(spec: &str) -> TextAttr {
4317    let mut attr = TextAttr::default();
4318    for token in spec.split(',') {
4319        let token = token.trim();
4320        if token.is_empty() {
4321            continue;
4322        }
4323        match token {
4324            "none" => {
4325                attr = TextAttr::default();
4326            }
4327            "bold" => attr.bold = true,
4328            "nobold" => attr.bold = false,
4329            "underline" => attr.underline = true,
4330            "nounderline" => attr.underline = false,
4331            "standout" => attr.standout = true,
4332            "nostandout" => attr.standout = false,
4333            "blink" => attr.blink = true,
4334            "noblink" => attr.blink = false,
4335            other => {
4336                // Inline port of the named/numeric colour parser the C
4337                // `match_colour()` (Src/prompt.c:1957) does for `fg=`/
4338                // `bg=` clauses. The 24-bit `#rrggbb` form and
4339                // `bright-foo` aliases are not surfaced here.
4340                let parse = |name: &str| -> Option<u8> {
4341                    match name {
4342                        "black" => Some(0),
4343                        "red" => Some(1),
4344                        "green" => Some(2),
4345                        "yellow" => Some(3),
4346                        "blue" => Some(4),
4347                        "magenta" => Some(5),
4348                        "cyan" => Some(6),
4349                        "white" => Some(7),
4350                        "default" => None,
4351                        n => n.parse::<u8>().ok(),
4352                    }
4353                };
4354                if let Some(rest) = other.strip_prefix("fg=") {
4355                    attr.fg_color = parse(rest);
4356                } else if let Some(rest) = other.strip_prefix("bg=") {
4357                    attr.bg_color = parse(rest);
4358                }
4359                // Anything else (hl=, layer=, opacity=, unknown name) is
4360                // silently dropped — same as the C source's "found = 0"
4361                // exit path at prompt.c:2122 when no clause matched.
4362            }
4363        }
4364    }
4365    attr
4366}
4367
4368/// Port of `ZR_equal(zr1, zr2)` macro from `Src/Zle/zle_refresh.c:74-82`.
4369/// Multibyte path: `chr == chr && atr == atr && (combining-cluster eq)`.
4370/// Non-multibyte path collapses to the same first conjunction. Rust uses
4371/// the derived `PartialEq` on `REFRESH_ELEMENT`.
4372#[inline]
4373#[allow(non_snake_case)]
4374pub fn ZR_equal(
4375    // c:74
4376    a: REFRESH_ELEMENT,
4377    b: REFRESH_ELEMENT,
4378) -> bool {
4379    a == b
4380}
4381
4382/// Port of `ZR_memcpy(d, s, l)` macro from `Src/Zle/zle_refresh.c:92`.
4383/// `#define ZR_memcpy(d, s, l)  memcpy((d), (s), (l)*sizeof(REFRESH_ELEMENT))`.
4384/// Copy `l` REFRESH_ELEMENT slots from `src` to `dst`.
4385#[inline]
4386#[allow(non_snake_case)]
4387pub fn ZR_memcpy(
4388    // c:92
4389    dst: &mut [REFRESH_ELEMENT],
4390    src: &[REFRESH_ELEMENT],
4391    l: usize,
4392) {
4393    dst[..l].copy_from_slice(&src[..l]);
4394}
4395
4396/// Port of `zr_end_ellipsis[]` from `Src/Zle/zle_refresh.c:269-281`.
4397/// "...>" rendered when a long line overflows past the right edge.
4398/// TXT_ERROR is the standard zsh-error highlight (set in zsh_h::TXT_ERROR).
4399pub static ZR_END_ELLIPSIS: &[REFRESH_ELEMENT] = &[
4400    // c:269
4401    REFRESH_ELEMENT { chr: ' ', atr: 0 },
4402    REFRESH_ELEMENT {
4403        chr: '.',
4404        atr: TXT_ERROR,
4405    },
4406    REFRESH_ELEMENT {
4407        chr: '.',
4408        atr: TXT_ERROR,
4409    },
4410    REFRESH_ELEMENT {
4411        chr: '.',
4412        atr: TXT_ERROR,
4413    },
4414    REFRESH_ELEMENT {
4415        chr: '.',
4416        atr: TXT_ERROR,
4417    },
4418    REFRESH_ELEMENT { chr: '>', atr: 0 },
4419];
4420
4421/// Port of `zr_mid_ellipsis1[]` from `zle_refresh.c:287-294`.
4422/// First half of " <.... ... >" mid-line cluster.
4423pub static ZR_MID_ELLIPSIS1: &[REFRESH_ELEMENT] = &[
4424    // c:287
4425    REFRESH_ELEMENT { chr: ' ', atr: 0 },
4426    REFRESH_ELEMENT { chr: '<', atr: 0 },
4427    REFRESH_ELEMENT {
4428        chr: '.',
4429        atr: TXT_ERROR,
4430    },
4431    REFRESH_ELEMENT {
4432        chr: '.',
4433        atr: TXT_ERROR,
4434    },
4435    REFRESH_ELEMENT {
4436        chr: '.',
4437        atr: TXT_ERROR,
4438    },
4439    REFRESH_ELEMENT {
4440        chr: '.',
4441        atr: TXT_ERROR,
4442    },
4443];
4444
4445/// Port of `zr_mid_ellipsis2[]` from `zle_refresh.c:298-301`.
4446/// Trailing close of the mid-line ellipsis cluster.
4447pub static ZR_MID_ELLIPSIS2: &[REFRESH_ELEMENT] = &[
4448    // c:298
4449    REFRESH_ELEMENT {
4450        chr: '>',
4451        atr: TXT_ERROR,
4452    },
4453    REFRESH_ELEMENT { chr: ' ', atr: 0 },
4454];
4455
4456/// Port of `zr_start_ellipsis[]` from `zle_refresh.c:305-311`.
4457/// "><..." rendered when a line begins past the left edge.
4458pub static ZR_START_ELLIPSIS: &[REFRESH_ELEMENT] = &[
4459    // c:305
4460    REFRESH_ELEMENT { chr: '>', atr: 0 },
4461    REFRESH_ELEMENT {
4462        chr: '.',
4463        atr: TXT_ERROR,
4464    },
4465    REFRESH_ELEMENT {
4466        chr: '.',
4467        atr: TXT_ERROR,
4468    },
4469    REFRESH_ELEMENT {
4470        chr: '.',
4471        atr: TXT_ERROR,
4472    },
4473    REFRESH_ELEMENT {
4474        chr: '.',
4475        atr: TXT_ERROR,
4476    },
4477];
4478
4479/// Port of the `tcinscost(X)` macro from `Src/Zle/zle_refresh.c:1782`.
4480/// `#define tcinscost(X) (tccan(TCMULTINS) ? tclen[TCMULTINS] : (X)*tclen[TCINS])`.
4481/// Cost (in chars) to insert `x` characters: the parametrised multi-insert
4482/// capability if the terminal has it, else `x` single-inserts. `tccan(X)`
4483/// is `tclen[X]` (zsh.h:2680); the `tclen` substrate (init.rs) is populated
4484/// by the termcap loader, so the real costs are read here.
4485#[inline]
4486pub fn tcinscost(x: i32) -> i32 {
4487    // c:1782 — `#define tcinscost(X)
4488    //            (tccan(TCMULTINS) ? tclen[TCMULTINS] : (X)*tclen[TCINS])`
4489    // tccan(X) is tclen[X] (zsh.h:2680). The tclen substrate (init.rs:108)
4490    // is now populated by the termcap loader, so read the real costs: a
4491    // parametrised multi-insert costs one capability; otherwise it's `x`
4492    // single-char inserts.
4493    use crate::ported::init::tclen;
4494    use crate::ported::zsh_h::{TCINS, TCMULTINS};
4495    let t = tclen.lock().unwrap();
4496    if t[TCMULTINS as usize] != 0 {
4497        t[TCMULTINS as usize]
4498    } else {
4499        x * t[TCINS as usize]
4500    }
4501}
4502
4503/// Port of the `tcdelcost(X)` macro from `Src/Zle/zle_refresh.c:1783`.
4504/// `#define tcdelcost(X) (tccan(TCMULTDEL) ? tclen[TCMULTDEL] : (X)*tclen[TCDEL])`.
4505#[inline]
4506pub fn tcdelcost(x: i32) -> i32 {
4507    // c:1783 — `#define tcdelcost(X)
4508    //            (tccan(TCMULTDEL) ? tclen[TCMULTDEL] : (X)*tclen[TCDEL])`
4509    // Mirror of tcinscost for the delete-character capabilities.
4510    use crate::ported::init::tclen;
4511    use crate::ported::zsh_h::{TCDEL, TCMULTDEL};
4512    let t = tclen.lock().unwrap();
4513    if t[TCMULTDEL as usize] != 0 {
4514        t[TCMULTDEL as usize]
4515    } else {
4516        x * t[TCDEL as usize]
4517    }
4518}
4519
4520/// Port of `tc_delchars(X)` macro from `Src/Zle/zle_refresh.c:1726`.
4521/// `(void) tcmultout(TCDEL, TCMULTDEL, (X))`. Emit `x` character-
4522/// delete escapes via the multi-form helper. Without curses substrate
4523/// it's a no-op.
4524#[inline]
4525pub fn tc_delchars(x: i32) {
4526    // c:1784 — `#define tc_delchars(X) (void) tcmultout(TCDEL, TCMULTDEL, (X))`.
4527    // Emit the terminal's delete-character capability `x` times. Used by
4528    // refreshline's diff path (c:1993, c:2048) to remove characters
4529    // without a full repaint.
4530    let _ = tcmultout(
4531        crate::ported::zsh_h::TCDEL,
4532        crate::ported::zsh_h::TCMULTDEL,
4533        x,
4534    );
4535}
4536
4537/// Port of `tc_inschars(X)` macro from `Src/Zle/zle_refresh.c:1727`.
4538/// `(void) tcmultout(TCINS, TCMULTINS, (X))`.
4539#[inline]
4540pub fn tc_inschars(x: i32) {
4541    // c:1785 — `tcmultout(TCINS, TCMULTINS, (X))`.
4542    let _ = tcmultout(
4543        crate::ported::zsh_h::TCINS,
4544        crate::ported::zsh_h::TCMULTINS,
4545        x,
4546    );
4547}
4548
4549/// Port of `tc_upcurs(X)` macro from `Src/Zle/zle_refresh.c:1728`.
4550/// `(void) tcmultout(TCUP, TCMULTUP, (X))`.
4551#[inline]
4552pub fn tc_upcurs(x: i32) {
4553    // c:1786 — `tcmultout(TCUP, TCMULTUP, (X))`.
4554    let _ = tcmultout(crate::ported::zsh_h::TCUP, crate::ported::zsh_h::TCMULTUP, x);
4555}
4556
4557/// Port of `tc_leftcurs(X)` macro from `Src/Zle/zle_refresh.c:1729`.
4558/// `(void) tcmultout(TCLEFT, TCMULTLEFT, (X))`.
4559#[inline]
4560pub fn tc_leftcurs(x: i32) {
4561    // c:1729
4562    let _ = tcmultout(
4563        crate::ported::zsh_h::TCLEFT,
4564        crate::ported::zsh_h::TCMULTLEFT,
4565        x,
4566    );
4567}
4568
4569// =====================================================================
4570// Refresh-cycle file-static int globals — `Src/Zle/zle_refresh.c:827-832`.
4571// `static int cleareol, clearf, put_rpmpt, oput_rpmpt, oxtabs,
4572//             numscrolls, onumscrolls;`
4573// Carried as AtomicI32 so the multi-threaded shell can safely flip
4574// them between widget invocations without locking.
4575// =====================================================================
4576
4577/// Port of `char *tcout_func_name;` from `Src/Zle/zle_refresh.c:246`.
4578/// Holds the name of the user `zle -T tc <fn>` redisplay-transform
4579/// function; cleared by `zle -T -r`. The refresh path invokes it
4580/// via `getshfunc(tcout_func_name)` (zle_refresh.c:2303).
4581pub static TCOUT_FUNC_NAME: std::sync::Mutex<Option<String>> = // c:246
4582    std::sync::Mutex::new(None);
4583
4584/// Port of `zattr pmpt_attr, rpmpt_attr, prompt_attr;` from
4585/// `Src/Zle/zle_refresh.c:152`. Captured text attributes for the
4586/// left prompt / right prompt / the prompt-tail position respectively.
4587///
4588/// Used by:
4589///   - zle_main.c:1238 — `pmpt_attr = txtcurrentattrs;` after
4590///     left-prompt expansion to remember the SGR state at prompt end.
4591///   - zle_main.c:1247 — same for `rpmpt_attr` after right-prompt.
4592///   - zle_main.c:1248 — `prompt_attr = …` derived from the two.
4593///   - zle_refresh.c:1163/1666 — `txtcurrentattrs = txtpendingattrs =
4594///     {pmpt,rpmpt}_attr;` to restore the captured state when
4595///     redrawing.
4596///   - zle_refresh.c:1657 — `treplaceattrs(pmpt_attr);` to flush.
4597///
4598/// `zattr` is `u64` (zsh.h:2689); AtomicU64 with Relaxed ordering
4599/// matches C's plain global-int read/write shape.
4600pub static PMPT_ATTR: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); // c:152
4601/// `RPMPT_ATTR` static.
4602pub static RPMPT_ATTR: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); // c:152
4603/// `PROMPT_ATTR` static.
4604pub static PROMPT_ATTR: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); // c:152
4605
4606/// Port of `static zattr ellipsis_attr` from `Src/Zle/zle_refresh.c`. The
4607/// attribute on the leading cell of the ">..."/"...<" scroll-ellipsis
4608/// indicators. C seeds it from the `special` zle_highlight; default until
4609/// that wiring lands.
4610pub static ELLIPSIS_ATTR: std::sync::atomic::AtomicU64 =
4611    std::sync::atomic::AtomicU64::new(0); // c:152
4612
4613/// Port of `static zattr special_attr` from `Src/Zle/zle_refresh.c`. The
4614/// attribute applied to the synthesized cells of the status line (the
4615/// `^X` control glyphs and `<....>` hex escapes) — `all_attr = special_attr`
4616/// at c:1430. C seeds it from the `special` zle_highlight; default until
4617/// that wiring lands (same status as `PROMPT_ATTR`/`ELLIPSIS_ATTR`).
4618pub static SPECIAL_ATTR: std::sync::atomic::AtomicU64 =
4619    std::sync::atomic::AtomicU64::new(0); // c:152
4620
4621/// Port of `static int cleareol` from `Src/Zle/zle_refresh.c:827`.
4622/// Clear-to-end-of-line flag — set when the terminal lacks `cleareod`
4623/// and we have to fall back to per-line clear.
4624pub static CLEAREOL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:827
4625
4626/// Port of `static int clearf` from `Src/Zle/zle_refresh.c:828`.
4627/// Set when `alwayslastprompt` was used immediately before the
4628/// current refresh — drives a special clear path.
4629pub static CLEARF: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:828
4630
4631/// Port of `static int put_rpmpt` from `Src/Zle/zle_refresh.c:829`.
4632/// Whether we should display the right-prompt this refresh.
4633pub static PUT_RPMPT: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:829
4634
4635/// Port of `static int oput_rpmpt` from `Src/Zle/zle_refresh.c:830`.
4636/// Whether the right-prompt was displayed last refresh.
4637pub static OPUT_RPMPT: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:830
4638
4639/// Port of `static int oxtabs` from `Src/Zle/zle_refresh.c:831`.
4640/// `oxtabs` flag — tabs expand to spaces if set.
4641pub static OXTABS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:831
4642
4643/// Port of `static int numscrolls` from `Src/Zle/zle_refresh.c:832`.
4644/// Count of scroll operations this refresh — used by `nextline` to
4645/// decide whether to abort line-loop processing.
4646pub static NUMSCROLLS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:832
4647
4648/// Port of `static int onumscrolls` from `Src/Zle/zle_refresh.c:832`.
4649/// Previous refresh's `numscrolls` value — `nextline` compares to
4650/// detect runaway scrolling.
4651pub static ONUMSCROLLS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:832
4652
4653// =====================================================================
4654// mod_export refresh-state globals — `Src/Zle/zle_refresh.c:157-188`.
4655// Exposed across translation units (other modules read them).
4656// AtomicI32 for safe lock-free access.
4657// =====================================================================
4658
4659/// Port of `mod_export int nlnct` from `Src/Zle/zle_refresh.c:157`.
4660/// Number of lines counted in the prompt+buffer for the current
4661/// refresh — drives nbuf allocation (`nlnct * winw` cells).
4662pub static NLNCT: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:157
4663
4664/// Port of `static REFRESH_STRING *nbuf` / `*obuf` from
4665/// `Src/Zle/zle_refresh.c:670`. The new (`NBUF`) and old (`OBUF`) video
4666/// buffers: one `REFRESH_STRING` (a `Vec<REFRESH_ELEMENT>`) per screen
4667/// line. `zrefresh` builds `NBUF`, `refreshline` diffs `NBUF[ln]` against
4668/// `OBUF[ln]` and emits the minimal terminal updates, then they are
4669/// swapped (c:954-955) so this frame's new buffer is next frame's old.
4670pub static NBUF: std::sync::Mutex<Vec<REFRESH_STRING>> = std::sync::Mutex::new(Vec::new()); // c:670
4671pub static OBUF: std::sync::Mutex<Vec<REFRESH_STRING>> = std::sync::Mutex::new(Vec::new()); // c:670
4672
4673/// Port of `static int winw` from `Src/Zle/zle_refresh.c:682`.
4674/// Terminal window width in cells; bounded by `zterm_columns`.
4675pub static WINW: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(80); // c:682
4676
4677/// Port of `static int winh` from `Src/Zle/zle_refresh.c:682`.
4678/// Terminal window height in cells; bounded by `zterm_lines`.
4679pub static WINH: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(24); // c:682
4680
4681/// Port of `static int more_start` from `Src/Zle/zle_refresh.c:672` —
4682/// "more text before start of screen?". Set by `scrollwindow` when the
4683/// buffer scrolls content off the top (c:808), reset each frame (c:1119);
4684/// the first-line ">..." indicator reads it (c:1643, not yet rendered).
4685pub static MORE_START: std::sync::atomic::AtomicI32 =
4686    std::sync::atomic::AtomicI32::new(0); // c:672
4687
4688/// Port of `static int more_end` from `Src/Zle/zle_refresh.c:672` — "more
4689/// text after end of screen?". Set by `snextline` when the status pane
4690/// scrolls content off the bottom (c:887/895); reset each frame (c:1119).
4691/// The "...<" end indicator reads it (the consumer is unported, like the
4692/// `more_start` ">..." one).
4693pub static MORE_END: std::sync::atomic::AtomicI32 =
4694    std::sync::atomic::AtomicI32::new(0); // c:672
4695
4696/// Port of `mod_export int resetneeded` from `Src/Zle/zle_refresh.c`.
4697/// Set when the display must be fully redrawn (e.g. after clear-screen or
4698/// a SIGWINCH); the ZLE loop honours it on its next pass. zshrs's ZLE
4699/// loop doesn't read it yet, so widgets that set it also trigger the
4700/// redraw directly — this is the name-parity anchor for when the loop's
4701/// resetneeded handling lands.
4702pub static RESETNEEDED: std::sync::atomic::AtomicI32 =
4703    std::sync::atomic::AtomicI32::new(0);
4704
4705/// Port of `static int lpromptw` from `Src/Zle/zle_refresh.c:676`.
4706/// Left prompt's on-screen width after expansion / truncation.
4707pub static LPROMPTW: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:676
4708
4709/// Port of `static int vcs` from `Src/Zle/zle_refresh.c:680`.
4710/// Video cursor column — physical column of the cursor on screen.
4711pub static VCS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:680
4712
4713/// Port of `static int vln` from `Src/Zle/zle_refresh.c:680`.
4714/// Video cursor line — physical row of the cursor on screen.
4715pub static VLN: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:680
4716
4717/// Port of `mod_export int showinglist` from `Src/Zle/zle_refresh.c:165`.
4718/// Non-zero when a completion-listing is currently displayed below
4719/// the prompt; refreshes need to redraw it on next paint.
4720pub static SHOWINGLIST: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:165
4721
4722/// Port of `mod_export int listshown` from `Src/Zle/zle_refresh.c:171`.
4723/// Number of completion-listing lines actually shown last refresh —
4724/// used by clear path to know how many lines to wipe.
4725pub static LISTSHOWN: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:171
4726
4727/// Port of `mod_export int lastlistlen` from `Src/Zle/zle_refresh.c:176`.
4728/// Length of the previous listing (separate from `listshown` because
4729/// the listing might be paginated).
4730pub static LASTLISTLEN: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:176
4731
4732/// Port of `static int vmaxln` from `Src/Zle/zle_refresh.c:680`.
4733/// Maximum line index reached during this refresh — used to decide
4734/// how much of the prior frame to clear.
4735pub static VMAXLN: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:680
4736
4737/// Port of `static int winprompt` from `Src/Zle/zle_refresh.c:680`.
4738/// Number of physical lines the prompt occupies on screen.
4739pub static WINPROMPT: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:680
4740
4741/// Port of `static int winpos` from `Src/Zle/zle_refresh.c:680`.
4742/// Horizontal scroll offset when the buffer is wider than winw.
4743pub static WINPOS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1); // c:680
4744
4745/// Port of `static int rwinh` from `Src/Zle/zle_refresh.c:682`.
4746/// Real terminal-line count (vs the TERM_SHORT-clamped `winh`).
4747pub static RWINH: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(24); // c:682
4748
4749/// Port of `static int lpromptwof` from `Src/Zle/zle_refresh.c:676`.
4750/// Left prompt's pre-wrap visible width (`lpromptw` may differ when
4751/// the prompt fills the line exactly and forces an extra row).
4752pub static LPROMPTWOF: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:676
4753
4754/// Port of `static int lprompth` from `Src/Zle/zle_refresh.c:676`.
4755/// Left prompt's height in rows.
4756pub static LPROMPTH: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:676
4757
4758/// Port of `static int rpromptw` from `Src/Zle/zle_refresh.c:676`.
4759/// Right prompt's on-screen width.
4760pub static RPROMPTW: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:676
4761
4762/// Port of `static int rprompth` from `Src/Zle/zle_refresh.c:676`.
4763/// Right prompt's height in rows.
4764pub static RPROMPTH: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:676
4765
4766/// Port of `static int olnct` from `Src/Zle/zle_refresh.c:157`.
4767/// Number of lines in the previous refresh — caller diff renders
4768/// against this.
4769pub static OLNCT: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:157
4770
4771
4772/// Port of `mod_export int trashedzle` from `Src/Zle/zle_refresh.c:181`.
4773/// Set when the on-screen line was wiped (by `trashzle`); next refresh
4774/// must do a full redraw.
4775pub static TRASHEDZLE: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:181
4776
4777/// Port of `mod_export int clearflag` from `Src/Zle/zle_refresh.c:183`.
4778/// Request a full screen-clear on next refresh (set by `clear-screen`
4779/// widget + Ctrl+L).
4780pub static CLEARFLAG: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:183
4781
4782/// Port of `mod_export int clearlist` from `Src/Zle/zle_refresh.c:188`.
4783/// Request the completion-listing be wiped on next refresh.
4784pub static CLEARLIST: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:188
4785
4786/// Port of `struct rparams` from `Src/Zle/zle_refresh.c:815`. Workspace
4787/// state threaded through `zrefresh` + `nextline` + `wpfx` — tracks the
4788/// current line being painted, scroll budget, video cursor, and the
4789/// in/out pointers into the video buffer.
4790///
4791/// C definition (c:815-824):
4792/// ```c
4793/// struct rparams {
4794///     int canscroll;
4795///     int ln;
4796///     int more_status;
4797///     int nvcs;
4798///     int nvln;
4799///     int tosln;
4800///     REFRESH_STRING s;
4801///     REFRESH_STRING sen;
4802/// };
4803/// typedef struct rparams *rparams;
4804/// ```
4805///
4806/// Rust port replaces `REFRESH_STRING s/sen` (raw pointers into the
4807/// video buffer) with `pos`/`end` byte indices for safe access.
4808#[derive(Debug, Clone, Default)]
4809#[allow(non_camel_case_types)]
4810pub struct rparams {
4811    // c:815
4812    /// Number of lines we are allowed to scroll.
4813    pub canscroll: i32, // c:816
4814    /// Current line we're working on.
4815    pub ln: i32, // c:817
4816    /// More stuff in status line.
4817    pub more_status: i32, // c:818
4818    /// Video cursor column.
4819    pub nvcs: i32, // c:819
4820    /// Video cursor line.
4821    pub nvln: i32, // c:820
4822    /// Tmp in statusline stuff.
4823    pub tosln: i32, // c:821
4824    /// Cursor index into the video buffer (was `REFRESH_STRING s`).
4825    pub pos: usize, // c:822
4826    /// End-of-line index (was `REFRESH_STRING sen`).
4827    pub end: usize, // c:823
4828}
4829
4830/// Port of `void set_region_highlight(UNUSED(Param pm), char **aval)`
4831/// from `Src/Zle/zle_refresh.c:488`. Setter for the `$region_highlight`
4832/// special parameter. C body: resize the `region_highlights[]` global
4833/// to `len(aval) + N_SPECIAL_HIGHLIGHTS`, freeing memo strings on
4834/// each replaced entry, then parse each `aval[i]` into one entry:
4835/// optional leading `P` sets `ZRH_PREDISPLAY`, two decimal indices
4836/// become `start`/`end`, `match_highlight()` parses the attribute
4837/// spec into `atr`+`atrmask` and optional `layer`, and a trailing
4838/// `memo=NAME` field is stored verbatim. Passing `aval=None` (the
4839/// `NULL` case from `unset_region_highlight`, c:595) truncates to
4840/// the special baseline. C uses a packed `struct region_highlight`
4841/// global; the Rust port stores parsed entries in
4842/// `REGION_HIGHLIGHTS` via the simplified `RegionHighlight` shape.
4843/// WARNING: param names don't match C — Rust=(aval) vs C=(pm, aval)
4844pub fn set_region_highlight(aval: Option<&[String]>) {
4845    // c:488
4846    let aval = match aval {
4847        // c:510 !aval
4848        Some(a) => a,
4849        None => {
4850            // c:495-508 — truncate to special baseline when aval is NULL.
4851            REGION_HIGHLIGHTS.lock().unwrap().clear();
4852            return; // c:511
4853        }
4854    };
4855    let mut rh = REGION_HIGHLIGHTS.lock().unwrap();
4856    rh.clear(); // c:500 free memos
4857    for entry in aval.iter() {
4858        // c:513
4859        let mut oldstrp: &str = entry.as_str(); // c:519
4860        let mut flags: i32 = 0; // c:525
4861        if oldstrp.starts_with('P') {
4862            // c:520
4863            flags = ZRH_PREDISPLAY; // c:521
4864            oldstrp = &oldstrp[1..]; // c:522
4865        }
4866        oldstrp = oldstrp.trim_start_matches(|c: char| c == ' ' || c == '\t'); // c:526
4867        let (start_val, rest1) = crate::ported::utils::zstrtol(oldstrp, 10); // c:529
4868        let start = if oldstrp.len() == rest1.len() {
4869            -1i32
4870        } else {
4871            start_val as i32
4872        }; // c:530-531
4873        let strp = rest1.trim_start_matches(|c: char| c == ' ' || c == '\t'); // c:533
4874        let (end_val, rest2) = crate::ported::utils::zstrtol(strp, 10); // c:537
4875        let end = if strp.len() == rest2.len() {
4876            -1i32
4877        } else {
4878            end_val as i32
4879        }; // c:537-538
4880        let strp = rest2.trim_start_matches(|c: char| c == ' ' || c == '\t'); // c:541
4881                                                                              // c:545 — match_highlight(strp, ...) into attr fields.
4882        let attr = match_highlight(strp);
4883        // c:551 — memo= field extraction.
4884        let memo = if let Some(rest) = strp.strip_prefix("memo=") {
4885            // c:517,551
4886            let end_pos = rest
4887                .find(|c: char| c == ',' || c == ' ' || c == '\t' || c == '\0')
4888                .unwrap_or(rest.len());
4889            Some(rest[..end_pos].to_string()) // c:581
4890        } else {
4891            None
4892        }; // c:583
4893        if start >= 0 && end >= 0 {
4894            rh.push(RegionHighlight {
4895                start: start as usize,
4896                end: end as usize,
4897                attr,
4898                memo,
4899                flags, // c:521 — ZRH_PREDISPLAY from the `P` prefix (was discarded)
4900            });
4901        }
4902    }
4903    // c:586 — freearray(av): aval owned by caller in Rust.
4904}
4905
4906/// Direct port of `void unset_region_highlight(Param pm, int exp)` from
4907/// `Src/Zle/zle_refresh.c:592`.
4908/// ```c
4909/// void unset_region_highlight(Param pm, int exp) {
4910///     if (exp) {
4911///         set_region_highlight(pm, NULL);
4912///         stdunsetfn(pm, exp);
4913///     }
4914/// }
4915/// ```
4916/// Unset hook for the `$region_highlight` special parameter: when the
4917/// user explicitly unsets it (`exp` true), clear the user region
4918/// highlights back to the special baseline (`set_region_highlight(NULL)`,
4919/// c:595) and run the standard parameter unset (c:596).
4920pub fn unset_region_highlight(pm: &mut crate::ported::zsh_h::param, exp: i32) {
4921    // c:592
4922    if exp != 0 {
4923        set_region_highlight(None); // c:595 set_region_highlight(pm, NULL)
4924        crate::ported::params::stdunsetfn(pm, exp); // c:596
4925    }
4926}
4927
4928/// Direct port of `char **get_region_highlight(Param pm)` from
4929/// `Src/Zle/zle_refresh.c:430`. The get hook for the `$region_highlight`
4930/// special parameter: format each user region highlight as
4931/// `"[P]start end <attr-spec>[ memo=NAME]"` (c:466-476) via the real
4932/// `output_highlight` (the highlight spec, `fg=red,bold`). C skips the
4933/// first `N_SPECIAL_HIGHLIGHTS` (4) cursor/region/isearch/suffix entries
4934/// (c:443); the Rust `REGION_HIGHLIGHTS` holds ONLY user entries (the
4935/// special baseline isn't stored there), so every entry is a user
4936/// highlight and no skip is needed. Empty store → empty array (c:437-438).
4937///
4938/// `RegionHighlight` stores `attr` as a `TextAttr` (no `atrmask`, since the
4939/// TextAttr-returning `match_highlight` dropped it), so the (atr, mask)
4940/// pair `output_highlight` needs is rebuilt from the set flag bits — exact
4941/// for positive specs (the common case); the explicit-"no"/layer semantics
4942/// aren't recoverable from `TextAttr` and are omitted.
4943pub fn get_region_highlight(_pm: &crate::ported::zsh_h::param) -> Vec<String> {
4944    // c:430
4945    use crate::ported::zsh_h::{
4946        zattr, TXTBGCOLOUR, TXTBOLDFACE, TXTFGCOLOUR, TXTSTANDOUT, TXTUNDERLINE,
4947        TXT_ATTR_BG_COL_SHIFT, TXT_ATTR_FG_COL_SHIFT,
4948    };
4949    let rh = REGION_HIGHLIGHTS.lock().unwrap();
4950    rh.iter()
4951        .map(|rhp| {
4952            let mut s = String::new();
4953            // c:466-468 — `sprintf("%s%s ", P?, "start end")`.
4954            if rhp.flags & ZRH_PREDISPLAY != 0 {
4955                s.push('P'); // c:467
4956            }
4957            s.push_str(&format!("{} {} ", rhp.start, rhp.end));
4958            // c:469 — output_highlight(atr, atrmask). Rebuild (atr, mask)
4959            // from the TextAttr: every set field contributes both its value
4960            // to `atr` and its flag bit to `mask`.
4961            let ta = &rhp.attr;
4962            let mut atr: zattr = 0;
4963            let mut mask: zattr = 0;
4964            if ta.bold {
4965                atr |= TXTBOLDFACE;
4966                mask |= TXTBOLDFACE;
4967            }
4968            if ta.underline {
4969                atr |= TXTUNDERLINE;
4970                mask |= TXTUNDERLINE;
4971            }
4972            if ta.standout {
4973                atr |= TXTSTANDOUT;
4974                mask |= TXTSTANDOUT;
4975            }
4976            if let Some(fg) = ta.fg_color {
4977                atr |= TXTFGCOLOUR | ((fg as zattr) << TXT_ATTR_FG_COL_SHIFT);
4978                mask |= TXTFGCOLOUR;
4979            }
4980            if let Some(bg) = ta.bg_color {
4981                atr |= TXTBGCOLOUR | ((bg as zattr) << TXT_ATTR_BG_COL_SHIFT);
4982                mask |= TXTBGCOLOUR;
4983            }
4984            s.push_str(&crate::ported::prompt::output_highlight(atr, mask));
4985            // c:473-475 — `memo=NAME`.
4986            if let Some(memo) = &rhp.memo {
4987                s.push_str(" memo=");
4988                s.push_str(memo);
4989            }
4990            s
4991        })
4992        .collect()
4993}
4994
4995/// Process-wide region-highlights table, the Rust analog of C's
4996/// `mod_export struct region_highlight *region_highlights` global
4997/// (`Src/Zle/zle_refresh.c:471`). Holds parsed `$region_highlight`
4998/// entries plus internal callers (isearch/region/suffix).
4999pub static REGION_HIGHLIGHTS: once_cell::sync::Lazy<std::sync::Mutex<Vec<RegionHighlight>>> =
5000    once_cell::sync::Lazy::new(|| std::sync::Mutex::new(Vec::new()));
5001
5002/// Port of `ZRH_PREDISPLAY` from `Src/Zle/zle.h` — bit flag set when
5003/// a `region_highlight` entry's indices are relative to the
5004/// predisplay buffer rather than `zleline`.
5005pub const ZRH_PREDISPLAY: i32 = 1;
5006
5007#[cfg(test)]
5008mod zr_tests {
5009    use super::*;
5010    use crate::ported::zle::zle_h::REFRESH_ELEMENT;
5011    use crate::ported::zsh_h::{TXTBOLDFACE, TXT_MULTIWORD_MASK};
5012
5013    fn re(c: char, a: u64) -> REFRESH_ELEMENT {
5014        REFRESH_ELEMENT { chr: c, atr: a }
5015    }
5016
5017    #[test]
5018    fn zr_memset_fills_slice() {
5019        let _g = crate::test_util::global_state_lock();
5020        let _g = zle_test_setup();
5021        // c:88-89 — `while (len--) *dst++ = rc`.
5022        let mut buf = [REFRESH_ELEMENT::default(); 4];
5023        let fill = re('x', 0);
5024        ZR_memset(&mut buf, fill, 3);
5025        assert_eq!(buf[0], fill);
5026        assert_eq!(buf[1], fill);
5027        assert_eq!(buf[2], fill);
5028        // 4th slot unchanged
5029        assert_eq!(buf[3], REFRESH_ELEMENT::default());
5030    }
5031
5032    #[test]
5033    fn zr_memset_clamps_to_dst_len() {
5034        let _g = crate::test_util::global_state_lock();
5035        let _g = zle_test_setup();
5036        let mut buf = [REFRESH_ELEMENT::default(); 2];
5037        let fill = re('y', 0);
5038        ZR_memset(&mut buf, fill, 99); // len > dst.len()
5039        assert_eq!(buf[0], fill);
5040        assert_eq!(buf[1], fill);
5041    }
5042
5043    #[test]
5044    fn zr_strlen_counts_to_nul() {
5045        let _g = crate::test_util::global_state_lock();
5046        let _g = zle_test_setup();
5047        // c:106 — `while (wstr++->chr != ZWC('\0')) len++`.
5048        let s = [re('h', 0), re('i', 0), re('\0', 0)];
5049        assert_eq!(ZR_strlen(&s), 2);
5050    }
5051
5052    #[test]
5053    fn zr_strlen_empty_starts_with_nul() {
5054        let _g = crate::test_util::global_state_lock();
5055        let _g = zle_test_setup();
5056        let s = [re('\0', 0)];
5057        assert_eq!(ZR_strlen(&s), 0);
5058    }
5059
5060    #[test]
5061    fn zr_strcpy_copies_through_nul() {
5062        let _g = crate::test_util::global_state_lock();
5063        let _g = zle_test_setup();
5064        // c:97 — `while ((*dst++ = *src++).chr != ZWC('\0'))`. NUL
5065        // included in copy.
5066        let src = [re('a', 0), re('b', 0), re('\0', 0)];
5067        let mut dst = [REFRESH_ELEMENT::default(); 5];
5068        ZR_strcpy(&mut dst, &src);
5069        assert_eq!(dst[0], re('a', 0));
5070        assert_eq!(dst[1], re('b', 0));
5071        assert_eq!(dst[2], re('\0', 0));
5072    }
5073
5074    #[test]
5075    fn zr_strncmp_equal_strings() {
5076        let _g = crate::test_util::global_state_lock();
5077        let _g = zle_test_setup();
5078        // c:127 — pair-equal in chr+atr: returns 0.
5079        let a = [re('h', 0), re('i', 0)];
5080        let b = [re('h', 0), re('i', 0)];
5081        assert_eq!(ZR_strncmp(&a, &b, 2), 0);
5082    }
5083
5084    #[test]
5085    fn zr_strncmp_diff_chr_returns_1() {
5086        let _g = crate::test_util::global_state_lock();
5087        let _g = zle_test_setup();
5088        let a = [re('h', 0), re('i', 0)];
5089        let b = [re('h', 0), re('o', 0)];
5090        // c:127 — `if (!ZR_equal(...)) return 1`.
5091        assert_eq!(ZR_strncmp(&a, &b, 2), 1);
5092    }
5093
5094    #[test]
5095    fn zr_strncmp_diff_atr_returns_1() {
5096        let _g = crate::test_util::global_state_lock();
5097        let _g = zle_test_setup();
5098        // c:127 — atr is part of equality.
5099        let a = [re('h', 0)];
5100        let b = [re('h', TXTBOLDFACE)];
5101        assert_eq!(ZR_strncmp(&a, &b, 1), 1);
5102    }
5103
5104    #[test]
5105    fn zr_strncmp_early_nul_old() {
5106        let _g = crate::test_util::global_state_lock();
5107        let _g = zle_test_setup();
5108        // c:124-126 — old has NUL → return !equal.
5109        let a = [re('\0', 0)];
5110        let b = [re('x', 0)];
5111        assert_eq!(ZR_strncmp(&a, &b, 1), 1); // not equal
5112        let a = [re('\0', 0)];
5113        let b = [re('\0', 0)];
5114        assert_eq!(ZR_strncmp(&a, &b, 1), 0); // equal NULs
5115    }
5116
5117    #[test]
5118    fn zr_strncmp_multiword_mask_skips_nul_check() {
5119        let _g = crate::test_util::global_state_lock();
5120        let _g = zle_test_setup();
5121        // c:124 — `(!(oldwstr->atr & TXT_MULTIWORD_MASK) && !oldwstr->chr)`.
5122        // If atr has MULTIWORD set, chr=='\0' is NOT a NUL terminator.
5123        let a = [re('\0', TXT_MULTIWORD_MASK)];
5124        let b = [re('\0', TXT_MULTIWORD_MASK)];
5125        // Both elements equal (same chr+atr) → returns 0; the
5126        // multiword mask path skips the early-NUL exit so we fall
5127        // through to the regular ZR_equal check.
5128        assert_eq!(ZR_strncmp(&a, &b, 1), 0);
5129    }
5130
5131    #[test]
5132    fn zr_equal_same_returns_true() {
5133        let _g = crate::test_util::global_state_lock();
5134        let _g = zle_test_setup();
5135        let a = re('a', 0);
5136        assert!(ZR_equal(a, a));
5137        let b = re('b', 0);
5138        assert!(!ZR_equal(a, b));
5139    }
5140
5141    #[test]
5142    fn zr_memcpy_copies_n_elements() {
5143        let _g = crate::test_util::global_state_lock();
5144        let _g = zle_test_setup();
5145        let mut dst = [re('\0', 0); 5];
5146        let src = [re('a', 0), re('b', 0), re('c', 0), re('d', 0), re('e', 0)];
5147        ZR_memcpy(&mut dst, &src, 3);
5148        assert_eq!(dst[0].chr, 'a');
5149        assert_eq!(dst[1].chr, 'b');
5150        assert_eq!(dst[2].chr, 'c');
5151        assert_eq!(dst[3].chr, '\0');
5152    }
5153
5154    #[test]
5155    fn ellipsis_sizes_match_table_lengths() {
5156        let _g = crate::test_util::global_state_lock();
5157        let _g = zle_test_setup();
5158        assert_eq!(ZR_END_ELLIPSIS_SIZE, 6);
5159        assert_eq!(ZR_MID_ELLIPSIS1_SIZE, 6);
5160        assert_eq!(ZR_MID_ELLIPSIS2_SIZE, 2);
5161        assert_eq!(ZR_START_ELLIPSIS_SIZE, 5);
5162    }
5163
5164    #[test]
5165    fn def_mwbuf_alloc_is_32() {
5166        let _g = crate::test_util::global_state_lock();
5167        let _g = zle_test_setup();
5168        assert_eq!(DEF_MWBUF_ALLOC, 32);
5169    }
5170
5171    /// c:1782-1783 — tcinscost/tcdelcost follow the literal C macro
5172    /// `(X)*tclen[TC*]` in the per-char branch. This previously asserted
5173    /// the old `x.max(0)` fake (`tcinscost(5)==5` regardless of tclen);
5174    /// now it pins the faithful formula with a deterministic single-char
5175    /// cost of 1, matching C exactly (including the unclamped negative,
5176    /// which refreshline never produces since `i` starts at 1).
5177    #[test]
5178    fn tc_costs_handle_negative() {
5179        use crate::ported::init::tclen;
5180        use crate::ported::zsh_h::{TCDEL, TCINS, TCMULTDEL, TCMULTINS};
5181        let _g = crate::test_util::global_state_lock();
5182        let _g2 = zle_test_setup();
5183
5184        let save = {
5185            let t = tclen.lock().unwrap();
5186            (
5187                t[TCMULTINS as usize],
5188                t[TCINS as usize],
5189                t[TCMULTDEL as usize],
5190                t[TCDEL as usize],
5191            )
5192        };
5193        {
5194            let mut t = tclen.lock().unwrap();
5195            t[TCMULTINS as usize] = 0; // no multi-cap → per-char branch
5196            t[TCINS as usize] = 1;
5197            t[TCMULTDEL as usize] = 0;
5198            t[TCDEL as usize] = 1;
5199        }
5200        assert_eq!(tcinscost(-1), -1); // c: literal (-1)*tclen[TCINS]
5201        assert_eq!(tcdelcost(-1), -1);
5202        assert_eq!(tcinscost(5), 5);
5203        assert_eq!(tcdelcost(5), 5);
5204
5205        let mut t = tclen.lock().unwrap();
5206        t[TCMULTINS as usize] = save.0;
5207        t[TCINS as usize] = save.1;
5208        t[TCMULTDEL as usize] = save.2;
5209        t[TCDEL as usize] = save.3;
5210    }
5211
5212    #[test]
5213    fn rparams_default_zeros_all_fields() {
5214        let _g = crate::test_util::global_state_lock();
5215        let _g = zle_test_setup();
5216        let r = rparams::default();
5217        assert_eq!(r.canscroll, 0);
5218        assert_eq!(r.ln, 0);
5219        assert_eq!(r.more_status, 0);
5220        assert_eq!(r.nvcs, 0);
5221        assert_eq!(r.nvln, 0);
5222        assert_eq!(r.tosln, 0);
5223        assert_eq!(r.pos, 0);
5224        assert_eq!(r.end, 0);
5225    }
5226}
5227
5228#[cfg(test)]
5229mod tests {
5230    use super::*;
5231
5232    /// zrefresh builds NBUF from the prompt + editable line (c:1208-1400):
5233    /// prompt cells, then line chars with tab→8-col-stop and control→`^X`
5234    /// expansion. Verifies the video buffer the diff machinery consumes.
5235    #[test]
5236    fn zrefresh_builds_nbuf_cells() {
5237        let _g = crate::test_util::global_state_lock();
5238        // Drive the editable line directly: "ab\tc\u{1}d".
5239        *ZLELINE.lock().unwrap() = "ab\tc\u{1}d".chars().collect();
5240        ZLECS.store(0, Ordering::SeqCst);
5241        ZLELL.store(6, Ordering::SeqCst);
5242
5243        zrefresh();
5244
5245        let nbuf = NBUF.lock().unwrap();
5246        // NBUF rows are now winw+2 cells, \0-terminated + padded (faithful to
5247        // C). Take the content up to the first \0 terminator.
5248        let row0: String = nbuf
5249            .first()
5250            .map(|r| r.iter().map(|c| c.chr).take_while(|&c| c != '\0').collect())
5251            .unwrap_or_default();
5252        // The line content (after whatever prompt prefix): "ab" then a tab
5253        // expanded to spaces landing on an 8-col stop, "c", "^A", "d".
5254        assert!(
5255            row0.contains("ab") && row0.ends_with("c^Ad"),
5256            "NBUF row 0 should render the line with tab + ^A expansion, got {:?}",
5257            row0
5258        );
5259    }
5260
5261    /// c:1293-1320 — the LIVE zrefresh build clusters a base char + combining
5262    /// marks into one cell via addmultiword when COMBININGCHARS is set, so NBUF
5263    /// holds a TXT_MULTIWORD_MASK cell rather than two separate cells. Pins the
5264    /// producer side of the multiword path in the multi-line renderer.
5265    #[test]
5266    fn zrefresh_clusters_combining_chars_in_nbuf() {
5267        let _g = crate::test_util::global_state_lock();
5268        let cc = crate::ported::zsh_h::opt_name(crate::ported::zsh_h::COMBININGCHARS);
5269        crate::ported::options::opt_state_set(cc, true);
5270        *ZLELINE.lock().unwrap() = "e\u{0301}".chars().collect(); // e + acute
5271        ZLECS.store(2, Ordering::SeqCst);
5272        ZLELL.store(2, Ordering::SeqCst);
5273        *NBUF.lock().unwrap() = vec![];
5274        *OBUF.lock().unwrap() = vec![];
5275
5276        let devnull =
5277            unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_WRONLY) };
5278        let old = crate::ported::init::SHTTY.load(Ordering::SeqCst);
5279        crate::ported::init::SHTTY.store(devnull, Ordering::SeqCst);
5280        zrefresh();
5281        crate::ported::init::SHTTY.store(old, Ordering::SeqCst);
5282        unsafe { libc::close(devnull) };
5283        crate::ported::options::opt_state_set(cc, false); // restore default-off
5284
5285        let has_multiword = NBUF
5286            .lock()
5287            .unwrap()
5288            .iter()
5289            .flat_map(|r| r.iter())
5290            .any(|c| c.atr & TXT_MULTIWORD_MASK != 0);
5291        assert!(
5292            has_multiword,
5293            "COMBININGCHARS build must cluster e+U+0301 into a TXT_MULTIWORD_MASK cell"
5294        );
5295    }
5296
5297    /// c:1303-1318 — the LIVE zrefresh build pads a wide (CJK) glyph with WEOF
5298    /// column-placeholders so it occupies its full display width. "日a" lays
5299    /// out as [日][WEOF][a]: the ideograph, one ZWC_WEOF placeholder (second
5300    /// column), then 'a' — not [日][a] which would mis-account columns.
5301    #[test]
5302    fn zrefresh_wide_char_pads_weof_in_nbuf() {
5303        let _g = crate::test_util::global_state_lock();
5304        *ZLELINE.lock().unwrap() = "日a".chars().collect();
5305        ZLECS.store(0, Ordering::SeqCst);
5306        ZLELL.store(2, Ordering::SeqCst);
5307        *NBUF.lock().unwrap() = vec![];
5308        *OBUF.lock().unwrap() = vec![];
5309
5310        let devnull =
5311            unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_WRONLY) };
5312        let old = crate::ported::init::SHTTY.load(Ordering::SeqCst);
5313        crate::ported::init::SHTTY.store(devnull, Ordering::SeqCst);
5314        zrefresh();
5315        crate::ported::init::SHTTY.store(old, Ordering::SeqCst);
5316        unsafe { libc::close(devnull) };
5317
5318        let row0 = NBUF.lock().unwrap().first().cloned().unwrap_or_default();
5319        let idx = row0
5320            .iter()
5321            .position(|c| c.chr == '日')
5322            .expect("ideograph must be in row 0");
5323        assert_eq!(
5324            row0.get(idx + 1).map(|c| c.chr),
5325            Some(ZWC_WEOF),
5326            "wide glyph's second column must be a WEOF placeholder"
5327        );
5328        assert_eq!(
5329            row0.get(idx + 2).map(|c| c.chr),
5330            Some('a'),
5331            "'a' must follow the WEOF placeholder, not sit in the wide char's column"
5332        );
5333    }
5334
5335    /// c:1474-1493 — with COMBININGCHARS off, a width-0 char is an orphan and
5336    /// renders as a `<%.04x>` hex escape rather than overlaying the previous
5337    /// glyph. "a" + U+0301 lays out as 'a' then "<0301>".
5338    #[test]
5339    fn zrefresh_renders_zero_width_orphan_as_hex() {
5340        let _g = crate::test_util::global_state_lock();
5341        let cc = crate::ported::zsh_h::opt_name(crate::ported::zsh_h::COMBININGCHARS);
5342        crate::ported::options::opt_state_set(cc, false); // off → marks are orphans
5343        *ZLELINE.lock().unwrap() = "a\u{0301}".chars().collect();
5344        ZLECS.store(0, Ordering::SeqCst);
5345        ZLELL.store(2, Ordering::SeqCst);
5346        *NBUF.lock().unwrap() = vec![];
5347        *OBUF.lock().unwrap() = vec![];
5348
5349        let devnull =
5350            unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_WRONLY) };
5351        let old = crate::ported::init::SHTTY.load(Ordering::SeqCst);
5352        crate::ported::init::SHTTY.store(devnull, Ordering::SeqCst);
5353        zrefresh();
5354        crate::ported::init::SHTTY.store(old, Ordering::SeqCst);
5355        unsafe { libc::close(devnull) };
5356
5357        let row0: String = NBUF
5358            .lock()
5359            .unwrap()
5360            .first()
5361            .map(|r| r.iter().map(|c| c.chr).take_while(|&c| c != '\0').collect())
5362            .unwrap_or_default();
5363        assert!(
5364            row0.contains("a<0301>"),
5365            "orphan combining mark must render as <0301> hex; got {:?}",
5366            row0
5367        );
5368    }
5369
5370    /// c:1643-1725 — the right prompt is placed and emitted when it is a
5371    /// single-line, non-empty prompt that fits on the first line. With RPROMPT
5372    /// set to "RP" and a short editable line, zrefresh emits "RP" to the right
5373    /// and records put_rpmpt/oput_rpmpt. (The whole block is gated on a
5374    /// non-empty fitting rprompt, so the default no-RPROMPT case never fires.)
5375    #[test]
5376    fn zrefresh_emits_right_prompt_when_it_fits() {
5377        use std::io::Read;
5378        use std::os::unix::io::FromRawFd;
5379        let _g = crate::test_util::global_state_lock();
5380        let _g2 = zle_test_setup();
5381        *crate::ported::zle::zle_main::RPROMPT.lock().unwrap() = "RP".to_string();
5382        RPROMPTW.store(2, Ordering::SeqCst);
5383        RPROMPTH.store(1, Ordering::SeqCst);
5384        TRASHEDZLE.store(0, Ordering::SeqCst);
5385        OPUT_RPMPT.store(0, Ordering::SeqCst); // not shown last frame
5386        LPROMPTW.store(0, Ordering::SeqCst);
5387        *ZLELINE.lock().unwrap() = "a".chars().collect();
5388        ZLECS.store(1, Ordering::SeqCst);
5389        ZLELL.store(1, Ordering::SeqCst);
5390        *NBUF.lock().unwrap() = vec![];
5391        *OBUF.lock().unwrap() = vec![];
5392        NLNCT.store(0, Ordering::SeqCst);
5393        OLNCT.store(0, Ordering::SeqCst);
5394
5395        let mut fds = [0i32; 2];
5396        assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0, "pipe()");
5397        let (rd, wr) = (fds[0], fds[1]);
5398        let old = crate::ported::init::SHTTY.load(Ordering::SeqCst);
5399        crate::ported::init::SHTTY.store(wr, Ordering::SeqCst);
5400        zrefresh();
5401        crate::ported::init::SHTTY.store(old, Ordering::SeqCst);
5402        unsafe { libc::close(wr) };
5403        // Restore globals so the rprompt state doesn't leak into other tests.
5404        *crate::ported::zle::zle_main::RPROMPT.lock().unwrap() = String::new();
5405        let put = PUT_RPMPT.load(Ordering::SeqCst);
5406        let oput = OPUT_RPMPT.load(Ordering::SeqCst);
5407        PUT_RPMPT.store(0, Ordering::SeqCst);
5408        OPUT_RPMPT.store(0, Ordering::SeqCst);
5409        RPROMPTW.store(0, Ordering::SeqCst);
5410        RPROMPTH.store(0, Ordering::SeqCst);
5411
5412        let mut out = Vec::new();
5413        let _ = unsafe { std::fs::File::from_raw_fd(rd) }.read_to_end(&mut out);
5414        let s = String::from_utf8_lossy(&out);
5415        assert!(
5416            s.contains("RP"),
5417            "right prompt 'RP' must be emitted to the right; got {:?}",
5418            s
5419        );
5420        assert_eq!(put, 1, "put_rpmpt set");
5421        assert_eq!(oput, 1, "oput_rpmpt carries put_rpmpt for the next frame (c:1738)");
5422    }
5423
5424    /// c:1741 — the closing moveto positions the cursor at the build-tracked
5425    /// video coords (nvln/nvcs), not the single-line `cursor_col / cols`
5426    /// recompute. With a hard newline ("a\nb") and the cursor at end, the
5427    /// cursor's video line is 1 — the recompute (which never sees the newline)
5428    /// would leave it on line 0. Pins the multi-line cursor placement.
5429    #[test]
5430    fn zrefresh_cursor_lands_on_second_video_line() {
5431        let _g = crate::test_util::global_state_lock();
5432        *ZLELINE.lock().unwrap() = "a\nb".chars().collect();
5433        ZLECS.store(3, Ordering::SeqCst); // cursor at end (after 'b')
5434        ZLELL.store(3, Ordering::SeqCst);
5435
5436        zrefresh();
5437
5438        // The hard newline must have produced a second video row holding "b".
5439        let rows: Vec<String> = NBUF
5440            .lock()
5441            .unwrap()
5442            .iter()
5443            .map(|r| r.iter().map(|c| c.chr).take_while(|&c| c != '\0').collect())
5444            .collect();
5445        assert!(
5446            rows.len() >= 2 && rows[1].contains('b'),
5447            "newline must create a 2nd video row with 'b'; rows={:?}",
5448            rows
5449        );
5450        // The cursor's final video line is row 1 (where 'b' is), proving the
5451        // moveto used nvln, not cursor_col/cols (which would give row 0).
5452        assert_eq!(
5453            VLN.load(Ordering::SeqCst),
5454            1,
5455            "cursor must land on video line 1 (nvln), not line 0 (recompute)"
5456        );
5457    }
5458
5459    /// Proof of the status-pane build (c:1423-1554): with STATUSLINE set, the
5460    /// editable line lands in an upper NBUF row and the status text renders in
5461    /// a row BELOW it via the snextline cascade, with control chars expanded to
5462    /// `^X` (c:1499-1508). Resets STATUSLINE to None before dropping the lock so
5463    /// the flag can't leak into another test (the more_start-leak failure mode).
5464    #[test]
5465    fn zrefresh_renders_statusline_below_editable() {
5466        let _g = crate::test_util::global_state_lock();
5467        *ZLELINE.lock().unwrap() = "ab".chars().collect();
5468        ZLECS.store(0, Ordering::SeqCst);
5469        ZLELL.store(2, Ordering::SeqCst);
5470        // Status with a control char to exercise the `^X` path (\u{1} → "^A").
5471        *crate::ported::zle::zle_main::STATUSLINE.lock().unwrap() =
5472            Some("x\u{1}y".to_string());
5473
5474        zrefresh();
5475
5476        // Snapshot rows (content up to the first \0 terminator) then clear the
5477        // status flag while still holding the global lock.
5478        let rows: Vec<String> = NBUF
5479            .lock()
5480            .unwrap()
5481            .iter()
5482            .map(|r| r.iter().map(|c| c.chr).take_while(|&c| c != '\0').collect())
5483            .collect();
5484        *crate::ported::zle::zle_main::STATUSLINE.lock().unwrap() = None;
5485
5486        let edit_row = rows.iter().position(|r| r.contains("ab"));
5487        let stat_row = rows.iter().position(|r| r.contains("x^Ay"));
5488        assert!(
5489            edit_row.is_some() && stat_row.is_some() && stat_row > edit_row,
5490            "status row (x^Ay) must render below the editable row (ab); rows={:?}",
5491            rows
5492        );
5493    }
5494
5495    /// c:1438-1473 — the status pane lays a wide (CJK) glyph at full width too:
5496    /// "日x" in the status renders as [日][WEOF][x], not [日][x]. Pins the
5497    /// status MB printable branch (now unblocked by ZWC_WEOF).
5498    #[test]
5499    fn zrefresh_statusline_wide_char_pads_weof() {
5500        let _g = crate::test_util::global_state_lock();
5501        *ZLELINE.lock().unwrap() = "a".chars().collect();
5502        ZLECS.store(0, Ordering::SeqCst);
5503        ZLELL.store(1, Ordering::SeqCst);
5504        *crate::ported::zle::zle_main::STATUSLINE.lock().unwrap() =
5505            Some("日x".to_string());
5506        *NBUF.lock().unwrap() = vec![];
5507        *OBUF.lock().unwrap() = vec![];
5508
5509        let devnull =
5510            unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_WRONLY) };
5511        let old = crate::ported::init::SHTTY.load(Ordering::SeqCst);
5512        crate::ported::init::SHTTY.store(devnull, Ordering::SeqCst);
5513        zrefresh();
5514        crate::ported::init::SHTTY.store(old, Ordering::SeqCst);
5515        unsafe { libc::close(devnull) };
5516
5517        let rows = NBUF.lock().unwrap().clone();
5518        *crate::ported::zle::zle_main::STATUSLINE.lock().unwrap() = None; // restore
5519
5520        let mut checked = false;
5521        for row in &rows {
5522            if let Some(idx) = row.iter().position(|c| c.chr == '日') {
5523                assert_eq!(
5524                    row.get(idx + 1).map(|c| c.chr),
5525                    Some(ZWC_WEOF),
5526                    "status wide glyph's 2nd column must be a WEOF placeholder"
5527                );
5528                assert_eq!(
5529                    row.get(idx + 2).map(|c| c.chr),
5530                    Some('x'),
5531                    "'x' must follow the WEOF placeholder in the status row"
5532                );
5533                checked = true;
5534            }
5535        }
5536        assert!(checked, "a status row containing 日 must exist; rows={:?}",
5537            rows.iter().map(|r| r.iter().map(|c| c.chr)
5538                .take_while(|&c| c != '\0').collect::<String>()).collect::<Vec<_>>());
5539    }
5540
5541    /// c:1474-1493 — a width-0 orphan in the status line renders as a hex
5542    /// escape too. With COMBININGCHARS off, status "x" + U+0301 lays out as
5543    /// "x<0301>".
5544    #[test]
5545    fn zrefresh_statusline_zero_width_orphan_as_hex() {
5546        let _g = crate::test_util::global_state_lock();
5547        let cc = crate::ported::zsh_h::opt_name(crate::ported::zsh_h::COMBININGCHARS);
5548        crate::ported::options::opt_state_set(cc, false);
5549        *ZLELINE.lock().unwrap() = "a".chars().collect();
5550        ZLECS.store(0, Ordering::SeqCst);
5551        ZLELL.store(1, Ordering::SeqCst);
5552        *crate::ported::zle::zle_main::STATUSLINE.lock().unwrap() =
5553            Some("x\u{0301}".to_string());
5554        *NBUF.lock().unwrap() = vec![];
5555        *OBUF.lock().unwrap() = vec![];
5556
5557        let devnull =
5558            unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_WRONLY) };
5559        let old = crate::ported::init::SHTTY.load(Ordering::SeqCst);
5560        crate::ported::init::SHTTY.store(devnull, Ordering::SeqCst);
5561        zrefresh();
5562        crate::ported::init::SHTTY.store(old, Ordering::SeqCst);
5563        unsafe { libc::close(devnull) };
5564
5565        let rows: Vec<String> = NBUF
5566            .lock()
5567            .unwrap()
5568            .iter()
5569            .map(|r| r.iter().map(|c| c.chr).take_while(|&c| c != '\0').collect())
5570            .collect();
5571        *crate::ported::zle::zle_main::STATUSLINE.lock().unwrap() = None; // restore
5572
5573        assert!(
5574            rows.iter().any(|r| r.contains("x<0301>")),
5575            "status orphan combining mark must render as <0301>; rows={:?}",
5576            rows
5577        );
5578    }
5579
5580    /// Proof of the diff path: with NBUF[0]="abc" and an empty OBUF,
5581    /// refreshline(0) must emit the new line's cells. Captures output by
5582    /// redirecting SHTTY to a pipe. This proves the NBUF→refreshline→
5583    /// zwcwrite→zwcputc chain writes real cell data before the live
5584    /// renderer is ever switched to it.
5585    #[test]
5586    fn refreshline_emits_new_line_cells() {
5587        use std::io::Read;
5588        use std::os::unix::io::FromRawFd;
5589        let _g = crate::test_util::global_state_lock();
5590        let _g2 = zle_test_setup();
5591
5592        let mut fds = [0i32; 2];
5593        assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0, "pipe()");
5594        let (rd, wr) = (fds[0], fds[1]);
5595        let old_shtty = crate::ported::init::SHTTY.load(Ordering::SeqCst);
5596        crate::ported::init::SHTTY.store(wr, Ordering::SeqCst);
5597
5598        *NBUF.lock().unwrap() = vec!["abc"
5599            .chars()
5600            .map(|c| REFRESH_ELEMENT { chr: c, atr: 0 })
5601            .collect()];
5602        *OBUF.lock().unwrap() = vec![];
5603        NLNCT.store(1, Ordering::SeqCst);
5604        OLNCT.store(0, Ordering::SeqCst);
5605        VCS.store(0, Ordering::SeqCst);
5606        VLN.store(0, Ordering::SeqCst);
5607        CLEAREOL.store(0, Ordering::SeqCst);
5608        LPROMPTW.store(0, Ordering::SeqCst);
5609        crate::ported::init::hasam.store(0, Ordering::SeqCst);
5610
5611        refreshline(0);
5612
5613        crate::ported::init::SHTTY.store(old_shtty, Ordering::SeqCst);
5614        unsafe { libc::close(wr) };
5615        let mut out = Vec::new();
5616        let mut f = unsafe { std::fs::File::from_raw_fd(rd) };
5617        let _ = f.read_to_end(&mut out);
5618        let s = String::from_utf8_lossy(&out);
5619        assert!(
5620            s.contains('a') && s.contains('b') && s.contains('c'),
5621            "refreshline should emit the new-line cells a/b/c; got {:?}",
5622            s
5623        );
5624    }
5625
5626    /// Proof of zwcputc's multiword path (c:633-643): a combining cluster
5627    /// stored by addmultiword (the producer, c:913) into NMWBUF is read back
5628    /// and emitted codepoint-by-codepoint when the cell carries
5629    /// TXT_MULTIWORD_MASK — the matching consumer half. Captures output via
5630    /// SHTTY→pipe like refreshline_emits_new_line_cells.
5631    #[test]
5632    fn zwcputc_emits_multiword_cluster() {
5633        use std::io::Read;
5634        use std::os::unix::io::FromRawFd;
5635        let _g = crate::test_util::global_state_lock();
5636
5637        // base 'e' + U+0301 combining acute → the cell dispatches to nmwbuf.
5638        let cluster = ['e', '\u{0301}'];
5639        let mut cell = REFRESH_ELEMENT::default();
5640        addmultiword(&mut cell, &cluster, cluster.len());
5641        assert!(
5642            cell.atr & TXT_MULTIWORD_MASK != 0,
5643            "addmultiword must set the multiword mask"
5644        );
5645
5646        let mut fds = [0i32; 2];
5647        assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0, "pipe()");
5648        let (rd, wr) = (fds[0], fds[1]);
5649        let old = SHTTY.load(Ordering::SeqCst);
5650        SHTTY.store(wr, Ordering::SeqCst);
5651
5652        zwcputc(&cell);
5653
5654        SHTTY.store(old, Ordering::SeqCst);
5655        unsafe { libc::close(wr) };
5656        let mut out = Vec::new();
5657        let mut f = unsafe { std::fs::File::from_raw_fd(rd) };
5658        let _ = f.read_to_end(&mut out);
5659        let s = String::from_utf8_lossy(&out);
5660        assert!(
5661            s.contains('e') && s.contains('\u{0301}'),
5662            "zwcputc must emit both cluster codepoints (e + combining acute); got {:?}",
5663            s
5664        );
5665    }
5666
5667    /// Swap validation: the LIVE zrefresh path now renders via the NBUF/
5668    /// OBUF diff (refreshline), not full-repaint. Driving the whole
5669    /// zrefresh with a clean first frame must emit the editable line.
5670    #[test]
5671    fn zrefresh_renders_line_via_diff() {
5672        use std::io::Read;
5673        use std::os::unix::io::FromRawFd;
5674        let _g = crate::test_util::global_state_lock();
5675        let _g2 = zle_test_setup();
5676
5677        // Clean video state so the first frame writes everything.
5678        *NBUF.lock().unwrap() = vec![];
5679        *OBUF.lock().unwrap() = vec![];
5680        NLNCT.store(0, Ordering::SeqCst);
5681        OLNCT.store(0, Ordering::SeqCst);
5682        VCS.store(0, Ordering::SeqCst);
5683        VLN.store(0, Ordering::SeqCst);
5684        LPROMPTW.store(0, Ordering::SeqCst);
5685        crate::ported::init::hasam.store(0, Ordering::SeqCst);
5686
5687        *ZLELINE.lock().unwrap() = "hello".chars().collect();
5688        ZLECS.store(5, Ordering::SeqCst);
5689        ZLELL.store(5, Ordering::SeqCst);
5690
5691        let mut fds = [0i32; 2];
5692        assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0, "pipe()");
5693        let (rd, wr) = (fds[0], fds[1]);
5694        let old_shtty = crate::ported::init::SHTTY.load(Ordering::SeqCst);
5695        crate::ported::init::SHTTY.store(wr, Ordering::SeqCst);
5696
5697        zrefresh();
5698
5699        crate::ported::init::SHTTY.store(old_shtty, Ordering::SeqCst);
5700        unsafe { libc::close(wr) };
5701        let mut out = Vec::new();
5702        let mut f = unsafe { std::fs::File::from_raw_fd(rd) };
5703        let _ = f.read_to_end(&mut out);
5704        let s = String::from_utf8_lossy(&out);
5705        assert!(
5706            s.contains("hello"),
5707            "live zrefresh (diff path) should render the line; got {:?}",
5708            s
5709        );
5710    }
5711
5712    /// Integration proof: the NBUF that zrefresh actually builds from the
5713    /// editable line renders correctly through refreshline. This validates
5714    /// the full build→diff→emit path end to end (zrefresh's NBUF cells are
5715    /// real, refreshline renders them) before the live output is ever
5716    /// switched from full-repaint to the diff.
5717    #[test]
5718    fn zrefresh_nbuf_renders_via_refreshline() {
5719        use std::io::Read;
5720        use std::os::unix::io::FromRawFd;
5721        let _g = crate::test_util::global_state_lock();
5722        let _g2 = zle_test_setup();
5723
5724        *ZLELINE.lock().unwrap() = "hello".chars().collect();
5725        ZLECS.store(5, Ordering::SeqCst);
5726        ZLELL.store(5, Ordering::SeqCst);
5727
5728        // Let zrefresh's full-repaint output go to /dev/null while it
5729        // builds NBUF (we only want the NBUF, not its escapes).
5730        let old_shtty = crate::ported::init::SHTTY.load(Ordering::SeqCst);
5731        let devnull = unsafe {
5732            libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_WRONLY)
5733        };
5734        crate::ported::init::SHTTY.store(devnull, Ordering::SeqCst);
5735        zrefresh(); // builds NBUF (and OBUF=previous via its swap)
5736        if devnull >= 0 {
5737            unsafe { libc::close(devnull) };
5738        }
5739
5740        // Render the just-built NBUF via refreshline, forcing a full write
5741        // (empty OBUF), capturing the output.
5742        let mut fds = [0i32; 2];
5743        assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0, "pipe()");
5744        let (rd, wr) = (fds[0], fds[1]);
5745        crate::ported::init::SHTTY.store(wr, Ordering::SeqCst);
5746        *OBUF.lock().unwrap() = vec![];
5747        OLNCT.store(0, Ordering::SeqCst);
5748        VCS.store(0, Ordering::SeqCst);
5749        VLN.store(0, Ordering::SeqCst);
5750        CLEAREOL.store(0, Ordering::SeqCst);
5751        LPROMPTW.store(0, Ordering::SeqCst);
5752        crate::ported::init::hasam.store(0, Ordering::SeqCst);
5753
5754        refreshline(0);
5755
5756        crate::ported::init::SHTTY.store(old_shtty, Ordering::SeqCst);
5757        unsafe { libc::close(wr) };
5758        let mut out = Vec::new();
5759        let mut f = unsafe { std::fs::File::from_raw_fd(rd) };
5760        let _ = f.read_to_end(&mut out);
5761        let s = String::from_utf8_lossy(&out);
5762        assert!(
5763            s.contains("hello"),
5764            "zrefresh-built NBUF should render 'hello' via refreshline; got {:?}",
5765            s
5766        );
5767    }
5768
5769    /// Diff-path proof, shorten case: OBUF="abcd" → NBUF="ab". The old
5770    /// trailing "cd" must be erased — when the terminal lacks clear-to-eol
5771    /// (none in a headless test), refreshline overwrites it with spaces.
5772    #[test]
5773    fn refreshline_erases_shortened_tail() {
5774        use std::io::Read;
5775        use std::os::unix::io::FromRawFd;
5776        let _g = crate::test_util::global_state_lock();
5777        let _g2 = zle_test_setup();
5778
5779        let mut fds = [0i32; 2];
5780        assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0, "pipe()");
5781        let (rd, wr) = (fds[0], fds[1]);
5782        let old_shtty = crate::ported::init::SHTTY.load(Ordering::SeqCst);
5783        crate::ported::init::SHTTY.store(wr, Ordering::SeqCst);
5784
5785        let mk = |s: &str| -> REFRESH_STRING {
5786            s.chars().map(|c| REFRESH_ELEMENT { chr: c, atr: 0 }).collect()
5787        };
5788        *OBUF.lock().unwrap() = vec![mk("abcd")];
5789        *NBUF.lock().unwrap() = vec![mk("ab")];
5790        NLNCT.store(1, Ordering::SeqCst);
5791        OLNCT.store(1, Ordering::SeqCst);
5792        VCS.store(0, Ordering::SeqCst);
5793        VLN.store(0, Ordering::SeqCst);
5794        CLEAREOL.store(0, Ordering::SeqCst);
5795        LPROMPTW.store(0, Ordering::SeqCst);
5796        crate::ported::init::hasam.store(0, Ordering::SeqCst);
5797        // No clear-to-eol capability → forces the space-overwrite path.
5798        tclen.lock().unwrap()[TCCLEAREOL as usize] = 0;
5799
5800        refreshline(0);
5801
5802        crate::ported::init::SHTTY.store(old_shtty, Ordering::SeqCst);
5803        unsafe { libc::close(wr) };
5804        let mut out = Vec::new();
5805        let mut f = unsafe { std::fs::File::from_raw_fd(rd) };
5806        let _ = f.read_to_end(&mut out);
5807        let s = String::from_utf8_lossy(&out);
5808        // The two old tail columns ("cd") are each overwritten with a space
5809        // (interspersed with cursor moves), so at least two spaces emit.
5810        assert!(
5811            s.matches(' ').count() >= 2,
5812            "shortened line should erase the 2 old tail cells with spaces; got {:?}",
5813            s
5814        );
5815    }
5816
5817    /// Diff-path proof, edit case: OBUF="abc" → NBUF="abd". After the
5818    /// common "ab" prefix, refreshline must emit the changed cell 'd'.
5819    /// Exercises the common-prefix-skip + change branch (not the all-new
5820    /// write path of the previous test).
5821    #[test]
5822    fn refreshline_emits_changed_cell_on_edit() {
5823        use std::io::Read;
5824        use std::os::unix::io::FromRawFd;
5825        let _g = crate::test_util::global_state_lock();
5826        let _g2 = zle_test_setup();
5827
5828        let mut fds = [0i32; 2];
5829        assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0, "pipe()");
5830        let (rd, wr) = (fds[0], fds[1]);
5831        let old_shtty = crate::ported::init::SHTTY.load(Ordering::SeqCst);
5832        crate::ported::init::SHTTY.store(wr, Ordering::SeqCst);
5833
5834        let mk = |s: &str| -> REFRESH_STRING {
5835            s.chars().map(|c| REFRESH_ELEMENT { chr: c, atr: 0 }).collect()
5836        };
5837        *OBUF.lock().unwrap() = vec![mk("abc")];
5838        *NBUF.lock().unwrap() = vec![mk("abd")];
5839        NLNCT.store(1, Ordering::SeqCst);
5840        OLNCT.store(1, Ordering::SeqCst);
5841        VCS.store(0, Ordering::SeqCst);
5842        VLN.store(0, Ordering::SeqCst);
5843        CLEAREOL.store(0, Ordering::SeqCst);
5844        LPROMPTW.store(0, Ordering::SeqCst);
5845        crate::ported::init::hasam.store(0, Ordering::SeqCst);
5846
5847        refreshline(0);
5848
5849        crate::ported::init::SHTTY.store(old_shtty, Ordering::SeqCst);
5850        unsafe { libc::close(wr) };
5851        let mut out = Vec::new();
5852        let mut f = unsafe { std::fs::File::from_raw_fd(rd) };
5853        let _ = f.read_to_end(&mut out);
5854        let s = String::from_utf8_lossy(&out);
5855        assert!(
5856            s.contains('d'),
5857            "edit should emit the changed cell 'd'; got {:?}",
5858            s
5859        );
5860    }
5861
5862    /// The refreshline diff must handle the ZWC_WEOF placeholder cells that
5863    /// wide glyphs now produce in NBUF: with old "日Y" and new "日X" (the wide
5864    /// ideograph + a trailing char that changed), the shared 日 + its WEOF
5865    /// column match and are skipped, only 'X' is emitted, and the WEOF
5866    /// placeholder is never written to the wire as a literal U+FFFF.
5867    #[test]
5868    fn refreshline_diffs_wide_char_line_correctly() {
5869        use std::io::Read;
5870        use std::os::unix::io::FromRawFd;
5871        let _g = crate::test_util::global_state_lock();
5872        let _g2 = zle_test_setup();
5873        WINW.store(8, Ordering::SeqCst);
5874
5875        let wide = |tail: char| -> REFRESH_STRING {
5876            let mut r = vec![
5877                REFRESH_ELEMENT { chr: '日', atr: 0 },
5878                REFRESH_ELEMENT { chr: ZWC_WEOF, atr: 0 },
5879                REFRESH_ELEMENT { chr: tail, atr: 0 },
5880            ];
5881            r.resize(10, REFRESH_ELEMENT::default());
5882            r
5883        };
5884        *OBUF.lock().unwrap() = vec![wide('Y')];
5885        *NBUF.lock().unwrap() = vec![wide('X')];
5886        NLNCT.store(1, Ordering::SeqCst);
5887        OLNCT.store(1, Ordering::SeqCst);
5888        VCS.store(0, Ordering::SeqCst);
5889        VLN.store(0, Ordering::SeqCst);
5890        CLEAREOL.store(0, Ordering::SeqCst);
5891        LPROMPTW.store(0, Ordering::SeqCst);
5892        crate::ported::init::hasam.store(0, Ordering::SeqCst);
5893
5894        let mut fds = [0i32; 2];
5895        assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0, "pipe()");
5896        let (rd, wr) = (fds[0], fds[1]);
5897        let old = crate::ported::init::SHTTY.load(Ordering::SeqCst);
5898        crate::ported::init::SHTTY.store(wr, Ordering::SeqCst);
5899        refreshline(0);
5900        crate::ported::init::SHTTY.store(old, Ordering::SeqCst);
5901        unsafe { libc::close(wr) };
5902        let mut out = Vec::new();
5903        let _ = unsafe { std::fs::File::from_raw_fd(rd) }.read_to_end(&mut out);
5904        let s = String::from_utf8_lossy(&out);
5905        assert!(
5906            s.contains('X')
5907                && !s.contains('\u{FFFF}')
5908                && !s.contains('日'),
5909            "wide-char diff must emit only 'X' (skip the matched 日+WEOF) and \
5910             never write the WEOF placeholder; got {:?}",
5911            s
5912        );
5913    }
5914
5915    /// c:1923/2089 — after refreshline edits "abc"→"abd", the video cursor
5916    /// (VCS) must land at column 3: moveto repositions to ccs=2 (past the
5917    /// "ab" common prefix), then writing 'd' advances it to 3. The previous
5918    /// snapshot port left the local `vcs` frozen at its initial 0, so it
5919    /// stored VCS=1 (0+1) — wrong. This pins the live-tracker fix.
5920    #[test]
5921    fn refreshline_tracks_vcs_across_prefix_skip() {
5922        let _g = crate::test_util::global_state_lock();
5923        let _g2 = zle_test_setup();
5924
5925        // Output goes to /dev/null; we only assert the VCS tracker here.
5926        let devnull = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_WRONLY) };
5927        let old_shtty = crate::ported::init::SHTTY.load(Ordering::SeqCst);
5928        crate::ported::init::SHTTY.store(devnull, Ordering::SeqCst);
5929
5930        let mk = |s: &str| -> REFRESH_STRING {
5931            s.chars().map(|c| REFRESH_ELEMENT { chr: c, atr: 0 }).collect()
5932        };
5933        *OBUF.lock().unwrap() = vec![mk("abc")];
5934        *NBUF.lock().unwrap() = vec![mk("abd")];
5935        NLNCT.store(1, Ordering::SeqCst);
5936        OLNCT.store(1, Ordering::SeqCst);
5937        VCS.store(0, Ordering::SeqCst);
5938        VLN.store(0, Ordering::SeqCst);
5939        CLEAREOL.store(0, Ordering::SeqCst);
5940        LPROMPTW.store(0, Ordering::SeqCst);
5941        crate::ported::init::hasam.store(0, Ordering::SeqCst);
5942
5943        refreshline(0);
5944
5945        crate::ported::init::SHTTY.store(old_shtty, Ordering::SeqCst);
5946        unsafe { libc::close(devnull) };
5947        assert_eq!(
5948            VCS.load(Ordering::SeqCst),
5949            3,
5950            "VCS must track to column 3 (ccs=2 after prefix skip + 1 written cell)"
5951        );
5952    }
5953
5954    /// c:1672-1674 — when a frame has more lines than the last, the
5955    /// newly-used lines must be cleared (cleareol). Growing "a" → "a\n"
5956    /// (line 1 empty) drives the clear-eol short-circuit (c:1776-1780):
5957    /// refreshline(1) emits moveto + the clear escape. With tccan(TCCLEAREOL)
5958    /// off this can't fire, so the test wires tclen[TCCLEAREOL]. Without the
5959    /// per-line cleareol, line 1 stays cleareol=0 and no clear is emitted.
5960    #[test]
5961    fn zrefresh_clears_newly_grown_line() {
5962        use std::io::Read;
5963        use std::os::unix::io::FromRawFd;
5964        use crate::ported::init::{tclen, tcstr};
5965        let _g = crate::test_util::global_state_lock();
5966        let _g2 = zle_test_setup();
5967
5968        let saved_tc = tclen.lock().unwrap()[TCCLEAREOL as usize];
5969        let saved_str = tcstr.lock().unwrap()[TCCLEAREOL as usize].clone();
5970        tclen.lock().unwrap()[TCCLEAREOL as usize] = 3; // tccan(TCCLEAREOL)
5971        // The real clear-to-end-of-LINE escape (CSI K), not CSI J.
5972        tcstr.lock().unwrap()[TCCLEAREOL as usize] = "\x1b[K".to_string();
5973
5974        *NBUF.lock().unwrap() = vec![];
5975        *OBUF.lock().unwrap() = vec![];
5976        NLNCT.store(0, Ordering::SeqCst);
5977        OLNCT.store(0, Ordering::SeqCst);
5978        VCS.store(0, Ordering::SeqCst);
5979        VLN.store(0, Ordering::SeqCst);
5980        LPROMPTW.store(0, Ordering::SeqCst);
5981        CLEAREOL.store(0, Ordering::SeqCst);
5982        crate::ported::init::hasam.store(0, Ordering::SeqCst);
5983
5984        // Frame 1: single line "a" → /dev/null (establishes OBUF/OLNCT=1).
5985        let devnull = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_WRONLY) };
5986        let old_shtty = crate::ported::init::SHTTY.load(Ordering::SeqCst);
5987        crate::ported::init::SHTTY.store(devnull, Ordering::SeqCst);
5988        *ZLELINE.lock().unwrap() = "a".chars().collect();
5989        ZLECS.store(1, Ordering::SeqCst);
5990        ZLELL.store(1, Ordering::SeqCst);
5991        zrefresh();
5992        unsafe { libc::close(devnull) };
5993
5994        // Frame 2: grow to two lines "a\n" (line 1 empty) → capture.
5995        let mut fds = [0i32; 2];
5996        assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0, "pipe()");
5997        let (rd, wr) = (fds[0], fds[1]);
5998        crate::ported::init::SHTTY.store(wr, Ordering::SeqCst);
5999        CLEAREOL.store(0, Ordering::SeqCst); // start the frame un-cleared
6000        *ZLELINE.lock().unwrap() = "a\n".chars().collect();
6001        ZLECS.store(2, Ordering::SeqCst);
6002        ZLELL.store(2, Ordering::SeqCst);
6003        zrefresh();
6004
6005        crate::ported::init::SHTTY.store(old_shtty, Ordering::SeqCst);
6006        unsafe { libc::close(wr) };
6007        tclen.lock().unwrap()[TCCLEAREOL as usize] = saved_tc;
6008        tcstr.lock().unwrap()[TCCLEAREOL as usize] = saved_str;
6009        let mut out = Vec::new();
6010        let mut f = unsafe { std::fs::File::from_raw_fd(rd) };
6011        let _ = f.read_to_end(&mut out);
6012        let s = String::from_utf8_lossy(&out);
6013        assert!(
6014            s.contains("\x1b[K"),
6015            "grown line 1 should be cleared to end-of-line (CSI K); got {:?}",
6016            s
6017        );
6018    }
6019
6020    /// c:1683-1691 — line-delete optimisation. Old buffer ["L0","XX","L2"],
6021    /// new buffer ["L0","L2","YY"]: at iln=1 the old line differs from the
6022    /// new but old line 2 ("L2") equals new line 1, so one TCDELLINE scrolls
6023    /// the rest into place instead of rewriting both lines. With
6024    /// tccan(TCDELLINE) wired, zrefresh must emit the delete-line escape.
6025    #[test]
6026    fn zrefresh_deletes_line_via_tcdelline() {
6027        use std::io::Read;
6028        use std::os::unix::io::FromRawFd;
6029        use crate::ported::init::{tclen, tcstr};
6030        let _g = crate::test_util::global_state_lock();
6031        let _g2 = zle_test_setup();
6032
6033        let di = crate::ported::zsh_h::TCDELLINE as usize;
6034        let saved_len = tclen.lock().unwrap()[di];
6035        let saved_str = tcstr.lock().unwrap()[di].clone();
6036        tclen.lock().unwrap()[di] = 3; // tccan(TCDELLINE)
6037        tcstr.lock().unwrap()[di] = "\x1b[M".to_string(); // delete-line escape
6038
6039        *NBUF.lock().unwrap() = vec![];
6040        *OBUF.lock().unwrap() = vec![];
6041        NLNCT.store(0, Ordering::SeqCst);
6042        OLNCT.store(0, Ordering::SeqCst);
6043        VMAXLN.store(0, Ordering::SeqCst);
6044        VCS.store(0, Ordering::SeqCst);
6045        VLN.store(0, Ordering::SeqCst);
6046        LPROMPTW.store(0, Ordering::SeqCst);
6047        CLEAREOL.store(0, Ordering::SeqCst);
6048        CLEARFLAG.store(0, Ordering::SeqCst);
6049        crate::ported::init::hasam.store(0, Ordering::SeqCst);
6050
6051        // Frame 1: ["L0","XX","L2"] → /dev/null (becomes OBUF).
6052        let devnull = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_WRONLY) };
6053        let old_shtty = crate::ported::init::SHTTY.load(Ordering::SeqCst);
6054        crate::ported::init::SHTTY.store(devnull, Ordering::SeqCst);
6055        *ZLELINE.lock().unwrap() = "L0\nXX\nL2".chars().collect();
6056        ZLECS.store(8, Ordering::SeqCst);
6057        ZLELL.store(8, Ordering::SeqCst);
6058        zrefresh();
6059        unsafe { libc::close(devnull) };
6060
6061        // Frame 2: ["L0","L2","YY"] → capture. old line 2 == new line 1.
6062        let mut fds = [0i32; 2];
6063        assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0, "pipe()");
6064        let (rd, wr) = (fds[0], fds[1]);
6065        crate::ported::init::SHTTY.store(wr, Ordering::SeqCst);
6066        VCS.store(0, Ordering::SeqCst);
6067        VLN.store(0, Ordering::SeqCst);
6068        *ZLELINE.lock().unwrap() = "L0\nL2\nYY".chars().collect();
6069        ZLECS.store(8, Ordering::SeqCst);
6070        ZLELL.store(8, Ordering::SeqCst);
6071        zrefresh();
6072
6073        crate::ported::init::SHTTY.store(old_shtty, Ordering::SeqCst);
6074        unsafe { libc::close(wr) };
6075        tclen.lock().unwrap()[di] = saved_len;
6076        tcstr.lock().unwrap()[di] = saved_str;
6077        let mut out = Vec::new();
6078        let mut f = unsafe { std::fs::File::from_raw_fd(rd) };
6079        let _ = f.read_to_end(&mut out);
6080        let s = String::from_utf8_lossy(&out);
6081        assert!(
6082            s.contains("\x1b[M"),
6083            "line-delete opt should emit the TCDELLINE escape; got {:?}",
6084            s
6085        );
6086    }
6087
6088    /// zrefresh converts each line char's overlay attr to the cell's zattr
6089    /// (c:1226-1248), so refreshline/zwcputc emit its colour. A bold region
6090    /// over the line must make those cells carry TXTBOLDFACE.
6091    #[test]
6092    fn zrefresh_nbuf_cells_carry_attr() {
6093        use crate::ported::zsh_h::TXTBOLDFACE;
6094        let _g = crate::test_util::global_state_lock();
6095        let _g2 = zle_test_setup();
6096        *ZLELINE.lock().unwrap() = "abc".chars().collect();
6097        ZLECS.store(0, Ordering::SeqCst);
6098        ZLELL.store(3, Ordering::SeqCst);
6099        // A bold region over the whole line (the path compute_render_attrs
6100        // reads — the highlight manager, not the REGION_HIGHLIGHTS static).
6101        let custom = TextAttr {
6102            bold: true,
6103            ..TextAttr::default()
6104        };
6105        highlight().lock().unwrap().add_region(0, 3, custom);
6106        zrefresh();
6107        let nbuf = NBUF.lock().unwrap();
6108        let row0 = nbuf.first().expect("NBUF has a row");
6109        let a_cell = row0
6110            .iter()
6111            .find(|c| c.chr == 'a')
6112            .expect("'a' cell present");
6113        assert!(
6114            a_cell.atr & TXTBOLDFACE != 0,
6115            "bold region -> cell carries TXTBOLDFACE, got atr={:#x}",
6116            a_cell.atr
6117        );
6118    }
6119
6120    // ═══════════════════════════════════════════════════════════════════
6121    // addmultiword — C-pinned tests covering pattern.c:913-936 push path.
6122    // Pin (1) `base.atr |= TXT_MULTIWORD_MASK`, (2) `nmwbuf[ind]` stores
6123    // the cluster count, (3) `nmwbuf[ind+1..ind+1+count]` stores the
6124    // cluster codepoints, (4) `base.chr` set to the buffer entry index,
6125    // (5) `nmw_ind` advances by `count + 1`, (6) the `nmw_ind` init=1
6126    // invariant from c:43 holds (entries never start at 0).
6127    //
6128    // Bug fixed: the prior body silently discarded `tptr`/`ichars`,
6129    // setting only the flag. Every cluster of combining marks landed
6130    // unstored in the buffer; readers dispatching on TXT_MULTIWORD_MASK
6131    // got an uninitialised index.
6132    // ═══════════════════════════════════════════════════════════════════
6133
6134    /// Restore the post-resetvideo invariant (c:745-747):
6135    /// `nmw_size = DEF_MWBUF_ALLOC; nmw_ind = 1`. C zsh always runs
6136    /// `resetvideo` before any `addmultiword` call; the tests below
6137    /// mirror that init so we exercise the same growth math the
6138    /// C source guarantees correctness for.
6139    fn reset_nmw_state() {
6140        NMW_IND.with(|c| c.set(1)); // c:746
6141        NMW_SIZE.with(|c| c.set(DEF_MWBUF_ALLOC)); // c:745
6142        NMWBUF.with(|b| {
6143            let mut buf = b.borrow_mut();
6144            buf.clear();
6145            buf.resize(DEF_MWBUF_ALLOC, 0); // c:747 zalloc(nmw_size)
6146        });
6147    }
6148
6149    /// `Src/Zle/zle_refresh.c:913-935` — basic single-cluster push:
6150    /// flag set, buffer layout [count, c0, c1], chr = old nmw_ind,
6151    /// nmw_ind advances by count+1.
6152    #[test]
6153    fn addmultiword_stores_cluster_and_sets_chr_index() {
6154        let _g = crate::test_util::global_state_lock();
6155        reset_nmw_state();
6156
6157        let mut base = REFRESH_ELEMENT { chr: '\0', atr: 0 };
6158        // Cluster: 'a' + 1 combining-mark codepoint.
6159        let cluster = ['a', '\u{0301}'];
6160        addmultiword(&mut base, &cluster, 2);
6161
6162        // c:920 — TXT_MULTIWORD_MASK set.
6163        assert_ne!(base.atr & TXT_MULTIWORD_MASK, 0, "c:920 flag set");
6164        // c:934 — base.chr stores the buffer index (1 was the old NMW_IND init).
6165        assert_eq!(base.chr as u32, 1, "c:934 base.chr = old nmw_ind");
6166
6167        NMWBUF.with(|b| {
6168            let buf = b.borrow();
6169            // c:930 — first slot: cluster count.
6170            assert_eq!(buf[1], 2, "c:930 nmwbuf[ind] = ichars");
6171            // c:931-932 — next `count` slots: the codepoints.
6172            assert_eq!(buf[2], 'a' as u32, "c:932 nmwbuf[ind+1] = tptr[0]");
6173            assert_eq!(buf[3], '\u{0301}' as u32, "c:932 nmwbuf[ind+2] = tptr[1]");
6174        });
6175        // c:935 — nmw_ind advanced by count + 1 = 3 (from 1 → 4).
6176        assert_eq!(NMW_IND.get(), 4, "c:935 nmw_ind += iadd (1 + 2 + 1)");
6177        reset_nmw_state();
6178    }
6179
6180    /// `Src/Zle/zle_refresh.c:921-927` — buffer auto-grow only triggers
6181    /// when `nmw_ind + iadd > nmw_size`. Post-resetvideo size=32 fits a
6182    /// small cluster without growth.
6183    #[test]
6184    fn addmultiword_no_grow_when_capacity_fits() {
6185        let _g = crate::test_util::global_state_lock();
6186        reset_nmw_state();
6187        let pre_size = NMW_SIZE.get();
6188        let mut base = REFRESH_ELEMENT { chr: '\0', atr: 0 };
6189        addmultiword(&mut base, &['x'], 1);
6190        assert_eq!(
6191            NMW_SIZE.get(),
6192            pre_size,
6193            "c:921 — no grow needed (1+2 ≤ 32)"
6194        );
6195        reset_nmw_state();
6196    }
6197
6198    /// `Src/Zle/zle_refresh.c:922-925` — large cluster (iadd > DEF_MWBUF_ALLOC)
6199    /// triggers a grow of `iadd` slots instead of `DEF_MWBUF_ALLOC`.
6200    #[test]
6201    fn addmultiword_grows_by_iadd_for_large_cluster() {
6202        let _g = crate::test_util::global_state_lock();
6203        reset_nmw_state();
6204        let pre_size = NMW_SIZE.get();
6205        let mut base = REFRESH_ELEMENT { chr: '\0', atr: 0 };
6206        // 40-codepoint cluster: iadd = 41 > DEF_MWBUF_ALLOC=32.
6207        let cluster: Vec<char> = (0..40)
6208            .map(|i| char::from_u32(0x300 + i as u32).unwrap())
6209            .collect();
6210        addmultiword(&mut base, &cluster, 40);
6211        // c:922 — mw_more = iadd = 41, nmw_size = pre_size + 41.
6212        assert_eq!(
6213            NMW_SIZE.get(),
6214            pre_size + 41,
6215            "c:922 — grow = iadd, not DEF_MWBUF_ALLOC"
6216        );
6217        reset_nmw_state();
6218    }
6219
6220    /// `Src/Zle/zle_refresh.c:43` — nmw_ind init = 1 invariant so a
6221    /// zero-index slot never appears (would compare equal to NUL).
6222    /// Two consecutive pushes land at indices 1 and 4 (1 + 1+2 for the
6223    /// first 2-cluster push, then +3 for the second).
6224    #[test]
6225    fn addmultiword_consecutive_pushes_land_at_correct_indices() {
6226        let _g = crate::test_util::global_state_lock();
6227        reset_nmw_state();
6228
6229        let mut a = REFRESH_ELEMENT { chr: '\0', atr: 0 };
6230        addmultiword(&mut a, &['e', '\u{0301}'], 2);
6231        let mut b = REFRESH_ELEMENT { chr: '\0', atr: 0 };
6232        addmultiword(&mut b, &['n', '\u{0303}'], 2);
6233
6234        // First push at nmw_ind=1; advanced to 4.
6235        assert_eq!(a.chr as u32, 1, "first push index");
6236        // Second push at nmw_ind=4; advanced to 7.
6237        assert_eq!(
6238            b.chr as u32, 4,
6239            "second push index (c:43 invariant — never 0)"
6240        );
6241        assert_eq!(NMW_IND.get(), 7, "after two 2-clusters: 1 + 3 + 3 = 7");
6242        reset_nmw_state();
6243    }
6244
6245    #[test]
6246    fn test_countprompt() {
6247        let _g = crate::test_util::global_state_lock();
6248        let _g = zle_test_setup();
6249        assert_eq!(countprompt("hello"), 5);
6250        assert_eq!(countprompt("\x1b[31mhello\x1b[0m"), 5);
6251        assert_eq!(countprompt("日本語"), 6); // 3 chars, 2 width each
6252    }
6253
6254    #[test]
6255    fn test_video_buffer() {
6256        let _g = crate::test_util::global_state_lock();
6257        let _g = zle_test_setup();
6258        let mut buf = VideoBuffer::new(80, 24);
6259        assert_eq!(buf.cols, 80);
6260        assert_eq!(buf.rows, 24);
6261
6262        buf.set(0, 0, RefreshElement::new('A'));
6263        assert_eq!(buf.get(0, 0).map(|e| e.chr), Some('A'));
6264
6265        buf.clear();
6266        assert_eq!(buf.get(0, 0).map(|e| e.chr), Some(' '));
6267    }
6268
6269    #[test]
6270    fn test_refresh_state() {
6271        let _g = crate::test_util::global_state_lock();
6272        let _g = zle_test_setup();
6273        let mut state = RefreshState::new();
6274        assert!(state.old_video.is_some());
6275        assert!(state.new_video.is_some());
6276
6277        state.swap_buffers();
6278        state.free_video();
6279        assert!(state.old_video.is_none());
6280    }
6281
6282    #[test]
6283    fn compute_render_attrs_empty_buffer_yields_empty_overlay() {
6284        let _g = crate::test_util::global_state_lock();
6285        let _g = zle_test_setup();
6286        assert!(compute_render_attrs().is_empty());
6287    }
6288
6289    #[test]
6290    fn compute_render_attrs_visual_mode_paints_mark_to_cursor_in_standout() {
6291        let _g = crate::test_util::global_state_lock();
6292        let _g = zle_test_setup();
6293        *ZLELINE.lock().unwrap() = "hello world".chars().collect();
6294        ZLELL.store(ZLELINE.lock().unwrap().len(), Ordering::SeqCst);
6295        MARK.store(2, Ordering::SeqCst);
6296        ZLECS.store(7, Ordering::SeqCst);
6297        REGION_ACTIVE.store(1, Ordering::SeqCst); // charwise visual
6298        let attrs = compute_render_attrs();
6299        assert_eq!(attrs.len(), 11);
6300        // [0..2) and [7..11) are unstyled.
6301        for slot in attrs.iter().take(2) {
6302            assert!(slot.is_none());
6303        }
6304        for slot in attrs.iter().skip(7) {
6305            assert!(slot.is_none());
6306        }
6307        // [2..7) painted in standout.
6308        for slot in attrs.iter().take(7).skip(2) {
6309            let attr = slot.expect("standout");
6310            assert!(attr.standout);
6311        }
6312    }
6313
6314    #[test]
6315    fn compute_render_attrs_visual_mode_handles_reverse_mark_order() {
6316        let _g = crate::test_util::global_state_lock();
6317        let _g = zle_test_setup();
6318        *ZLELINE.lock().unwrap() = "abcdef".chars().collect();
6319        ZLELL.store(6, Ordering::SeqCst);
6320        MARK.store(5, Ordering::SeqCst);
6321        ZLECS.store(1, Ordering::SeqCst);
6322        REGION_ACTIVE.store(2, Ordering::SeqCst); // linewise — same swap behavior
6323        let attrs = compute_render_attrs();
6324        // Range collapses to (1..5).
6325        assert!(attrs[0].is_none());
6326        for slot in attrs.iter().take(5).skip(1) {
6327            assert!(slot.unwrap().standout);
6328        }
6329        assert!(attrs[5].is_none());
6330    }
6331
6332    #[test]
6333    fn match_highlight_handles_combined_attrs() {
6334        let _g = crate::test_util::global_state_lock();
6335        let _g = zle_test_setup();
6336        let attr = match_highlight("bold,fg=red,underline");
6337        assert!(attr.bold);
6338        assert!(attr.underline);
6339        assert_eq!(attr.fg_color, Some(1));
6340    }
6341
6342    #[test]
6343    fn match_highlight_named_and_numeric_colors() {
6344        let _g = crate::test_util::global_state_lock();
6345        let _g = zle_test_setup();
6346        assert_eq!(match_highlight("fg=cyan").fg_color, Some(6));
6347        assert_eq!(match_highlight("bg=42").bg_color, Some(42));
6348        // Out-of-range numeric → ignored (parse fails for u8).
6349        assert_eq!(match_highlight("fg=999").fg_color, None);
6350    }
6351
6352    #[test]
6353    fn match_highlight_negation_clears_attr() {
6354        let _g = crate::test_util::global_state_lock();
6355        let _g = zle_test_setup();
6356        let attr = match_highlight("bold,nobold,underline");
6357        assert!(!attr.bold);
6358        assert!(attr.underline);
6359    }
6360
6361    #[test]
6362    fn match_highlight_none_resets_everything() {
6363        let _g = crate::test_util::global_state_lock();
6364        let _g = zle_test_setup();
6365        let attr = match_highlight("bold,fg=red,none,underline");
6366        // After `none` the only thing surviving is the trailing `underline`.
6367        assert!(!attr.bold);
6368        assert!(attr.underline);
6369        assert_eq!(attr.fg_color, None);
6370    }
6371
6372    #[test]
6373    fn zle_set_highlight_populates_categories_and_defaults() {
6374        let _g = crate::test_util::global_state_lock();
6375        let _g = zle_test_setup();
6376        let mut mgr = HighlightManager::new();
6377        let entries = ["region:fg=red,bold", "isearch:fg=blue"];
6378        zle_set_highlight(&mut mgr, &entries);
6379        let region = mgr.category_attrs[&HighlightCategory::Region];
6380        assert!(region.bold);
6381        assert_eq!(region.fg_color, Some(1));
6382        let isearch = mgr.category_attrs[&HighlightCategory::Isearch];
6383        assert_eq!(isearch.fg_color, Some(4));
6384        // Suffix wasn't set: defaults to bold (zle_refresh.c:401).
6385        let suffix = mgr.category_attrs[&HighlightCategory::Suffix];
6386        assert!(suffix.bold);
6387        // Special wasn't set: defaults to standout (zle_refresh.c:396).
6388        let special = mgr.category_attrs[&HighlightCategory::Special];
6389        assert!(special.standout);
6390    }
6391
6392    #[test]
6393    fn zle_set_highlight_none_clears_every_slot() {
6394        let _g = crate::test_util::global_state_lock();
6395        let _g = zle_test_setup();
6396        let mut mgr = HighlightManager::new();
6397        zle_set_highlight(&mut mgr, &["none"]);
6398        for cat in [
6399            HighlightCategory::Region,
6400            HighlightCategory::Isearch,
6401            HighlightCategory::Suffix,
6402            HighlightCategory::Paste,
6403        ] {
6404            let attr = mgr.category_attrs[&cat];
6405            assert_eq!(attr, TextAttr::default());
6406        }
6407    }
6408
6409    #[test]
6410    fn compute_render_attrs_visual_uses_zle_highlight_region_attr() {
6411        let _g = crate::test_util::global_state_lock();
6412        let _g = zle_test_setup();
6413        // When the user sets `zle_highlight=(region:fg=red,bold)` via
6414        // zle_set_highlight, vi visual-mode should paint the region
6415        // with that attr instead of the default standout.
6416        zle_reset();
6417        *ZLELINE.lock().unwrap() = "abcde".chars().collect();
6418        ZLELL.store(5, Ordering::SeqCst);
6419        MARK.store(1, Ordering::SeqCst);
6420        ZLECS.store(4, Ordering::SeqCst);
6421        REGION_ACTIVE.store(1, Ordering::SeqCst);
6422        zle_set_highlight(&mut highlight().lock().unwrap(), &["region:fg=red,bold"]);
6423        let attrs = compute_render_attrs();
6424        for slot in attrs.iter().take(4).skip(1) {
6425            let a = slot.expect("region painted");
6426            assert!(a.bold);
6427            assert_eq!(a.fg_color, Some(1));
6428            // Standout shouldn't be auto-set when user overrode.
6429            assert!(!a.standout);
6430        }
6431    }
6432
6433    #[test]
6434    fn compute_render_attrs_explicit_regions_override_default() {
6435        let _g = crate::test_util::global_state_lock();
6436        let _g = zle_test_setup();
6437        *ZLELINE.lock().unwrap() = "abcde".chars().collect();
6438        ZLELL.store(5, Ordering::SeqCst);
6439        let custom = TextAttr {
6440            bold: true,
6441            fg_color: Some(1),
6442            ..TextAttr::default()
6443        };
6444        highlight().lock().unwrap().add_region(1, 4, custom);
6445        let attrs = compute_render_attrs();
6446        assert!(attrs[0].is_none());
6447        for slot in attrs.iter().take(4).skip(1) {
6448            let a = slot.expect("custom");
6449            assert!(a.bold);
6450            assert_eq!(a.fg_color, Some(1));
6451        }
6452        assert!(attrs[4].is_none());
6453    }
6454
6455    // ═══════════════════════════════════════════════════════════════════
6456    // C-parity tests pinning Src/Zle/zle_refresh.c ZR_* helpers.
6457    // ═══════════════════════════════════════════════════════════════════
6458
6459    /// `ZR_strlen` returns 0 for an empty (NUL-terminated) buffer.
6460    /// C `Src/Zle/zle_refresh.c:102`:
6461    ///   `int len = 0; while (wstr++->chr != '\0') len++; return len;`
6462    #[test]
6463    fn ZR_strlen_empty_terminated_returns_zero() {
6464        let _g = crate::test_util::global_state_lock();
6465        let buf = [REFRESH_ELEMENT { chr: '\0', atr: 0 }];
6466        assert_eq!(ZR_strlen(&buf), 0);
6467    }
6468
6469    /// `ZR_strlen` counts chars up to (not including) the NUL.
6470    #[test]
6471    fn ZR_strlen_counts_chars_before_nul() {
6472        let _g = crate::test_util::global_state_lock();
6473        let buf = [
6474            REFRESH_ELEMENT { chr: 'a', atr: 0 },
6475            REFRESH_ELEMENT { chr: 'b', atr: 0 },
6476            REFRESH_ELEMENT { chr: 'c', atr: 0 },
6477            REFRESH_ELEMENT { chr: '\0', atr: 0 },
6478        ];
6479        assert_eq!(ZR_strlen(&buf), 3);
6480    }
6481
6482    /// `ZR_strlen` on a buffer with NO NUL returns slice len.
6483    /// Rust port adds bounds check (C UB on no-NUL buffer).
6484    #[test]
6485    fn ZR_strlen_no_nul_returns_full_len() {
6486        let _g = crate::test_util::global_state_lock();
6487        let buf = [
6488            REFRESH_ELEMENT { chr: 'x', atr: 0 },
6489            REFRESH_ELEMENT { chr: 'y', atr: 0 },
6490        ];
6491        assert_eq!(ZR_strlen(&buf), 2, "no NUL → bounded by slice");
6492    }
6493
6494    /// `tcoutclear(cap)` runs without panic for each clear capability.
6495    #[test]
6496    fn tcoutclear_runs_without_panic() {
6497        let _g = crate::test_util::global_state_lock();
6498        tcoutclear(crate::ported::zsh_h::TCCLEARSCREEN);
6499        tcoutclear(crate::ported::zsh_h::TCCLEAREOL);
6500    }
6501
6502    /// `zle_free_highlight()` runs without panic (no-op when no
6503    /// highlights present). C: clears highlight tables.
6504    #[test]
6505    fn zle_free_highlight_no_panic() {
6506        let _g = crate::test_util::global_state_lock();
6507        zle_free_highlight();
6508        zle_free_highlight();
6509    }
6510
6511    // ═══════════════════════════════════════════════════════════════════
6512    // C-parity tests for Src/Zle/zle_refresh.c ZR_* primitives.
6513    // ═══════════════════════════════════════════════════════════════════
6514
6515    /// c:86-89 — `ZR_memset(dst, rc, n)` fills first n cells with rc.
6516    #[test]
6517    fn zr_memset_fills_first_n_cells() {
6518        let _g = crate::test_util::global_state_lock();
6519        let mut buf: Vec<REFRESH_ELEMENT> = vec![REFRESH_ELEMENT { chr: 'X', atr: 0 }; 10];
6520        let rc = REFRESH_ELEMENT { chr: 'A', atr: 0 };
6521        ZR_memset(&mut buf, rc, 5);
6522        for i in 0..5 {
6523            assert_eq!(buf[i].chr, 'A', "cell {} must be filled", i);
6524        }
6525        for i in 5..10 {
6526            assert_eq!(buf[i].chr, 'X', "cell {} must remain", i);
6527        }
6528    }
6529
6530    /// c:86 — `ZR_memset(dst, rc, 0)` is no-op.
6531    #[test]
6532    fn zr_memset_zero_len_no_op() {
6533        let _g = crate::test_util::global_state_lock();
6534        let mut buf: Vec<REFRESH_ELEMENT> = vec![REFRESH_ELEMENT { chr: 'X', atr: 0 }; 3];
6535        let rc = REFRESH_ELEMENT { chr: 'A', atr: 0 };
6536        ZR_memset(&mut buf, rc, 0);
6537        for elt in &buf {
6538            assert_eq!(elt.chr, 'X', "len=0 must not modify any cell");
6539        }
6540    }
6541
6542    /// c:86 — `ZR_memset` clamps to dst.len() when n > dst.len()
6543    /// (Rust port-safety pin; C would overrun).
6544    #[test]
6545    fn zr_memset_clamps_to_dst_len() {
6546        let _g = crate::test_util::global_state_lock();
6547        let mut buf: Vec<REFRESH_ELEMENT> = vec![REFRESH_ELEMENT { chr: 'X', atr: 0 }; 3];
6548        let rc = REFRESH_ELEMENT { chr: 'A', atr: 0 };
6549        // n=100 but dst has 3 — must not panic.
6550        ZR_memset(&mut buf, rc, 100);
6551        for elt in &buf {
6552            assert_eq!(elt.chr, 'A');
6553        }
6554    }
6555
6556    /// c:95-97 — `ZR_strcpy` copies including the NUL terminator.
6557    #[test]
6558    fn zr_strcpy_includes_nul_terminator() {
6559        let _g = crate::test_util::global_state_lock();
6560        let src: Vec<REFRESH_ELEMENT> = vec![
6561            REFRESH_ELEMENT { chr: 'a', atr: 0 },
6562            REFRESH_ELEMENT { chr: 'b', atr: 0 },
6563            REFRESH_ELEMENT { chr: 'c', atr: 0 },
6564            REFRESH_ELEMENT { chr: '\0', atr: 0 },
6565        ];
6566        let mut dst: Vec<REFRESH_ELEMENT> = vec![REFRESH_ELEMENT { chr: 'X', atr: 0 }; 5];
6567        ZR_strcpy(&mut dst, &src);
6568        assert_eq!(dst[0].chr, 'a');
6569        assert_eq!(dst[1].chr, 'b');
6570        assert_eq!(dst[2].chr, 'c');
6571        assert_eq!(dst[3].chr, '\0', "NUL terminator must be copied");
6572    }
6573
6574    /// c:102-109 — `ZR_strlen(empty)` returns 0 (early-NUL).
6575    #[test]
6576    fn zr_strlen_immediate_nul_returns_zero() {
6577        let _g = crate::test_util::global_state_lock();
6578        let buf: Vec<REFRESH_ELEMENT> = vec![REFRESH_ELEMENT { chr: '\0', atr: 0 }];
6579        assert_eq!(ZR_strlen(&buf), 0);
6580    }
6581
6582    /// c:102-109 — `ZR_strlen` counts up to (not including) NUL.
6583    #[test]
6584    fn zr_strlen_counts_until_nul() {
6585        let _g = crate::test_util::global_state_lock();
6586        let buf: Vec<REFRESH_ELEMENT> = vec![
6587            REFRESH_ELEMENT { chr: 'a', atr: 0 },
6588            REFRESH_ELEMENT { chr: 'b', atr: 0 },
6589            REFRESH_ELEMENT { chr: 'c', atr: 0 },
6590            REFRESH_ELEMENT { chr: '\0', atr: 0 },
6591            REFRESH_ELEMENT { chr: 'x', atr: 0 }, // past NUL — not counted
6592        ];
6593        assert_eq!(ZR_strlen(&buf), 3);
6594    }
6595
6596    /// c:120-133 — `ZR_strncmp(equal, equal, n)` returns 0 for any n.
6597    #[test]
6598    fn zr_strncmp_equal_strings_return_zero() {
6599        let _g = crate::test_util::global_state_lock();
6600        let a: Vec<REFRESH_ELEMENT> = vec![
6601            REFRESH_ELEMENT { chr: 'a', atr: 0 },
6602            REFRESH_ELEMENT { chr: 'b', atr: 0 },
6603        ];
6604        let b = a.clone();
6605        assert_eq!(ZR_strncmp(&a, &b, 2), 0);
6606        assert_eq!(ZR_strncmp(&a, &b, 1), 0, "prefix match also returns 0");
6607    }
6608
6609    /// c:120-133 — `ZR_strncmp(differ, ...)` returns 1 on first diff.
6610    #[test]
6611    fn zr_strncmp_diff_returns_one() {
6612        let _g = crate::test_util::global_state_lock();
6613        let a: Vec<REFRESH_ELEMENT> = vec![
6614            REFRESH_ELEMENT { chr: 'a', atr: 0 },
6615            REFRESH_ELEMENT { chr: 'b', atr: 0 },
6616        ];
6617        let b: Vec<REFRESH_ELEMENT> = vec![
6618            REFRESH_ELEMENT { chr: 'a', atr: 0 },
6619            REFRESH_ELEMENT { chr: 'X', atr: 0 },
6620        ];
6621        assert_eq!(ZR_strncmp(&a, &b, 2), 1, "differ at idx 1 → 1");
6622        assert_eq!(ZR_strncmp(&a, &b, 1), 0, "first char matches → 0");
6623    }
6624
6625    /// c:120-133 — `ZR_strncmp(_, _, 0)` returns 0 (loop never runs).
6626    #[test]
6627    fn zr_strncmp_zero_len_returns_zero() {
6628        let _g = crate::test_util::global_state_lock();
6629        let a: Vec<REFRESH_ELEMENT> = vec![REFRESH_ELEMENT { chr: 'a', atr: 0 }];
6630        let b: Vec<REFRESH_ELEMENT> = vec![REFRESH_ELEMENT { chr: 'z', atr: 0 }];
6631        assert_eq!(ZR_strncmp(&a, &b, 0), 0, "n=0 → no comparison");
6632    }
6633
6634    // ═══════════════════════════════════════════════════════════════════
6635    // Additional C-parity tests for Src/Zle/zle_refresh.c
6636    // c:33 ZR_memset / c:96 ZR_strcpy / c:143 ZR_strlen / c:230 ZR_strncmp
6637    // c:448 zle_free_highlight / c:465 tcoutclear / c:476 zwcputc /
6638    // c:708 scrollwindow / c:909 zrefresh / c:1097 wpfxlen
6639    // ═══════════════════════════════════════════════════════════════════
6640
6641    /// c:143 — `ZR_strlen` returns usize (compile-time type pin).
6642    #[test]
6643    fn zr_strlen_returns_usize_type() {
6644        let _: usize = ZR_strlen(&[]);
6645    }
6646
6647    /// c:143 — `ZR_strlen(empty)` returns 0.
6648    #[test]
6649    fn zr_strlen_empty_slice_returns_zero() {
6650        assert_eq!(ZR_strlen(&[]), 0);
6651    }
6652
6653    /// c:230 — `ZR_strncmp` is reflexive for same input.
6654    #[test]
6655    fn zr_strncmp_reflexive_returns_zero() {
6656        let a: Vec<REFRESH_ELEMENT> = vec![
6657            REFRESH_ELEMENT { chr: 'a', atr: 0 },
6658            REFRESH_ELEMENT { chr: 'b', atr: 0 },
6659        ];
6660        assert_eq!(ZR_strncmp(&a, &a, 2), 0, "self-compare must be 0");
6661    }
6662
6663    /// c:230 — `ZR_strncmp(empty, empty, 0)` returns 0.
6664    #[test]
6665    fn zr_strncmp_both_empty_zero_len_returns_zero() {
6666        let empty: Vec<REFRESH_ELEMENT> = vec![];
6667        assert_eq!(ZR_strncmp(&empty, &empty, 0), 0);
6668    }
6669
6670    /// c:448 — `zle_free_highlight` idempotent.
6671    #[test]
6672    fn zle_free_highlight_idempotent() {
6673        let _g = crate::test_util::global_state_lock();
6674        let _g2 = zle_test_setup();
6675        for _ in 0..5 {
6676            zle_free_highlight();
6677        }
6678    }
6679
6680    /// c:607 — `tcoutclear(cap)` safe for both clear-line and clear-screen.
6681    #[test]
6682    fn tcoutclear_both_modes_safe() {
6683        let _g = crate::test_util::global_state_lock();
6684        let _g2 = zle_test_setup();
6685        tcoutclear(crate::ported::zsh_h::TCCLEAREOL);
6686        tcoutclear(crate::ported::zsh_h::TCCLEARSCREEN);
6687    }
6688
6689    /// c:803-808 — scrollwindow(tline) rotates the video buffer: line
6690    /// `tline` lifts out, lower lines shift up, the lifted line wraps to
6691    /// the bottom, and more_start is set when scrolling from the top.
6692    #[test]
6693    fn scrollwindow_rotates_buffer_and_sets_more_start() {
6694        let _g = crate::test_util::global_state_lock();
6695        let _g2 = zle_test_setup();
6696        let mk = |s: &str| -> REFRESH_STRING {
6697            s.chars().map(|c| REFRESH_ELEMENT { chr: c, atr: 0 }).collect()
6698        };
6699        WINH.store(4, Ordering::SeqCst);
6700        MORE_START.store(0, Ordering::SeqCst);
6701        *NBUF.lock().unwrap() = vec![mk("L0"), mk("L1"), mk("L2"), mk("L3")];
6702
6703        scrollwindow(0); // lift L0, shift up, L0 wraps to bottom
6704
6705        let rows: Vec<String> = NBUF
6706            .lock()
6707            .unwrap()
6708            .iter()
6709            .map(|r| r.iter().map(|c| c.chr).collect())
6710            .collect();
6711        assert_eq!(rows, vec!["L1", "L2", "L3", "L0"], "rotate-left by one");
6712        assert_eq!(
6713            MORE_START.load(Ordering::SeqCst),
6714            1,
6715            "scrolling from the top sets more_start"
6716        );
6717    }
6718
6719    /// c:807 — scrolling from a non-zero line does NOT set more_start, and
6720    /// rotates only the window from `tline` down.
6721    #[test]
6722    fn scrollwindow_from_nonzero_line() {
6723        let _g = crate::test_util::global_state_lock();
6724        let _g2 = zle_test_setup();
6725        let mk = |s: &str| -> REFRESH_STRING {
6726            s.chars().map(|c| REFRESH_ELEMENT { chr: c, atr: 0 }).collect()
6727        };
6728        WINH.store(4, Ordering::SeqCst);
6729        MORE_START.store(0, Ordering::SeqCst);
6730        *NBUF.lock().unwrap() = vec![mk("L0"), mk("L1"), mk("L2"), mk("L3")];
6731
6732        scrollwindow(1); // rotate [1..4): L1 wraps to bottom, L0 untouched
6733
6734        let rows: Vec<String> = NBUF
6735            .lock()
6736            .unwrap()
6737            .iter()
6738            .map(|r| r.iter().map(|c| c.chr).collect())
6739            .collect();
6740        assert_eq!(rows, vec!["L0", "L2", "L3", "L1"], "rotate from line 1");
6741        assert_eq!(
6742            MORE_START.load(Ordering::SeqCst),
6743            0,
6744            "non-top scroll must not set more_start"
6745        );
6746    }
6747
6748    /// scrollwindow on an out-of-range / negative tline is a safe no-op
6749    /// (the Vec may be shorter than winh).
6750    #[test]
6751    fn scrollwindow_out_of_range_no_panic() {
6752        let _g = crate::test_util::global_state_lock();
6753        let _g2 = zle_test_setup();
6754        *NBUF.lock().unwrap() = vec![];
6755        scrollwindow(-1);
6756        scrollwindow(0);
6757        scrollwindow(i32::MAX);
6758        scrollwindow(i32::MIN);
6759    }
6760
6761    /// c:1097 — `wpfxlen(empty, empty)` returns 0.
6762    #[test]
6763    fn wpfxlen_both_empty_returns_zero() {
6764        assert_eq!(wpfxlen(&[], &[]), 0);
6765    }
6766
6767    /// c:1097 — `wpfxlen(a, a)` returns slice len (full match).
6768    #[test]
6769    fn wpfxlen_identical_returns_full_len() {
6770        let s: Vec<REFRESH_ELEMENT> = vec![
6771            REFRESH_ELEMENT { chr: 'a', atr: 0 },
6772            REFRESH_ELEMENT { chr: 'b', atr: 0 },
6773            REFRESH_ELEMENT { chr: 'c', atr: 0 },
6774        ];
6775        assert_eq!(wpfxlen(&s, &s), s.len(), "identical → full prefix");
6776    }
6777
6778    /// c:476 — `zwcputc` is safe for any char (including high codepoints).
6779    #[test]
6780    fn zwcputc_any_char_no_panic() {
6781        let _g = crate::test_util::global_state_lock();
6782        let _g2 = zle_test_setup();
6783        for c in ['\0', 'a', '日', '\u{1F600}', '\u{10FFFF}'] {
6784            zwcputc(&REFRESH_ELEMENT { chr: c, atr: 0 });
6785        }
6786    }
6787
6788    // ═══════════════════════════════════════════════════════════════════
6789    // Additional C-parity tests for Src/Zle/zle_refresh.c
6790    // c:33 ZR_memset / c:96 ZR_strcpy / c:143 ZR_strlen / c:230 ZR_strncmp /
6791    // c:496 zwcwrite / c:1097 wpfxlen / c:1603 moveto / c:1629 tcmultout
6792    // ═══════════════════════════════════════════════════════════════════
6793
6794    /// c:33 — `ZR_memset` on empty slice is safe.
6795    #[test]
6796    fn zr_memset_empty_slice_no_panic() {
6797        let mut buf: Vec<REFRESH_ELEMENT> = vec![];
6798        ZR_memset(&mut buf, REFRESH_ELEMENT { chr: ' ', atr: 0 }, 0);
6799    }
6800
6801    /// c:33 — `ZR_memset(buf, fill, n)` writes `fill` n times.
6802    #[test]
6803    fn zr_memset_writes_fill_value() {
6804        let mut buf: Vec<REFRESH_ELEMENT> = vec![
6805            REFRESH_ELEMENT { chr: 'a', atr: 0 },
6806            REFRESH_ELEMENT { chr: 'b', atr: 0 },
6807            REFRESH_ELEMENT { chr: 'c', atr: 0 },
6808        ];
6809        let fill = REFRESH_ELEMENT { chr: 'X', atr: 0 };
6810        ZR_memset(&mut buf, fill, 3);
6811        for e in &buf {
6812            assert_eq!(e.chr, 'X', "ZR_memset must fill with X");
6813        }
6814    }
6815
6816    /// c:96 — `ZR_strcpy` empty src is safe.
6817    #[test]
6818    fn zr_strcpy_empty_src_no_panic() {
6819        let mut dst: Vec<REFRESH_ELEMENT> = vec![REFRESH_ELEMENT { chr: '\0', atr: 0 }];
6820        ZR_strcpy(&mut dst, &[]);
6821    }
6822
6823    /// c:230 — `ZR_strncmp(empty, empty, 0)` returns 0.
6824    #[test]
6825    fn zr_strncmp_empty_inputs_returns_zero() {
6826        let r = ZR_strncmp(&[], &[], 0);
6827        assert_eq!(r, 0, "empty + empty + 0 → 0");
6828    }
6829
6830    /// c:230 — `ZR_strncmp` returns i32 (compile-time type pin).
6831    #[test]
6832    fn zr_strncmp_returns_i32_type() {
6833        let _: i32 = ZR_strncmp(&[], &[], 0);
6834    }
6835
6836    /// c:230 — empty + empty + nonzero cap still 0.
6837    #[test]
6838    fn zr_strncmp_empty_with_nonzero_cap_returns_zero() {
6839        assert_eq!(ZR_strncmp(&[], &[], 5), 0);
6840    }
6841
6842    /// c:496 — `zwcwrite(&[], 0)` empty string is safe.
6843    #[test]
6844    fn zwcwrite_empty_no_panic() {
6845        let _g = crate::test_util::global_state_lock();
6846        let _g2 = zle_test_setup();
6847        zwcwrite(&[], 0);
6848    }
6849
6850    /// c:655-660 — zwcwrite writes the first `i` cells and returns the
6851    /// count, clamped to the available length.
6852    #[test]
6853    fn zwcwrite_returns_clamped_cell_count() {
6854        let _g = crate::test_util::global_state_lock();
6855        let _g2 = zle_test_setup();
6856        let cells = [
6857            REFRESH_ELEMENT { chr: 'a', atr: 0 },
6858            REFRESH_ELEMENT { chr: 'b', atr: 0 },
6859            REFRESH_ELEMENT { chr: 'c', atr: 0 },
6860        ];
6861        assert_eq!(zwcwrite(&cells, 2), 2, "c:660 — returns i");
6862        assert_eq!(zwcwrite(&cells, 5), 3, "clamped to available length");
6863        assert_eq!(zwcwrite(&cells, 0), 0);
6864    }
6865
6866    /// c:1097 — `wpfxlen` returns usize (compile-time type pin).
6867    #[test]
6868    fn wpfxlen_returns_usize_type() {
6869        let _: usize = wpfxlen(&[], &[]);
6870    }
6871
6872    /// c:1603 — `moveto(0, 0)` returns void (compile-time type pin).
6873    #[test]
6874    fn moveto_returns_void_type() {
6875        let _g = crate::test_util::global_state_lock();
6876        let _g2 = zle_test_setup();
6877        let _: () = moveto(0, 0);
6878    }
6879
6880    /// c:2159-2204 — `moveto` updates the video-position trackers (vln, vcs)
6881    /// to the target, so the diff engine's next-frame cursor logic is correct.
6882    #[test]
6883    fn moveto_updates_vcs_vln() {
6884        use std::sync::atomic::Ordering;
6885        let _g = crate::test_util::global_state_lock();
6886        let _g2 = zle_test_setup();
6887        VCS.store(0, Ordering::SeqCst);
6888        VLN.store(0, Ordering::SeqCst);
6889        moveto(3, 7);
6890        assert_eq!(VLN.load(Ordering::SeqCst), 3, "moveto must set VLN to row");
6891        assert_eq!(VCS.load(Ordering::SeqCst), 7, "moveto must set VCS to col");
6892    }
6893
6894    /// c:1629 — `tcmultout(0, 0, 0)` returns i32 (compile-time type pin).
6895    #[test]
6896    fn tcmultout_returns_i32_type() {
6897        let _g = crate::test_util::global_state_lock();
6898        let _g2 = zle_test_setup();
6899        let _: i32 = tcmultout(0, 0, 0);
6900    }
6901
6902    /// Integration: the consolidated scroll machinery as the live build will
6903    /// drive it — write a row, advance with `nextline`; once content exceeds
6904    /// `winh`, `nextline` scrolls the global NBUF (oldest row off the top,
6905    /// newest visible). Proves nextline + scrollwindow + the global NBUF
6906    /// behave together before the build is wired to them.
6907    #[test]
6908    fn build_loop_scrolls_global_nbuf_past_winh() {
6909        let _g = crate::test_util::global_state_lock();
6910        let _g2 = zle_test_setup();
6911        let winw = 4i32;
6912        let winh = 3i32;
6913        WINW.store(winw, Ordering::SeqCst);
6914        WINH.store(winh, Ordering::SeqCst);
6915        VMAXLN.store(0, Ordering::SeqCst);
6916        NUMSCROLLS.store(0, Ordering::SeqCst);
6917        ONUMSCROLLS.store(0, Ordering::SeqCst);
6918        // winh+1 row slots, each winw+2 cells (as resetvideo allocates).
6919        *NBUF.lock().unwrap() =
6920            vec![vec![REFRESH_ELEMENT::default(); (winw + 2) as usize]; (winh + 1) as usize];
6921
6922        // Drive the build's row loop: advance with nextline BETWEEN lines
6923        // (as the build does — a line, then the wrap/newline advance), then
6924        // stamp NBUF[ln][0] with the line label. Five labels into a 3-row
6925        // window scroll twice.
6926        let mut rpms = rparams::default();
6927        rpms.nvln = -1; // cursor not yet placed
6928        for (i, label) in ['0', '1', '2', '3', '4'].iter().enumerate() {
6929            if i > 0 {
6930                nextline(&mut rpms, 0);
6931            }
6932            {
6933                let mut nbuf = NBUF.lock().unwrap();
6934                let ln = rpms.ln as usize;
6935                if ln < nbuf.len() && !nbuf[ln].is_empty() {
6936                    nbuf[ln][0] = REFRESH_ELEMENT { chr: *label, atr: 0 };
6937                }
6938            }
6939            // The build advances rpms.pos as it writes cells; here one cell
6940            // was written, so the next nextline terminates the line AFTER the
6941            // stamped cell (at pos=1) rather than clearing it at pos=0.
6942            rpms.pos = 1;
6943        }
6944
6945        // After 5 labels in a 3-row window the visible rows hold the last
6946        // three line labels in order (older lines scrolled off the top).
6947        let visible: String = {
6948            let nbuf = NBUF.lock().unwrap();
6949            (0..winh as usize)
6950                .map(|i| nbuf[i][0].chr)
6951                .collect()
6952        };
6953        assert_eq!(visible, "234", "oldest lines scrolled off; newest visible");
6954    }
6955
6956    /// c:700-721 — freevideo clears the global NBUF/OBUF (the lifecycle pair
6957    /// of resetvideo); the empty Vecs are C's `nbuf = obuf = NULL` state.
6958    #[test]
6959    fn freevideo_clears_global_nbuf_obuf() {
6960        let _g = crate::test_util::global_state_lock();
6961        let _g2 = zle_test_setup();
6962        *NBUF.lock().unwrap() = vec![vec![REFRESH_ELEMENT::default(); 6]; 3];
6963        *OBUF.lock().unwrap() = vec![vec![REFRESH_ELEMENT::default(); 6]; 3];
6964        // c:710-714 — seed the multiword stores so the MB-free path is covered.
6965        NMWBUF.with(|b| *b.borrow_mut() = vec![0, 5, 6]);
6966        OMWBUF.with(|b| *b.borrow_mut() = vec![0, 7]);
6967        NMW_SIZE.with(|c| c.set(3));
6968        OMW_SIZE.with(|c| c.set(2));
6969        NMW_IND.with(|c| c.set(9));
6970
6971        freevideo();
6972
6973        assert!(NBUF.lock().unwrap().is_empty(), "NBUF freed");
6974        assert!(OBUF.lock().unwrap().is_empty(), "OBUF freed");
6975        NMWBUF.with(|b| assert!(b.borrow().is_empty(), "NMWBUF freed"));
6976        OMWBUF.with(|b| assert!(b.borrow().is_empty(), "OMWBUF freed"));
6977        assert_eq!(NMW_SIZE.with(|c| c.get()), 0, "nmw_size reset");
6978        assert_eq!(OMW_SIZE.with(|c| c.get()), 0, "omw_size reset");
6979        assert_eq!(NMW_IND.with(|c| c.get()), 1, "nmw_ind reset to 1 (c:713)");
6980    }
6981
6982    /// c:737-755 — resetvideo allocates the global NBUF/OBUF: (winh+1) rows
6983    /// of (winw+2) cells each (matching C's global nbuf/obuf), so the buffer
6984    /// lifecycle is on the same buffers as nextline/snextline/scrollwindow.
6985    #[test]
6986    fn resetvideo_allocates_global_nbuf_obuf() {
6987        let _g = crate::test_util::global_state_lock();
6988        let _g2 = zle_test_setup();
6989        *NBUF.lock().unwrap() = vec![];
6990        *OBUF.lock().unwrap() = vec![];
6991
6992        let mut state = RefreshState::new();
6993        resetvideo(&mut state);
6994
6995        let winw = WINW.load(Ordering::SeqCst);
6996        let winh = WINH.load(Ordering::SeqCst);
6997        let nbuf = NBUF.lock().unwrap();
6998        let obuf = OBUF.lock().unwrap();
6999        assert_eq!(nbuf.len() as i32, winh + 1, "NBUF has winh+1 rows");
7000        assert_eq!(obuf.len() as i32, winh + 1, "OBUF has winh+1 rows");
7001        assert!(!nbuf.is_empty());
7002        assert_eq!(nbuf[0].len() as i32, winw + 2, "row is winw+2 cells");
7003    }
7004
7005    /// c:1663-1671 — when the live build scrolls content off the top (a
7006    /// buffer taller than winh, cursor at the end so nextline never bails),
7007    /// MORE_START is set and line 0 is overwritten with the ">..." indicator.
7008    /// Deterministic regardless of the terminal-derived winh: 300 newlines
7009    /// exceed any winh.
7010    #[test]
7011    fn zrefresh_more_start_indicator_on_scroll() {
7012        let _g = crate::test_util::global_state_lock();
7013        let _g2 = zle_test_setup();
7014        *crate::ported::zle::zle_main::LPROMPT.lock().unwrap() = String::new();
7015        let line: Vec<char> = std::iter::repeat('\n').take(300).collect();
7016        let n = line.len();
7017        *ZLELINE.lock().unwrap() = line;
7018        ZLECS.store(n, Ordering::SeqCst); // cursor at end → no bail
7019        ZLELL.store(n, Ordering::SeqCst);
7020        *NBUF.lock().unwrap() = vec![];
7021        *OBUF.lock().unwrap() = vec![];
7022        NLNCT.store(0, Ordering::SeqCst);
7023        OLNCT.store(0, Ordering::SeqCst);
7024        VCS.store(0, Ordering::SeqCst);
7025        VLN.store(0, Ordering::SeqCst);
7026
7027        let devnull = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_WRONLY) };
7028        let old = crate::ported::init::SHTTY.load(Ordering::SeqCst);
7029        crate::ported::init::SHTTY.store(devnull, Ordering::SeqCst);
7030        zrefresh();
7031        crate::ported::init::SHTTY.store(old, Ordering::SeqCst);
7032        unsafe { libc::close(devnull) };
7033
7034        assert_eq!(
7035            MORE_START.load(Ordering::SeqCst),
7036            1,
7037            "a buffer taller than winh must scroll → more_start"
7038        );
7039        let row0: String = NBUF.lock().unwrap()[0]
7040            .iter()
7041            .map(|c| c.chr)
7042            .take_while(|&c| c != '\0')
7043            .collect();
7044        assert!(
7045            row0.starts_with(">...."),
7046            "line 0 shows the start-ellipsis indicator; got {:?}",
7047            row0
7048        );
7049    }
7050
7051    /// c:1194 + c:1750 — the numscrolls lifecycle. A frame resets NUMSCROLLS
7052    /// to 0 before building (so it doesn't accumulate across frames), nextline
7053    /// increments it per scroll, and frame-end stores it into ONUMSCROLLS for
7054    /// the next frame's bail heuristic. Seed both high/stale, drive a tall
7055    /// (scrolling) frame, and assert: NUMSCROLLS came back small (reset, then
7056    /// counted real scrolls — not 1000+), and ONUMSCROLLS == NUMSCROLLS (carry).
7057    #[test]
7058    fn zrefresh_numscrolls_resets_and_carries_to_onumscrolls() {
7059        let _g = crate::test_util::global_state_lock();
7060        let _g2 = zle_test_setup();
7061        *crate::ported::zle::zle_main::LPROMPT.lock().unwrap() = String::new();
7062        // Stale values from "previous frames" that must be overwritten.
7063        NUMSCROLLS.store(1000, Ordering::SeqCst);
7064        ONUMSCROLLS.store(999, Ordering::SeqCst);
7065
7066        let line: Vec<char> = std::iter::repeat('\n').take(300).collect();
7067        let n = line.len();
7068        *ZLELINE.lock().unwrap() = line;
7069        ZLECS.store(n, Ordering::SeqCst); // cursor at end → scroll, no bail
7070        ZLELL.store(n, Ordering::SeqCst);
7071        *NBUF.lock().unwrap() = vec![];
7072        *OBUF.lock().unwrap() = vec![];
7073        NLNCT.store(0, Ordering::SeqCst);
7074        OLNCT.store(0, Ordering::SeqCst);
7075        VCS.store(0, Ordering::SeqCst);
7076        VLN.store(0, Ordering::SeqCst);
7077
7078        let devnull =
7079            unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_WRONLY) };
7080        let old = crate::ported::init::SHTTY.load(Ordering::SeqCst);
7081        crate::ported::init::SHTTY.store(devnull, Ordering::SeqCst);
7082        zrefresh();
7083        crate::ported::init::SHTTY.store(old, Ordering::SeqCst);
7084        unsafe { libc::close(devnull) };
7085
7086        let ns = NUMSCROLLS.load(Ordering::SeqCst);
7087        let ons = ONUMSCROLLS.load(Ordering::SeqCst);
7088        assert!(
7089            ns > 0 && ns < 1000,
7090            "NUMSCROLLS must reset (not accumulate past the seeded 1000) and \
7091             count real scrolls; got {}",
7092            ns
7093        );
7094        assert_eq!(ons, ns, "frame-end must store ONUMSCROLLS = NUMSCROLLS (c:1750)");
7095    }
7096
7097    /// c:1119 — zrefresh resets MORE_START/MORE_END each frame before the
7098    /// build, so a scroll flag set on a previous frame doesn't stick. A
7099    /// short (non-scrolling) frame must leave them 0.
7100    #[test]
7101    fn zrefresh_resets_more_flags_each_frame() {
7102        let _g = crate::test_util::global_state_lock();
7103        let _g2 = zle_test_setup();
7104        MORE_START.store(1, Ordering::SeqCst); // leaked from a prior frame
7105        MORE_END.store(1, Ordering::SeqCst);
7106        *ZLELINE.lock().unwrap() = "x".chars().collect();
7107        ZLECS.store(1, Ordering::SeqCst);
7108        ZLELL.store(1, Ordering::SeqCst);
7109        *NBUF.lock().unwrap() = vec![];
7110        *OBUF.lock().unwrap() = vec![];
7111        NLNCT.store(0, Ordering::SeqCst);
7112        OLNCT.store(0, Ordering::SeqCst);
7113        VCS.store(0, Ordering::SeqCst);
7114        VLN.store(0, Ordering::SeqCst);
7115
7116        let devnull = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_WRONLY) };
7117        let old = crate::ported::init::SHTTY.load(Ordering::SeqCst);
7118        crate::ported::init::SHTTY.store(devnull, Ordering::SeqCst);
7119        zrefresh();
7120        crate::ported::init::SHTTY.store(old, Ordering::SeqCst);
7121        unsafe { libc::close(devnull) };
7122
7123        assert_eq!(MORE_START.load(Ordering::SeqCst), 0, "more_start reset");
7124        assert_eq!(MORE_END.load(Ordering::SeqCst), 0, "more_end reset");
7125    }
7126
7127    /// c:850-853 — nextline BAILS (returns 1) at the bottom when scrolling
7128    /// would push the cursor (nvln) off-screen, so the build stops adding
7129    /// lines. The build now honours this return (breaks the line loop).
7130    #[test]
7131    fn nextline_bails_to_keep_cursor_visible() {
7132        let _g = crate::test_util::global_state_lock();
7133        let _g2 = zle_test_setup();
7134        WINW.store(4, Ordering::SeqCst);
7135        WINH.store(3, Ordering::SeqCst);
7136        NUMSCROLLS.store(0, Ordering::SeqCst);
7137        ONUMSCROLLS.store(0, Ordering::SeqCst);
7138        *NBUF.lock().unwrap() = vec![vec![REFRESH_ELEMENT::default(); 6]; 4];
7139
7140        let mut rpms = rparams::default();
7141        rpms.ln = 2; // winh - 1, bottom row
7142        rpms.nvln = 1; // cursor on line 1 (not -1, not winh-1, <= winh/2 path)
7143        rpms.canscroll = 0;
7144        let ret = nextline(&mut rpms, 0);
7145        assert_eq!(ret, 1, "must bail rather than scroll the cursor off-screen");
7146        assert_eq!(rpms.ln, 2, "ln unchanged on bail (no advance, no scroll)");
7147    }
7148
7149    /// c:875-905 — snextline (status-area row advance, now real): off the
7150    /// bottom row it terminates + advances; at the bottom with room above
7151    /// (tosln > ln, nvln > 1) it scrolls and decrements tosln/nvln.
7152    #[test]
7153    fn snextline_advances_then_scrolls_status_pane() {
7154        let _g = crate::test_util::global_state_lock();
7155        let _g2 = zle_test_setup();
7156        WINW.store(4, Ordering::SeqCst);
7157        WINH.store(3, Ordering::SeqCst);
7158        let row = |c: char| -> REFRESH_STRING {
7159            vec![REFRESH_ELEMENT { chr: c, atr: 0 }; 6]
7160        };
7161        *NBUF.lock().unwrap() = vec![row('A'), row('B'), row('C')];
7162
7163        // Not at bottom → terminate + advance.
7164        let mut rpms = rparams::default();
7165        rpms.ln = 0;
7166        rpms.pos = 2;
7167        snextline(&mut rpms);
7168        assert_eq!(rpms.ln, 1, "advanced");
7169        assert_eq!(rpms.pos, 0);
7170        assert_eq!(rpms.end, 4);
7171        assert_eq!(NBUF.lock().unwrap()[0][2].chr, '\0', "terminated at pos");
7172
7173        // At bottom, tosln > ln and nvln > 1 → scroll, tosln--/nvln--.
7174        let mut rpms2 = rparams::default();
7175        rpms2.ln = 2; // winh - 1
7176        rpms2.tosln = 5;
7177        rpms2.nvln = 2;
7178        snextline(&mut rpms2);
7179        assert_eq!(rpms2.tosln, 4, "tosln decremented");
7180        assert_eq!(rpms2.nvln, 1, "nvln decremented after scroll");
7181        assert_eq!(
7182            NBUF.lock().unwrap()[0][0].chr,
7183            'B',
7184            "scrollwindow(0) rotated row 1 up to row 0"
7185        );
7186    }
7187
7188    /// c:897-899 — snextline's final-else: at the bottom row with the status
7189    /// pane already collapsed (tosln <= ln, and not (tosln > 2 && nvln > 1)),
7190    /// it sets `more_status` and scrolls from tosln+1. This is the gate that
7191    /// drives the new "<....>" more_status indicator (c:1599-1635) in zrefresh.
7192    #[test]
7193    fn snextline_sets_more_status_on_full_pane() {
7194        let _g = crate::test_util::global_state_lock();
7195        let _g2 = zle_test_setup();
7196        WINW.store(4, Ordering::SeqCst);
7197        WINH.store(3, Ordering::SeqCst); // winh - 1 == 2
7198        MORE_START.store(0, Ordering::SeqCst);
7199        let row = |c: char| -> REFRESH_STRING {
7200            vec![REFRESH_ELEMENT { chr: c, atr: 0 }; 6]
7201        };
7202        *NBUF.lock().unwrap() = vec![row('A'), row('B'), row('C')];
7203
7204        let mut rpms = rparams::default();
7205        rpms.ln = 2; // winh - 1
7206        rpms.tosln = 2; // not > ln, and not > 2
7207        rpms.nvln = 1; // not > 1
7208        snextline(&mut rpms);
7209
7210        assert_eq!(rpms.more_status, 1, "more_status set on a full status pane");
7211        // scrollwindow(tosln+1)=scrollwindow(3): range [3,winh=3) is empty, so
7212        // no rotation and (tline!=0) no MORE_START — confirm no leak.
7213        assert_eq!(
7214            MORE_START.load(Ordering::SeqCst),
7215            0,
7216            "scrollwindow(tosln+1) must not set MORE_START"
7217        );
7218    }
7219
7220    /// c:841-873 — nextline (now on the global NBUF): off the bottom row it
7221    /// marks the wrap/terminator, advances ln, allocates the next row, and
7222    /// resets pos/end. At the bottom row it scrolls the buffer instead.
7223    #[test]
7224    fn nextline_advances_then_scrolls_on_global_nbuf() {
7225        let _g = crate::test_util::global_state_lock();
7226        let _g2 = zle_test_setup();
7227        WINW.store(4, Ordering::SeqCst);
7228        WINH.store(3, Ordering::SeqCst);
7229        NUMSCROLLS.store(0, Ordering::SeqCst);
7230        ONUMSCROLLS.store(0, Ordering::SeqCst);
7231        let row = |c: char| -> REFRESH_STRING {
7232            vec![REFRESH_ELEMENT { chr: c, atr: 0 }; 6] // winw + 2 cells
7233        };
7234        *NBUF.lock().unwrap() = vec![row('A'), row('B'), row('C')];
7235
7236        // From ln=0 (not bottom), wrapped: advance + wrap marker.
7237        let mut rpms = rparams::default();
7238        rpms.ln = 0;
7239        rpms.pos = 2;
7240        rpms.nvln = -1;
7241        let ret = nextline(&mut rpms, 1);
7242        assert_eq!(ret, 0);
7243        assert_eq!(rpms.ln, 1, "advanced to next line");
7244        assert_eq!(rpms.pos, 0); // c:871
7245        assert_eq!(rpms.end, 4); // c:872 — winw
7246        {
7247            let nbuf = NBUF.lock().unwrap();
7248            assert_eq!(nbuf[0][5].chr, '\n', "wrap marker at winw+1"); // c:844
7249            assert_eq!(nbuf[0][2].chr, '\0', "terminated at pos"); // c:845
7250        }
7251
7252        // From ln=winh-1 (bottom) with nvln=-1: scroll instead of advance.
7253        let mut rpms2 = rparams::default();
7254        rpms2.ln = 2;
7255        rpms2.nvln = -1;
7256        let ret2 = nextline(&mut rpms2, 0);
7257        assert_eq!(ret2, 0);
7258        assert_eq!(rpms2.ln, 2, "stays at the bottom row after scroll");
7259        // scrollwindow(0) rotated the buffer up: old row 1 ('B') is now row 0.
7260        assert_eq!(
7261            NBUF.lock().unwrap()[0][0].chr,
7262            'B',
7263            "buffer scrolled: row 1 rose to row 0"
7264        );
7265    }
7266
7267    /// c:430-479 — get_region_highlight formats each user region highlight
7268    /// as "start end <spec>", with the attribute rendered as the real
7269    /// highlight spec (fg=red) now that output_highlight is faithful; an
7270    /// empty store yields an empty array.
7271    #[test]
7272    fn get_region_highlight_formats_user_entries() {
7273        let _g = crate::test_util::global_state_lock();
7274        let _g2 = zle_test_setup();
7275
7276        set_region_highlight(Some(&["0 5 fg=red".to_string()]));
7277        let arr = get_region_highlight(&crate::ported::zsh_h::param::default());
7278        assert_eq!(arr.len(), 1, "one user highlight → one entry; got {:?}", arr);
7279        assert_eq!(arr[0], "0 5 fg=red", "spec round-trips, not SGR");
7280
7281        let mut pm = crate::ported::zsh_h::param::default();
7282        unset_region_highlight(&mut pm, 1);
7283        assert!(
7284            get_region_highlight(&pm).is_empty(),
7285            "no highlights → empty array"
7286        );
7287    }
7288
7289    /// c:592 — unset_region_highlight clears the user region highlights
7290    /// (set_region_highlight(NULL)) and runs the standard unset only when
7291    /// the parameter is explicitly unset (exp != 0); exp == 0 is a no-op.
7292    #[test]
7293    fn unset_region_highlight_clears_only_on_exp() {
7294        let _g = crate::test_util::global_state_lock();
7295        let _g2 = zle_test_setup();
7296
7297        // Seed a user region highlight.
7298        set_region_highlight(Some(&["0 5 fg=red".to_string()]));
7299        let with_user = REGION_HIGHLIGHTS.lock().unwrap().len();
7300        assert!(with_user > 0, "set_region_highlight should add a user entry");
7301
7302        let mut pm = crate::ported::zsh_h::param::default();
7303        // exp == 0 → no change.
7304        unset_region_highlight(&mut pm, 0);
7305        assert_eq!(
7306            REGION_HIGHLIGHTS.lock().unwrap().len(),
7307            with_user,
7308            "exp=0 must be a no-op"
7309        );
7310        // exp != 0 → user highlights cleared to the special baseline.
7311        unset_region_highlight(&mut pm, 1);
7312        assert!(
7313            REGION_HIGHLIGHTS.lock().unwrap().len() < with_user,
7314            "exp!=0 must clear the user highlights"
7315        );
7316    }
7317
7318    /// c:152 — zrefresh publishes the prompt's trailing attribute to the
7319    /// global PROMPT_ATTR, which refreshline (TCDEL) and tcoutclear read so
7320    /// deleted/cleared cells carry the prompt's colour. A bold prompt must
7321    /// leave PROMPT_ATTR carrying TXTBOLDFACE.
7322    #[test]
7323    fn zrefresh_publishes_prompt_attr() {
7324        use crate::ported::zsh_h::TXTBOLDFACE;
7325        let _g = crate::test_util::global_state_lock();
7326        let _g2 = zle_test_setup();
7327
7328        let saved = crate::ported::zle::zle_main::LPROMPT.lock().unwrap().clone();
7329        *crate::ported::zle::zle_main::LPROMPT.lock().unwrap() = "\x1b[1mPS>".to_string();
7330        PROMPT_ATTR.store(0, Ordering::SeqCst);
7331        *ZLELINE.lock().unwrap() = "x".chars().collect();
7332        ZLECS.store(1, Ordering::SeqCst);
7333        ZLELL.store(1, Ordering::SeqCst);
7334        *NBUF.lock().unwrap() = vec![];
7335        *OBUF.lock().unwrap() = vec![];
7336        NLNCT.store(0, Ordering::SeqCst);
7337        OLNCT.store(0, Ordering::SeqCst);
7338        VCS.store(0, Ordering::SeqCst);
7339        VLN.store(0, Ordering::SeqCst);
7340
7341        let devnull = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_WRONLY) };
7342        let old = crate::ported::init::SHTTY.load(Ordering::SeqCst);
7343        crate::ported::init::SHTTY.store(devnull, Ordering::SeqCst);
7344        zrefresh();
7345        crate::ported::init::SHTTY.store(old, Ordering::SeqCst);
7346        unsafe { libc::close(devnull) };
7347        *crate::ported::zle::zle_main::LPROMPT.lock().unwrap() = saved;
7348
7349        assert_ne!(
7350            PROMPT_ATTR.load(Ordering::SeqCst) & TXTBOLDFACE,
7351            0,
7352            "bold prompt must publish TXTBOLDFACE to PROMPT_ATTR"
7353        );
7354    }
7355
7356    /// c:676 — zrefresh syncs LPROMPTW to the left-prompt width each frame,
7357    /// enabling refreshline's prompt-skip (dead while LPROMPTW stayed 0).
7358    #[test]
7359    fn zrefresh_syncs_lpromptw_from_prompt() {
7360        let _g = crate::test_util::global_state_lock();
7361        let _g2 = zle_test_setup();
7362
7363        let saved_prompt = crate::ported::zle::zle_main::LPROMPT.lock().unwrap().clone();
7364        *crate::ported::zle::zle_main::LPROMPT.lock().unwrap() = "abc".to_string();
7365        LPROMPTW.store(999, Ordering::SeqCst); // bogus stale value
7366        *ZLELINE.lock().unwrap() = "x".chars().collect();
7367        ZLECS.store(1, Ordering::SeqCst);
7368        ZLELL.store(1, Ordering::SeqCst);
7369        *NBUF.lock().unwrap() = vec![];
7370        *OBUF.lock().unwrap() = vec![];
7371        NLNCT.store(0, Ordering::SeqCst);
7372        OLNCT.store(0, Ordering::SeqCst);
7373        VCS.store(0, Ordering::SeqCst);
7374        VLN.store(0, Ordering::SeqCst);
7375
7376        let devnull = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_WRONLY) };
7377        let old = crate::ported::init::SHTTY.load(Ordering::SeqCst);
7378        crate::ported::init::SHTTY.store(devnull, Ordering::SeqCst);
7379        zrefresh();
7380        crate::ported::init::SHTTY.store(old, Ordering::SeqCst);
7381        unsafe { libc::close(devnull) };
7382        *crate::ported::zle::zle_main::LPROMPT.lock().unwrap() = saved_prompt;
7383
7384        assert_eq!(
7385            LPROMPTW.load(Ordering::SeqCst),
7386            3,
7387            "LPROMPTW must sync to the width of prompt \"abc\""
7388        );
7389    }
7390
7391    /// c:729-734 — zrefresh syncs the global video width to the terminal
7392    /// each frame, so the cursor primitives don't read the stale 80-col
7393    /// default. After a frame, WINW must equal adjustcolumns(), regardless
7394    /// of any earlier bogus value.
7395    #[test]
7396    fn zrefresh_syncs_winw_to_terminal() {
7397        use crate::ported::utils::adjustcolumns;
7398        let _g = crate::test_util::global_state_lock();
7399        let _g2 = zle_test_setup();
7400
7401        WINW.store(999, Ordering::SeqCst); // bogus stale value
7402        *ZLELINE.lock().unwrap() = "x".chars().collect();
7403        ZLECS.store(1, Ordering::SeqCst);
7404        ZLELL.store(1, Ordering::SeqCst);
7405        *NBUF.lock().unwrap() = vec![];
7406        *OBUF.lock().unwrap() = vec![];
7407        NLNCT.store(0, Ordering::SeqCst);
7408        OLNCT.store(0, Ordering::SeqCst);
7409        VCS.store(0, Ordering::SeqCst);
7410        VLN.store(0, Ordering::SeqCst);
7411
7412        let devnull = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_WRONLY) };
7413        let old = crate::ported::init::SHTTY.load(Ordering::SeqCst);
7414        crate::ported::init::SHTTY.store(devnull, Ordering::SeqCst);
7415        zrefresh();
7416        crate::ported::init::SHTTY.store(old, Ordering::SeqCst);
7417        unsafe { libc::close(devnull) };
7418
7419        assert_eq!(
7420            WINW.load(Ordering::SeqCst),
7421            adjustcolumns() as i32,
7422            "zrefresh must sync WINW to the terminal width"
7423        );
7424    }
7425
7426    /// c:2435-2442 — redisplay homes the cursor (with a safety CR), moves up
7427    /// over the prompt height, flags a full redraw (resetneeded=1,
7428    /// clearflag=0), and returns 0.
7429    #[test]
7430    fn redisplay_homes_and_sets_flags() {
7431        use std::io::Read;
7432        use std::os::unix::io::FromRawFd;
7433        let _g = crate::test_util::global_state_lock();
7434        let _g2 = zle_test_setup();
7435
7436        CLEARFLAG.store(1, Ordering::SeqCst);
7437        RESETNEEDED.store(0, Ordering::SeqCst);
7438        LPROMPTH.store(1, Ordering::SeqCst); // tc_upcurs(0) → no movement
7439        *ZLELINE.lock().unwrap() = "x".chars().collect();
7440        ZLECS.store(1, Ordering::SeqCst);
7441        ZLELL.store(1, Ordering::SeqCst);
7442        *NBUF.lock().unwrap() = vec![];
7443        *OBUF.lock().unwrap() = vec![];
7444        NLNCT.store(0, Ordering::SeqCst);
7445        OLNCT.store(0, Ordering::SeqCst);
7446        VCS.store(0, Ordering::SeqCst);
7447        VLN.store(0, Ordering::SeqCst);
7448
7449        let mut fds = [0i32; 2];
7450        assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0, "pipe()");
7451        let (rd, wr) = (fds[0], fds[1]);
7452        let old = crate::ported::init::SHTTY.load(Ordering::SeqCst);
7453        crate::ported::init::SHTTY.store(wr, Ordering::SeqCst);
7454
7455        let ret = redisplay();
7456
7457        crate::ported::init::SHTTY.store(old, Ordering::SeqCst);
7458        unsafe { libc::close(wr) };
7459        let mut out = Vec::new();
7460        let _ = unsafe { std::fs::File::from_raw_fd(rd) }.read_to_end(&mut out);
7461        let s = String::from_utf8_lossy(&out);
7462
7463        assert_eq!(ret, 0, "redisplay returns 0");
7464        assert!(s.contains('\r'), "redisplay emits the safety CR; got {:?}", s);
7465        assert_eq!(CLEARFLAG.load(Ordering::SeqCst), 0, "clearflag zeroed");
7466        assert_eq!(RESETNEEDED.load(Ordering::SeqCst), 1, "resetneeded set");
7467    }
7468
7469    /// c:2424-2430 — clearscreen emits the terminal's clear capability (not
7470    /// a hardcoded CSI 2J), zeroes clearflag, sets resetneeded, and returns 0.
7471    #[test]
7472    fn clearscreen_uses_clear_cap_and_sets_flags() {
7473        use std::io::Read;
7474        use std::os::unix::io::FromRawFd;
7475        use crate::ported::init::{tclen, tcstr};
7476        let _g = crate::test_util::global_state_lock();
7477        let _g2 = zle_test_setup();
7478
7479        let ci = crate::ported::zsh_h::TCCLEARSCREEN as usize;
7480        let save_len = tclen.lock().unwrap()[ci];
7481        let save_str = tcstr.lock().unwrap()[ci].clone();
7482        tclen.lock().unwrap()[ci] = 4;
7483        tcstr.lock().unwrap()[ci] = "\x1b[2J".to_string();
7484
7485        CLEARFLAG.store(1, Ordering::SeqCst);
7486        RESETNEEDED.store(0, Ordering::SeqCst);
7487        // c:2429 — clearscreen must reexpandprompt(): seed a raw template and a
7488        // deliberately stale expansion so we can prove it was re-run.
7489        *crate::ported::zle::zle_main::RAW_LP.lock().unwrap() = "%% > ".to_string();
7490        *ZLELINE.lock().unwrap() = "x".chars().collect();
7491        ZLECS.store(1, Ordering::SeqCst);
7492        ZLELL.store(1, Ordering::SeqCst);
7493        *NBUF.lock().unwrap() = vec![];
7494        *OBUF.lock().unwrap() = vec![];
7495        NLNCT.store(0, Ordering::SeqCst);
7496        OLNCT.store(0, Ordering::SeqCst);
7497        VCS.store(0, Ordering::SeqCst);
7498        VLN.store(0, Ordering::SeqCst);
7499
7500        let mut fds = [0i32; 2];
7501        assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0, "pipe()");
7502        let (rd, wr) = (fds[0], fds[1]);
7503        let old = crate::ported::init::SHTTY.load(Ordering::SeqCst);
7504        crate::ported::init::SHTTY.store(wr, Ordering::SeqCst);
7505
7506        let ret = clearscreen();
7507
7508        crate::ported::init::SHTTY.store(old, Ordering::SeqCst);
7509        unsafe { libc::close(wr) };
7510        tclen.lock().unwrap()[ci] = save_len;
7511        tcstr.lock().unwrap()[ci] = save_str;
7512        let mut out = Vec::new();
7513        let _ = unsafe { std::fs::File::from_raw_fd(rd) }.read_to_end(&mut out);
7514        let s = String::from_utf8_lossy(&out);
7515
7516        assert_eq!(ret, 0, "clearscreen returns 0");
7517        assert!(s.contains("\x1b[2J"), "must emit the clear capability; got {:?}", s);
7518        assert_eq!(CLEARFLAG.load(Ordering::SeqCst), 0, "clearflag zeroed");
7519        assert_eq!(RESETNEEDED.load(Ordering::SeqCst), 1, "resetneeded set");
7520        // c:2429 — the raw "%% > " template was re-expanded to "% > ", proving
7521        // reexpandprompt() ran (it was previously deferred).
7522        assert_eq!(
7523            crate::ported::zle::zle_main::prompt(),
7524            "% > ",
7525            "clearscreen must reexpandprompt the raw template"
7526        );
7527    }
7528
7529    /// c:946-956 — bufswap exchanges the global NBUF and OBUF buffers so
7530    /// last frame's NBUF becomes this frame's OBUF (the diff baseline).
7531    #[test]
7532    fn bufswap_exchanges_global_nbuf_obuf() {
7533        let _g = crate::test_util::global_state_lock();
7534        let _g2 = zle_test_setup();
7535        let mk = |s: &str| -> REFRESH_STRING {
7536            s.chars().map(|c| REFRESH_ELEMENT { chr: c, atr: 0 }).collect()
7537        };
7538        *NBUF.lock().unwrap() = vec![mk("new")];
7539        *OBUF.lock().unwrap() = vec![mk("old")];
7540        bufswap();
7541        let nb: String = NBUF.lock().unwrap()[0].iter().map(|c| c.chr).collect();
7542        let ob: String = OBUF.lock().unwrap()[0].iter().map(|c| c.chr).collect();
7543        assert_eq!(nb, "old", "NBUF must hold the previously-old buffer");
7544        assert_eq!(ob, "new", "OBUF must hold the previously-new buffer");
7545    }
7546
7547    /// c:960-968 — bufswap also swaps the multiword stores (NMWBUF/OMWBUF +
7548    /// nmw_size/omw_size) and resets nmw_ind = 1, so last frame's clusters
7549    /// stay on the diff's old side while the next build refills from a fresh
7550    /// index. Required for the live zwcputc multiword reader (c:633-643).
7551    #[test]
7552    fn bufswap_swaps_multiword_buffers_and_resets_index() {
7553        let _g = crate::test_util::global_state_lock();
7554        let _g2 = zle_test_setup();
7555        NMWBUF.with(|b| *b.borrow_mut() = vec![0, 11, 22]);
7556        OMWBUF.with(|b| *b.borrow_mut() = vec![0, 99]);
7557        NMW_SIZE.with(|c| c.set(3));
7558        OMW_SIZE.with(|c| c.set(2));
7559        NMW_IND.with(|c| c.set(7));
7560        *NBUF.lock().unwrap() = vec![];
7561        *OBUF.lock().unwrap() = vec![];
7562
7563        bufswap();
7564
7565        NMWBUF.with(|b| {
7566            assert_eq!(*b.borrow(), vec![0, 99], "NMWBUF now holds the old store")
7567        });
7568        OMWBUF.with(|b| {
7569            assert_eq!(*b.borrow(), vec![0, 11, 22], "OMWBUF now holds the new store")
7570        });
7571        assert_eq!(NMW_SIZE.with(|c| c.get()), 2, "nmw_size swapped");
7572        assert_eq!(OMW_SIZE.with(|c| c.get()), 3, "omw_size swapped");
7573        assert_eq!(NMW_IND.with(|c| c.get()), 1, "nmw_ind reset to 1 (c:967)");
7574    }
7575
7576    /// c:1965-1971 — automargin deferred-last-character: on a hasam terminal a
7577    /// full line's final glyph is written last, repositioned via moveto, and
7578    /// emitted with its FULL cell (chr + atr). Here the last cell carries bold,
7579    /// so the emit must include the bold SGR. With winw=4, an insert of 'W'
7580    /// onto old "XYZ"/new "WXYZ" fills the line (ccs==winw), sets char_ins and
7581    /// ins_last, and leaves the bold 'Z' deferred.
7582    #[test]
7583    fn refreshline_automargin_deferred_char_keeps_attr() {
7584        use std::io::Read;
7585        use std::os::unix::io::FromRawFd;
7586        use crate::ported::init::{tclen, tcstr};
7587        use crate::ported::zsh_h::TXTBOLDFACE;
7588        let _g = crate::test_util::global_state_lock();
7589        let _g2 = zle_test_setup();
7590
7591        let ins = crate::ported::zsh_h::TCINS as usize;
7592        let mins = crate::ported::zsh_h::TCMULTINS as usize;
7593        let del = crate::ported::zsh_h::TCDEL as usize;
7594        let s_ins = (tclen.lock().unwrap()[ins], tcstr.lock().unwrap()[ins].clone());
7595        let s_mins = tclen.lock().unwrap()[mins];
7596        let s_del = tclen.lock().unwrap()[del];
7597        tclen.lock().unwrap()[ins] = 1;
7598        tcstr.lock().unwrap()[ins] = "\x1b[@".to_string();
7599        tclen.lock().unwrap()[mins] = 0;
7600        tclen.lock().unwrap()[del] = 0;
7601        // Attribute SGRs are gated off on TERM_UNKNOWN/BAD/NOUP terminals; the
7602        // test harness sets one of those, so clear TERMFLAGS to let bold emit.
7603        let save_tf = crate::ported::params::TERMFLAGS.load(Ordering::SeqCst);
7604        crate::ported::params::TERMFLAGS.store(0, Ordering::SeqCst);
7605
7606        WINW.store(4, Ordering::SeqCst);
7607        WINH.store(24, Ordering::SeqCst);
7608        crate::ported::init::hasam.store(1, Ordering::SeqCst); // automargin
7609        PUT_RPMPT.store(0, Ordering::SeqCst);
7610        OPUT_RPMPT.store(0, Ordering::SeqCst);
7611        LPROMPTW.store(0, Ordering::SeqCst);
7612        CLEAREOL.store(0, Ordering::SeqCst);
7613        let cell = |c: char, a: u64| REFRESH_ELEMENT { chr: c, atr: a };
7614        let mut nl: REFRESH_STRING =
7615            vec![cell('W', 0), cell('X', 0), cell('Y', 0), cell('Z', TXTBOLDFACE)];
7616        nl.resize(6, REFRESH_ELEMENT::default());
7617        let mut ol: REFRESH_STRING = vec![cell('X', 0), cell('Y', 0), cell('Z', 0)];
7618        ol.resize(6, REFRESH_ELEMENT::default());
7619        *NBUF.lock().unwrap() = vec![nl];
7620        *OBUF.lock().unwrap() = vec![ol];
7621        NLNCT.store(1, Ordering::SeqCst);
7622        OLNCT.store(1, Ordering::SeqCst);
7623        VCS.store(0, Ordering::SeqCst);
7624        VLN.store(0, Ordering::SeqCst);
7625
7626        let mut fds = [0i32; 2];
7627        assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0, "pipe()");
7628        let (rd, wr) = (fds[0], fds[1]);
7629        let old = crate::ported::init::SHTTY.load(Ordering::SeqCst);
7630        crate::ported::init::SHTTY.store(wr, Ordering::SeqCst);
7631        refreshline(0);
7632        crate::ported::init::SHTTY.store(old, Ordering::SeqCst);
7633        unsafe { libc::close(wr) };
7634        tclen.lock().unwrap()[ins] = s_ins.0;
7635        tcstr.lock().unwrap()[ins] = s_ins.1;
7636        tclen.lock().unwrap()[mins] = s_mins;
7637        tclen.lock().unwrap()[del] = s_del;
7638        crate::ported::init::hasam.store(0, Ordering::SeqCst);
7639        crate::ported::params::TERMFLAGS.store(save_tf, Ordering::SeqCst);
7640
7641        let mut out = Vec::new();
7642        let _ = unsafe { std::fs::File::from_raw_fd(rd) }.read_to_end(&mut out);
7643        let s = String::from_utf8_lossy(&out).into_owned();
7644        // The bold SGR (CSI 1 m) must appear, proving the deferred 'Z' carried
7645        // its attribute rather than being emitted with atr 0.
7646        assert!(
7647            s.contains("\x1b[1m") && s.contains('Z'),
7648            "deferred automargin char must keep its bold attr; got {:?}",
7649            s
7650        );
7651    }
7652
7653    /// c:2070-2076 — refreshline's insert-char optimisation. With old line
7654    /// "bc" and new line "abc", inserting one char ('a') makes nl+1 == ol, so
7655    /// tcinscost(1) < wpfxlen(ol, nl+1)=2 and the path fires: emit the TCINS
7656    /// sequence (tc_inschars) then write the new char (zwrite). The earlier
7657    /// port advanced nl but emitted neither — this pins both primitives.
7658    #[test]
7659    fn refreshline_insert_path_emits_tcins_and_new_char() {
7660        use std::io::Read;
7661        use std::os::unix::io::FromRawFd;
7662        use crate::ported::init::{tclen, tcstr};
7663        let _g = crate::test_util::global_state_lock();
7664        let _g2 = zle_test_setup();
7665
7666        let ins = crate::ported::zsh_h::TCINS as usize;
7667        let mins = crate::ported::zsh_h::TCMULTINS as usize;
7668        let del = crate::ported::zsh_h::TCDEL as usize;
7669        let save_ins_len = tclen.lock().unwrap()[ins];
7670        let save_ins_str = tcstr.lock().unwrap()[ins].clone();
7671        let save_mins_len = tclen.lock().unwrap()[mins];
7672        let save_del_len = tclen.lock().unwrap()[del];
7673
7674        // Cheap single-char insert; no multi-form (tcinscost(1)=1); no TCDEL so
7675        // the delete try is skipped and control reaches the insert block.
7676        tclen.lock().unwrap()[ins] = 1;
7677        tcstr.lock().unwrap()[ins] = "\x1b[@".to_string();
7678        tclen.lock().unwrap()[mins] = 0;
7679        tclen.lock().unwrap()[del] = 0;
7680
7681        let winw = WINW.load(Ordering::SeqCst).max(8);
7682        WINH.store(24, Ordering::SeqCst);
7683        PUT_RPMPT.store(0, Ordering::SeqCst);
7684        OPUT_RPMPT.store(0, Ordering::SeqCst);
7685        let mk = |s: &str| -> REFRESH_STRING {
7686            let mut r: REFRESH_STRING =
7687                s.chars().map(|c| REFRESH_ELEMENT { chr: c, atr: 0 }).collect();
7688            r.resize((winw + 2) as usize, REFRESH_ELEMENT::default());
7689            r
7690        };
7691        *NBUF.lock().unwrap() = vec![mk("abc")];
7692        *OBUF.lock().unwrap() = vec![mk("bc")];
7693        NLNCT.store(1, Ordering::SeqCst);
7694        OLNCT.store(1, Ordering::SeqCst);
7695        VCS.store(0, Ordering::SeqCst);
7696        VLN.store(0, Ordering::SeqCst);
7697        CLEAREOL.store(0, Ordering::SeqCst);
7698        LPROMPTW.store(0, Ordering::SeqCst);
7699        crate::ported::init::hasam.store(0, Ordering::SeqCst);
7700
7701        let mut fds = [0i32; 2];
7702        assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0, "pipe()");
7703        let (rd, wr) = (fds[0], fds[1]);
7704        let old = crate::ported::init::SHTTY.load(Ordering::SeqCst);
7705        crate::ported::init::SHTTY.store(wr, Ordering::SeqCst);
7706        refreshline(0);
7707        crate::ported::init::SHTTY.store(old, Ordering::SeqCst);
7708        unsafe { libc::close(wr) };
7709        let mut out = Vec::new();
7710        let _ = unsafe { std::fs::File::from_raw_fd(rd) }.read_to_end(&mut out);
7711        let s = String::from_utf8_lossy(&out).into_owned();
7712
7713        tclen.lock().unwrap()[ins] = save_ins_len;
7714        tcstr.lock().unwrap()[ins] = save_ins_str;
7715        tclen.lock().unwrap()[mins] = save_mins_len;
7716        tclen.lock().unwrap()[del] = save_del_len;
7717
7718        assert!(
7719            s.contains("\x1b[@") && s.contains('a'),
7720            "insert path must emit the TCINS sequence and the new char 'a'; got {:?}",
7721            s
7722        );
7723    }
7724
7725    /// c:1934 — the rubbish-cleanup cost gate `tcdelcost(i) <= i + 1`. After a
7726    /// cheap insert leaves char_ins>0 and nl exhausts, the leftover columns are
7727    /// cleaned by tc_delchars only when deleting is no costlier than padding;
7728    /// otherwise spaces are written. With TCDEL present but EXPENSIVE
7729    /// (tcdelcost(1)=5 > 2), the fixed gate must pad — NOT emit the delete
7730    /// sequence. The earlier `i_pad <= i_pad + 1` tautology always deleted.
7731    #[test]
7732    fn refreshline_cleanup_pads_when_delete_costlier() {
7733        use std::io::Read;
7734        use std::os::unix::io::FromRawFd;
7735        use crate::ported::init::{tclen, tcstr};
7736        let _g = crate::test_util::global_state_lock();
7737        let _g2 = zle_test_setup();
7738
7739        let ins = crate::ported::zsh_h::TCINS as usize;
7740        let mins = crate::ported::zsh_h::TCMULTINS as usize;
7741        let del = crate::ported::zsh_h::TCDEL as usize;
7742        let mdel = crate::ported::zsh_h::TCMULTDEL as usize;
7743        let save = |i: usize| (tclen.lock().unwrap()[i], tcstr.lock().unwrap()[i].clone());
7744        let (s_ins_l, s_ins_s) = save(ins);
7745        let (s_mins_l, _) = save(mins);
7746        let (s_del_l, s_del_s) = save(del);
7747        let (s_mdel_l, _) = save(mdel);
7748
7749        // Cheap insert (fires → char_ins=1), but DELETE is expensive: a single
7750        // delete costs 5 > i_pad+1 = 2, so the cleanup must pad with a space.
7751        tclen.lock().unwrap()[ins] = 1;
7752        tcstr.lock().unwrap()[ins] = "\x1b[@".to_string();
7753        tclen.lock().unwrap()[mins] = 0;
7754        tclen.lock().unwrap()[del] = 5;
7755        tcstr.lock().unwrap()[del] = "\x1bDEL".to_string();
7756        tclen.lock().unwrap()[mdel] = 0;
7757
7758        let winw = WINW.load(Ordering::SeqCst).max(8);
7759        WINH.store(24, Ordering::SeqCst);
7760        PUT_RPMPT.store(0, Ordering::SeqCst);
7761        OPUT_RPMPT.store(0, Ordering::SeqCst);
7762        let mk = |s: &str| -> REFRESH_STRING {
7763            let mut r: REFRESH_STRING =
7764                s.chars().map(|c| REFRESH_ELEMENT { chr: c, atr: 0 }).collect();
7765            r.resize((winw + 2) as usize, REFRESH_ELEMENT::default());
7766            r
7767        };
7768        *NBUF.lock().unwrap() = vec![mk("abc")];
7769        *OBUF.lock().unwrap() = vec![mk("bc")];
7770        NLNCT.store(1, Ordering::SeqCst);
7771        OLNCT.store(1, Ordering::SeqCst);
7772        VCS.store(0, Ordering::SeqCst);
7773        VLN.store(0, Ordering::SeqCst);
7774        CLEAREOL.store(0, Ordering::SeqCst);
7775        LPROMPTW.store(0, Ordering::SeqCst);
7776        crate::ported::init::hasam.store(0, Ordering::SeqCst);
7777
7778        let mut fds = [0i32; 2];
7779        assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0, "pipe()");
7780        let (rd, wr) = (fds[0], fds[1]);
7781        let old = crate::ported::init::SHTTY.load(Ordering::SeqCst);
7782        crate::ported::init::SHTTY.store(wr, Ordering::SeqCst);
7783        refreshline(0);
7784        crate::ported::init::SHTTY.store(old, Ordering::SeqCst);
7785        unsafe { libc::close(wr) };
7786        let mut out = Vec::new();
7787        let _ = unsafe { std::fs::File::from_raw_fd(rd) }.read_to_end(&mut out);
7788        let s = String::from_utf8_lossy(&out).into_owned();
7789
7790        tclen.lock().unwrap()[ins] = s_ins_l;
7791        tcstr.lock().unwrap()[ins] = s_ins_s;
7792        tclen.lock().unwrap()[mins] = s_mins_l;
7793        tclen.lock().unwrap()[del] = s_del_l;
7794        tcstr.lock().unwrap()[del] = s_del_s;
7795        tclen.lock().unwrap()[mdel] = s_mdel_l;
7796
7797        // Insert fired (we reached the cleanup), and the gate chose pad: the
7798        // expensive TCDEL sequence must be absent.
7799        assert!(
7800            s.contains("\x1b[@"),
7801            "insert must fire so char_ins>0 reaches the cleanup; got {:?}",
7802            s
7803        );
7804        assert!(
7805            !s.contains("\x1bDEL"),
7806            "delete costlier than padding → must NOT emit TCDEL; got {:?}",
7807            s
7808        );
7809    }
7810
7811    /// c:1817/1997 — zr_pad carries prompt_attr, so cells the diff clears/pads
7812    /// keep the prompt's colour. Same cleanup scenario as above but with
7813    /// PROMPT_ATTR = bold: the pad space emitted via zputc(&zr_pad) must carry
7814    /// the bold SGR (the earlier port hardcoded atr 0, dropping it).
7815    #[test]
7816    fn refreshline_cleanup_pad_carries_prompt_attr() {
7817        use std::io::Read;
7818        use std::os::unix::io::FromRawFd;
7819        use crate::ported::init::{tclen, tcstr};
7820        use crate::ported::zsh_h::TXTBOLDFACE;
7821        let _g = crate::test_util::global_state_lock();
7822        let _g2 = zle_test_setup();
7823
7824        let ins = crate::ported::zsh_h::TCINS as usize;
7825        let mins = crate::ported::zsh_h::TCMULTINS as usize;
7826        let del = crate::ported::zsh_h::TCDEL as usize;
7827        let s_ins = (tclen.lock().unwrap()[ins], tcstr.lock().unwrap()[ins].clone());
7828        let s_mins = tclen.lock().unwrap()[mins];
7829        let s_del = tclen.lock().unwrap()[del];
7830        tclen.lock().unwrap()[ins] = 1;
7831        tcstr.lock().unwrap()[ins] = "\x1b[@".to_string();
7832        tclen.lock().unwrap()[mins] = 0;
7833        tclen.lock().unwrap()[del] = 5; // expensive → cleanup pads (c:1997)
7834
7835        let save_tf = crate::ported::params::TERMFLAGS.load(Ordering::SeqCst);
7836        crate::ported::params::TERMFLAGS.store(0, Ordering::SeqCst); // let bold emit
7837        let save_pa = PROMPT_ATTR.load(Ordering::SeqCst);
7838        PROMPT_ATTR.store(TXTBOLDFACE, Ordering::SeqCst); // zr_pad picks this up
7839
7840        let winw = WINW.load(Ordering::SeqCst).max(8);
7841        WINH.store(24, Ordering::SeqCst);
7842        PUT_RPMPT.store(0, Ordering::SeqCst);
7843        OPUT_RPMPT.store(0, Ordering::SeqCst);
7844        let mk = |s: &str| -> REFRESH_STRING {
7845            let mut r: REFRESH_STRING =
7846                s.chars().map(|c| REFRESH_ELEMENT { chr: c, atr: 0 }).collect();
7847            r.resize((winw + 2) as usize, REFRESH_ELEMENT::default());
7848            r
7849        };
7850        *NBUF.lock().unwrap() = vec![mk("abc")];
7851        *OBUF.lock().unwrap() = vec![mk("bc")];
7852        NLNCT.store(1, Ordering::SeqCst);
7853        OLNCT.store(1, Ordering::SeqCst);
7854        VCS.store(0, Ordering::SeqCst);
7855        VLN.store(0, Ordering::SeqCst);
7856        CLEAREOL.store(0, Ordering::SeqCst);
7857        LPROMPTW.store(0, Ordering::SeqCst);
7858        crate::ported::init::hasam.store(0, Ordering::SeqCst);
7859
7860        let mut fds = [0i32; 2];
7861        assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0, "pipe()");
7862        let (rd, wr) = (fds[0], fds[1]);
7863        let old = crate::ported::init::SHTTY.load(Ordering::SeqCst);
7864        crate::ported::init::SHTTY.store(wr, Ordering::SeqCst);
7865        refreshline(0);
7866        crate::ported::init::SHTTY.store(old, Ordering::SeqCst);
7867        unsafe { libc::close(wr) };
7868        tclen.lock().unwrap()[ins] = s_ins.0;
7869        tcstr.lock().unwrap()[ins] = s_ins.1;
7870        tclen.lock().unwrap()[mins] = s_mins;
7871        tclen.lock().unwrap()[del] = s_del;
7872        PROMPT_ATTR.store(save_pa, Ordering::SeqCst);
7873        crate::ported::params::TERMFLAGS.store(save_tf, Ordering::SeqCst);
7874
7875        let mut out = Vec::new();
7876        let _ = unsafe { std::fs::File::from_raw_fd(rd) }.read_to_end(&mut out);
7877        let s = String::from_utf8_lossy(&out).into_owned();
7878        assert!(
7879            s.contains("\x1b[1m"),
7880            "the cleanup pad (zr_pad) must carry prompt_attr (bold SGR); got {:?}",
7881            s
7882        );
7883    }
7884
7885    /// c:2247-2250 — tc_rightcurs prefers the real loaded TCMULTRIGHT
7886    /// capability (with the move count substituted) over a hardcoded CSI C;
7887    /// with no termcap entry it emits the ANSI default. Pins the capability
7888    /// preference against the old unconditional CSI C.
7889    #[test]
7890    fn tc_rightcurs_prefers_loaded_capability() {
7891        use std::io::Read;
7892        use std::os::unix::io::FromRawFd;
7893        use crate::ported::init::{tclen, tcstr};
7894        let _g = crate::test_util::global_state_lock();
7895        let _g2 = zle_test_setup();
7896
7897        let mr = crate::ported::zsh_h::TCMULTRIGHT as usize;
7898        let hp = crate::ported::zsh_h::TCHORIZPOS as usize;
7899        let save_mr_len = tclen.lock().unwrap()[mr];
7900        let save_mr_str = tcstr.lock().unwrap()[mr].clone();
7901        let save_hp_len = tclen.lock().unwrap()[hp];
7902
7903        let capture = |f: &dyn Fn()| -> String {
7904            let mut fds = [0i32; 2];
7905            assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0, "pipe()");
7906            let (rd, wr) = (fds[0], fds[1]);
7907            let old = crate::ported::init::SHTTY.load(Ordering::SeqCst);
7908            crate::ported::init::SHTTY.store(wr, Ordering::SeqCst);
7909            f();
7910            crate::ported::init::SHTTY.store(old, Ordering::SeqCst);
7911            unsafe { libc::close(wr) };
7912            let mut out = Vec::new();
7913            let _ = unsafe { std::fs::File::from_raw_fd(rd) }.read_to_end(&mut out);
7914            String::from_utf8_lossy(&out).into_owned()
7915        };
7916
7917        let nt = crate::ported::zsh_h::TCNEXTTAB as usize;
7918        let save_nt_len = tclen.lock().unwrap()[nt];
7919
7920        VCS.store(0, Ordering::SeqCst);
7921        VLN.store(0, Ordering::SeqCst);
7922        // Loaded TCMULTRIGHT capability is used with the count substituted.
7923        tclen.lock().unwrap()[mr] = 3;
7924        tcstr.lock().unwrap()[mr] = "\x1bX%dY".to_string();
7925        tclen.lock().unwrap()[hp] = 0;
7926        let with_cap = capture(&|| tc_rightcurs(5));
7927        assert_eq!(with_cap, "\x1bX5Y", "should use loaded TCMULTRIGHT with count");
7928
7929        // No usable capability and nothing to re-output (no tab cap, not in the
7930        // prompt, empty video buffer): faithful C falls to the last resort and
7931        // pads with spaces — "not my fault your terminal can't go right"
7932        // (zle_refresh.c:2314-2315). The earlier port invented a CSI-C here.
7933        tclen.lock().unwrap()[mr] = 0;
7934        tclen.lock().unwrap()[nt] = 0;
7935        LPROMPTW.store(0, Ordering::SeqCst);
7936        NBUF.lock().unwrap().clear();
7937        let headless = capture(&|| tc_rightcurs(5));
7938        assert_eq!(headless, "     ", "no cap → space pad (c:2314-2315)");
7939
7940        tclen.lock().unwrap()[nt] = save_nt_len;
7941        tclen.lock().unwrap()[mr] = save_mr_len;
7942        tcstr.lock().unwrap()[mr] = save_mr_str;
7943        tclen.lock().unwrap()[hp] = save_hp_len;
7944    }
7945
7946    /// c:2195-2212 — moving the cursor down past the drawn region
7947    /// (vmaxln-1) must emit real newlines, which scroll/create lines. The
7948    /// old CSI-H port jumped without creating lines. From (0,0) with
7949    /// vmaxln=1, moveto(3,0) emits CR + 3 newlines and lands VLN at 3.
7950    #[test]
7951    fn moveto_down_past_vmaxln_emits_newlines() {
7952        use std::io::Read;
7953        use std::os::unix::io::FromRawFd;
7954        use crate::ported::init::tclen;
7955        let _g = crate::test_util::global_state_lock();
7956        let _g2 = zle_test_setup();
7957
7958        // No TCDOWN capability so the newline fallback is taken.
7959        let save_d = tclen.lock().unwrap()[crate::ported::zsh_h::TCDOWN as usize];
7960        let save_md = tclen.lock().unwrap()[crate::ported::zsh_h::TCMULTDOWN as usize];
7961        tclen.lock().unwrap()[crate::ported::zsh_h::TCDOWN as usize] = 0;
7962        tclen.lock().unwrap()[crate::ported::zsh_h::TCMULTDOWN as usize] = 0;
7963
7964        VCS.store(0, Ordering::SeqCst);
7965        VLN.store(0, Ordering::SeqCst);
7966        VMAXLN.store(1, Ordering::SeqCst); // nothing drawn below row 0
7967
7968        let mut fds = [0i32; 2];
7969        assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0, "pipe()");
7970        let (rd, wr) = (fds[0], fds[1]);
7971        let old = crate::ported::init::SHTTY.load(Ordering::SeqCst);
7972        crate::ported::init::SHTTY.store(wr, Ordering::SeqCst);
7973
7974        moveto(3, 0);
7975
7976        crate::ported::init::SHTTY.store(old, Ordering::SeqCst);
7977        unsafe { libc::close(wr) };
7978        tclen.lock().unwrap()[crate::ported::zsh_h::TCDOWN as usize] = save_d;
7979        tclen.lock().unwrap()[crate::ported::zsh_h::TCMULTDOWN as usize] = save_md;
7980        let mut out = Vec::new();
7981        let _ = unsafe { std::fs::File::from_raw_fd(rd) }.read_to_end(&mut out);
7982        let s = String::from_utf8_lossy(&out);
7983        assert_eq!(VLN.load(Ordering::SeqCst), 3, "moveto must land VLN at 3");
7984        assert!(
7985            s.contains("\n\n\n"),
7986            "down-past-vmaxln must emit newlines to create lines; got {:?}",
7987            s
7988        );
7989    }
7990
7991    /// c:2745-2764 — singmoveto positions the cursor on the current line
7992    /// using the GLOBAL vcs (the VCS atomic), as C does. With no multi-left
7993    /// capability and a target in the left half, it homes via CR (vcs=0)
7994    /// then moves right, landing VCS at the target. The old port threaded a
7995    /// throwaway RefreshState (vcs always 0); this pins the global tracking.
7996    #[test]
7997    fn singmoveto_tracks_global_vcs() {
7998        use std::io::Read;
7999        use std::os::unix::io::FromRawFd;
8000        use crate::ported::init::tclen;
8001        let _g = crate::test_util::global_state_lock();
8002        let _g2 = zle_test_setup();
8003
8004        let li = crate::ported::zsh_h::TCMULTLEFT as usize;
8005        let save_li = tclen.lock().unwrap()[li];
8006        tclen.lock().unwrap()[li] = 0; // no multi-left → CR-home path
8007
8008        let mut fds = [0i32; 2];
8009        assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0, "pipe()");
8010        let (rd, wr) = (fds[0], fds[1]);
8011        let old = crate::ported::init::SHTTY.load(Ordering::SeqCst);
8012        crate::ported::init::SHTTY.store(wr, Ordering::SeqCst);
8013
8014        // From column 10, target 2 (<= vcs/2): CR home then move right.
8015        VCS.store(10, Ordering::SeqCst);
8016        singmoveto(2);
8017        let vcs_after = VCS.load(Ordering::SeqCst);
8018        // Already at target: early return, no further output.
8019        singmoveto(2);
8020
8021        crate::ported::init::SHTTY.store(old, Ordering::SeqCst);
8022        unsafe { libc::close(wr) };
8023        tclen.lock().unwrap()[li] = save_li;
8024        let mut out = Vec::new();
8025        let _ = unsafe { std::fs::File::from_raw_fd(rd) }.read_to_end(&mut out);
8026        let s = String::from_utf8_lossy(&out);
8027        assert_eq!(vcs_after, 2, "singmoveto must land global VCS at the target");
8028        assert!(s.contains('\r'), "CR-home optimisation should emit \\r; got {:?}", s);
8029    }
8030
8031    /// c:2700-2740 — singlerefresh's line-render loop emits the visible line
8032    /// by diffing nbuf[0] against obuf[0]. With a blank old line, the whole new
8033    /// line "abc" goes out through the "old ended" zwrite branch (c:2718-2722).
8034    /// The earlier port omitted this loop entirely (it jumped to singmoveto).
8035    #[test]
8036    fn singlerefresh_renders_line_against_blank_old() {
8037        use std::io::Read;
8038        use std::os::unix::io::FromRawFd;
8039        let _g = crate::test_util::global_state_lock();
8040        let _g2 = zle_test_setup();
8041        WINW.store(80, Ordering::SeqCst);
8042        LPROMPTW.store(0, Ordering::SeqCst);
8043        crate::ported::init::hasam.store(0, Ordering::SeqCst);
8044        WINPOS.store(-1, Ordering::SeqCst);
8045        WINPROMPT.store(0, Ordering::SeqCst);
8046        // nbuf[0] is overwritten by singlerefresh; obuf[0] is the blank old line.
8047        *NBUF.lock().unwrap() = vec![vec![REFRESH_ELEMENT::default(); 82]];
8048        *OBUF.lock().unwrap() = vec![vec![REFRESH_ELEMENT::default(); 82]];
8049        VCS.store(0, Ordering::SeqCst);
8050        VLN.store(0, Ordering::SeqCst);
8051
8052        let line: Vec<char> = "abc".chars().collect();
8053        let mut fds = [0i32; 2];
8054        assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0, "pipe()");
8055        let (rd, wr) = (fds[0], fds[1]);
8056        let old = crate::ported::init::SHTTY.load(Ordering::SeqCst);
8057        crate::ported::init::SHTTY.store(wr, Ordering::SeqCst);
8058        singlerefresh(&line, line.len() as i32, line.len() as i32);
8059        crate::ported::init::SHTTY.store(old, Ordering::SeqCst);
8060        unsafe { libc::close(wr) };
8061        let mut out = Vec::new();
8062        let _ = unsafe { std::fs::File::from_raw_fd(rd) }.read_to_end(&mut out);
8063        let s = String::from_utf8_lossy(&out);
8064        assert!(
8065            s.contains("abc"),
8066            "singlerefresh must emit the visible line 'abc'; got {:?}",
8067            s
8068        );
8069    }
8070
8071    /// c:2497-2519 — with COMBININGCHARS set, singlerefresh's build clusters a
8072    /// base char + its combining marks into ONE cell via addmultiword (the
8073    /// producer the multiword zwcputc reader consumes), so the glyph emits as
8074    /// "e"+U+0301. The earlier stub (ichars=1) let the combining mark fall to
8075    /// the non-printable branch and render as a "<0301>" hex escape.
8076    #[test]
8077    fn singlerefresh_clusters_combining_chars() {
8078        use std::io::Read;
8079        use std::os::unix::io::FromRawFd;
8080        let _g = crate::test_util::global_state_lock();
8081        let _g2 = zle_test_setup();
8082        let cc = crate::ported::zsh_h::opt_name(crate::ported::zsh_h::COMBININGCHARS);
8083        crate::ported::options::opt_state_set(cc, true);
8084
8085        WINW.store(80, Ordering::SeqCst);
8086        LPROMPTW.store(0, Ordering::SeqCst);
8087        crate::ported::init::hasam.store(0, Ordering::SeqCst);
8088        WINPOS.store(-1, Ordering::SeqCst);
8089        WINPROMPT.store(0, Ordering::SeqCst);
8090        *NBUF.lock().unwrap() = vec![vec![REFRESH_ELEMENT::default(); 82]];
8091        *OBUF.lock().unwrap() = vec![vec![REFRESH_ELEMENT::default(); 82]];
8092        VCS.store(0, Ordering::SeqCst);
8093        VLN.store(0, Ordering::SeqCst);
8094
8095        let line: Vec<char> = "e\u{0301}".chars().collect(); // e + combining acute
8096        let mut fds = [0i32; 2];
8097        assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0, "pipe()");
8098        let (rd, wr) = (fds[0], fds[1]);
8099        let old = crate::ported::init::SHTTY.load(Ordering::SeqCst);
8100        crate::ported::init::SHTTY.store(wr, Ordering::SeqCst);
8101        singlerefresh(&line, line.len() as i32, line.len() as i32);
8102        crate::ported::init::SHTTY.store(old, Ordering::SeqCst);
8103        unsafe { libc::close(wr) };
8104        crate::ported::options::opt_state_set(cc, false); // restore default-off
8105
8106        let mut out = Vec::new();
8107        let _ = unsafe { std::fs::File::from_raw_fd(rd) }.read_to_end(&mut out);
8108        let s = String::from_utf8_lossy(&out);
8109        assert!(
8110            s.contains('e') && s.contains('\u{0301}') && !s.contains("0301>"),
8111            "combining mark must cluster onto 'e' (emit e+U+0301), not a <hex> \
8112             escape; got {:?}",
8113            s
8114        );
8115    }
8116
8117    /// c:2514-2517 — a wide (CJK) glyph occupies two columns: the leading cell
8118    /// holds the char and a trailing WEOF placeholder fills the second column.
8119    /// The WEOF must be ZWC_WEOF, not '\0' — otherwise ZR_strcpy/ZR_strlen
8120    /// treat it as the line terminator and everything after the wide char is
8121    /// dropped. "日a" must render BOTH the ideograph and the 'a'.
8122    #[test]
8123    fn singlerefresh_wide_char_does_not_truncate_line() {
8124        use std::io::Read;
8125        use std::os::unix::io::FromRawFd;
8126        let _g = crate::test_util::global_state_lock();
8127        let _g2 = zle_test_setup();
8128        WINW.store(80, Ordering::SeqCst);
8129        LPROMPTW.store(0, Ordering::SeqCst);
8130        crate::ported::init::hasam.store(0, Ordering::SeqCst);
8131        WINPOS.store(-1, Ordering::SeqCst);
8132        WINPROMPT.store(0, Ordering::SeqCst);
8133        *NBUF.lock().unwrap() = vec![vec![REFRESH_ELEMENT::default(); 82]];
8134        *OBUF.lock().unwrap() = vec![vec![REFRESH_ELEMENT::default(); 82]];
8135        VCS.store(0, Ordering::SeqCst);
8136        VLN.store(0, Ordering::SeqCst);
8137
8138        let line: Vec<char> = "日a".chars().collect(); // width-2 ideograph + 'a'
8139        let mut fds = [0i32; 2];
8140        assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0, "pipe()");
8141        let (rd, wr) = (fds[0], fds[1]);
8142        let old = crate::ported::init::SHTTY.load(Ordering::SeqCst);
8143        crate::ported::init::SHTTY.store(wr, Ordering::SeqCst);
8144        singlerefresh(&line, line.len() as i32, line.len() as i32);
8145        crate::ported::init::SHTTY.store(old, Ordering::SeqCst);
8146        unsafe { libc::close(wr) };
8147        let mut out = Vec::new();
8148        let _ = unsafe { std::fs::File::from_raw_fd(rd) }.read_to_end(&mut out);
8149        let s = String::from_utf8_lossy(&out);
8150        assert!(
8151            s.contains('日') && s.contains('a') && !s.contains('\u{FFFF}'),
8152            "wide char must not truncate the line: both '日' and 'a' render, \
8153             WEOF placeholder not emitted; got {:?}",
8154            s
8155        );
8156    }
8157
8158    /// c:2462-2588 — singlerefresh persists the SINGLELINEZLE horizontal scroll
8159    /// position via the WINPOS/WINPROMPT statics (it was stubbed to a local
8160    /// `winpos = -1` that never wrote back). Seed a bogus stale WINPOS=99 with
8161    /// an 8-cell line whose cursor (nvcs=8) is outside that window: the load
8162    /// triggers a recompute to the centred winpos = nvcs - winw/2 = 3, which is
8163    /// stored back. The old local-only stub never wrote WINPOS, so it would
8164    /// stay 99.
8165    #[test]
8166    fn singlerefresh_persists_winpos_to_static() {
8167        let _g = crate::test_util::global_state_lock();
8168        let _g2 = zle_test_setup();
8169        WINW.store(10, Ordering::SeqCst);
8170        LPROMPTW.store(0, Ordering::SeqCst);
8171        crate::ported::init::hasam.store(0, Ordering::SeqCst);
8172        *NBUF.lock().unwrap() = vec![vec![REFRESH_ELEMENT::default(); 12]];
8173        *OBUF.lock().unwrap() = vec![vec![REFRESH_ELEMENT::default(); 12]];
8174        VCS.store(0, Ordering::SeqCst);
8175        VLN.store(0, Ordering::SeqCst);
8176        // Bogus persisted values from a "prior keystroke".
8177        WINPOS.store(99, Ordering::SeqCst);
8178        WINPROMPT.store(77, Ordering::SeqCst);
8179
8180        let line: Vec<char> = "01234567".chars().collect();
8181        let devnull =
8182            unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_WRONLY) };
8183        let old = crate::ported::init::SHTTY.load(Ordering::SeqCst);
8184        crate::ported::init::SHTTY.store(devnull, Ordering::SeqCst);
8185        singlerefresh(&line, line.len() as i32, line.len() as i32);
8186        crate::ported::init::SHTTY.store(old, Ordering::SeqCst);
8187        unsafe { libc::close(devnull) };
8188
8189        assert_eq!(
8190            WINPOS.load(Ordering::SeqCst),
8191            3,
8192            "winpos must load the persisted static, recompute (nvcs-winw/2=3), \
8193             and store back — not stay at the bogus 99 (old local-only stub)"
8194        );
8195        assert_eq!(
8196            WINPROMPT.load(Ordering::SeqCst),
8197            0,
8198            "winprompt must be recomputed+stored (winpos>=lpromptw → 0), not 77"
8199        );
8200    }
8201
8202    /// c:2320-2331 — tc_downcurs prefers the terminal down capability, but
8203    /// with none available it must emit real newlines + CR (which scroll/
8204    /// create lines a plain CSI B cannot) and return -1. The old port faked
8205    /// it as an unconditional CSI B with no return.
8206    #[test]
8207    fn tc_downcurs_newline_fallback_and_capability() {
8208        use std::io::Read;
8209        use std::os::unix::io::FromRawFd;
8210        use crate::ported::init::{tclen, tcstr};
8211        let _g = crate::test_util::global_state_lock();
8212        let _g2 = zle_test_setup();
8213
8214        let di = crate::ported::zsh_h::TCDOWN as usize;
8215        let mi = crate::ported::zsh_h::TCMULTDOWN as usize;
8216        // Snapshot in separate statements: locking `tclen` twice inside one
8217        // tuple expression keeps both guards alive simultaneously and
8218        // self-deadlocks the non-reentrant Mutex.
8219        let save_di_len = tclen.lock().unwrap()[di];
8220        let save_di_str = tcstr.lock().unwrap()[di].clone();
8221        let save_mi_len = tclen.lock().unwrap()[mi];
8222
8223        // No down capability → newline fallback, returns -1.
8224        {
8225            let mut t = tclen.lock().unwrap();
8226            t[di] = 0;
8227            t[mi] = 0;
8228        }
8229        let mut fds = [0i32; 2];
8230        assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0, "pipe()");
8231        let (rd, wr) = (fds[0], fds[1]);
8232        let old = crate::ported::init::SHTTY.load(Ordering::SeqCst);
8233        crate::ported::init::SHTTY.store(wr, Ordering::SeqCst);
8234        let ret = tc_downcurs(3);
8235        crate::ported::init::SHTTY.store(old, Ordering::SeqCst);
8236        unsafe { libc::close(wr) };
8237        let mut out = Vec::new();
8238        let _ = unsafe { std::fs::File::from_raw_fd(rd) }.read_to_end(&mut out);
8239        assert_eq!(ret, -1, "newline fallback returns -1 (column reset)");
8240        assert_eq!(
8241            String::from_utf8_lossy(&out),
8242            "\n\n\n\r",
8243            "no down-cap → 3 newlines + CR"
8244        );
8245
8246        // Single-shot down capability available → uses it, returns 0.
8247        {
8248            let mut t = tclen.lock().unwrap();
8249            t[di] = 4;
8250        }
8251        tcstr.lock().unwrap()[di] = "\x1b[B".to_string();
8252        let mut fds2 = [0i32; 2];
8253        assert_eq!(unsafe { libc::pipe(fds2.as_mut_ptr()) }, 0, "pipe()");
8254        let (rd2, wr2) = (fds2[0], fds2[1]);
8255        crate::ported::init::SHTTY.store(wr2, Ordering::SeqCst);
8256        let ret2 = tc_downcurs(2);
8257        crate::ported::init::SHTTY.store(old, Ordering::SeqCst);
8258        unsafe { libc::close(wr2) };
8259        let mut out2 = Vec::new();
8260        let _ = unsafe { std::fs::File::from_raw_fd(rd2) }.read_to_end(&mut out2);
8261        assert_eq!(ret2, 0, "capability path returns 0 (column preserved)");
8262        assert_eq!(
8263            String::from_utf8_lossy(&out2),
8264            "\x1b[B\x1b[B",
8265            "down-cap looped ct times, no newlines"
8266        );
8267
8268        tclen.lock().unwrap()[di] = save_di_len;
8269        tcstr.lock().unwrap()[di] = save_di_str;
8270        tclen.lock().unwrap()[mi] = save_mi_len;
8271    }
8272
8273    /// c:1782-1783 — tcinscost/tcdelcost read the real termcap costs from
8274    /// tclen: a parametrised multi-cap costs one capability, otherwise it
8275    /// is `x` single-char ops. Pins the formula against the old `x.max(0)`
8276    /// fake that ignored tclen entirely.
8277    #[test]
8278    fn tc_ins_del_cost_use_real_tclen() {
8279        use crate::ported::init::tclen;
8280        use crate::ported::zsh_h::{TCDEL, TCINS, TCMULTDEL, TCMULTINS};
8281        let _g = crate::test_util::global_state_lock();
8282        let _g2 = zle_test_setup();
8283
8284        // Snapshot + restore the four tclen slots we mutate.
8285        let save = {
8286            let t = tclen.lock().unwrap();
8287            (
8288                t[TCMULTINS as usize],
8289                t[TCINS as usize],
8290                t[TCMULTDEL as usize],
8291                t[TCDEL as usize],
8292            )
8293        };
8294
8295        // Per-char fallback: no multi-cap → x * single-cap cost.
8296        {
8297            let mut t = tclen.lock().unwrap();
8298            t[TCMULTINS as usize] = 0;
8299            t[TCINS as usize] = 2;
8300            t[TCMULTDEL as usize] = 0;
8301            t[TCDEL as usize] = 3;
8302        }
8303        assert_eq!(tcinscost(4), 8, "insert: 4 chars * tclen[TCINS]=2");
8304        assert_eq!(tcdelcost(4), 12, "delete: 4 chars * tclen[TCDEL]=3");
8305
8306        // Multi-cap available: flat one-capability cost regardless of x.
8307        {
8308            let mut t = tclen.lock().unwrap();
8309            t[TCMULTINS as usize] = 5;
8310            t[TCMULTDEL as usize] = 7;
8311        }
8312        assert_eq!(tcinscost(4), 5, "insert: parametrised tclen[TCMULTINS]=5");
8313        assert_eq!(tcdelcost(4), 7, "delete: parametrised tclen[TCMULTDEL]=7");
8314
8315        let mut t = tclen.lock().unwrap();
8316        t[TCMULTINS as usize] = save.0;
8317        t[TCINS as usize] = save.1;
8318        t[TCMULTDEL as usize] = save.2;
8319        t[TCDEL as usize] = save.3;
8320    }
8321
8322    /// c:143 — `ZR_strlen` returns usize (compile-time type pin).
8323    #[test]
8324    fn zr_strlen_returns_usize_type_pin2() {
8325        let _: usize = ZR_strlen(&[]);
8326    }
8327
8328    /// c:143 — `ZR_strlen` is pure across 4 inputs.
8329    #[test]
8330    fn zr_strlen_pure_full_sweep() {
8331        let cases: Vec<Vec<REFRESH_ELEMENT>> = vec![
8332            vec![],
8333            vec![REFRESH_ELEMENT { chr: 'a', atr: 0 }],
8334            vec![REFRESH_ELEMENT { chr: '\0', atr: 0 }],
8335            vec![
8336                REFRESH_ELEMENT { chr: 'x', atr: 0 },
8337                REFRESH_ELEMENT { chr: 'y', atr: 0 },
8338            ],
8339        ];
8340        for c in &cases {
8341            let first = ZR_strlen(c);
8342            for _ in 0..3 {
8343                assert_eq!(ZR_strlen(c), first, "ZR_strlen must be pure");
8344            }
8345        }
8346    }
8347
8348    // ═══════════════════════════════════════════════════════════════════
8349    // Additional C-parity tests for Src/Zle/zle_refresh.c
8350    // c:143 ZR_strlen / c:230 ZR_strncmp / c:476 zwcputc / c:496 zwcwrite /
8351    // c:448 zle_free_highlight / c:465 tcoutclear / c:1097 wpfxlen / c:33 ZR_memset
8352    // ═══════════════════════════════════════════════════════════════════
8353
8354    /// c:143 — `ZR_strlen(empty)` returns 0.
8355    #[test]
8356    fn zr_strlen_empty_returns_zero() {
8357        assert_eq!(ZR_strlen(&[]), 0, "empty → 0 length");
8358    }
8359
8360    /// c:230 — `ZR_strncmp` is BOOLEAN (0=equal, 1=different), NOT
8361    /// a signed ordinal like C `strncmp`. Pin the symmetric-boolean
8362    /// contract per Src/Zle/zle_refresh.c:127 `return 1` arm.
8363    /// Distinct inputs must produce 1 in BOTH directions.
8364    #[test]
8365    fn zr_strncmp_symmetric_boolean_for_distinct() {
8366        let a = [REFRESH_ELEMENT { chr: 'a', atr: 0 }];
8367        let b = [REFRESH_ELEMENT { chr: 'b', atr: 0 }];
8368        let ab = ZR_strncmp(&a, &b, 1);
8369        let ba = ZR_strncmp(&b, &a, 1);
8370        assert_eq!(ab, 1, "ZR_strncmp(a, b, 1) = 1 (different)");
8371        assert_eq!(ba, 1, "ZR_strncmp(b, a, 1) = 1 (different, symmetric)");
8372    }
8373
8374    /// c:230 — `ZR_strncmp(x, x, n)` reflexive: same input → 0 (alt).
8375    #[test]
8376    fn zr_strncmp_reflexive_returns_zero_alt() {
8377        let buf = [
8378            REFRESH_ELEMENT { chr: 'x', atr: 0 },
8379            REFRESH_ELEMENT { chr: 'y', atr: 0 },
8380        ];
8381        assert_eq!(
8382            ZR_strncmp(&buf, &buf, 2),
8383            0,
8384            "ZR_strncmp(x, x, n) must be 0"
8385        );
8386    }
8387
8388    /// c:496 — `zwcwrite(&[], 0)` is idempotent across many calls.
8389    #[test]
8390    fn zwcwrite_empty_idempotent() {
8391        let _g = crate::test_util::global_state_lock();
8392        let _g2 = zle_test_setup();
8393        for _ in 0..10 {
8394            zwcwrite(&[], 0);
8395        }
8396    }
8397
8398    /// c:476 — `zwcputc` returns void; safe for various chars.
8399    #[test]
8400    fn zwcputc_various_chars_safe() {
8401        let _g = crate::test_util::global_state_lock();
8402        let _g2 = zle_test_setup();
8403        for c in ['a', '\n', '\t', '\0', '日'] {
8404            zwcputc(&REFRESH_ELEMENT { chr: c, atr: 0 });
8405        }
8406    }
8407
8408    /// c:448 — `zle_free_highlight` is idempotent (alt 5-call).
8409    #[test]
8410    fn zle_free_highlight_idempotent_alt() {
8411        let _g = crate::test_util::global_state_lock();
8412        let _g2 = zle_test_setup();
8413        for _ in 0..5 {
8414            zle_free_highlight();
8415        }
8416    }
8417
8418    /// c:607 — `tcoutclear` for each clear capability is safe.
8419    #[test]
8420    fn tcoutclear_both_arms_safe() {
8421        let _g = crate::test_util::global_state_lock();
8422        let _g2 = zle_test_setup();
8423        tcoutclear(crate::ported::zsh_h::TCCLEARSCREEN);
8424        tcoutclear(crate::ported::zsh_h::TCCLEAREOL);
8425    }
8426
8427    /// c:1097 — `wpfxlen(empty, empty)` returns 0 (alt name).
8428    #[test]
8429    fn wpfxlen_both_empty_returns_zero_alt() {
8430        assert_eq!(wpfxlen(&[], &[]), 0, "empty + empty → 0 common prefix");
8431    }
8432
8433    /// c:1097 — `wpfxlen` is symmetric: wpfxlen(a, b) == wpfxlen(b, a).
8434    #[test]
8435    fn wpfxlen_symmetric() {
8436        let a = [
8437            REFRESH_ELEMENT { chr: 'a', atr: 0 },
8438            REFRESH_ELEMENT { chr: 'b', atr: 0 },
8439        ];
8440        let b = [
8441            REFRESH_ELEMENT { chr: 'a', atr: 0 },
8442            REFRESH_ELEMENT { chr: 'c', atr: 0 },
8443        ];
8444        assert_eq!(
8445            wpfxlen(&a, &b),
8446            wpfxlen(&b, &a),
8447            "wpfxlen must be symmetric"
8448        );
8449    }
8450
8451    /// c:1097 — `wpfxlen(x, x)` returns full length (perfect prefix match).
8452    #[test]
8453    fn wpfxlen_identical_inputs_full_match() {
8454        let buf = [
8455            REFRESH_ELEMENT { chr: 'a', atr: 0 },
8456            REFRESH_ELEMENT { chr: 'b', atr: 0 },
8457            REFRESH_ELEMENT { chr: 'c', atr: 0 },
8458        ];
8459        let n = wpfxlen(&buf, &buf);
8460        assert_eq!(n, 3, "x vs x → full length 3");
8461    }
8462
8463    /// c:33 — `ZR_memset` n=0 on empty buf is safe.
8464    #[test]
8465    fn zr_memset_zero_n_safe() {
8466        let mut buf: Vec<REFRESH_ELEMENT> = vec![];
8467        ZR_memset(&mut buf, REFRESH_ELEMENT { chr: 'x', atr: 0 }, 0);
8468    }
8469}