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 std::io::{self, Write};
6
7
8// TextAttr / RefreshElement / VideoBuffer / RefreshState — Rust-side
9// aggregates over zsh's C flat-globals (`winw`/`winh`/`vcs`/`vln`/
10// `lpromptw`/`rpromptw`/`region_highlights[]`/`nbuf`/`obuf` in
11// `Src/Zle/zle_refresh.c`). The C side represents these as separate
12// file-scope statics + bitmap-packed `zattr` cells; this port collects
13// them into structs for ergonomic access. Eventual unification target
14// (mirroring `Src/zsh.h:2685` `pub type zattr = u64`):
15// - `TextAttr` → `zattr` (u64 packed bitmap)
16// - `RefreshElement` → `zle_h::REFRESH_ELEMENT`
17// - `VideoBuffer` → raw `Vec<REFRESH_ELEMENT>` for `nbuf`/`obuf`
18// - `RefreshState` → discrete file-scope statics
19
20/// Unpacked-bool form of `zattr` (C's u64 packed attribute bitmap from
21/// `Src/zsh.h:2685`, ported as `pub type zattr = u64`). C stores
22/// attributes inline in `REFRESH_ELEMENT.atr` (a `zattr`); this port
23/// pre-unpacks to a 6-field struct for ergonomic access.
24#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
25pub struct TextAttr {
26 pub bold: bool,
27 pub underline: bool,
28 pub standout: bool,
29 pub blink: bool,
30 pub fg_color: Option<u8>,
31 pub bg_color: Option<u8>,
32}
33
34impl TextAttr {
35 /// Render this attribute set as the corresponding ANSI SGR
36 /// escape. Loose equivalent of `tsetcap()` from
37 /// Src/Zle/zle_refresh.c (which emits termcap-derived sequences
38 /// from per-cell attr changes during the diff/paint cycle).
39 pub fn to_ansi(&self) -> String {
40 let mut codes = Vec::new();
41 if self.bold {
42 codes.push("1".to_string());
43 }
44 if self.underline {
45 codes.push("4".to_string());
46 }
47 if self.standout {
48 codes.push("7".to_string());
49 }
50 if self.blink {
51 codes.push("5".to_string());
52 }
53 if let Some(fg) = self.fg_color {
54 codes.push(format!("38;5;{}", fg));
55 }
56 if let Some(bg) = self.bg_color {
57 codes.push(format!("48;5;{}", bg));
58 }
59 if codes.is_empty() {
60 String::new()
61 } else {
62 format!("\x1b[{}m", codes.join(";"))
63 }
64 }
65}
66
67/// Display cell. Loosely equivalent to zsh's `REFRESH_ELEMENT`
68/// (legit-ported at `zle_h.rs:688` as
69/// `pub struct REFRESH_ELEMENT { chr: REFRESH_CHAR, atr: zattr }`).
70/// Adds a `width: u8` field C doesn't have and uses `TextAttr` for
71/// `atr` instead of the C `zattr` bitmap.
72#[derive(Debug, Clone, Default)]
73pub struct RefreshElement {
74 pub chr: char,
75 pub atr: TextAttr,
76 pub width: u8,
77}
78
79impl RefreshElement {
80 /// Construct a refresh cell holding a single character with
81 /// default attributes. Equivalent shape to a freshly-zeroed
82 /// `REFRESH_ELEMENT` from Src/Zle/zle_refresh.h.
83 pub fn new(chr: char) -> Self {
84 let width = unicode_width::UnicodeWidthChar::width(chr).unwrap_or(1) as u8;
85 RefreshElement {
86 chr,
87 atr: TextAttr::default(),
88 width,
89 }
90 }
91
92 /// Construct a refresh cell with explicit text attributes.
93 /// Used by callers painting attributed regions (visual-mode
94 /// standout, isearch underline, etc.) directly into a
95 /// `VideoBuffer`.
96 pub fn with_attr(chr: char, atr: TextAttr) -> Self {
97 let width = unicode_width::UnicodeWidthChar::width(chr).unwrap_or(1) as u8;
98 RefreshElement { chr, atr, width }
99 }
100}
101
102/// 2D screen-buffer container. C uses `REFRESH_STRING nbuf[]` and
103/// `obuf[]` flat arrays of `REFRESH_ELEMENT *` (zle_refresh.c
104/// globals); this struct wraps a single 2D Vec for the per-frame
105/// new/old buffer pair.
106#[derive(Debug, Clone)]
107pub struct VideoBuffer {
108 /// Buffer contents — 2D array of lines.
109 pub lines: Vec<Vec<RefreshElement>>,
110 /// Number of columns.
111 pub cols: usize,
112 /// Number of rows.
113 pub rows: usize,
114}
115
116impl VideoBuffer {
117 /// Allocate a fresh video buffer of `cols × rows` filled with
118 /// blank cells. Equivalent to `resetvideo()` at
119 /// Src/Zle/zle_refresh.c:725 which allocates `nlnct * winw`
120 /// cells for `nbuf` each refresh.
121 pub fn new(cols: usize, rows: usize) -> Self {
122 let lines = vec![vec![RefreshElement::new(' '); cols]; rows];
123 VideoBuffer { lines, cols, rows }
124 }
125
126 /// Reset every cell to a blank-attribute space. Used by
127 /// `zrefresh()` between frames to wipe the working buffer
128 /// before the new paint pass — see `freevideo()` at
129 /// zle_refresh.c:700 for the equivalent role.
130 pub fn clear(&mut self) {
131 for line in &mut self.lines {
132 for elem in line.iter_mut() {
133 *elem = RefreshElement::new(' ');
134 }
135 }
136 }
137
138 /// Reshape the buffer for a new terminal size. Equivalent to
139 /// the cols/lines update + `nbuf`/`obuf` reallocation chain in
140 /// zle_refresh.c that fires on SIGWINCH (see the `winw`/`winh`
141 /// re-read in `zrefresh()` at zle_refresh.c:975).
142 pub fn resize(&mut self, cols: usize, rows: usize) {
143 self.cols = cols;
144 self.rows = rows;
145 self.lines
146 .resize(rows, vec![RefreshElement::new(' '); cols]);
147 for line in &mut self.lines {
148 line.resize(cols, RefreshElement::new(' '));
149 }
150 }
151
152 /// Write a single cell into the buffer; out-of-range writes are
153 /// silently dropped (matches the C source's bounds check before
154 /// `nbuf[row][col] = ...` in zle_refresh.c).
155 pub fn set(&mut self, row: usize, col: usize, elem: RefreshElement) {
156 if row < self.rows && col < self.cols {
157 self.lines[row][col] = elem;
158 }
159 }
160
161 /// Read a single cell. Returns None for out-of-range coords —
162 /// the C source's index path is unchecked (uses `winw`/`nlnct`
163 /// invariants).
164 pub fn get(&self, row: usize, col: usize) -> Option<&RefreshElement> {
165 self.lines.get(row).and_then(|line| line.get(col))
166 }
167}
168
169/// Composite of zle_refresh.c globals (winw/winh/vcs/vln/vmaxln,
170/// oldmax, lastrow, lastcol, more_status, etc.) collected into one
171/// struct. C uses separate file-statics per name
172/// (`int winw, winh, vcs, vln, ...`).
173#[derive(Debug, Clone, Default)]
174pub struct RefreshState {
175 /// Number of columns.
176 pub columns: usize, // winw, window width // c:682
177 /// Number of lines.
178 pub lines: usize, // winh, window height // c:682
179 /// Current line on screen (cursor row).
180 pub vln: usize, // video cursor position line // c:680
181 /// Current column on screen (cursor col).
182 pub vcs: usize, // video cursor position column // c:680
183 /// Prompt width (left).
184 pub lpromptw: usize, // prompt widths on screen // c:676
185 /// Right prompt width.
186 pub rpromptw: usize, // prompt widths on screen // c:676
187 /// Scroll offset for horizontal scrolling.
188 pub scrolloff: usize,
189 /// Region highlight start.
190 pub region_highlight_start: Option<usize>,
191 /// Region highlight end.
192 pub region_highlight_end: Option<usize>,
193 /// Old video buffer.
194 pub old_video: Option<VideoBuffer>,
195 /// New video buffer.
196 pub new_video: Option<VideoBuffer>,
197 /// Prompt string (left).
198 pub lpromptbuf: String,
199 /// Right prompt string.
200 pub rpromptbuf: String,
201 /// Whether we need full redraw.
202 pub need_full_redraw: bool,
203 /// Predisplay string (before main buffer).
204 pub predisplay: String,
205 /// Postdisplay string (after main buffer).
206 pub postdisplay: String,
207}
208
209impl RefreshState {
210 /// Build the initial refresh state at zleread() entry.
211 /// Equivalent to the global `nbuf`/`obuf`/`vln`/`vcs`
212 /// allocation + reset performed by `resetvideo()` at
213 /// Src/Zle/zle_refresh.c:725 — terminal size queried once,
214 /// both video buffers allocated, `need_full_redraw` set so the
215 /// first paint touches every cell.
216 pub fn new() -> Self {
217 let (cols, rows) = (
218 crate::ported::utils::adjustcolumns(),
219 crate::ported::utils::adjustlines(),
220 );
221 RefreshState {
222 columns: cols,
223 lines: rows,
224 old_video: Some(VideoBuffer::new(cols, rows)),
225 new_video: Some(VideoBuffer::new(cols, rows)),
226 need_full_redraw: true,
227 ..Default::default()
228 }
229 }
230
231 /// Reallocate the video buffers for the current terminal size
232 /// and arm a full redraw on the next paint. Equivalent to
233 /// `resetvideo()` from Src/Zle/zle_refresh.c:725 invoked after
234 /// SIGWINCH (the C source calls it from `adjustwinsize()` in
235 /// Src/init.c).
236 pub fn reset_video(&mut self) {
237 let (cols, rows) = (
238 crate::ported::utils::adjustcolumns(),
239 crate::ported::utils::adjustlines(),
240 );
241 self.columns = cols;
242 self.lines = rows;
243 self.old_video = Some(VideoBuffer::new(cols, rows));
244 self.new_video = Some(VideoBuffer::new(cols, rows));
245 self.need_full_redraw = true;
246 }
247
248 /// Drop both video buffers — used at ZLE shutdown. Equivalent
249 /// to `freevideo()` from Src/Zle/zle_refresh.c:700.
250 pub fn free_video(&mut self) {
251 self.old_video = None;
252 self.new_video = None;
253 }
254
255 /// Promote the freshly-painted buffer to "previously displayed"
256 /// and clear the new-buffer slate for the next frame.
257 /// Equivalent to `bufswap()` from Src/Zle/zle_refresh.c:946 —
258 /// the C source swaps `nbuf` and `obuf` pointers and zeroes the
259 /// new `nbuf` so the diff loop has a clean target.
260 pub fn swap_buffers(&mut self) {
261 std::mem::swap(&mut self.old_video, &mut self.new_video);
262 if let Some(ref mut new) = self.new_video {
263 new.clear();
264 }
265 }
266}
267use HighlightCategory as HC;
268use crate::ported::zsh_h::TXT_MULTIWORD_MASK;
269
270 /// Main refresh function — redraws the line.
271 /// Port of `zrefresh()` from Src/Zle/zle_refresh.c. The C source paints
272 /// a full virtual-screen diff against the previous frame; this Rust
273 /// port renders the single line each call but adds three behaviors
274 /// the previous bare-buffer version was missing:
275 /// * region-attribute overlay (zle_refresh.c `region_highlights[]`),
276 /// * vi visual-mode auto-region (mirrors zle_refresh.c's check of
277 /// `region_active` to paint mark..zlecs in standout),
278 /// * RPS1 / right-prompt rendering at the right margin
279 /// (zle_refresh.c `put_rpromptbuf` path).
280
281// --- AUTO: cross-zle hoisted-fn use glob ---
282#[allow(unused_imports)]
283#[allow(unused_imports)]
284use crate::ported::zle::zle_main::*;
285#[allow(unused_imports)]
286use crate::ported::zle::zle_misc::*;
287#[allow(unused_imports)]
288use crate::ported::zle::zle_hist::*;
289#[allow(unused_imports)]
290use crate::ported::zle::zle_move::*;
291#[allow(unused_imports)]
292use crate::ported::zle::zle_word::*;
293#[allow(unused_imports)]
294use crate::ported::zle::zle_params::*;
295#[allow(unused_imports)]
296use crate::ported::zle::zle_vi::*;
297#[allow(unused_imports)]
298use crate::ported::zle::zle_utils::*;
299#[allow(unused_imports)]
300use crate::ported::zle::zle_tricky::*;
301#[allow(unused_imports)]
302use crate::ported::zle::textobjects::*;
303#[allow(unused_imports)]
304use crate::ported::zle::deltochar::*;
305
306 pub fn zrefresh() { // c:975
307 // c:975 — full repaint pipeline. C writes every byte through
308 // `tputs(..., putshout)` / `fputs(..., shout)`. Rust
309 // collects the rendered escape stream into a String
310 // and writes it to SHTTY in one shot — matches C's
311 // shout destination and reduces syscall count.
312 use std::fmt::Write as FmtWrite;
313 let mut handle = String::new();
314
315 let (cols, _rows) = (crate::ported::utils::adjustcolumns(), crate::ported::utils::adjustlines());
316
317 let prompt = prompt().to_string();
318 let rprompt = rprompt().to_string();
319 let cursor = crate::ported::zle::zle_main::ZLECS.load(std::sync::atomic::Ordering::SeqCst);
320
321 let prompt_width = countprompt(&prompt);
322 let rprompt_width = countprompt(&rprompt);
323 let buffer_before_cursor: String = crate::ported::zle::zle_main::ZLELINE.lock().unwrap()[..cursor.min(crate::ported::zle::zle_main::ZLELINE.lock().unwrap().len())]
324 .iter()
325 .collect();
326 let cursor_col = prompt_width + countprompt(&buffer_before_cursor);
327
328 // Horizontal scroll if the cursor approaches the right edge.
329 // Mirrors zle_refresh.c's `winw` clamp logic — without the full
330 // multi-line wrap path our single-line shell uses scroll instead.
331 let scroll_margin = 8;
332 let effective_cols = cols.saturating_sub(1);
333 let scroll_offset = if cursor_col >= effective_cols.saturating_sub(scroll_margin) {
334 cursor_col.saturating_sub(effective_cols / 2)
335 } else {
336 0
337 };
338
339 // Compose the per-buffer-char attribute overlay before paint, so
340 // we don't have to re-walk the highlight list per char during write.
341 let attrs = compute_render_attrs();
342
343 let _ = write!(handle, "\r\x1b[K");
344
345 // Prompt — drawn unless we've scrolled past it. Skip
346 // `scroll_offset` visible chars from the prompt (inlined
347 // from the deleted skip_chars helper) — ANSI escape
348 // sequences are skipped unconditionally so they don't
349 // count against width.
350 if scroll_offset < prompt_width {
351 let mut width = 0;
352 let mut byte_idx = 0;
353 let mut in_escape = false;
354 for (i, c) in prompt.char_indices() {
355 if width >= scroll_offset {
356 byte_idx = i;
357 break;
358 }
359 if in_escape {
360 if c.is_ascii_alphabetic() {
361 in_escape = false;
362 }
363 } else if c == '\x1b' {
364 in_escape = true;
365 } else {
366 width += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
367 }
368 byte_idx = i + c.len_utf8();
369 }
370 let _ = write!(handle, "{}", &prompt[byte_idx..]);
371 }
372
373 // Compute the visible byte/char range of the buffer after scroll.
374 let buffer_start = scroll_offset.saturating_sub(prompt_width);
375 // Width budget for buffer = total cols - prompt drawn - rprompt reserve.
376 let drawn_prompt_width = prompt_width.saturating_sub(scroll_offset);
377 let rprompt_reserve = if rprompt_width > 0 {
378 rprompt_width + 1
379 } else {
380 0
381 };
382 let buffer_budget = effective_cols
383 .saturating_sub(drawn_prompt_width)
384 .saturating_sub(rprompt_reserve);
385
386 // Walk the buffer chars from buffer_start, applying overlay attrs.
387 let mut current_attr: Option<TextAttr> = None;
388 let line_snapshot = crate::ported::zle::zle_main::ZLELINE.lock().unwrap().clone();
389 for (written, (idx, ch)) in line_snapshot
390 .iter()
391 .enumerate()
392 .skip(buffer_start)
393 .enumerate()
394 {
395 if written >= buffer_budget {
396 break;
397 }
398 let want_attr = attrs.get(idx).and_then(|a| *a);
399 if want_attr != current_attr {
400 let _ = write!(handle, "\x1b[0m");
401 if let Some(a) = want_attr {
402 let _ = write!(handle, "{}", a.to_ansi());
403 }
404 current_attr = want_attr;
405 }
406 let _ = write!(handle, "{}", ch);
407 }
408 // Reset SGR before the rprompt / cursor jump.
409 if current_attr.is_some() {
410 let _ = write!(handle, "\x1b[0m");
411 }
412
413 // Right prompt — paint at the absolute right margin if there's
414 // room. Mirrors put_rpromptbuf in zle_refresh.c which writes RPS1
415 // at column (winw - rpromptw).
416 if rprompt_width > 0 && rprompt_width + 2 < effective_cols {
417 let rprompt_col = effective_cols.saturating_sub(rprompt_width);
418 let _ = write!(handle, "\r\x1b[{}C{}\x1b[0m", rprompt_col, rprompt);
419 }
420
421 // Cursor positioning (1-based column in ANSI).
422 let display_cursor_col = cursor_col.saturating_sub(scroll_offset);
423 let _ = write!(handle, "\r\x1b[{}C", display_cursor_col);
424
425 // c:1488 — `fwrite(out, ..., shout); fflush(shout);`. Single
426 // write_loop emits the whole frame to SHTTY (stdout
427 // fallback). Replaces the prior `stdout.lock()`
428 // fake that wrote refresh output to stdout instead
429 // of the controlling tty.
430 use std::sync::atomic::Ordering;
431 let fd = crate::ported::init::SHTTY.load(Ordering::Relaxed);
432 let out_fd = if fd >= 0 { fd } else { 1 };
433 let _ = crate::ported::utils::write_loop(out_fd, handle.as_bytes());
434 }
435
436 /// Build the per-character attribute overlay used by `zrefresh`.
437 /// One slot per char in `zleline`; `None` means "default attrs",
438 /// `Some(attr)` means apply `attr` for that cell.
439 ///
440 /// Port of the inner loop in `zrefresh()` (Src/Zle/zle_refresh.c) that
441 /// consults `region_highlights[]` for each visible cell. The vi
442 /// visual-mode region is synthesised from `region_active` + `mark`
443 /// here so `v` selects visibly without callers having to push a
444 /// region themselves — matching zle_refresh.c's auto-promotion of
445 /// `region_active` into a paintable highlight.
446 pub fn compute_render_attrs() -> Vec<Option<TextAttr>> {
447 let buf_len = crate::ported::zle::zle_main::ZLELINE.lock().unwrap().len();
448 let mut attrs: Vec<Option<TextAttr>> = vec![None; buf_len];
449
450 // Visual-region attr: prefer the user's `region:` setting from
451 // $zle_highlight (populated by zle_set_highlight); fall back to
452 // standout per zsh's default at zle_refresh.c:397.
453 let visual_attr = crate::ported::zle::zle_main::highlight().lock().unwrap()
454 .category_attrs
455 .get(&HighlightCategory::Region)
456 .copied()
457 .unwrap_or(TextAttr {
458 standout: true,
459 ..TextAttr::default()
460 });
461
462 if crate::ported::zle::zle_main::REGION_ACTIVE.load(std::sync::atomic::Ordering::SeqCst) != 0 {
463 let (lo, hi) = if crate::ported::zle::zle_main::MARK.load(std::sync::atomic::Ordering::SeqCst) <= crate::ported::zle::zle_main::ZLECS.load(std::sync::atomic::Ordering::SeqCst) {
464 (crate::ported::zle::zle_main::MARK.load(std::sync::atomic::Ordering::SeqCst), crate::ported::zle::zle_main::ZLECS.load(std::sync::atomic::Ordering::SeqCst))
465 } else {
466 (crate::ported::zle::zle_main::ZLECS.load(std::sync::atomic::Ordering::SeqCst), crate::ported::zle::zle_main::MARK.load(std::sync::atomic::Ordering::SeqCst))
467 };
468 let lo = lo.min(buf_len);
469 let hi = hi.min(buf_len);
470 for slot in attrs.iter_mut().take(hi).skip(lo) {
471 *slot = Some(visual_attr);
472 }
473 }
474 for region in &crate::ported::zle::zle_main::highlight().lock().unwrap().regions {
475 let start = region.start.min(buf_len);
476 let end = region.end.min(buf_len);
477 for slot in attrs.iter_mut().take(end).skip(start) {
478 *slot = Some(region.attr);
479 }
480 }
481 attrs
482 }
483
484 /// Full screen refresh - clears and redraws everything.
485 pub fn full_refresh() -> io::Result<()> {
486 use std::sync::atomic::Ordering;
487 let fd = crate::ported::init::SHTTY.load(Ordering::Relaxed);
488 let out = if fd >= 0 { fd } else { 1 };
489 let _ = crate::ported::utils::write_loop(out, b"\x1b[2J\x1b[H");
490 zrefresh();
491 Ok(())
492 }
493
494 /// Partial refresh (optimize for minimal updates).
495 pub fn partial_refresh() -> io::Result<()> {
496 zrefresh();
497 Ok(())
498 }
499
500 /// Direct port of `void clearscreen(UNUSED(char **args))` from
501 /// `Src/Zle/zle_refresh.c:2366`. Writes CSI 2J + CSI H to the
502 /// shell-output fd, then re-renders. Was a `print!` fake.
503 pub fn clearscreen() { // c:2366
504 let _ = crate::ported::utils::write_loop({ use std::sync::atomic::Ordering; let f = crate::ported::init::SHTTY.load(Ordering::Relaxed); if f >= 0 { f } else { 1 } }, b"\x1b[2J\x1b[H");
505 zrefresh();
506 }
507
508 /// Direct port of `void redisplay(UNUSED(char **args))` from
509 /// `Src/Zle/zle_refresh.c:2377`. C kicks `resetneeded = 1` and
510 /// returns; Rust just re-runs zrefresh which equivalently
511 /// repaints from current state.
512 pub fn redisplay() { // c:2377
513 zrefresh();
514 }
515
516 /// Direct port of `void moveto(int ln, int cl)` from
517 /// `Src/Zle/zle_refresh.c:2105`. C uses termcap `cm` / `cup`
518 /// strings to teleport the cursor; Rust emits the equivalent
519 /// CSI ; H sequence (rows/cols 1-indexed per ANSI). Was a
520 /// `print!` fake.
521 pub fn moveto(row: usize, col: usize) { // c:2105
522 let s = format!("\x1b[{};{}H", row + 1, col + 1);
523 let _ = crate::ported::utils::write_loop({ use std::sync::atomic::Ordering; let f = crate::ported::init::SHTTY.load(Ordering::Relaxed); if f >= 0 { f } else { 1 } }, s.as_bytes());
524 }
525
526 /// Port of `void tc_downcurs(int ct)` from
527 /// `Src/Zle/zle_refresh.c:2126`. C emits the termcap `do`/`down`
528 /// capability `ct` times; Rust emits the parametrised CSI B.
529 pub fn tc_downcurs(count: usize) {
530 if count > 0 {
531 let s = format!("\x1b[{}B", count);
532 let _ = crate::ported::utils::write_loop({ use std::sync::atomic::Ordering; let f = crate::ported::init::SHTTY.load(Ordering::Relaxed); if f >= 0 { f } else { 1 } }, s.as_bytes());
533 }
534 }
535
536 /// Port of `void tc_rightcurs(int ct)` from
537 /// `Src/Zle/zle_refresh.c:2150`. CSI C parametrised cursor-right.
538 pub fn tc_rightcurs(count: usize) {
539 if count > 0 {
540 let s = format!("\x1b[{}C", count);
541 let _ = crate::ported::utils::write_loop({ use std::sync::atomic::Ordering; let f = crate::ported::init::SHTTY.load(Ordering::Relaxed); if f >= 0 { f } else { 1 } }, s.as_bytes());
542 }
543 }
544
545 /// Port of `void scrollwindow(int tline)` from
546 /// `Src/Zle/zle_refresh.c:1991`. Positive lines → scroll up (CSI S),
547 /// negative → scroll down (CSI T).
548 pub fn scrollwindow(lines: i32) {
549 let s = if lines > 0 {
550 format!("\x1b[{}S", lines)
551 } else if lines < 0 {
552 format!("\x1b[{}T", -lines)
553 } else {
554 return;
555 };
556 let _ = crate::ported::utils::write_loop({ use std::sync::atomic::Ordering; let f = crate::ported::init::SHTTY.load(Ordering::Relaxed); if f >= 0 { f } else { 1 } }, s.as_bytes());
557 }
558
559 /// Port of `void singlerefresh(ZLE_STRING_T tmpline, int tmpll,
560 /// int tmpcs)` from `Src/Zle/zle_refresh.c:2397`. C builds a
561 /// fresh single-line video buffer for `read -e` / `vared` style
562 /// non-multiline editing; Rust defers to `zrefresh` which
563 /// handles single-line as a special case of multi-line.
564 pub fn singlerefresh() { // c:2397
565 zrefresh();
566 }
567
568 /// Port of `void refreshline(int ln)` from
569 /// `Src/Zle/zle_refresh.c:1543`. Forces a single-line repaint;
570 /// our zrefresh repaints the whole video buffer regardless.
571 pub fn refreshline(_line: usize) {
572 zrefresh();
573 }
574
575 /// Port of `void zwcputc(const REFRESH_ELEMENT *c)` from
576 /// `Src/Zle/zle_refresh.c`. C: `putc(c->chr, shout)`. Rust:
577 /// encodes the char as UTF-8 bytes and writes to the shell-out
578 /// fd.
579 pub fn zwcputc(c: char) {
580 let mut buf = [0u8; 4];
581 let s = c.encode_utf8(&mut buf);
582 let _ = crate::ported::utils::write_loop({ use std::sync::atomic::Ordering; let f = crate::ported::init::SHTTY.load(Ordering::Relaxed); if f >= 0 { f } else { 1 } }, s.as_bytes());
583 }
584
585 /// Port of `void zwcwrite(const REFRESH_STRING s, size_t i)`
586 /// from `Src/Zle/zle_refresh.c`. C: `fwrite(s, sizeof(*s), i,
587 /// shout)`. Rust writes the UTF-8 bytes to shout.
588 pub fn zwcwrite(s: &str) {
589 let _ = crate::ported::utils::write_loop({ use std::sync::atomic::Ordering; let f = crate::ported::init::SHTTY.load(Ordering::Relaxed); if f >= 0 { f } else { 1 } }, s.as_bytes());
590 }
591
592
593/// Calculate visible width of a prompt string — port of `countprompt()`
594/// from Src/prompt.c:1140. The C function counts cells while skipping
595/// the `Inpar..Outpar` (zsh's `%{...%}`) invisible-region tokens; this
596/// Rust port skips ANSI escape sequences instead, which is what the
597/// expanded prompt buffer contains by the time the refresh path uses it.
598/// The C variant outputs width AND height via out-pointers; this port
599/// returns width only (the only field the refresh path consumes here).
600fn countprompt(s: &str) -> usize {
601 let mut width = 0;
602 let mut in_escape = false;
603
604 for c in s.chars() {
605 if in_escape {
606 if c.is_ascii_alphabetic() {
607 in_escape = false;
608 }
609 } else if c == '\x1b' {
610 in_escape = true;
611 } else {
612 width += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
613 }
614 }
615
616 width
617}
618
619// RegionHighlight / HighlightCategory / HighlightManager — Rust-side
620// aggregates over zsh's C `region_highlights[N_SPECIAL_HIGHLIGHTS]`
621// array + per-category attr globals (`default_attr`/`special_attr`/
622// `ellipsis_attr` from `Src/Zle/zle_refresh.c`). C uses bare integer
623// indexing into a fixed-size array; this port uses a typed enum +
624// HashMap. Eventual unification: collapse into discrete file-scope
625// statics matching the C layout.
626
627/// Simplified region-highlight entry. Loosely equivalent to
628/// `struct region_highlight` (legit-ported at `zle_h.rs:613` with
629/// different fields: start/end/atr/flags/memo/layer).
630#[derive(Debug, Clone)]
631pub struct RegionHighlight {
632 pub start: usize,
633 pub end: usize,
634 pub attr: TextAttr,
635 pub memo: Option<String>,
636}
637
638/// Identifies a fixed slot in zsh's
639/// `region_highlights[N_SPECIAL_HIGHLIGHTS]` array (zle_refresh.c
640/// indices 0=region, 1=isearch, 2=suffix, 3=paste) plus the
641/// standalone default/special/ellipsis attr globals
642/// (`default_attr`/`special_attr`/`ellipsis_attr`). C uses bare
643/// integer indexing — no enum.
644#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
645pub enum HighlightCategory {
646 Region,
647 Isearch,
648 Suffix,
649 Paste,
650 Default,
651 Special,
652 Ellipsis,
653}
654
655/// Collects C's `region_highlights[]` array + per-category attr
656/// globals (`default_attr`/`special_attr`/`ellipsis_attr` from
657/// zle_refresh.c) into one container.
658#[derive(Debug, Default)]
659pub struct HighlightManager {
660 pub regions: Vec<RegionHighlight>,
661 /// Per-category attrs from `$zle_highlight`. Index by
662 /// `HighlightCategory`. Equivalent to the per-slot atr storage
663 /// in `region_highlights[]` and the
664 /// `default_attr`/`special_attr`/`ellipsis_attr` globals in
665 /// Src/Zle/zle_refresh.c — populated by `zle_set_highlight()`.
666 pub category_attrs: std::collections::HashMap<HighlightCategory, TextAttr>,
667}
668
669impl HighlightManager {
670 pub fn new() -> Self {
671 HighlightManager {
672 regions: Vec::new(),
673 category_attrs: std::collections::HashMap::new(),
674 }
675 }
676
677 /// Set region highlight. Equivalent to
678 /// `set_region_highlight()` from zle_refresh.c.
679 pub fn set_region_highlight(&mut self, start: usize, end: usize, attr: TextAttr) {
680 self.regions.push(RegionHighlight {
681 start,
682 end,
683 attr,
684 memo: None,
685 });
686 }
687
688 /// Get region highlight for position. Equivalent to
689 /// `get_region_highlight()` from zle_refresh.c.
690 pub fn get_region_highlight(&self, pos: usize) -> Option<&RegionHighlight> {
691 self.regions.iter().find(|r| pos >= r.start && pos < r.end)
692 }
693
694 /// Unset region highlight. Equivalent to
695 /// `unset_region_highlight()` from zle_refresh.c.
696 pub fn unset_region_highlight(&mut self) {
697 self.regions.clear();
698 }
699
700 /// Free highlight resources. Equivalent to
701 /// `zle_free_highlight()` from zle_refresh.c.
702 pub fn free(&mut self) {
703 self.regions.clear();
704 }
705}
706
707/// Port of `void tcout(int cap)` from `Src/Zle/zle_refresh.c:2339`.
708/// C looks up the termcap string via `tcstr[cap]` and writes it
709/// through `tputs(..., putshout)` to the shell-output fd. Rust port
710/// takes the resolved escape string directly (skipping the
711/// `tcstr[]` index lookup, since termcap probing isn't fully wired)
712/// and writes the bytes to `SHTTY` via `write_loop`.
713///
714/// Falls back to stdout (fd 1) when `SHTTY` is unset — covers the
715/// non-interactive paths (tests, batch evaluation) where there's no
716/// dedicated shell-output fd yet.
717/// WARNING: signature change — C=(int cap) vs Rust=(cap: &str).
718pub fn tcout(cap: &str) { // c:2339
719 use std::sync::atomic::Ordering;
720 let fd = crate::ported::init::SHTTY.load(Ordering::Relaxed);
721 if fd >= 0 {
722 let _ = crate::ported::utils::write_loop(fd, cap.as_bytes());
723 } else {
724 let _ = crate::ported::utils::write_loop(1, cap.as_bytes());
725 }
726 // c:2346 — `SELECT_ADD_COST(tclen[cap])` — without per-cap tclen
727 // table, the cost accounting is dropped (no scheduling
728 // consumer reads it yet).
729}
730
731/// Port of `void tcoutarg(int cap, int arg)` from
732/// `Src/Zle/zle_refresh.c:2351`. C calls `tgoto(tcstr[cap], arg, arg)`
733/// to expand termcap `%d` / parametrised escape codes. Rust port
734/// does a literal `%d → arg` substring substitution (mirrors the
735/// most common case; doesn't handle the rare termcap `%p1%d`
736/// parametrisation that `tgoto` handles).
737/// WARNING: signature change — C=(int cap, int arg) vs Rust=(cap: &str, arg: i32).
738pub fn tcoutarg(cap: &str, arg: i32) { // c:2351
739 use std::sync::atomic::Ordering;
740 // c:2355 — `result = tgoto(tcstr[cap], arg, arg);`
741 let s = cap.replace("%d", &arg.to_string());
742 let fd = crate::ported::init::SHTTY.load(Ordering::Relaxed);
743 let out_fd = if fd >= 0 { fd } else { 1 };
744 let _ = crate::ported::utils::write_loop(out_fd, s.as_bytes()); // c:2359
745}
746
747/// Port of `void tcmultout(int cap, int multcap, int ct)` from
748/// `Src/Zle/zle_refresh.c:2163`. The C version tries the multi-arg
749/// `multcap` capability first (`tcoutarg(multcap, ct)`) and only
750/// falls back to a single-cap loop when `multcap` is unavailable.
751/// Rust port (without termcap probe) goes straight to the loop —
752/// `count` repeats of the same single-shot string.
753/// WARNING: signature change — C=(int cap, int multcap, int ct) vs Rust=(cap: &str, count: i32).
754pub fn tcmultout(cap: &str, count: i32) { // c:2163
755 use std::sync::atomic::Ordering;
756 let fd = crate::ported::init::SHTTY.load(Ordering::Relaxed);
757 let out_fd = if fd >= 0 { fd } else { 1 };
758 for _ in 0..count { // c:2173 single-cap loop
759 let _ = crate::ported::utils::write_loop(out_fd, cap.as_bytes());
760 }
761}
762
763/// Port of `void tcoutclear(int cap)` from
764/// `Src/Zle/zle_refresh.c:607`. C dispatches on `cap` (a termcap
765/// index — TCCLEAREOL/TCCLEAREOD/TCCLEARSCREEN) to emit the
766/// corresponding escape. Rust collapses to a bool `to_end`:
767/// `true` → clear-to-end (CSI J), `false` → clear-entire-screen
768/// (CSI 2J).
769/// WARNING: signature change — C=(int cap) vs Rust=(to_end: bool).
770pub fn tcoutclear(to_end: bool) { // c:607
771 use std::sync::atomic::Ordering;
772 let bytes: &[u8] = if to_end {
773 b"\x1b[J" // CSI J — clear to end of screen
774 } else {
775 b"\x1b[2J" // CSI 2J — clear entire screen
776 };
777 let fd = crate::ported::init::SHTTY.load(Ordering::Relaxed);
778 let out_fd = if fd >= 0 { fd } else { 1 };
779 let _ = crate::ported::utils::write_loop(out_fd, bytes);
780}
781
782/// Initialize ZLE refresh subsystem
783/// Port of zle_refresh_boot() from zle_refresh.c
784pub fn zle_refresh_boot() -> RefreshState {
785 RefreshState::new()
786}
787
788/// Cleanup ZLE refresh subsystem
789/// Port of zle_refresh_finish() from zle_refresh.c
790pub fn zle_refresh_finish(state: &mut RefreshState) {
791 state.free_video();
792}
793
794/// Parse a highlight attribute spec (the part after the `category:` prefix)
795/// into a `TextAttr`. Accepts a comma-separated list of:
796/// * `bold` / `nobold`,
797/// * `underline` / `nounderline`,
798/// * `standout` / `nostandout`,
799/// * `blink` / `noblink`,
800/// * `fg=N` / `bg=N` where N is 0..=255 (256-colour palette index) or
801/// one of the named ANSI colours below,
802/// * `none` (clears every attr).
803///
804/// ZLE-region subset of `match_highlight` (Src/prompt.c:2031),
805/// restricted to the tokens users actually set in `$zle_highlight`.
806/// The `hl=`/`layer=`/`opacity=` clauses (prompt.c:2042-2094) are
807/// not surfaced here — those are prompt-system hooks that don't
808/// apply to ZLE region paint.
809pub fn match_highlight(spec: &str) -> TextAttr {
810 let mut attr = TextAttr::default();
811 for token in spec.split(',') {
812 let token = token.trim();
813 if token.is_empty() {
814 continue;
815 }
816 match token {
817 "none" => {
818 attr = TextAttr::default();
819 }
820 "bold" => attr.bold = true,
821 "nobold" => attr.bold = false,
822 "underline" => attr.underline = true,
823 "nounderline" => attr.underline = false,
824 "standout" => attr.standout = true,
825 "nostandout" => attr.standout = false,
826 "blink" => attr.blink = true,
827 "noblink" => attr.blink = false,
828 other => {
829 if let Some(rest) = other.strip_prefix("fg=") {
830 attr.fg_color = match_colour(rest);
831 } else if let Some(rest) = other.strip_prefix("bg=") {
832 attr.bg_color = match_colour(rest);
833 }
834 // Anything else (hl=, layer=, opacity=, unknown name) is
835 // silently dropped — same as the C source's "found = 0"
836 // exit path at prompt.c:2122 when no clause matched.
837 }
838 }
839 }
840 attr
841}
842
843/// Parse a colour token (named or numeric) into a 256-colour palette index.
844/// Mirrors the eight ANSI base names + 256-colour numeric form supported
845/// by `match_colour()` (Src/prompt.c, called from `match_highlight`). The
846/// 24-bit `#rrggbb` form and `bright-foo` aliases are not surfaced.
847fn match_colour(name: &str) -> Option<u8> {
848 match name {
849 "black" => Some(0),
850 "red" => Some(1),
851 "green" => Some(2),
852 "yellow" => Some(3),
853 "blue" => Some(4),
854 "magenta" => Some(5),
855 "cyan" => Some(6),
856 "white" => Some(7),
857 "default" => None,
858 n => n.parse::<u8>().ok(),
859 }
860}
861
862/// Apply a `$zle_highlight` array to the manager.
863/// Port of `zle_set_highlight()` from Src/Zle/zle_refresh.c:322. Walks
864/// each `category:spec` entry, parses the spec via `match_highlight`,
865/// and stores it in `category_attrs`. Categories not mentioned keep the
866/// zsh defaults, applied here on first call: `region` and `special`
867/// default to `standout`, `isearch` to `underline`, `suffix` to `bold`
868/// — direct ports of zle_refresh.c:395-402.
869/// WARNING: param names don't match C — Rust=(manager, atrs) vs C=()
870pub fn zle_set_highlight(manager: &mut HighlightManager, atrs: &[&str]) {
871
872 let mut seen = std::collections::HashSet::new();
873 for entry in atrs {
874 if entry.is_empty() {
875 continue;
876 }
877 if *entry == "none" {
878 // zle_refresh.c:355-360 — `none` clears every category.
879 for cat in [
880 HC::Region,
881 HC::Isearch,
882 HC::Suffix,
883 HC::Paste,
884 HC::Default,
885 HC::Special,
886 HC::Ellipsis,
887 ] {
888 manager.category_attrs.insert(cat, TextAttr::default());
889 seen.insert(cat);
890 }
891 continue;
892 }
893 let (prefix, rest) = match entry.split_once(':') {
894 Some(t) => t,
895 None => continue,
896 };
897 let cat = match prefix {
898 "region" => HC::Region,
899 "isearch" => HC::Isearch,
900 "suffix" => HC::Suffix,
901 "paste" => HC::Paste,
902 "default" => HC::Default,
903 "special" => HC::Special,
904 "ellipsis" => HC::Ellipsis,
905 _ => continue,
906 };
907 manager.category_attrs.insert(cat, match_highlight(rest));
908 seen.insert(cat);
909 }
910
911 // Defaults for unset slots — zle_refresh.c:395-402.
912 let default_standout = TextAttr {
913 standout: true,
914 ..TextAttr::default()
915 };
916 let default_underline = TextAttr {
917 underline: true,
918 ..TextAttr::default()
919 };
920 let default_bold = TextAttr {
921 bold: true,
922 ..TextAttr::default()
923 };
924 if !seen.contains(&HC::Region) {
925 manager.category_attrs.insert(HC::Region, default_standout);
926 }
927 if !seen.contains(&HC::Isearch) {
928 manager.category_attrs.insert(HC::Isearch, default_underline);
929 }
930 if !seen.contains(&HC::Suffix) {
931 manager.category_attrs.insert(HC::Suffix, default_bold);
932 }
933 if !seen.contains(&HC::Special) {
934 manager.category_attrs.insert(HC::Special, default_standout);
935 }
936}
937
938#[cfg(test)]
939mod tests {
940 use super::*;
941
942 #[test]
943 fn test_countprompt() {
944 let _g = crate::ported::zle::zle_main::zle_test_setup();
945 assert_eq!(countprompt("hello"), 5);
946 assert_eq!(countprompt("\x1b[31mhello\x1b[0m"), 5);
947 assert_eq!(countprompt("日本語"), 6); // 3 chars, 2 width each
948 }
949
950 #[test]
951 fn test_video_buffer() {
952 let _g = crate::ported::zle::zle_main::zle_test_setup();
953 let mut buf = VideoBuffer::new(80, 24);
954 assert_eq!(buf.cols, 80);
955 assert_eq!(buf.rows, 24);
956
957 buf.set(0, 0, RefreshElement::new('A'));
958 assert_eq!(buf.get(0, 0).map(|e| e.chr), Some('A'));
959
960 buf.clear();
961 assert_eq!(buf.get(0, 0).map(|e| e.chr), Some(' '));
962 }
963
964 #[test]
965 fn test_refresh_state() {
966 let _g = crate::ported::zle::zle_main::zle_test_setup();
967 let mut state = RefreshState::new();
968 assert!(state.old_video.is_some());
969 assert!(state.new_video.is_some());
970
971 state.swap_buffers();
972 state.free_video();
973 assert!(state.old_video.is_none());
974 }
975
976 #[test]
977 fn compute_render_attrs_empty_buffer_yields_empty_overlay() {
978 let _g = crate::ported::zle::zle_main::zle_test_setup();
979 assert!(compute_render_attrs().is_empty());
980 }
981
982 #[test]
983 fn compute_render_attrs_visual_mode_paints_mark_to_cursor_in_standout() {
984 let _g = crate::ported::zle::zle_main::zle_test_setup();
985 *crate::ported::zle::zle_main::ZLELINE.lock().unwrap() = "hello world".chars().collect();
986 crate::ported::zle::zle_main::ZLELL.store(crate::ported::zle::zle_main::ZLELINE.lock().unwrap().len(), std::sync::atomic::Ordering::SeqCst);
987 crate::ported::zle::zle_main::MARK.store(2, std::sync::atomic::Ordering::SeqCst);
988 crate::ported::zle::zle_main::ZLECS.store(7, std::sync::atomic::Ordering::SeqCst);
989 crate::ported::zle::zle_main::REGION_ACTIVE.store(1, std::sync::atomic::Ordering::SeqCst); // charwise visual
990 let attrs = compute_render_attrs();
991 assert_eq!(attrs.len(), 11);
992 // [0..2) and [7..11) are unstyled.
993 for slot in attrs.iter().take(2) {
994 assert!(slot.is_none());
995 }
996 for slot in attrs.iter().skip(7) {
997 assert!(slot.is_none());
998 }
999 // [2..7) painted in standout.
1000 for slot in attrs.iter().take(7).skip(2) {
1001 let attr = slot.expect("standout");
1002 assert!(attr.standout);
1003 }
1004 }
1005
1006 #[test]
1007 fn compute_render_attrs_visual_mode_handles_reverse_mark_order() {
1008 let _g = crate::ported::zle::zle_main::zle_test_setup();
1009 *crate::ported::zle::zle_main::ZLELINE.lock().unwrap() = "abcdef".chars().collect();
1010 crate::ported::zle::zle_main::ZLELL.store(6, std::sync::atomic::Ordering::SeqCst);
1011 crate::ported::zle::zle_main::MARK.store(5, std::sync::atomic::Ordering::SeqCst);
1012 crate::ported::zle::zle_main::ZLECS.store(1, std::sync::atomic::Ordering::SeqCst);
1013 crate::ported::zle::zle_main::REGION_ACTIVE.store(2, std::sync::atomic::Ordering::SeqCst); // linewise — same swap behavior
1014 let attrs = compute_render_attrs();
1015 // Range collapses to (1..5).
1016 assert!(attrs[0].is_none());
1017 for slot in attrs.iter().take(5).skip(1) {
1018 assert!(slot.unwrap().standout);
1019 }
1020 assert!(attrs[5].is_none());
1021 }
1022
1023 #[test]
1024 fn match_highlight_handles_combined_attrs() {
1025 let _g = crate::ported::zle::zle_main::zle_test_setup();
1026 let attr = match_highlight("bold,fg=red,underline");
1027 assert!(attr.bold);
1028 assert!(attr.underline);
1029 assert_eq!(attr.fg_color, Some(1));
1030 }
1031
1032 #[test]
1033 fn match_highlight_named_and_numeric_colors() {
1034 let _g = crate::ported::zle::zle_main::zle_test_setup();
1035 assert_eq!(match_highlight("fg=cyan").fg_color, Some(6));
1036 assert_eq!(match_highlight("bg=42").bg_color, Some(42));
1037 // Out-of-range numeric → ignored (parse fails for u8).
1038 assert_eq!(match_highlight("fg=999").fg_color, None);
1039 }
1040
1041 #[test]
1042 fn match_highlight_negation_clears_attr() {
1043 let _g = crate::ported::zle::zle_main::zle_test_setup();
1044 let attr = match_highlight("bold,nobold,underline");
1045 assert!(!attr.bold);
1046 assert!(attr.underline);
1047 }
1048
1049 #[test]
1050 fn match_highlight_none_resets_everything() {
1051 let _g = crate::ported::zle::zle_main::zle_test_setup();
1052 let attr = match_highlight("bold,fg=red,none,underline");
1053 // After `none` the only thing surviving is the trailing `underline`.
1054 assert!(!attr.bold);
1055 assert!(attr.underline);
1056 assert_eq!(attr.fg_color, None);
1057 }
1058
1059 #[test]
1060 fn zle_set_highlight_populates_categories_and_defaults() {
1061 let _g = crate::ported::zle::zle_main::zle_test_setup();
1062 let mut mgr = HighlightManager::new();
1063 let entries = ["region:fg=red,bold", "isearch:fg=blue"];
1064 zle_set_highlight(&mut mgr, &entries);
1065 let region = mgr.category_attrs[&HighlightCategory::Region];
1066 assert!(region.bold);
1067 assert_eq!(region.fg_color, Some(1));
1068 let isearch = mgr.category_attrs[&HighlightCategory::Isearch];
1069 assert_eq!(isearch.fg_color, Some(4));
1070 // Suffix wasn't set: defaults to bold (zle_refresh.c:401).
1071 let suffix = mgr.category_attrs[&HighlightCategory::Suffix];
1072 assert!(suffix.bold);
1073 // Special wasn't set: defaults to standout (zle_refresh.c:396).
1074 let special = mgr.category_attrs[&HighlightCategory::Special];
1075 assert!(special.standout);
1076 }
1077
1078 #[test]
1079 fn zle_set_highlight_none_clears_every_slot() {
1080 let _g = crate::ported::zle::zle_main::zle_test_setup();
1081 let mut mgr = HighlightManager::new();
1082 zle_set_highlight(&mut mgr, &["none"]);
1083 for cat in [
1084 HighlightCategory::Region,
1085 HighlightCategory::Isearch,
1086 HighlightCategory::Suffix,
1087 HighlightCategory::Paste,
1088 ] {
1089 let attr = mgr.category_attrs[&cat];
1090 assert_eq!(attr, TextAttr::default());
1091 }
1092 }
1093
1094 #[test]
1095 fn compute_render_attrs_visual_uses_zle_highlight_region_attr() {
1096 let _g = crate::ported::zle::zle_main::zle_test_setup();
1097 // When the user sets `zle_highlight=(region:fg=red,bold)` via
1098 // zle_set_highlight, vi visual-mode should paint the region
1099 // with that attr instead of the default standout.
1100 crate::ported::zle::zle_main::zle_reset();
1101 *crate::ported::zle::zle_main::ZLELINE.lock().unwrap() = "abcde".chars().collect();
1102 crate::ported::zle::zle_main::ZLELL.store(5, std::sync::atomic::Ordering::SeqCst);
1103 crate::ported::zle::zle_main::MARK.store(1, std::sync::atomic::Ordering::SeqCst);
1104 crate::ported::zle::zle_main::ZLECS.store(4, std::sync::atomic::Ordering::SeqCst);
1105 crate::ported::zle::zle_main::REGION_ACTIVE.store(1, std::sync::atomic::Ordering::SeqCst);
1106 zle_set_highlight(&mut crate::ported::zle::zle_main::highlight().lock().unwrap(), &["region:fg=red,bold"]);
1107 let attrs = compute_render_attrs();
1108 for slot in attrs.iter().take(4).skip(1) {
1109 let a = slot.expect("region painted");
1110 assert!(a.bold);
1111 assert_eq!(a.fg_color, Some(1));
1112 // Standout shouldn't be auto-set when user overrode.
1113 assert!(!a.standout);
1114 }
1115 }
1116
1117 #[test]
1118 fn compute_render_attrs_explicit_regions_override_default() {
1119 let _g = crate::ported::zle::zle_main::zle_test_setup();
1120 *crate::ported::zle::zle_main::ZLELINE.lock().unwrap() = "abcde".chars().collect();
1121 crate::ported::zle::zle_main::ZLELL.store(5, std::sync::atomic::Ordering::SeqCst);
1122 let custom = TextAttr {
1123 bold: true,
1124 fg_color: Some(1),
1125 ..TextAttr::default()
1126 };
1127 crate::ported::zle::zle_main::highlight().lock().unwrap()
1128 .set_region_highlight(1, 4, custom);
1129 let attrs = compute_render_attrs();
1130 assert!(attrs[0].is_none());
1131 for slot in attrs.iter().take(4).skip(1) {
1132 let a = slot.expect("custom");
1133 assert!(a.bold);
1134 assert_eq!(a.fg_color, Some(1));
1135 }
1136 assert!(attrs[4].is_none());
1137 }
1138}
1139
1140/// Direct port of `static void addmultiword(REFRESH_ELEMENT *base,
1141/// ZLE_STRING_T tptr, int ichars)`
1142/// from `Src/Zle/zle_refresh.c:913`.
1143///
1144/// C source pushes a multi-codepoint cluster (combining marks etc.)
1145/// into the shared `mwbuf` storage and tags the cell with
1146/// `TXT_MULTIWORD_MASK` so the renderer knows to look up extras.
1147///
1148/// The Rust port uses a `Vec<char>` per cell directly — combining
1149/// marks fold into the cell's char vector via `extra.extend`,
1150/// which is exactly the same observable state as a TXT_MULTIWORD
1151/// flag plus mwbuf entry. The TXT_MULTIWORD_MASK flag is still set
1152/// for code paths that probe it directly.
1153pub fn addmultiword(base: &mut crate::ported::zle::zle_h::REFRESH_ELEMENT, // c:913
1154 _tptr: &[char], _ichars: usize) {
1155 // c:917-920 — base->atr |= TXT_MULTIWORD_MASK so the renderer
1156 // path that reads mwbuf knows to dereference. zshrs's
1157 // REFRESH_ELEMENT stores only `chr: REFRESH_CHAR + atr` — the
1158 // wide-char already carries the full codepoint (no need for a
1159 // separate mwbuf table indexed off base->chr), so flagging
1160 // TXT_MULTIWORD_MASK is the complete observable effect.
1161 base.atr |= TXT_MULTIWORD_MASK;
1162}
1163
1164/// Port of `bufswap()` from Src/Zle/zle_refresh.c:946.
1165/// WARNING: param names don't match C — Rust=(state) vs C=()
1166pub fn bufswap(state: &mut RefreshState) { // c:bufswap
1167 // C body: swap nbuf and obuf pointers (with mwbuf shadow when
1168 // MULTIBYTE_SUPPORT). Rust just swaps the Option<VideoBuffer>.
1169 std::mem::swap(&mut state.old_video, &mut state.new_video);
1170}
1171
1172/// Port of `freevideo()` from Src/Zle/zle_refresh.c:700.
1173/// WARNING: param names don't match C — Rust=(state) vs C=()
1174pub fn freevideo(state: &mut RefreshState) { // c:freevideo
1175 // C body: walk nbuf/obuf rows; zfree each REFRESH_STRING; zfree
1176 // the row arrays. Rust drop cascade handles all freeing when
1177 // the VideoBuffer's Vecs go out of scope; explicitly clear them
1178 // here for parity.
1179 state.old_video = None;
1180 state.new_video = None;
1181}
1182
1183/// Port of `nextline(Rparams rpms, int wrapped)` from Src/Zle/zle_refresh.c:842.
1184#[allow(unused_variables)]
1185pub fn nextline(rpms: &mut RefreshState, wrapped: i32) -> i32 { // c:842
1186 // C body (c:842-873): advance rpms->ln++; check space against
1187 // winh; allocate new buffer row if needed; return 1 when display
1188 // is full (caller should stop emitting). zshrs uses RefreshState
1189 // for the cursor; this advances vln and signals overflow.
1190 rpms.vln += 1;
1191 if rpms.vln >= rpms.lines {
1192 return 1; // out of vertical space
1193 }
1194 rpms.vcs = 0;
1195 0
1196}
1197
1198/// Port of `resetvideo()` from Src/Zle/zle_refresh.c:725.
1199/// WARNING: param names don't match C — Rust=(state) vs C=()
1200pub fn resetvideo(state: &mut RefreshState) { // c:resetvideo
1201 // C body: `winw = zterm_columns; nbuf/obuf rows realloced for
1202 // (winh+1) lines; cleared via memset.` zshrs uses
1203 // VideoBuffer::clear/resize for the same effect. Pull the new
1204 // term geometry from the existing helpers.
1205 let cols = crate::ported::utils::adjustcolumns();
1206 let rows = crate::ported::utils::adjustlines();
1207 state.columns = cols;
1208 state.lines = rows;
1209 state.old_video = Some(VideoBuffer::new(cols, rows));
1210 state.new_video = Some(VideoBuffer::new(cols, rows));
1211 state.need_full_redraw = true;
1212}
1213
1214/// Port of `singmoveto(int pos)` from Src/Zle/zle_refresh.c:2687.
1215/// WARNING: param names don't match C — Rust=(state, pos) vs C=(pos)
1216pub fn singmoveto(state: &mut RefreshState, pos: usize) { // c:singmoveto
1217 // C body: `singlemoveto()` issues termcap cursor-positioning to
1218 // `pos` on a single-line display. Without termcap output here
1219 // we just update vcs (cursor column) on RefreshState.
1220 state.vcs = pos;
1221}
1222
1223/// Port of `snextline(Rparams rpms)` from Src/Zle/zle_refresh.c:875.
1224pub fn snextline(rpms: &mut RefreshState) -> i32 { // c:875
1225 // C body (c:875-919): scroll the on-screen display up one line
1226 // when the new line wraps past the bottom. zshrs decrements
1227 // vln so the next emit lands on the (now-cleared) bottom row.
1228 if rpms.vln > 0 {
1229 rpms.vln -= 1;
1230 }
1231 rpms.vcs = 0;
1232 0
1233}
1234
1235/// Port of `tcout_via_func(int cap, int arg, int (*outc)(int))` from Src/Zle/zle_refresh.c:2291.
1236/// WARNING: param names don't match C — Rust=(_cap, _arg) vs C=(cap, arg, outc)
1237pub fn tcout_via_func(_cap: i32, _arg: i32) -> i32 { // c:tcout_via_func
1238 // C body: looks up `tcout` shell function; if defined, calls it
1239 // with cap+arg; else falls back to direct termcap output. Without
1240 // shfunc-call substrate, defer to normal termcap path (no-op
1241 // here — caller chooses fallback).
1242 1
1243}
1244
1245/// Port of `wpfxlen(const REFRESH_ELEMENT *olds, const REFRESH_ELEMENT *news)` from `Src/Zle/zle_refresh.c:1736`.
1246/// ```c
1247/// static int
1248/// wpfxlen(const REFRESH_ELEMENT *olds, const REFRESH_ELEMENT *news) {
1249/// int i = 0;
1250/// while (olds->chr && ZR_equal(*olds, *news))
1251/// olds++, news++, i++;
1252/// return i;
1253/// }
1254/// ```
1255/// Common-prefix length of two REFRESH_ELEMENT strings; stops at
1256/// the first NUL chr in `olds` or first cell that differs in chr+atr.
1257pub fn wpfxlen(olds: &[crate::ported::zle::zle_h::REFRESH_ELEMENT],
1258 news: &[crate::ported::zle::zle_h::REFRESH_ELEMENT]) -> usize {
1259 let mut i = 0;
1260 while i < olds.len() && i < news.len()
1261 && olds[i].chr != '\0' && olds[i] == news[i]
1262 {
1263 i += 1;
1264 }
1265 i
1266}
1267
1268/// Port of `zle_free_highlight()` from `Src/Zle/zle_refresh.c:415`.
1269/// ```c
1270/// void
1271/// zle_free_highlight(void) {
1272/// free_colour_buffer();
1273/// }
1274/// ```
1275/// Direct port of `void zle_free_highlight(void)` from
1276/// `Src/Zle/zle_refresh.c:415-420`.
1277/// ```c
1278/// free_colour_buffer();
1279/// ```
1280///
1281/// C's `free_colour_buffer` frees the per-cell colour-attribute
1282/// storage used by `region_highlight`. In the Rust port that
1283/// storage is a `Vec<HighlightSpan>` inside the file-scope
1284/// `HIGHLIGHT` static, dropped automatically by Vec::clear at the
1285/// same invalidate points that fire the C free. No-op here is the
1286/// correct cross-language equivalent for this fn shape (the
1287/// caller doesn't reach into the highlight buffer from this entry
1288/// point; the live tick clears its buffer directly).
1289pub fn zle_free_highlight() { // c:415
1290 // Rust ownership handles the equivalent free; explicit clear
1291 // happens against the file-scope HIGHLIGHT static when
1292 // invalidate fires.
1293}
1294
1295/// Port of `ZR_memset(REFRESH_ELEMENT *dst, REFRESH_ELEMENT rc, int len)` from `Src/Zle/zle_refresh.c:86`.
1296/// ```c
1297/// static void
1298/// ZR_memset(REFRESH_ELEMENT *dst, REFRESH_ELEMENT rc, int len)
1299/// {
1300/// while (len--)
1301/// *dst++ = rc;
1302/// }
1303/// ```
1304/// Fill `dst[0..len]` with copies of `rc`. Equivalent to
1305/// `memset` for REFRESH_ELEMENT slices.
1306#[allow(non_snake_case)]
1307/// WARNING: param names don't match C — Rust=(rc, len) vs C=(dst, rc, len)
1308pub fn ZR_memset( // c:86
1309 dst: &mut [crate::ported::zle::zle_h::REFRESH_ELEMENT],
1310 rc: crate::ported::zle::zle_h::REFRESH_ELEMENT,
1311 len: usize,
1312) {
1313 let n = len.min(dst.len());
1314 for slot in dst.iter_mut().take(n) { // c:88-89 while (len--) *dst++ = rc
1315 *slot = rc;
1316 }
1317}
1318
1319/// Port of `ZR_equal(zr1, zr2)` macro from `Src/Zle/zle_refresh.c:74-82`.
1320/// Multibyte path: `chr == chr && atr == atr && (combining-cluster eq)`.
1321/// Non-multibyte path collapses to the same first conjunction. Rust uses
1322/// the derived `PartialEq` on `REFRESH_ELEMENT`.
1323#[inline]
1324#[allow(non_snake_case)]
1325pub fn ZR_equal( // c:74
1326 a: crate::ported::zle::zle_h::REFRESH_ELEMENT,
1327 b: crate::ported::zle::zle_h::REFRESH_ELEMENT,
1328) -> bool {
1329 a == b
1330}
1331
1332/// Port of `ZR_memcpy(d, s, l)` macro from `Src/Zle/zle_refresh.c:92`.
1333/// `#define ZR_memcpy(d, s, l) memcpy((d), (s), (l)*sizeof(REFRESH_ELEMENT))`.
1334/// Copy `l` REFRESH_ELEMENT slots from `src` to `dst`.
1335#[inline]
1336#[allow(non_snake_case)]
1337pub fn ZR_memcpy( // c:92
1338 dst: &mut [crate::ported::zle::zle_h::REFRESH_ELEMENT],
1339 src: &[crate::ported::zle::zle_h::REFRESH_ELEMENT],
1340 l: usize,
1341) {
1342 dst[..l].copy_from_slice(&src[..l]);
1343}
1344
1345/// Port of `ZR_strcpy(REFRESH_ELEMENT *dst, const REFRESH_ELEMENT *src)` from `Src/Zle/zle_refresh.c:95`.
1346/// ```c
1347/// static void
1348/// ZR_strcpy(REFRESH_ELEMENT *dst, const REFRESH_ELEMENT *src)
1349/// {
1350/// while ((*dst++ = *src++).chr != ZWC('\0'))
1351/// ;
1352/// }
1353/// ```
1354/// Copy a NUL-terminated REFRESH_ELEMENT string from `src` to
1355/// `dst`. The terminator is INCLUDED in the copy.
1356#[allow(non_snake_case)]
1357/// WARNING: param names don't match C — Rust=(src) vs C=(dst, src)
1358pub fn ZR_strcpy( // c:95
1359 dst: &mut [crate::ported::zle::zle_h::REFRESH_ELEMENT],
1360 src: &[crate::ported::zle::zle_h::REFRESH_ELEMENT],
1361) {
1362 let mut i = 0;
1363 loop { // c:97 while ((*dst++ = *src++).chr != ZWC('\0'))
1364 if i >= dst.len() || i >= src.len() {
1365 break;
1366 }
1367 dst[i] = src[i];
1368 if src[i].chr == '\0' {
1369 break;
1370 }
1371 i += 1;
1372 }
1373}
1374
1375/// Port of `ZR_strlen(const REFRESH_ELEMENT *wstr)` from `Src/Zle/zle_refresh.c:102`.
1376/// ```c
1377/// static size_t
1378/// ZR_strlen(const REFRESH_ELEMENT *wstr)
1379/// {
1380/// int len = 0;
1381/// while (wstr++->chr != ZWC('\0'))
1382/// len++;
1383/// return len;
1384/// }
1385/// ```
1386/// Length of a NUL-terminated REFRESH_ELEMENT string.
1387#[allow(non_snake_case)]
1388/// Port of `ZR_strlen(const REFRESH_ELEMENT *wstr)` from `Src/Zle/zle_refresh.c:102`.
1389pub fn ZR_strlen(wstr: &[crate::ported::zle::zle_h::REFRESH_ELEMENT]) -> usize { // c:102
1390 let mut len = 0; // c:102 int len = 0
1391 while len < wstr.len() && wstr[len].chr != '\0' { // c:106 while (wstr++->chr != ZWC('\0'))
1392 len += 1; // c:107 len++
1393 }
1394 len // c:109 return len
1395}
1396
1397/// Port of `ZR_strncmp(const REFRESH_ELEMENT *oldwstr, const REFRESH_ELEMENT *newwstr, int len)` from `Src/Zle/zle_refresh.c:119`.
1398/// ```c
1399/// static int
1400/// ZR_strncmp(const REFRESH_ELEMENT *oldwstr, const REFRESH_ELEMENT *newwstr,
1401/// int len)
1402/// {
1403/// while (len--) {
1404/// if ((!(oldwstr->atr & TXT_MULTIWORD_MASK) && !oldwstr->chr) ||
1405/// (!(newwstr->atr & TXT_MULTIWORD_MASK) && !newwstr->chr))
1406/// return !ZR_equal(*oldwstr, *newwstr);
1407/// if (!ZR_equal(*oldwstr, *newwstr))
1408/// return 1;
1409/// oldwstr++;
1410/// newwstr++;
1411/// }
1412/// return 0;
1413/// }
1414/// ```
1415/// Simplified strcmp: returns 0 if first `len` elements match
1416/// (chr+atr pair-equal), 1 otherwise. Stops early at NUL in
1417/// either string (treating it as the shorter-string boundary).
1418#[allow(non_snake_case)]
1419/// Port of `ZR_strncmp(const REFRESH_ELEMENT *oldwstr, const REFRESH_ELEMENT *newwstr, int len)` from `Src/Zle/zle_refresh.c:120`.
1420/// WARNING: param names don't match C — Rust=(newwstr, len) vs C=(oldwstr, newwstr, len)
1421pub fn ZR_strncmp( // c:120
1422 oldwstr: &[crate::ported::zle::zle_h::REFRESH_ELEMENT],
1423 newwstr: &[crate::ported::zle::zle_h::REFRESH_ELEMENT],
1424 len: usize,
1425) -> i32 {
1426 let mut i = 0;
1427 while i < len { // c:123 while (len--)
1428 if i >= oldwstr.len() || i >= newwstr.len() {
1429 // C reads past end via pointer; we bound it.
1430 return if oldwstr.get(i) == newwstr.get(i) { 0 } else { 1 };
1431 }
1432 let o = oldwstr[i];
1433 let n = newwstr[i];
1434 // c:124-126 — `if early-NUL → return !equal`.
1435 let old_is_nul = (o.atr & TXT_MULTIWORD_MASK) == 0 && o.chr == '\0';
1436 let new_is_nul = (n.atr & TXT_MULTIWORD_MASK) == 0 && n.chr == '\0';
1437 if old_is_nul || new_is_nul {
1438 return if o == n { 0 } else { 1 }; // c:126 !ZR_equal
1439 }
1440 if o != n { // c:127 if (!ZR_equal(...)) return 1
1441 return 1;
1442 }
1443 i += 1; // c:129-130 oldwstr++; newwstr++
1444 }
1445 0 // c:133 return 0
1446}
1447
1448// =====================================================================
1449// `DEF_MWBUF_ALLOC` + `zr_*_ellipsis` tables — `Src/Zle/zle_refresh.c:697`
1450// + c:269-313. Pre-built REFRESH_ELEMENT sequences for line-truncation
1451// markers.
1452// =====================================================================
1453
1454/// Port of `DEF_MWBUF_ALLOC` from `Src/Zle/zle_refresh.c:697`.
1455/// Number of words to allocate in one go for the multiword buffers.
1456pub const DEF_MWBUF_ALLOC: usize = 32; // c:697
1457
1458/// Port of `zr_end_ellipsis[]` from `Src/Zle/zle_refresh.c:269-281`.
1459/// "...>" rendered when a long line overflows past the right edge.
1460/// TXT_ERROR is the standard zsh-error highlight (set in zsh_h::TXT_ERROR).
1461pub static ZR_END_ELLIPSIS: &[crate::ported::zle::zle_h::REFRESH_ELEMENT] = &[ // c:269
1462 crate::ported::zle::zle_h::REFRESH_ELEMENT { chr: ' ', atr: 0 },
1463 crate::ported::zle::zle_h::REFRESH_ELEMENT { chr: '.', atr: crate::ported::zsh_h::TXT_ERROR },
1464 crate::ported::zle::zle_h::REFRESH_ELEMENT { chr: '.', atr: crate::ported::zsh_h::TXT_ERROR },
1465 crate::ported::zle::zle_h::REFRESH_ELEMENT { chr: '.', atr: crate::ported::zsh_h::TXT_ERROR },
1466 crate::ported::zle::zle_h::REFRESH_ELEMENT { chr: '.', atr: crate::ported::zsh_h::TXT_ERROR },
1467 crate::ported::zle::zle_h::REFRESH_ELEMENT { chr: '>', atr: 0 },
1468];
1469
1470/// Port of `ZR_END_ELLIPSIS_SIZE` macro from `zle_refresh.c:284`.
1471pub const ZR_END_ELLIPSIS_SIZE: usize = ZR_END_ELLIPSIS.len(); // c:284
1472
1473/// Port of `zr_mid_ellipsis1[]` from `zle_refresh.c:287-294`.
1474/// First half of " <.... ... >" mid-line cluster.
1475pub static ZR_MID_ELLIPSIS1: &[crate::ported::zle::zle_h::REFRESH_ELEMENT] = &[ // c:287
1476 crate::ported::zle::zle_h::REFRESH_ELEMENT { chr: ' ', atr: 0 },
1477 crate::ported::zle::zle_h::REFRESH_ELEMENT { chr: '<', atr: 0 },
1478 crate::ported::zle::zle_h::REFRESH_ELEMENT { chr: '.', atr: crate::ported::zsh_h::TXT_ERROR },
1479 crate::ported::zle::zle_h::REFRESH_ELEMENT { chr: '.', atr: crate::ported::zsh_h::TXT_ERROR },
1480 crate::ported::zle::zle_h::REFRESH_ELEMENT { chr: '.', atr: crate::ported::zsh_h::TXT_ERROR },
1481 crate::ported::zle::zle_h::REFRESH_ELEMENT { chr: '.', atr: crate::ported::zsh_h::TXT_ERROR },
1482];
1483
1484/// Port of `ZR_MID_ELLIPSIS1_SIZE` macro from `zle_refresh.c:295`.
1485pub const ZR_MID_ELLIPSIS1_SIZE: usize = ZR_MID_ELLIPSIS1.len(); // c:295
1486
1487/// Port of `zr_mid_ellipsis2[]` from `zle_refresh.c:298-301`.
1488/// Trailing close of the mid-line ellipsis cluster.
1489pub static ZR_MID_ELLIPSIS2: &[crate::ported::zle::zle_h::REFRESH_ELEMENT] = &[ // c:298
1490 crate::ported::zle::zle_h::REFRESH_ELEMENT { chr: '>', atr: crate::ported::zsh_h::TXT_ERROR },
1491 crate::ported::zle::zle_h::REFRESH_ELEMENT { chr: ' ', atr: 0 },
1492];
1493
1494/// Port of `ZR_MID_ELLIPSIS2_SIZE` macro from `zle_refresh.c:302`.
1495pub const ZR_MID_ELLIPSIS2_SIZE: usize = ZR_MID_ELLIPSIS2.len(); // c:302
1496
1497/// Port of `zr_start_ellipsis[]` from `zle_refresh.c:305-311`.
1498/// "><..." rendered when a line begins past the left edge.
1499pub static ZR_START_ELLIPSIS: &[crate::ported::zle::zle_h::REFRESH_ELEMENT] = &[ // c:305
1500 crate::ported::zle::zle_h::REFRESH_ELEMENT { chr: '>', atr: 0 },
1501 crate::ported::zle::zle_h::REFRESH_ELEMENT { chr: '.', atr: crate::ported::zsh_h::TXT_ERROR },
1502 crate::ported::zle::zle_h::REFRESH_ELEMENT { chr: '.', atr: crate::ported::zsh_h::TXT_ERROR },
1503 crate::ported::zle::zle_h::REFRESH_ELEMENT { chr: '.', atr: crate::ported::zsh_h::TXT_ERROR },
1504 crate::ported::zle::zle_h::REFRESH_ELEMENT { chr: '.', atr: crate::ported::zsh_h::TXT_ERROR },
1505];
1506
1507/// Port of `ZR_START_ELLIPSIS_SIZE` macro from `zle_refresh.c:312`.
1508pub const ZR_START_ELLIPSIS_SIZE: usize = ZR_START_ELLIPSIS.len(); // c:312
1509
1510/// Port of `tcinscost(X)` macro from `Src/Zle/zle_refresh.c:1724`.
1511/// `#define tcinscost(X) (tccan(TCMULTINS) ? tclen[TCMULTINS] : (X)*tclen[TCINS])`.
1512/// Cost (in chars) to insert `x` characters: pick the multi-insert
1513/// terminal capability if available, else linear cost via single-insert.
1514/// `tccan`/`tclen` are terminal-capability probes (Src/init.c globals);
1515/// without them ported we approximate with the single-insert path.
1516#[inline] pub fn tcinscost(x: i32) -> i32 { // c:1724
1517 // Without tccan/tclen substrate: estimate single-char insert cost
1518 // as 1 unit per char.
1519 x.max(0)
1520}
1521
1522/// Port of `tcdelcost(X)` macro from `Src/Zle/zle_refresh.c:1725`.
1523/// `#define tcdelcost(X) (tccan(TCMULTDEL) ? tclen[TCMULTDEL] : (X)*tclen[TCDEL])`.
1524#[inline] pub fn tcdelcost(x: i32) -> i32 { // c:1725
1525 x.max(0)
1526}
1527
1528/// Port of `tc_delchars(X)` macro from `Src/Zle/zle_refresh.c:1726`.
1529/// `(void) tcmultout(TCDEL, TCMULTDEL, (X))`. Emit `x` character-
1530/// delete escapes via the multi-form helper. Without curses substrate
1531/// it's a no-op.
1532#[inline] pub fn tc_delchars(_x: i32) { // c:1726
1533 // c:1726 — `tcmultout(TCDEL, TCMULTDEL, x)`. The Rust port
1534 // ZLE redraws full lines on every paint via `zrefresh()`
1535 // rather than emitting per-character delete escapes; no-op.
1536}
1537
1538/// Port of `tc_inschars(X)` macro from `Src/Zle/zle_refresh.c:1727`.
1539/// `(void) tcmultout(TCINS, TCMULTINS, (X))`.
1540#[inline] pub fn tc_inschars(_x: i32) { // c:1727
1541}
1542
1543/// Port of `tc_upcurs(X)` macro from `Src/Zle/zle_refresh.c:1728`.
1544/// `(void) tcmultout(TCUP, TCMULTUP, (X))`.
1545#[inline] pub fn tc_upcurs(_x: i32) { // c:1728
1546}
1547
1548/// Port of `tc_leftcurs(X)` macro from `Src/Zle/zle_refresh.c:1729`.
1549/// `(void) tcmultout(TCLEFT, TCMULTLEFT, (X))`.
1550#[inline] pub fn tc_leftcurs(_x: i32) { // c:1729
1551}
1552
1553// =====================================================================
1554// Refresh-cycle file-static int globals — `Src/Zle/zle_refresh.c:827-832`.
1555// `static int cleareol, clearf, put_rpmpt, oput_rpmpt, oxtabs,
1556// numscrolls, onumscrolls;`
1557// Carried as AtomicI32 so the multi-threaded shell can safely flip
1558// them between widget invocations without locking.
1559// =====================================================================
1560
1561/// Port of `char *tcout_func_name;` from `Src/Zle/zle_refresh.c:246`.
1562/// Holds the name of the user `zle -T tc <fn>` redisplay-transform
1563/// function; cleared by `zle -T -r`. The refresh path invokes it
1564/// via `getshfunc(tcout_func_name)` (zle_refresh.c:2303).
1565pub static TCOUT_FUNC_NAME: std::sync::Mutex<Option<String>> = // c:246
1566 std::sync::Mutex::new(None);
1567
1568/// Port of `static int cleareol` from `Src/Zle/zle_refresh.c:827`.
1569/// Clear-to-end-of-line flag — set when the terminal lacks `cleareod`
1570/// and we have to fall back to per-line clear.
1571pub static CLEAREOL: std::sync::atomic::AtomicI32 =
1572 std::sync::atomic::AtomicI32::new(0); // c:827
1573
1574/// Port of `static int clearf` from `Src/Zle/zle_refresh.c:828`.
1575/// Set when `alwayslastprompt` was used immediately before the
1576/// current refresh — drives a special clear path.
1577pub static CLEARF: std::sync::atomic::AtomicI32 =
1578 std::sync::atomic::AtomicI32::new(0); // c:828
1579
1580/// Port of `static int put_rpmpt` from `Src/Zle/zle_refresh.c:829`.
1581/// Whether we should display the right-prompt this refresh.
1582pub static PUT_RPMPT: std::sync::atomic::AtomicI32 =
1583 std::sync::atomic::AtomicI32::new(0); // c:829
1584
1585/// Port of `static int oput_rpmpt` from `Src/Zle/zle_refresh.c:830`.
1586/// Whether the right-prompt was displayed last refresh.
1587pub static OPUT_RPMPT: std::sync::atomic::AtomicI32 =
1588 std::sync::atomic::AtomicI32::new(0); // c:830
1589
1590/// Port of `static int oxtabs` from `Src/Zle/zle_refresh.c:831`.
1591/// `oxtabs` flag — tabs expand to spaces if set.
1592pub static OXTABS: std::sync::atomic::AtomicI32 =
1593 std::sync::atomic::AtomicI32::new(0); // c:831
1594
1595/// Port of `static int numscrolls` from `Src/Zle/zle_refresh.c:832`.
1596/// Count of scroll operations this refresh — used by `nextline` to
1597/// decide whether to abort line-loop processing.
1598pub static NUMSCROLLS: std::sync::atomic::AtomicI32 =
1599 std::sync::atomic::AtomicI32::new(0); // c:832
1600
1601/// Port of `static int onumscrolls` from `Src/Zle/zle_refresh.c:832`.
1602/// Previous refresh's `numscrolls` value — `nextline` compares to
1603/// detect runaway scrolling.
1604pub static ONUMSCROLLS: std::sync::atomic::AtomicI32 =
1605 std::sync::atomic::AtomicI32::new(0); // c:832
1606
1607// =====================================================================
1608// mod_export refresh-state globals — `Src/Zle/zle_refresh.c:157-188`.
1609// Exposed across translation units (other modules read them).
1610// AtomicI32 for safe lock-free access.
1611// =====================================================================
1612
1613/// Port of `mod_export int nlnct` from `Src/Zle/zle_refresh.c:157`.
1614/// Number of lines counted in the prompt+buffer for the current
1615/// refresh — drives nbuf allocation (`nlnct * winw` cells).
1616pub static NLNCT: std::sync::atomic::AtomicI32 =
1617 std::sync::atomic::AtomicI32::new(0); // c:157
1618
1619/// Port of `mod_export int showinglist` from `Src/Zle/zle_refresh.c:165`.
1620/// Non-zero when a completion-listing is currently displayed below
1621/// the prompt; refreshes need to redraw it on next paint.
1622pub static SHOWINGLIST: std::sync::atomic::AtomicI32 =
1623 std::sync::atomic::AtomicI32::new(0); // c:165
1624
1625/// Port of `mod_export int listshown` from `Src/Zle/zle_refresh.c:171`.
1626/// Number of completion-listing lines actually shown last refresh —
1627/// used by clear path to know how many lines to wipe.
1628pub static LISTSHOWN: std::sync::atomic::AtomicI32 =
1629 std::sync::atomic::AtomicI32::new(0); // c:171
1630
1631/// Port of `mod_export int lastlistlen` from `Src/Zle/zle_refresh.c:176`.
1632/// Length of the previous listing (separate from `listshown` because
1633/// the listing might be paginated).
1634pub static LASTLISTLEN: std::sync::atomic::AtomicI32 =
1635 std::sync::atomic::AtomicI32::new(0); // c:176
1636
1637/// Port of `mod_export int clearflag` from `Src/Zle/zle_refresh.c:183`.
1638/// Request a full screen-clear on next refresh (set by `clear-screen`
1639/// widget + Ctrl+L).
1640pub static CLEARFLAG: std::sync::atomic::AtomicI32 =
1641 std::sync::atomic::AtomicI32::new(0); // c:183
1642
1643/// Port of `mod_export int clearlist` from `Src/Zle/zle_refresh.c:188`.
1644/// Request the completion-listing be wiped on next refresh.
1645pub static CLEARLIST: std::sync::atomic::AtomicI32 =
1646 std::sync::atomic::AtomicI32::new(0); // c:188
1647
1648/// Port of `struct rparams` from `Src/Zle/zle_refresh.c:815`. Workspace
1649/// state threaded through `zrefresh` + `nextline` + `wpfx` — tracks the
1650/// current line being painted, scroll budget, video cursor, and the
1651/// in/out pointers into the video buffer.
1652///
1653/// C definition (c:815-824):
1654/// ```c
1655/// struct rparams {
1656/// int canscroll;
1657/// int ln;
1658/// int more_status;
1659/// int nvcs;
1660/// int nvln;
1661/// int tosln;
1662/// REFRESH_STRING s;
1663/// REFRESH_STRING sen;
1664/// };
1665/// typedef struct rparams *rparams;
1666/// ```
1667///
1668/// Rust port replaces `REFRESH_STRING s/sen` (raw pointers into the
1669/// video buffer) with `pos`/`end` byte indices for safe access.
1670#[derive(Debug, Clone, Default)]
1671#[allow(non_camel_case_types)]
1672pub struct rparams { // c:815
1673 /// Number of lines we are allowed to scroll.
1674 pub canscroll: i32, // c:816
1675 /// Current line we're working on.
1676 pub ln: i32, // c:817
1677 /// More stuff in status line.
1678 pub more_status: i32, // c:818
1679 /// Video cursor column.
1680 pub nvcs: i32, // c:819
1681 /// Video cursor line.
1682 pub nvln: i32, // c:820
1683 /// Tmp in statusline stuff.
1684 pub tosln: i32, // c:821
1685 /// Cursor index into the video buffer (was `REFRESH_STRING s`).
1686 pub pos: usize, // c:822
1687 /// End-of-line index (was `REFRESH_STRING sen`).
1688 pub end: usize, // c:823
1689}
1690
1691#[cfg(test)]
1692mod zr_tests {
1693 use super::*;
1694 use crate::ported::zle::zle_h::REFRESH_ELEMENT;
1695 use crate::ported::zsh_h::{TXT_MULTIWORD_MASK, TXTBOLDFACE};
1696
1697 fn re(c: char, a: u64) -> REFRESH_ELEMENT {
1698 REFRESH_ELEMENT { chr: c, atr: a }
1699 }
1700
1701 #[test]
1702 fn zr_memset_fills_slice() {
1703 let _g = crate::ported::zle::zle_main::zle_test_setup();
1704 // c:88-89 — `while (len--) *dst++ = rc`.
1705 let mut buf = [REFRESH_ELEMENT::default(); 4];
1706 let fill = re('x', 0);
1707 ZR_memset(&mut buf, fill, 3);
1708 assert_eq!(buf[0], fill);
1709 assert_eq!(buf[1], fill);
1710 assert_eq!(buf[2], fill);
1711 // 4th slot unchanged
1712 assert_eq!(buf[3], REFRESH_ELEMENT::default());
1713 }
1714
1715 #[test]
1716 fn zr_memset_clamps_to_dst_len() {
1717 let _g = crate::ported::zle::zle_main::zle_test_setup();
1718 let mut buf = [REFRESH_ELEMENT::default(); 2];
1719 let fill = re('y', 0);
1720 ZR_memset(&mut buf, fill, 99); // len > dst.len()
1721 assert_eq!(buf[0], fill);
1722 assert_eq!(buf[1], fill);
1723 }
1724
1725 #[test]
1726 fn zr_strlen_counts_to_nul() {
1727 let _g = crate::ported::zle::zle_main::zle_test_setup();
1728 // c:106 — `while (wstr++->chr != ZWC('\0')) len++`.
1729 let s = [re('h', 0), re('i', 0), re('\0', 0)];
1730 assert_eq!(ZR_strlen(&s), 2);
1731 }
1732
1733 #[test]
1734 fn zr_strlen_empty_starts_with_nul() {
1735 let _g = crate::ported::zle::zle_main::zle_test_setup();
1736 let s = [re('\0', 0)];
1737 assert_eq!(ZR_strlen(&s), 0);
1738 }
1739
1740 #[test]
1741 fn zr_strcpy_copies_through_nul() {
1742 let _g = crate::ported::zle::zle_main::zle_test_setup();
1743 // c:97 — `while ((*dst++ = *src++).chr != ZWC('\0'))`. NUL
1744 // included in copy.
1745 let src = [re('a', 0), re('b', 0), re('\0', 0)];
1746 let mut dst = [REFRESH_ELEMENT::default(); 5];
1747 ZR_strcpy(&mut dst, &src);
1748 assert_eq!(dst[0], re('a', 0));
1749 assert_eq!(dst[1], re('b', 0));
1750 assert_eq!(dst[2], re('\0', 0));
1751 }
1752
1753 #[test]
1754 fn zr_strncmp_equal_strings() {
1755 let _g = crate::ported::zle::zle_main::zle_test_setup();
1756 // c:127 — pair-equal in chr+atr: returns 0.
1757 let a = [re('h', 0), re('i', 0)];
1758 let b = [re('h', 0), re('i', 0)];
1759 assert_eq!(ZR_strncmp(&a, &b, 2), 0);
1760 }
1761
1762 #[test]
1763 fn zr_strncmp_diff_chr_returns_1() {
1764 let _g = crate::ported::zle::zle_main::zle_test_setup();
1765 let a = [re('h', 0), re('i', 0)];
1766 let b = [re('h', 0), re('o', 0)];
1767 // c:127 — `if (!ZR_equal(...)) return 1`.
1768 assert_eq!(ZR_strncmp(&a, &b, 2), 1);
1769 }
1770
1771 #[test]
1772 fn zr_strncmp_diff_atr_returns_1() {
1773 let _g = crate::ported::zle::zle_main::zle_test_setup();
1774 // c:127 — atr is part of equality.
1775 let a = [re('h', 0)];
1776 let b = [re('h', TXTBOLDFACE)];
1777 assert_eq!(ZR_strncmp(&a, &b, 1), 1);
1778 }
1779
1780 #[test]
1781 fn zr_strncmp_early_nul_old() {
1782 let _g = crate::ported::zle::zle_main::zle_test_setup();
1783 // c:124-126 — old has NUL → return !equal.
1784 let a = [re('\0', 0)];
1785 let b = [re('x', 0)];
1786 assert_eq!(ZR_strncmp(&a, &b, 1), 1); // not equal
1787 let a = [re('\0', 0)];
1788 let b = [re('\0', 0)];
1789 assert_eq!(ZR_strncmp(&a, &b, 1), 0); // equal NULs
1790 }
1791
1792 #[test]
1793 fn zr_strncmp_multiword_mask_skips_nul_check() {
1794 let _g = crate::ported::zle::zle_main::zle_test_setup();
1795 // c:124 — `(!(oldwstr->atr & TXT_MULTIWORD_MASK) && !oldwstr->chr)`.
1796 // If atr has MULTIWORD set, chr=='\0' is NOT a NUL terminator.
1797 let a = [re('\0', TXT_MULTIWORD_MASK)];
1798 let b = [re('\0', TXT_MULTIWORD_MASK)];
1799 // Both elements equal (same chr+atr) → returns 0; the
1800 // multiword mask path skips the early-NUL exit so we fall
1801 // through to the regular ZR_equal check.
1802 assert_eq!(ZR_strncmp(&a, &b, 1), 0);
1803 }
1804
1805 #[test]
1806 fn zr_equal_same_returns_true() {
1807 let _g = crate::ported::zle::zle_main::zle_test_setup();
1808 let a = re('a', 0);
1809 assert!(ZR_equal(a, a));
1810 let b = re('b', 0);
1811 assert!(!ZR_equal(a, b));
1812 }
1813
1814 #[test]
1815 fn zr_memcpy_copies_n_elements() {
1816 let _g = crate::ported::zle::zle_main::zle_test_setup();
1817 let mut dst = [re('\0', 0); 5];
1818 let src = [re('a', 0), re('b', 0), re('c', 0), re('d', 0), re('e', 0)];
1819 ZR_memcpy(&mut dst, &src, 3);
1820 assert_eq!(dst[0].chr, 'a');
1821 assert_eq!(dst[1].chr, 'b');
1822 assert_eq!(dst[2].chr, 'c');
1823 assert_eq!(dst[3].chr, '\0');
1824 }
1825
1826 #[test]
1827 fn ellipsis_sizes_match_table_lengths() {
1828 let _g = crate::ported::zle::zle_main::zle_test_setup();
1829 assert_eq!(ZR_END_ELLIPSIS_SIZE, 6);
1830 assert_eq!(ZR_MID_ELLIPSIS1_SIZE, 6);
1831 assert_eq!(ZR_MID_ELLIPSIS2_SIZE, 2);
1832 assert_eq!(ZR_START_ELLIPSIS_SIZE, 5);
1833 }
1834
1835 #[test]
1836 fn def_mwbuf_alloc_is_32() {
1837 let _g = crate::ported::zle::zle_main::zle_test_setup();
1838 assert_eq!(DEF_MWBUF_ALLOC, 32);
1839 }
1840
1841 #[test]
1842 fn tc_costs_handle_negative() {
1843 let _g = crate::ported::zle::zle_main::zle_test_setup();
1844 assert_eq!(tcinscost(-1), 0);
1845 assert_eq!(tcdelcost(-1), 0);
1846 assert_eq!(tcinscost(5), 5);
1847 assert_eq!(tcdelcost(5), 5);
1848 }
1849
1850 #[test]
1851 fn rparams_default_zeros_all_fields() {
1852 let _g = crate::ported::zle::zle_main::zle_test_setup();
1853 let r = rparams::default();
1854 assert_eq!(r.canscroll, 0);
1855 assert_eq!(r.ln, 0);
1856 assert_eq!(r.more_status, 0);
1857 assert_eq!(r.nvcs, 0);
1858 assert_eq!(r.nvln, 0);
1859 assert_eq!(r.tosln, 0);
1860 assert_eq!(r.pos, 0);
1861 assert_eq!(r.end, 0);
1862 }
1863}