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}