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 show_top_overlay(&mut self, component: Box<dyn Component>) -> u64 {
91 use crate::tui::overlay::{OverlayAnchor, OverlayOptions, SizeValue};
92 self.show_overlay(
93 component,
94 OverlayOptions {
95 width: Some(SizeValue::Percent(100.0)),
96 anchor: Some(OverlayAnchor::TopLeft),
97 ..Default::default()
98 },
99 )
100 }
101
102 pub fn hide_overlay(&mut self, id: u64) {
103 self.root.hide_overlay(id);
104 self.dirty = true;
105 }
106
107 pub fn pop_overlay(&mut self) {
108 self.root.pop_overlay();
109 self.dirty = true;
110 }
111
112 pub fn has_overlays(&self) -> bool {
113 self.root.has_overlays()
114 }
115
116 pub fn route_input(&mut self, key: &KeyEvent) -> bool {
122 self.root.handle_input(key)
123 }
124
125 pub fn route_paste(&mut self, text: &str) -> bool {
127 for entry in self.root.overlay_stack_mut().iter_mut().rev() {
129 if !entry.hidden {
130 entry.component.handle_paste(text);
131 return true;
132 }
133 }
134 false
135 }
136
137 pub fn render(
142 &mut self,
143 width: usize,
144 height: usize,
145 writer: &mut dyn Write,
146 ) -> io::Result<()> {
147 self.width = width;
148 self.height = height;
149 self.root.set_term_height(height);
150
151 let mut lines = self.root.render(width);
153
154 for line in lines.iter_mut() {
156 *line = normalize_terminal_output(line);
157 }
158
159 let cursor_pos = self
161 .screen
162 .render(lines.clone(), width as u16, height as u16, writer)?;
163
164 if let Some((row, col)) = cursor_pos {
166 self.position_hard_cursor(row, col, writer)?;
167 }
168
169 self.dirty = false;
170 Ok(())
171 }
172
173 pub fn finalize(&mut self, writer: &mut dyn Write) -> io::Result<()> {
175 self.screen.finalize(writer)
176 }
177
178 fn position_hard_cursor(
179 &mut self,
180 row: usize,
181 col: usize,
182 writer: &mut dyn Write,
183 ) -> io::Result<()> {
184 let total = self.screen.total_lines();
185 if total == 0 {
186 return Ok(());
187 }
188 let target_row = row.min(total.saturating_sub(1));
189 let target_col = col.min(self.width.saturating_sub(1));
190
191 let current_row = self.screen.hardware_cursor_row();
194 let row_delta = target_row as i32 - current_row as i32;
195 let mut buf = String::new();
196 if row_delta > 0 {
197 buf.push_str(&format!("\x1b[{}B", row_delta));
198 } else if row_delta < 0 {
199 buf.push_str(&format!("\x1b[{}A", -row_delta));
200 }
201 buf.push_str(&format!("\x1b[{}G", target_col + 1));
203
204 if !buf.is_empty() {
205 write!(writer, "{}", buf)?;
206 writer.flush()?;
207 }
208
209 self.screen.set_hardware_cursor_row(target_row);
212
213 Ok(())
214 }
215}
216
217impl Default for TUI {
218 fn default() -> Self {
219 Self::new()
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use crate::tui::overlay::{OverlayAnchor, OverlayOptions, SizeValue};
227
228 struct TestComponent {
229 text: String,
230 }
231
232 impl Component for TestComponent {
233 fn render(&mut self, _width: usize) -> Vec<String> {
234 vec![self.text.clone()]
235 }
236
237 fn handle_input(&mut self, _key: &crossterm::event::KeyEvent) -> bool {
238 false
239 }
240
241 fn invalidate(&mut self) {}
242 }
243
244 #[test]
245 fn test_tui_new() {
246 let tui = TUI::new();
247 assert!(!tui.has_overlays());
248 assert_eq!(tui.full_redraw_count(), 0);
249 }
250
251 #[test]
252 fn test_show_and_hide_overlay() {
253 let mut tui = TUI::new();
254 let id = tui.show_overlay(
255 Box::new(TestComponent {
256 text: "overlay".into(),
257 }),
258 OverlayOptions::default(),
259 );
260 assert!(tui.has_overlays());
261 tui.hide_overlay(id);
262 assert!(!tui.has_overlays());
263 }
264
265 #[test]
266 fn test_pop_overlay() {
267 let mut tui = TUI::new();
268 tui.show_overlay(
269 Box::new(TestComponent { text: "a".into() }),
270 OverlayOptions::default(),
271 );
272 tui.show_overlay(
273 Box::new(TestComponent { text: "b".into() }),
274 OverlayOptions::default(),
275 );
276 assert!(tui.has_overlays());
277 tui.pop_overlay();
278 assert!(tui.has_overlays()); tui.pop_overlay();
280 assert!(!tui.has_overlays());
281 }
282
283 #[test]
284 fn test_cursor_marker_extraction() {
285 use crate::tui::screen::Screen;
286 let screen = Screen::new();
287 let mut lines = vec![
288 "line 1".to_string(),
289 format!("before{}after", CURSOR_MARKER),
290 "line 3".to_string(),
291 ];
292 let pos = screen.extract_cursor_marker(&mut lines, 10);
293 assert!(pos.is_some());
294 let (row, col) = pos.unwrap();
295 assert_eq!(row, 1);
296 assert_eq!(col, 6); assert_eq!(lines[1], "beforeafter");
298 assert!(!lines[1].contains(CURSOR_MARKER));
299 }
300
301 #[test]
302 fn test_cursor_marker_outside_viewport() {
303 use crate::tui::screen::Screen;
304 let screen = Screen::new();
305 let mut lines = vec![
307 format!("{}marker", CURSOR_MARKER),
308 "b".to_string(),
309 "c".to_string(),
310 "d".to_string(),
311 "e".to_string(),
312 ];
313 let pos = screen.extract_cursor_marker(&mut lines, 2);
314 assert!(pos.is_none()); }
316
317 #[test]
318 fn test_overlay_layout_center_default() {
319 let mut c = Container::new();
321 c.set_term_height(24);
322 let child = crate::tui::components::Text::new("test", 0, 0, None);
323 c.show_overlay(Box::new(child), OverlayOptions::default());
324 let lines = c.render(80);
325 assert!(!lines.is_empty());
326 }
327
328 #[test]
329 fn test_overlay_layout_percent_width() {
330 let mut c = Container::new();
331 c.set_term_height(24);
332 let child = crate::tui::components::Text::new("x", 0, 0, None);
333 c.show_overlay(
334 Box::new(child),
335 OverlayOptions {
336 width: Some(SizeValue::Percent(50.0)),
337 ..Default::default()
338 },
339 );
340 let lines = c.render(80);
341 assert!(!lines.is_empty());
342 }
343
344 #[test]
345 fn test_overlay_layout_margin() {
346 let mut c = Container::new();
347 c.set_term_height(24);
348 let child = crate::tui::components::Text::new("test", 0, 0, None);
349 c.show_overlay(
350 Box::new(child),
351 OverlayOptions {
352 margin: Some(crate::tui::overlay::OverlayMargin {
353 top: 2,
354 right: 2,
355 bottom: 2,
356 left: 2,
357 }),
358 anchor: Some(OverlayAnchor::TopLeft),
359 ..Default::default()
360 },
361 );
362 let lines = c.render(80);
363 assert!(!lines.is_empty());
364 }
365}