tuxtui_core/
terminal.rs

1//! Terminal management and frame orchestration.
2
3use crate::backend::Backend;
4use crate::buffer::Buffer;
5use crate::geometry::Rect;
6
7/// Options for configuring a terminal.
8#[derive(Debug, Clone)]
9pub struct TerminalOptions {
10    /// Enable alternate screen
11    pub alternate_screen: bool,
12    /// Hide cursor during rendering
13    pub hide_cursor: bool,
14}
15
16impl Default for TerminalOptions {
17    fn default() -> Self {
18        Self {
19            alternate_screen: true,
20            hide_cursor: true,
21        }
22    }
23}
24
25/// A terminal interface managing rendering and buffering.
26///
27/// The terminal orchestrates frame rendering, maintains double buffers,
28/// and coordinates with the backend.
29///
30/// # Example
31///
32/// ```ignore
33/// use tuxtui_core::terminal::Terminal;
34/// use tuxtui_core::backend::TestBackend;
35///
36/// let backend = TestBackend::new(80, 24);
37/// let mut terminal = Terminal::new(backend).unwrap();
38/// terminal.draw(|frame| {
39///     // Render widgets
40/// }).unwrap();
41/// ```
42pub struct Terminal<B: Backend> {
43    backend: B,
44    buffers: [Buffer; 2],
45    current: usize,
46    hidden_cursor: bool,
47}
48
49impl<B: Backend> Terminal<B> {
50    /// Create a new terminal with the given backend.
51    pub fn new(backend: B) -> Result<Self, B::Error> {
52        Self::with_options(backend, TerminalOptions::default())
53    }
54
55    /// Create a new terminal with options.
56    pub fn with_options(mut backend: B, options: TerminalOptions) -> Result<Self, B::Error> {
57        let size = backend.size()?;
58
59        if options.alternate_screen {
60            backend.enter_alternate_screen()?;
61        }
62
63        if options.hide_cursor {
64            backend.hide_cursor()?;
65        }
66
67        backend.enable_raw_mode()?;
68        backend.clear()?;
69        backend.flush()?;
70
71        Ok(Self {
72            backend,
73            buffers: [Buffer::empty(size), Buffer::empty(size)],
74            current: 0,
75            hidden_cursor: options.hide_cursor,
76        })
77    }
78
79    /// Get the size of the terminal.
80    pub fn size(&self) -> Result<Rect, B::Error> {
81        self.backend.size()
82    }
83
84    /// Get the current viewport.
85    #[must_use]
86    pub fn viewport(&self) -> Rect {
87        self.buffers[self.current].area
88    }
89
90    /// Clear the terminal.
91    pub fn clear(&mut self) -> Result<(), B::Error> {
92        self.backend.clear()?;
93        self.buffers[self.current].clear();
94        Ok(())
95    }
96
97    /// Draw a frame using the provided closure.
98    ///
99    /// # Example
100    ///
101    /// ```ignore
102    /// terminal.draw(|frame| {
103    ///     let area = frame.area();
104    ///     frame.render_widget(widget, area);
105    /// })?;
106    /// ```
107    pub fn draw<F>(&mut self, render: F) -> Result<(), B::Error>
108    where
109        F: FnOnce(&mut Frame<'_>),
110    {
111        // Check for resize
112        let size = self.backend.size()?;
113        if size != self.buffers[self.current].area {
114            self.resize(size)?;
115        }
116
117        let next = (self.current + 1) % 2;
118        self.buffers[next].clear();
119
120        // Render to next buffer
121        let mut frame = Frame {
122            buffer: &mut self.buffers[next],
123            area: size,
124        };
125        render(&mut frame);
126
127        // Compute diff and render
128        let diff = self.buffers[self.current].diff(&self.buffers[next]);
129        for change in diff {
130            for cell in change.cells {
131                self.backend.draw_cell(change.x, change.y, cell)?;
132            }
133        }
134
135        self.backend.flush()?;
136        self.current = next;
137
138        Ok(())
139    }
140
141    /// Resize the terminal buffers.
142    fn resize(&mut self, size: Rect) -> Result<(), B::Error> {
143        self.buffers[0].resize(size);
144        self.buffers[1].resize(size);
145        self.backend.clear()?;
146        Ok(())
147    }
148
149    /// Show the cursor.
150    pub fn show_cursor(&mut self) -> Result<(), B::Error> {
151        self.backend.show_cursor()?;
152        self.hidden_cursor = false;
153        Ok(())
154    }
155
156    /// Hide the cursor.
157    pub fn hide_cursor(&mut self) -> Result<(), B::Error> {
158        self.backend.hide_cursor()?;
159        self.hidden_cursor = true;
160        Ok(())
161    }
162
163    /// Set the cursor position.
164    pub fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), B::Error> {
165        self.backend.set_cursor(x, y)
166    }
167
168    /// Get mutable access to the backend.
169    pub fn backend_mut(&mut self) -> &mut B {
170        &mut self.backend
171    }
172
173    /// Flush the backend.
174    pub fn flush(&mut self) -> Result<(), B::Error> {
175        self.backend.flush()
176    }
177}
178
179impl<B: Backend> Drop for Terminal<B> {
180    fn drop(&mut self) {
181        let _ = self.backend.disable_raw_mode();
182        let _ = self.backend.leave_alternate_screen();
183        if self.hidden_cursor {
184            let _ = self.backend.show_cursor();
185        }
186        let _ = self.backend.flush();
187    }
188}
189
190/// A frame for rendering widgets.
191///
192/// Frames provide access to a buffer and the rendering area during a draw call.
193pub struct Frame<'a> {
194    buffer: &'a mut Buffer,
195    area: Rect,
196}
197
198impl<'a> Frame<'a> {
199    /// Get the rendering area.
200    #[must_use]
201    pub const fn area(&self) -> Rect {
202        self.area
203    }
204
205    /// Get mutable access to the buffer.
206    pub fn buffer_mut(&mut self) -> &mut Buffer {
207        self.buffer
208    }
209
210    /// Render a widget at the given area.
211    pub fn render_widget<W>(&mut self, widget: W, area: Rect)
212    where
213        W: Widget,
214    {
215        widget.render(area, self.buffer);
216    }
217}
218
219/// A widget that can be rendered to a buffer.
220pub trait Widget {
221    /// Render this widget into the given area of the buffer.
222    fn render(self, area: Rect, buf: &mut Buffer);
223}
224
225/// Implement Widget for string slices for convenience.
226impl Widget for &str {
227    fn render(self, area: Rect, buf: &mut Buffer) {
228        if area.height > 0 {
229            buf.set_string(area.x, area.y, self, crate::style::Style::default());
230        }
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237    use crate::backend::TestBackend;
238
239    #[test]
240    fn test_terminal_creation() {
241        let backend = TestBackend::new(80, 24);
242        let terminal = Terminal::new(backend);
243        assert!(terminal.is_ok());
244    }
245
246    #[test]
247    fn test_terminal_draw() {
248        let backend = TestBackend::new(80, 24);
249        let mut terminal = Terminal::new(backend).unwrap();
250
251        let result = terminal.draw(|frame| {
252            let area = frame.area();
253            assert_eq!(area.width, 80);
254            assert_eq!(area.height, 24);
255        });
256
257        assert!(result.is_ok());
258    }
259}