1use std::io::{self, Write};
2
3use crossterm::event::KeyEvent;
4
5use crate::tui::Component;
6use crate::tui::container::Container;
7use crate::tui::overlay::OverlayOptions;
8use crate::tui::screen::Screen;
9use crate::tui::util::normalize_terminal_output;
10
11pub const CURSOR_MARKER: &str = "\x1b_pi:c\x07";
13
14pub struct TUI {
23 pub root: Container,
27
28 screen: Screen,
30 width: usize,
32 height: usize,
33 dirty: bool,
35}
36
37impl TUI {
38 pub fn new() -> Self {
39 Self {
40 root: Container::new(),
41 screen: Screen::new(),
42 width: 80,
43 height: 24,
44 dirty: true,
45 }
46 }
47
48 pub fn screen_mut(&mut self) -> &mut Screen {
51 &mut self.screen
52 }
53
54 pub fn full_redraw_count(&self) -> usize {
55 self.screen.full_redraw_count()
56 }
57
58 pub fn set_clear_on_shrink(&mut self, enabled: bool) {
59 self.screen.set_clear_on_shrink(enabled);
60 }
61
62 pub fn set_dimensions(&mut self, width: usize, height: usize) {
63 self.width = width;
64 self.height = height;
65 self.root.set_term_height(height);
66 }
67
68 pub fn get_dimensions(&self) -> (usize, usize) {
69 (self.width, self.height)
70 }
71
72 pub fn request_render(&mut self) {
73 self.dirty = true;
74 }
75
76 pub fn is_dirty(&self) -> bool {
77 self.dirty
78 }
79
80 pub fn show_overlay(&mut self, component: Box<dyn Component>, options: OverlayOptions) -> u64 {
83 let id = self.root.show_overlay(component, options);
84 self.dirty = true;
85 id
86 }
87
88 pub fn hide_overlay(&mut self, id: u64) {
89 self.root.hide_overlay(id);
90 self.dirty = true;
91 }
92
93 pub fn pop_overlay(&mut self) {
94 self.root.pop_overlay();
95 self.dirty = true;
96 }
97
98 pub fn has_overlays(&self) -> bool {
99 self.root.has_overlays()
100 }
101
102 pub fn route_input(&mut self, key: &KeyEvent) -> bool {
108 self.root.handle_input(key)
109 }
110
111 pub fn route_paste(&mut self, text: &str) -> bool {
113 for entry in self.root.overlay_stack_mut().iter_mut().rev() {
115 if !entry.hidden {
116 entry.component.handle_paste(text);
117 return true;
118 }
119 }
120 false
121 }
122
123 pub fn render(
128 &mut self,
129 width: usize,
130 height: usize,
131 writer: &mut dyn Write,
132 ) -> io::Result<()> {
133 self.width = width;
134 self.height = height;
135 self.root.set_term_height(height);
136
137 let mut lines = self.root.render(width);
139
140 for line in lines.iter_mut() {
142 *line = normalize_terminal_output(line);
143 }
144
145 let cursor_pos = self
147 .screen
148 .render(lines.clone(), width as u16, height as u16, writer)?;
149
150 if let Some((row, col)) = cursor_pos {
152 self.position_hard_cursor(row, col, writer)?;
153 }
154
155 self.dirty = false;
156 Ok(())
157 }
158
159 pub fn finalize(&mut self, writer: &mut dyn Write) -> io::Result<()> {
161 self.screen.finalize(writer)
162 }
163
164 fn position_hard_cursor(
165 &mut self,
166 row: usize,
167 col: usize,
168 writer: &mut dyn Write,
169 ) -> io::Result<()> {
170 let total = self.screen.total_lines();
171 if total == 0 {
172 return Ok(());
173 }
174 let target_row = row.min(total.saturating_sub(1));
175 let target_col = col.min(self.width.saturating_sub(1));
176
177 let current_row = self.screen.hardware_cursor_row();
180 let row_delta = target_row as i32 - current_row as i32;
181 let mut buf = String::new();
182 if row_delta > 0 {
183 buf.push_str(&format!("\x1b[{}B", row_delta));
184 } else if row_delta < 0 {
185 buf.push_str(&format!("\x1b[{}A", -row_delta));
186 }
187 buf.push_str(&format!("\x1b[{}G", target_col + 1));
189
190 if !buf.is_empty() {
191 write!(writer, "{}", buf)?;
192 writer.flush()?;
193 }
194
195 self.screen.set_hardware_cursor_row(target_row);
198
199 Ok(())
200 }
201}
202
203impl Default for TUI {
204 fn default() -> Self {
205 Self::new()
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use crate::tui::overlay::{OverlayAnchor, OverlayOptions, SizeValue};
213
214 struct TestComponent {
215 text: String,
216 }
217
218 impl Component for TestComponent {
219 fn render(&mut self, _width: usize) -> Vec<String> {
220 vec![self.text.clone()]
221 }
222
223 fn handle_input(&mut self, _key: &crossterm::event::KeyEvent) -> bool {
224 false
225 }
226
227 fn invalidate(&mut self) {}
228 }
229
230 #[test]
231 fn test_tui_new() {
232 let tui = TUI::new();
233 assert!(!tui.has_overlays());
234 assert_eq!(tui.full_redraw_count(), 0);
235 }
236
237 #[test]
238 fn test_show_and_hide_overlay() {
239 let mut tui = TUI::new();
240 let id = tui.show_overlay(
241 Box::new(TestComponent {
242 text: "overlay".into(),
243 }),
244 OverlayOptions::default(),
245 );
246 assert!(tui.has_overlays());
247 tui.hide_overlay(id);
248 assert!(!tui.has_overlays());
249 }
250
251 #[test]
252 fn test_pop_overlay() {
253 let mut tui = TUI::new();
254 tui.show_overlay(
255 Box::new(TestComponent { text: "a".into() }),
256 OverlayOptions::default(),
257 );
258 tui.show_overlay(
259 Box::new(TestComponent { text: "b".into() }),
260 OverlayOptions::default(),
261 );
262 assert!(tui.has_overlays());
263 tui.pop_overlay();
264 assert!(tui.has_overlays()); tui.pop_overlay();
266 assert!(!tui.has_overlays());
267 }
268
269 #[test]
270 fn test_cursor_marker_extraction() {
271 use crate::tui::screen::Screen;
272 let screen = Screen::new();
273 let mut lines = vec![
274 "line 1".to_string(),
275 format!("before{}after", CURSOR_MARKER),
276 "line 3".to_string(),
277 ];
278 let pos = screen.extract_cursor_marker(&mut lines, 10);
279 assert!(pos.is_some());
280 let (row, col) = pos.unwrap();
281 assert_eq!(row, 1);
282 assert_eq!(col, 6); assert_eq!(lines[1], "beforeafter");
284 assert!(!lines[1].contains(CURSOR_MARKER));
285 }
286
287 #[test]
288 fn test_cursor_marker_outside_viewport() {
289 use crate::tui::screen::Screen;
290 let screen = Screen::new();
291 let mut lines = vec![
293 format!("{}marker", CURSOR_MARKER),
294 "b".to_string(),
295 "c".to_string(),
296 "d".to_string(),
297 "e".to_string(),
298 ];
299 let pos = screen.extract_cursor_marker(&mut lines, 2);
300 assert!(pos.is_none()); }
302
303 #[test]
304 fn test_overlay_layout_center_default() {
305 let mut c = Container::new();
307 c.set_term_height(24);
308 let child = crate::tui::components::Text::new("test", 0, 0, None);
309 c.show_overlay(Box::new(child), OverlayOptions::default());
310 let lines = c.render(80);
311 assert!(!lines.is_empty());
312 }
313
314 #[test]
315 fn test_overlay_layout_percent_width() {
316 let mut c = Container::new();
317 c.set_term_height(24);
318 let child = crate::tui::components::Text::new("x", 0, 0, None);
319 c.show_overlay(
320 Box::new(child),
321 OverlayOptions {
322 width: Some(SizeValue::Percent(50.0)),
323 ..Default::default()
324 },
325 );
326 let lines = c.render(80);
327 assert!(!lines.is_empty());
328 }
329
330 #[test]
331 fn test_overlay_layout_margin() {
332 let mut c = Container::new();
333 c.set_term_height(24);
334 let child = crate::tui::components::Text::new("test", 0, 0, None);
335 c.show_overlay(
336 Box::new(child),
337 OverlayOptions {
338 margin: Some(crate::tui::overlay::OverlayMargin {
339 top: 2,
340 right: 2,
341 bottom: 2,
342 left: 2,
343 }),
344 anchor: Some(OverlayAnchor::TopLeft),
345 ..Default::default()
346 },
347 );
348 let lines = c.render(80);
349 assert!(!lines.is_empty());
350 }
351}