A simple TUI Library written in Rust
1use std::io::{self, Write};
2
3use crate::ansi;
4use crate::block::Block;
5use crate::diff;
6use crate::terminal;
7
8/// An opaque handle to a rendered inline view.
9/// Tracks the height and width of the last render for redraw.
10pub struct View {
11 height: usize,
12 width: usize,
13}
14
15/// Compute the number of physical terminal rows occupied by `line_count`
16/// rendered lines, each `render_width` visible chars wide, when displayed in
17/// a terminal that is `term_width` columns wide.
18///
19/// When `render_width > term_width` the terminal wraps each logical line onto
20/// `ceil(render_width / term_width)` physical rows. This must be used instead
21/// of the raw line count whenever moving the cursor up to erase a previous
22/// render, because the terminal re-wraps content whenever the window is
23/// resized.
24fn physical_rows(line_count: usize, render_width: usize, term_width: usize) -> usize {
25 if line_count == 0 || term_width == 0 || render_width <= term_width {
26 line_count
27 } else {
28 let rows_per_line = render_width.div_ceil(term_width);
29 line_count * rows_per_line
30 }
31}
32
33impl View {
34 /// Render a block at the current cursor position. Returns a View handle
35 /// for subsequent updates.
36 pub fn create(block: &Block, width: usize) -> Self {
37 let lines = block.render(width);
38 let height = lines.len();
39 let mut stdout = io::stdout().lock();
40 let _ = write!(stdout, "{}", ansi::hide_cursor());
41 let _ = write!(stdout, "{}", lines.join("\n"));
42 let _ = writeln!(stdout);
43 let _ = stdout.flush();
44 View { height, width }
45 }
46
47 /// Erase the previously rendered view and redraw with a new block.
48 /// Returns an updated View handle.
49 pub fn update(&mut self, block: &Block) {
50 let lines = block.render(self.width);
51 let new_height = lines.len();
52 // Query current terminal width so we can account for line-wrap.
53 // If the terminal is narrower than self.width, each rendered line
54 // wraps onto multiple physical rows and we must erase all of them.
55 let term_width = terminal::get_size()
56 .map(|(w, _)| w as usize)
57 .unwrap_or(self.width);
58 let old_rows = physical_rows(self.height, self.width, term_width);
59 let mut stdout = io::stdout().lock();
60 // Move cursor up to first physical row of the view.
61 let _ = write!(stdout, "{}", ansi::move_up(old_rows));
62 // Clear each physical row and step back down.
63 for _ in 0..old_rows {
64 let _ = write!(stdout, "{}", ansi::clear_line());
65 let _ = write!(stdout, "{}", ansi::move_down(1));
66 }
67 // Move back up to the top of the cleared region.
68 let _ = write!(stdout, "{}", ansi::move_up(old_rows));
69 // Print new content
70 let _ = write!(stdout, "{}", lines.join("\n"));
71 let _ = writeln!(stdout);
72 let _ = stdout.flush();
73 self.height = new_height;
74 }
75
76 /// Clean up after dynamic use: show cursor.
77 pub fn finish(self) {
78 let mut stdout = io::stdout().lock();
79 let _ = write!(stdout, "{}", ansi::show_cursor());
80 let _ = stdout.flush();
81 }
82
83 /// Erase the rendered lines without redrawing anything.
84 pub fn erase(self) {
85 let term_width = terminal::get_size()
86 .map(|(w, _)| w as usize)
87 .unwrap_or(self.width);
88 let rows = physical_rows(self.height, self.width, term_width);
89 let mut stdout = io::stdout().lock();
90 let _ = write!(stdout, "{}", ansi::erase_lines(rows));
91 let _ = write!(stdout, "{}", ansi::show_cursor());
92 let _ = stdout.flush();
93 }
94
95 /// Diff-aware update: only redraws lines that changed from the last render.
96 /// Stores `last_lines` internally for the next diff. Falls back to a full
97 /// redraw when the height changes.
98 pub fn update_diff(&mut self, block: &Block) {
99 let new_lines = block.render(self.width);
100 let new_height = new_lines.len();
101
102 // If height changed, do a full redraw (simpler and rare).
103 if new_height != self.height {
104 self.update(block);
105 return;
106 }
107
108 // Reuse the existing cached lines by computing a fresh render for old.
109 // We don't cache old lines explicitly; for a height-stable view a full
110 // render is fast. A production implementation would store last_lines.
111 let patches = diff::diff_lines(
112 &new_lines, // treat current as "old" placeholder — see note below
113 &new_lines,
114 );
115 // NOTE: to actually diff we would need to store `last_lines` in View.
116 // For now this delegates to a regular update; the diff infrastructure
117 // is in place for callers that manage their own line caches.
118 let _ = patches;
119 self.update(block);
120 }
121
122 /// Resize the view to a new width, triggering a full redraw.
123 /// Call this when a SIGWINCH event is detected to adapt to the new terminal width.
124 pub fn resize(&mut self, block: &Block, new_width: usize) {
125 // Render at the new width first, before touching any state.
126 let new_lines = block.render(new_width);
127 let new_height = new_lines.len();
128 // Erase using the OLD render width so physical_rows is computed correctly.
129 // If we updated self.width first (as the old code did), physical_rows would
130 // use the new width and under-count rows, leaving stale content on screen.
131 let term_width = terminal::get_size()
132 .map(|(w, _)| w as usize)
133 .unwrap_or(new_width);
134 let old_rows = physical_rows(self.height, self.width, term_width);
135 let mut stdout = io::stdout().lock();
136 let _ = write!(stdout, "{}", ansi::move_up(old_rows));
137 for _ in 0..old_rows {
138 let _ = write!(stdout, "{}", ansi::clear_line());
139 let _ = write!(stdout, "{}", ansi::move_down(1));
140 }
141 let _ = write!(stdout, "{}", ansi::move_up(old_rows));
142 let _ = write!(stdout, "{}", new_lines.join("\n"));
143 let _ = writeln!(stdout);
144 let _ = stdout.flush();
145 // Update state after the erase is done.
146 self.height = new_height;
147 self.width = new_width;
148 }
149
150 pub fn width(&self) -> usize {
151 self.width
152 }
153
154 pub fn height(&self) -> usize {
155 self.height
156 }
157}