Skip to main content

zsh/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
7use super::main::Zle;
8
9/// Text attributes for display
10#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
11pub struct TextAttr {
12    pub bold: bool,
13    pub underline: bool,
14    pub standout: bool,
15    pub blink: bool,
16    pub fg_color: Option<u8>,
17    pub bg_color: Option<u8>,
18}
19
20impl TextAttr {
21    pub fn to_ansi(&self) -> String {
22        let mut codes = Vec::new();
23        if self.bold {
24            codes.push("1".to_string());
25        }
26        if self.underline {
27            codes.push("4".to_string());
28        }
29        if self.standout {
30            codes.push("7".to_string());
31        }
32        if self.blink {
33            codes.push("5".to_string());
34        }
35        if let Some(fg) = self.fg_color {
36            codes.push(format!("38;5;{}", fg));
37        }
38        if let Some(bg) = self.bg_color {
39            codes.push(format!("48;5;{}", bg));
40        }
41        if codes.is_empty() {
42            String::new()
43        } else {
44            format!("\x1b[{}m", codes.join(";"))
45        }
46    }
47}
48
49/// A single display element (character + attributes)
50#[derive(Debug, Clone, Default)]
51pub struct RefreshElement {
52    pub chr: char,
53    pub atr: TextAttr,
54    pub width: u8,
55}
56
57impl RefreshElement {
58    pub fn new(chr: char) -> Self {
59        let width = unicode_width::UnicodeWidthChar::width(chr).unwrap_or(1) as u8;
60        RefreshElement {
61            chr,
62            atr: TextAttr::default(),
63            width,
64        }
65    }
66
67    pub fn with_attr(chr: char, atr: TextAttr) -> Self {
68        let width = unicode_width::UnicodeWidthChar::width(chr).unwrap_or(1) as u8;
69        RefreshElement { chr, atr, width }
70    }
71}
72
73/// Video buffer for screen state
74#[derive(Debug, Clone)]
75pub struct VideoBuffer {
76    /// Buffer contents - 2D array of lines
77    pub lines: Vec<Vec<RefreshElement>>,
78    /// Number of columns
79    pub cols: usize,
80    /// Number of rows
81    pub rows: usize,
82}
83
84impl VideoBuffer {
85    pub fn new(cols: usize, rows: usize) -> Self {
86        let lines = vec![vec![RefreshElement::new(' '); cols]; rows];
87        VideoBuffer { lines, cols, rows }
88    }
89
90    pub fn clear(&mut self) {
91        for line in &mut self.lines {
92            for elem in line.iter_mut() {
93                *elem = RefreshElement::new(' ');
94            }
95        }
96    }
97
98    pub fn resize(&mut self, cols: usize, rows: usize) {
99        self.cols = cols;
100        self.rows = rows;
101        self.lines
102            .resize(rows, vec![RefreshElement::new(' '); cols]);
103        for line in &mut self.lines {
104            line.resize(cols, RefreshElement::new(' '));
105        }
106    }
107
108    pub fn set(&mut self, row: usize, col: usize, elem: RefreshElement) {
109        if row < self.rows && col < self.cols {
110            self.lines[row][col] = elem;
111        }
112    }
113
114    pub fn get(&self, row: usize, col: usize) -> Option<&RefreshElement> {
115        self.lines.get(row).and_then(|line| line.get(col))
116    }
117}
118
119/// Refresh parameters
120#[derive(Debug, Clone, Default)]
121pub struct RefreshState {
122    /// Number of columns
123    pub columns: usize,
124    /// Number of lines  
125    pub lines: usize,
126    /// Current line on screen (cursor row)
127    pub vln: usize,
128    /// Current column on screen (cursor col)
129    pub vcs: usize,
130    /// Prompt width (left)
131    pub lpromptw: usize,
132    /// Right prompt width
133    pub rpromptw: usize,
134    /// Scroll offset for horizontal scrolling
135    pub scrolloff: usize,
136    /// Region highlight start
137    pub region_highlight_start: Option<usize>,
138    /// Region highlight end
139    pub region_highlight_end: Option<usize>,
140    /// Old video buffer
141    pub old_video: Option<VideoBuffer>,
142    /// New video buffer
143    pub new_video: Option<VideoBuffer>,
144    /// Prompt string (left)
145    pub lpromptbuf: String,
146    /// Right prompt string
147    pub rpromptbuf: String,
148    /// Whether we need full redraw
149    pub need_full_redraw: bool,
150    /// Predisplay string (before main buffer)
151    pub predisplay: String,
152    /// Postdisplay string (after main buffer)
153    pub postdisplay: String,
154}
155
156impl RefreshState {
157    pub fn new() -> Self {
158        let (cols, rows) = get_terminal_size();
159        RefreshState {
160            columns: cols,
161            lines: rows,
162            old_video: Some(VideoBuffer::new(cols, rows)),
163            new_video: Some(VideoBuffer::new(cols, rows)),
164            need_full_redraw: true,
165            ..Default::default()
166        }
167    }
168
169    pub fn reset_video(&mut self) {
170        let (cols, rows) = get_terminal_size();
171        self.columns = cols;
172        self.lines = rows;
173        self.old_video = Some(VideoBuffer::new(cols, rows));
174        self.new_video = Some(VideoBuffer::new(cols, rows));
175        self.need_full_redraw = true;
176    }
177
178    pub fn free_video(&mut self) {
179        self.old_video = None;
180        self.new_video = None;
181    }
182
183    pub fn swap_buffers(&mut self) {
184        std::mem::swap(&mut self.old_video, &mut self.new_video);
185        if let Some(ref mut new) = self.new_video {
186            new.clear();
187        }
188    }
189}
190
191impl Zle {
192    /// Main refresh function - redraws the screen
193    /// Port of zrefresh() from zle_refresh.c
194    pub fn zrefresh(&mut self) {
195        let stdout = io::stdout();
196        let mut handle = stdout.lock();
197
198        // Get terminal size
199        let (cols, _rows) = get_terminal_size();
200
201        // Build the display line
202        let prompt = self.prompt();
203        let buffer: String = self.zleline.iter().collect();
204        let cursor = self.zlecs;
205
206        // Calculate display positions
207        let prompt_width = visible_width(prompt);
208        let buffer_before_cursor: String = self.zleline[..cursor.min(self.zleline.len())]
209            .iter()
210            .collect();
211        let cursor_col = prompt_width + visible_width(&buffer_before_cursor);
212
213        // Handle horizontal scrolling if line is too long
214        let scroll_margin = 8;
215        let effective_cols = cols.saturating_sub(1);
216
217        let scroll_offset = if cursor_col >= effective_cols.saturating_sub(scroll_margin) {
218            cursor_col.saturating_sub(effective_cols / 2)
219        } else {
220            0
221        };
222
223        // Move to start of line and clear
224        let _ = write!(handle, "\r\x1b[K");
225
226        // Draw prompt (if not scrolled past)
227        if scroll_offset < prompt_width {
228            let visible_prompt = skip_chars(prompt, scroll_offset);
229            let _ = write!(handle, "{}", visible_prompt);
230        }
231
232        // Draw buffer content
233        let buffer_start = scroll_offset.saturating_sub(prompt_width);
234        let visible_buffer = skip_chars(&buffer, buffer_start);
235        let truncated = truncate_to_width(
236            &visible_buffer,
237            effective_cols.saturating_sub(prompt_width.saturating_sub(scroll_offset)),
238        );
239        let _ = write!(handle, "{}", truncated);
240
241        // Position cursor
242        let display_cursor_col = cursor_col.saturating_sub(scroll_offset);
243        let _ = write!(handle, "\r\x1b[{}C", display_cursor_col);
244
245        let _ = handle.flush();
246    }
247
248    /// Full screen refresh - clears and redraws everything
249    pub fn full_refresh(&mut self) -> io::Result<()> {
250        print!("\x1b[2J\x1b[H");
251        self.zrefresh();
252        io::stdout().flush()
253    }
254
255    /// Partial refresh (optimize for minimal updates)
256    pub fn partial_refresh(&mut self) -> io::Result<()> {
257        self.zrefresh();
258        io::stdout().flush()
259    }
260
261    /// Clear the screen
262    /// Port of clearscreen() from zle_refresh.c
263    pub fn clearscreen(&mut self) {
264        print!("\x1b[2J\x1b[H");
265        let _ = io::stdout().flush();
266        self.zrefresh();
267    }
268
269    /// Redisplay the current line
270    /// Port of redisplay() from zle_refresh.c
271    pub fn redisplay(&mut self) {
272        self.zrefresh();
273    }
274
275    /// Move cursor to position
276    /// Port of moveto() from zle_refresh.c
277    pub fn moveto(&mut self, row: usize, col: usize) {
278        // ANSI escape: ESC [ row ; col H (1-indexed)
279        print!("\x1b[{};{}H", row + 1, col + 1);
280        let _ = io::stdout().flush();
281    }
282
283    /// Move cursor down
284    /// Port of tc_downcurs() from zle_refresh.c  
285    pub fn tc_downcurs(&mut self, count: usize) {
286        if count > 0 {
287            print!("\x1b[{}B", count);
288            let _ = io::stdout().flush();
289        }
290    }
291
292    /// Move cursor right
293    /// Port of tc_rightcurs() from zle_refresh.c
294    pub fn tc_rightcurs(&mut self, count: usize) {
295        if count > 0 {
296            print!("\x1b[{}C", count);
297            let _ = io::stdout().flush();
298        }
299    }
300
301    /// Scroll window up
302    /// Port of scrollwindow() from zle_refresh.c
303    pub fn scrollwindow(&mut self, lines: i32) {
304        if lines > 0 {
305            // Scroll up
306            print!("\x1b[{}S", lines);
307        } else if lines < 0 {
308            // Scroll down
309            print!("\x1b[{}T", -lines);
310        }
311        let _ = io::stdout().flush();
312    }
313
314    /// Single line refresh
315    /// Port of singlerefresh() from zle_refresh.c
316    pub fn singlerefresh(&mut self) {
317        self.zrefresh();
318    }
319
320    /// Refresh a single line
321    /// Port of refreshline() from zle_refresh.c
322    pub fn refreshline(&mut self, _line: usize) {
323        self.zrefresh();
324    }
325
326    /// Write a wide character
327    /// Port of zwcputc() from zle_refresh.c
328    pub fn zwcputc(&self, c: char) {
329        print!("{}", c);
330    }
331
332    /// Write a string of wide characters
333    /// Port of zwcwrite() from zle_refresh.c
334    pub fn zwcwrite(&self, s: &str) {
335        print!("{}", s);
336    }
337}
338
339/// Get terminal size
340pub fn get_terminal_size() -> (usize, usize) {
341    unsafe {
342        let mut ws: libc::winsize = std::mem::zeroed();
343        if libc::ioctl(0, libc::TIOCGWINSZ, &mut ws) == 0 {
344            (ws.ws_col as usize, ws.ws_row as usize)
345        } else {
346            (80, 24) // Default
347        }
348    }
349}
350
351/// Calculate visible width of a string (handling ANSI escapes)
352fn visible_width(s: &str) -> usize {
353    let mut width = 0;
354    let mut in_escape = false;
355
356    for c in s.chars() {
357        if in_escape {
358            if c.is_ascii_alphabetic() {
359                in_escape = false;
360            }
361        } else if c == '\x1b' {
362            in_escape = true;
363        } else {
364            width += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
365        }
366    }
367
368    width
369}
370
371/// Skip N visible characters from a string
372fn skip_chars(s: &str, n: usize) -> &str {
373    let mut width = 0;
374    let mut byte_idx = 0;
375    let mut in_escape = false;
376
377    for (i, c) in s.char_indices() {
378        if width >= n {
379            byte_idx = i;
380            break;
381        }
382
383        if in_escape {
384            if c.is_ascii_alphabetic() {
385                in_escape = false;
386            }
387        } else if c == '\x1b' {
388            in_escape = true;
389        } else {
390            width += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
391        }
392        byte_idx = i + c.len_utf8();
393    }
394
395    &s[byte_idx..]
396}
397
398/// Truncate string to fit within given width
399fn truncate_to_width(s: &str, max_width: usize) -> &str {
400    let mut width = 0;
401    let mut byte_idx = s.len();
402    let mut in_escape = false;
403
404    for (i, c) in s.char_indices() {
405        if in_escape {
406            if c.is_ascii_alphabetic() {
407                in_escape = false;
408            }
409        } else if c == '\x1b' {
410            in_escape = true;
411        } else {
412            let char_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
413            if width + char_width > max_width {
414                byte_idx = i;
415                break;
416            }
417            width += char_width;
418        }
419    }
420
421    &s[..byte_idx]
422}
423
424/// Region highlight entry
425#[derive(Debug, Clone)]
426pub struct RegionHighlight {
427    pub start: usize,
428    pub end: usize,
429    pub attr: TextAttr,
430    pub memo: Option<String>,
431}
432
433/// Highlight manager
434#[derive(Debug, Default)]
435pub struct HighlightManager {
436    pub regions: Vec<RegionHighlight>,
437}
438
439impl HighlightManager {
440    pub fn new() -> Self {
441        HighlightManager {
442            regions: Vec::new(),
443        }
444    }
445
446    /// Set region highlight
447    /// Port of set_region_highlight() from zle_refresh.c
448    pub fn set_region_highlight(&mut self, start: usize, end: usize, attr: TextAttr) {
449        self.regions.push(RegionHighlight {
450            start,
451            end,
452            attr,
453            memo: None,
454        });
455    }
456
457    /// Get region highlight for position
458    /// Port of get_region_highlight() from zle_refresh.c  
459    pub fn get_region_highlight(&self, pos: usize) -> Option<&RegionHighlight> {
460        self.regions.iter().find(|r| pos >= r.start && pos < r.end)
461    }
462
463    /// Unset region highlight
464    /// Port of unset_region_highlight() from zle_refresh.c
465    pub fn unset_region_highlight(&mut self) {
466        self.regions.clear();
467    }
468
469    /// Free highlight resources
470    /// Port of zle_free_highlight() from zle_refresh.c
471    pub fn free(&mut self) {
472        self.regions.clear();
473    }
474}
475
476/// Terminal output functions
477/// Port of tcout() family from zle_refresh.c
478
479pub fn tcout(cap: &str) {
480    print!("{}", cap);
481}
482
483pub fn tcoutarg(cap: &str, arg: i32) {
484    // Simple substitution for %d in capability string
485    let s = cap.replace("%d", &arg.to_string());
486    print!("{}", s);
487}
488
489pub fn tcmultout(cap: &str, count: i32) {
490    for _ in 0..count {
491        print!("{}", cap);
492    }
493}
494
495pub fn tcoutclear(to_end: bool) {
496    if to_end {
497        print!("\x1b[J"); // Clear to end of screen
498    } else {
499        print!("\x1b[2J"); // Clear entire screen
500    }
501}
502
503/// Initialize ZLE refresh subsystem
504/// Port of zle_refresh_boot() from zle_refresh.c
505pub fn zle_refresh_boot() -> RefreshState {
506    RefreshState::new()
507}
508
509/// Cleanup ZLE refresh subsystem
510/// Port of zle_refresh_finish() from zle_refresh.c
511pub fn zle_refresh_finish(state: &mut RefreshState) {
512    state.free_video();
513}
514
515/// Set ZLE highlight
516/// Port of zle_set_highlight() from zle_refresh.c
517pub fn zle_set_highlight(_highlight: &str) {
518    // Parse highlight specification and apply
519    // Format: "region:standout" or "special:fg=red,bg=blue"
520}
521
522#[cfg(test)]
523mod tests {
524    use super::*;
525
526    #[test]
527    fn test_visible_width() {
528        assert_eq!(visible_width("hello"), 5);
529        assert_eq!(visible_width("\x1b[31mhello\x1b[0m"), 5);
530        assert_eq!(visible_width("日本語"), 6); // 3 chars, 2 width each
531    }
532
533    #[test]
534    fn test_video_buffer() {
535        let mut buf = VideoBuffer::new(80, 24);
536        assert_eq!(buf.cols, 80);
537        assert_eq!(buf.rows, 24);
538
539        buf.set(0, 0, RefreshElement::new('A'));
540        assert_eq!(buf.get(0, 0).map(|e| e.chr), Some('A'));
541
542        buf.clear();
543        assert_eq!(buf.get(0, 0).map(|e| e.chr), Some(' '));
544    }
545
546    #[test]
547    fn test_refresh_state() {
548        let mut state = RefreshState::new();
549        assert!(state.old_video.is_some());
550        assert!(state.new_video.is_some());
551
552        state.swap_buffers();
553        state.free_video();
554        assert!(state.old_video.is_none());
555    }
556}