Skip to main content

rpi_tui/
tui_main_screen.rs

1//! TuiMainScreen - Main screen TUI with terminal scrollback.
2//!
3//! This mode renders content vertically and allows the terminal to handle scrolling.
4//! It's suitable for simple output that should be preserved in terminal history.
5
6use std::any::Any;
7use std::sync::{Arc, Mutex};
8
9use super::component::Component;
10use super::container::Container;
11use super::layout::composite_tui_line;
12use super::overlay::OverlayManager;
13use super::tui::{OverlayHandle, OverlayOptions, TuiMode, TuiStopOptions, TUI};
14use crate::terminal::Terminal;
15
16/// Main screen TUI that uses terminal scrollback for scrolling.
17pub struct TuiMainScreen {
18    terminal: Box<dyn Terminal>,
19    container: Container,
20    running: Mutex<bool>,
21    show_hardware_cursor: Mutex<bool>,
22    clear_on_shrink: Mutex<bool>,
23    focused: Mutex<Option<Arc<dyn Component>>>,
24    full_redraw_count: Mutex<usize>,
25    overlays: Arc<OverlayManager>,
26}
27
28impl TuiMainScreen {
29    /// Create a new main screen TUI.
30    pub fn new(
31        terminal: Box<dyn Terminal>,
32        show_hardware_cursor: bool,
33        _log_directory: Option<&str>,
34    ) -> Self {
35        Self {
36            terminal,
37            container: Container::new(),
38            running: Mutex::new(false),
39            show_hardware_cursor: Mutex::new(show_hardware_cursor),
40            clear_on_shrink: Mutex::new(false),
41            focused: Mutex::new(None),
42            full_redraw_count: Mutex::new(0),
43            overlays: Arc::new(OverlayManager::new()),
44        }
45    }
46}
47
48impl Component for TuiMainScreen {
49    fn render(&self, width: usize) -> Vec<String> {
50        self.container.render(width)
51    }
52
53    fn invalidate(&self) {
54        self.container.invalidate();
55    }
56
57    fn as_any(&self) -> &dyn Any {
58        self
59    }
60}
61
62impl TUI for TuiMainScreen {
63    fn mode(&self) -> TuiMode {
64        TuiMode::Regular
65    }
66
67    fn terminal(&self) -> &dyn Terminal {
68        self.terminal.as_ref()
69    }
70
71    fn children(&self) -> Vec<Arc<dyn Component>> {
72        self.container.get_children()
73    }
74
75    fn add_child(&self, component: Arc<dyn Component>) {
76        self.container.add_child(component);
77    }
78
79    fn remove_child(&self, component: &Arc<dyn Component>) {
80        self.container.remove_child(component);
81    }
82
83    fn clear(&self) {
84        self.container.clear();
85    }
86
87    fn get_show_hardware_cursor(&self) -> bool {
88        self.show_hardware_cursor
89            .lock()
90            .map(|s| *s)
91            .unwrap_or(false)
92    }
93
94    fn set_show_hardware_cursor(&self, enabled: bool) {
95        if let Ok(mut show) = self.show_hardware_cursor.lock() {
96            *show = enabled;
97        }
98    }
99
100    fn get_clear_on_shrink(&self) -> bool {
101        self.clear_on_shrink.lock().map(|s| *s).unwrap_or(false)
102    }
103
104    fn set_clear_on_shrink(&self, enabled: bool) {
105        if let Ok(mut clear) = self.clear_on_shrink.lock() {
106            *clear = enabled;
107        }
108    }
109
110    fn set_focus(&self, component: Option<Arc<dyn Component>>) {
111        if let Ok(mut focused) = self.focused.lock() {
112            *focused = component;
113        }
114    }
115
116    fn get_focus(&self) -> Option<Arc<dyn Component>> {
117        self.focused.lock().ok()?.clone()
118    }
119
120    fn show_overlay(
121        &self,
122        component: Arc<dyn Component>,
123        options: Option<OverlayOptions>,
124    ) -> Arc<dyn OverlayHandle> {
125        let handle = self.overlays.add(component, options.unwrap_or_default());
126        self.request_render(false);
127        Arc::new(ManagedOverlayHandle {
128            handle,
129            hidden: Mutex::new(false),
130            focused: Mutex::new(true),
131        })
132    }
133
134    fn hide_overlay(&self) {
135        self.overlays.remove_topmost();
136        self.request_render(false);
137    }
138
139    fn has_overlay(&self) -> bool {
140        !self.overlays.get_visible().is_empty()
141    }
142
143    fn start(&self) {
144        if let Ok(mut running) = self.running.lock() {
145            *running = true;
146        }
147        self.terminal.show_cursor();
148        self.request_render(false);
149    }
150
151    fn stop(&self, _options: TuiStopOptions) {
152        if let Ok(mut running) = self.running.lock() {
153            *running = false;
154        }
155        self.terminal.show_cursor();
156        self.terminal.stop();
157    }
158
159    fn render_now(&self, force: bool) {
160        if force {
161            if let Ok(mut count) = self.full_redraw_count.lock() {
162                *count += 1;
163            }
164        }
165
166        let width = self.terminal.columns();
167        let lines = self.render(width);
168
169        // Write the base transcript first. A trailing newline leaves the cursor
170        // one row below it, which lets the temporary overlay be painted with
171        // relative cursor movement without turning it into scrollback content.
172        for (i, line) in lines.iter().enumerate() {
173            if i > 0 {
174                self.terminal.write("\r\n");
175            }
176            self.terminal.write(line);
177        }
178
179        self.paint_overlays(&lines, width);
180
181        self.terminal.flush();
182    }
183
184    fn request_render(&self, _force: bool) {
185        // Simple immediate render for main screen
186        self.render_now(false);
187    }
188
189    fn full_redraws(&self) -> usize {
190        self.full_redraw_count.lock().map(|c| *c).unwrap_or(0)
191    }
192}
193
194impl TuiMainScreen {
195    fn paint_overlays(&self, base_lines: &[String], width: usize) {
196        let height = self.terminal.rows().max(1);
197        let overlays = self.overlays.render_visible(width, height);
198        if overlays.is_empty() || base_lines.is_empty() {
199            return;
200        }
201
202        // Move from the cursor at the end of the base output back to its first
203        // row, paint each overlay in place, then restore the cursor. The next
204        // regular render appends fresh transcript output and naturally replaces
205        // the temporary visual layer.
206        let base_height = base_lines.len().min(height);
207        self.terminal.write("\r\n");
208        self.terminal.write(&format!("\x1b[{}A\x1b[s", base_height));
209
210        for overlay in overlays {
211            let (x, y, overlay_width) = (overlay.x, overlay.y, overlay.width);
212            let overlay_lines = overlay.lines;
213            let overlay_height = overlay_lines.len();
214            // Restore the first base row before positioning each overlay so
215            // multiple overlays retain their z-order and anchor independently.
216            self.terminal.write("\x1b[u");
217            self.terminal.write(&format!("\x1b[{}B", y));
218            for (index, line) in overlay_lines.iter().take(overlay_height).enumerate() {
219                if index > 0 {
220                    self.terminal.write("\x1b[1B");
221                }
222                let row = y + index;
223                let base_line = base_lines.get(row).map(String::as_str).unwrap_or("");
224                let composite = composite_tui_line(base_line, line, x, overlay_width, width);
225                self.terminal.write("\r");
226                self.terminal.write(&composite);
227            }
228        }
229        // Return the cursor to the append position below the base transcript.
230        self.terminal.write("\x1b[u");
231        self.terminal.write(&format!("\x1b[{}B", base_height));
232    }
233}
234
235struct ManagedOverlayHandle {
236    handle: super::overlay::OverlayHandle,
237    hidden: Mutex<bool>,
238    focused: Mutex<bool>,
239}
240
241impl OverlayHandle for ManagedOverlayHandle {
242    fn hide(&self) {
243        self.handle.hide();
244        if let Ok(mut hidden) = self.hidden.lock() {
245            *hidden = true;
246        }
247    }
248    fn set_hidden(&self, hidden: bool) {
249        self.handle.set_visible(!hidden);
250        if let Ok(mut current) = self.hidden.lock() {
251            *current = hidden;
252        }
253    }
254    fn is_hidden(&self) -> bool {
255        self.hidden.lock().map(|hidden| *hidden).unwrap_or(true)
256    }
257    fn focus(&self) {
258        if let Ok(mut focused) = self.focused.lock() {
259            *focused = true;
260        }
261    }
262    fn is_focused(&self) -> bool {
263        self.focused.lock().map(|focused| *focused).unwrap_or(false)
264    }
265}