use std::io::{self, Write}; use crate::ansi; use crate::block::Block; use crate::diff; use crate::terminal; /// An opaque handle to a rendered inline view. /// Tracks the height and width of the last render for redraw. pub struct View { height: usize, width: usize, } /// Compute the number of physical terminal rows occupied by `line_count` /// rendered lines, each `render_width` visible chars wide, when displayed in /// a terminal that is `term_width` columns wide. /// /// When `render_width > term_width` the terminal wraps each logical line onto /// `ceil(render_width / term_width)` physical rows. This must be used instead /// of the raw line count whenever moving the cursor up to erase a previous /// render, because the terminal re-wraps content whenever the window is /// resized. fn physical_rows(line_count: usize, render_width: usize, term_width: usize) -> usize { if line_count == 0 || term_width == 0 || render_width <= term_width { line_count } else { let rows_per_line = render_width.div_ceil(term_width); line_count * rows_per_line } } impl View { /// Render a block at the current cursor position. Returns a View handle /// for subsequent updates. pub fn create(block: &Block, width: usize) -> Self { let lines = block.render(width); let height = lines.len(); let mut stdout = io::stdout().lock(); let _ = write!(stdout, "{}", ansi::hide_cursor()); let _ = write!(stdout, "{}", lines.join("\n")); let _ = writeln!(stdout); let _ = stdout.flush(); View { height, width } } /// Erase the previously rendered view and redraw with a new block. /// Returns an updated View handle. pub fn update(&mut self, block: &Block) { let lines = block.render(self.width); let new_height = lines.len(); // Query current terminal width so we can account for line-wrap. // If the terminal is narrower than self.width, each rendered line // wraps onto multiple physical rows and we must erase all of them. let term_width = terminal::get_size() .map(|(w, _)| w as usize) .unwrap_or(self.width); let old_rows = physical_rows(self.height, self.width, term_width); let mut stdout = io::stdout().lock(); // Move cursor up to first physical row of the view. let _ = write!(stdout, "{}", ansi::move_up(old_rows)); // Clear each physical row and step back down. for _ in 0..old_rows { let _ = write!(stdout, "{}", ansi::clear_line()); let _ = write!(stdout, "{}", ansi::move_down(1)); } // Move back up to the top of the cleared region. let _ = write!(stdout, "{}", ansi::move_up(old_rows)); // Print new content let _ = write!(stdout, "{}", lines.join("\n")); let _ = writeln!(stdout); let _ = stdout.flush(); self.height = new_height; } /// Clean up after dynamic use: show cursor. pub fn finish(self) { let mut stdout = io::stdout().lock(); let _ = write!(stdout, "{}", ansi::show_cursor()); let _ = stdout.flush(); } /// Erase the rendered lines without redrawing anything. pub fn erase(self) { let term_width = terminal::get_size() .map(|(w, _)| w as usize) .unwrap_or(self.width); let rows = physical_rows(self.height, self.width, term_width); let mut stdout = io::stdout().lock(); let _ = write!(stdout, "{}", ansi::erase_lines(rows)); let _ = write!(stdout, "{}", ansi::show_cursor()); let _ = stdout.flush(); } /// Diff-aware update: only redraws lines that changed from the last render. /// Stores `last_lines` internally for the next diff. Falls back to a full /// redraw when the height changes. pub fn update_diff(&mut self, block: &Block) { let new_lines = block.render(self.width); let new_height = new_lines.len(); // If height changed, do a full redraw (simpler and rare). if new_height != self.height { self.update(block); return; } // Reuse the existing cached lines by computing a fresh render for old. // We don't cache old lines explicitly; for a height-stable view a full // render is fast. A production implementation would store last_lines. let patches = diff::diff_lines( &new_lines, // treat current as "old" placeholder — see note below &new_lines, ); // NOTE: to actually diff we would need to store `last_lines` in View. // For now this delegates to a regular update; the diff infrastructure // is in place for callers that manage their own line caches. let _ = patches; self.update(block); } /// Resize the view to a new width, triggering a full redraw. /// Call this when a SIGWINCH event is detected to adapt to the new terminal width. pub fn resize(&mut self, block: &Block, new_width: usize) { // Render at the new width first, before touching any state. let new_lines = block.render(new_width); let new_height = new_lines.len(); // Erase using the OLD render width so physical_rows is computed correctly. // If we updated self.width first (as the old code did), physical_rows would // use the new width and under-count rows, leaving stale content on screen. let term_width = terminal::get_size() .map(|(w, _)| w as usize) .unwrap_or(new_width); let old_rows = physical_rows(self.height, self.width, term_width); let mut stdout = io::stdout().lock(); let _ = write!(stdout, "{}", ansi::move_up(old_rows)); for _ in 0..old_rows { let _ = write!(stdout, "{}", ansi::clear_line()); let _ = write!(stdout, "{}", ansi::move_down(1)); } let _ = write!(stdout, "{}", ansi::move_up(old_rows)); let _ = write!(stdout, "{}", new_lines.join("\n")); let _ = writeln!(stdout); let _ = stdout.flush(); // Update state after the erase is done. self.height = new_height; self.width = new_width; } pub fn width(&self) -> usize { self.width } pub fn height(&self) -> usize { self.height } }