tuxtui_core/
backend.rs

1//! Platform-agnostic terminal backend trait.
2
3use crate::buffer::{Buffer, Cell};
4use crate::geometry::{Position, Rect};
5use crate::style::Style;
6use core::fmt;
7
8/// A terminal backend abstraction.
9///
10/// Backends implement low-level terminal operations like clearing, cursor
11/// management, and cell rendering.
12///
13/// # Example
14///
15/// ```ignore
16/// use tuxtui_core::backend::Backend;
17///
18/// fn render<B: Backend>(backend: &mut B) -> Result<(), B::Error> {
19///     backend.clear()?;
20///     backend.set_cursor(0, 0)?;
21///     backend.flush()?;
22///     Ok(())
23/// }
24/// ```
25pub trait Backend {
26    /// The error type for this backend.
27    type Error: fmt::Debug + fmt::Display;
28
29    /// Get the size of the terminal in columns and rows.
30    fn size(&self) -> Result<Rect, Self::Error>;
31
32    /// Clear the entire screen.
33    fn clear(&mut self) -> Result<(), Self::Error>;
34
35    /// Clear a specific region.
36    fn clear_region(&mut self, region: Rect) -> Result<(), Self::Error> {
37        // Default implementation using individual cell writes
38        for y in region.top()..region.bottom() {
39            for x in region.left()..region.right() {
40                self.draw_cell(x, y, &Cell::default())?;
41            }
42        }
43        Ok(())
44    }
45
46    /// Hide the cursor.
47    fn hide_cursor(&mut self) -> Result<(), Self::Error>;
48
49    /// Show the cursor.
50    fn show_cursor(&mut self) -> Result<(), Self::Error>;
51
52    /// Get the current cursor position.
53    fn get_cursor(&mut self) -> Result<Position, Self::Error>;
54
55    /// Set the cursor position.
56    fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), Self::Error>;
57
58    /// Draw a single cell at the given position.
59    fn draw_cell(&mut self, x: u16, y: u16, cell: &Cell) -> Result<(), Self::Error>;
60
61    /// Set the current style for subsequent operations.
62    fn set_style(&mut self, style: Style) -> Result<(), Self::Error>;
63
64    /// Reset all styling to default.
65    fn reset_style(&mut self) -> Result<(), Self::Error>;
66
67    /// Flush any buffered output to the terminal.
68    fn flush(&mut self) -> Result<(), Self::Error>;
69
70    /// Enable raw mode.
71    fn enable_raw_mode(&mut self) -> Result<(), Self::Error>;
72
73    /// Disable raw mode.
74    fn disable_raw_mode(&mut self) -> Result<(), Self::Error>;
75
76    /// Enter alternate screen.
77    fn enter_alternate_screen(&mut self) -> Result<(), Self::Error>;
78
79    /// Leave alternate screen.
80    fn leave_alternate_screen(&mut self) -> Result<(), Self::Error>;
81
82    /// Enable mouse capture (if supported).
83    #[cfg(feature = "scrolling-regions")]
84    fn set_scroll_region(&mut self, top: u16, bottom: u16) -> Result<(), Self::Error> {
85        let _ = (top, bottom);
86        Ok(())
87    }
88
89    /// Clear scroll region (if supported).
90    #[cfg(feature = "scrolling-regions")]
91    fn clear_scroll_region(&mut self) -> Result<(), Self::Error> {
92        Ok(())
93    }
94}
95
96/// A test backend for unit testing and snapshot testing.
97///
98/// Records operations and maintains a virtual terminal buffer.
99///
100/// # Example
101///
102/// ```
103/// use tuxtui_core::backend::{Backend, TestBackend};
104/// use tuxtui_core::geometry::Rect;
105///
106/// let mut backend = TestBackend::new(80, 24);
107/// backend.clear().unwrap();
108/// let size = backend.size().unwrap();
109/// assert_eq!(size.width, 80);
110/// assert_eq!(size.height, 24);
111/// ```
112pub struct TestBackend {
113    width: u16,
114    height: u16,
115    buffer: Buffer,
116    cursor_visible: bool,
117    cursor_position: Position,
118}
119
120impl TestBackend {
121    /// Create a new test backend with the given dimensions.
122    #[must_use]
123    pub fn new(width: u16, height: u16) -> Self {
124        Self {
125            width,
126            height,
127            buffer: Buffer::empty(Rect::new(0, 0, width, height)),
128            cursor_visible: true,
129            cursor_position: Position::new(0, 0),
130        }
131    }
132
133    /// Get the current buffer content.
134    #[must_use]
135    pub fn buffer(&self) -> &Buffer {
136        &self.buffer
137    }
138
139    /// Get a mutable reference to the buffer.
140    pub fn buffer_mut(&mut self) -> &mut Buffer {
141        &mut self.buffer
142    }
143
144    /// Resize the test backend.
145    pub fn resize(&mut self, width: u16, height: u16) {
146        self.width = width;
147        self.height = height;
148        self.buffer.resize(Rect::new(0, 0, width, height));
149    }
150
151    /// Get the cursor visibility state.
152    #[must_use]
153    pub const fn is_cursor_visible(&self) -> bool {
154        self.cursor_visible
155    }
156
157    /// Assert that the buffer contains the expected string at the given position.
158    ///
159    /// # Panics
160    ///
161    /// Panics if the buffer content doesn't match.
162    pub fn assert_buffer_equals(&self, expected: &str) {
163        let actual = format!("{}", self.buffer);
164        assert_eq!(actual.trim(), expected.trim(), "Buffer mismatch");
165    }
166}
167
168impl Backend for TestBackend {
169    type Error = TestBackendError;
170
171    fn size(&self) -> Result<Rect, Self::Error> {
172        Ok(Rect::new(0, 0, self.width, self.height))
173    }
174
175    fn clear(&mut self) -> Result<(), Self::Error> {
176        self.buffer.clear();
177        Ok(())
178    }
179
180    fn clear_region(&mut self, region: Rect) -> Result<(), Self::Error> {
181        self.buffer.clear_region(region);
182        Ok(())
183    }
184
185    fn hide_cursor(&mut self) -> Result<(), Self::Error> {
186        self.cursor_visible = false;
187        Ok(())
188    }
189
190    fn show_cursor(&mut self) -> Result<(), Self::Error> {
191        self.cursor_visible = true;
192        Ok(())
193    }
194
195    fn get_cursor(&mut self) -> Result<Position, Self::Error> {
196        Ok(self.cursor_position)
197    }
198
199    fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), Self::Error> {
200        self.cursor_position = Position::new(x, y);
201        Ok(())
202    }
203
204    fn draw_cell(&mut self, x: u16, y: u16, cell: &Cell) -> Result<(), Self::Error> {
205        self.buffer.set(x, y, cell.symbol.as_str(), cell.style);
206        Ok(())
207    }
208
209    fn set_style(&mut self, _style: Style) -> Result<(), Self::Error> {
210        Ok(())
211    }
212
213    fn reset_style(&mut self) -> Result<(), Self::Error> {
214        Ok(())
215    }
216
217    fn flush(&mut self) -> Result<(), Self::Error> {
218        Ok(())
219    }
220
221    fn enable_raw_mode(&mut self) -> Result<(), Self::Error> {
222        Ok(())
223    }
224
225    fn disable_raw_mode(&mut self) -> Result<(), Self::Error> {
226        Ok(())
227    }
228
229    fn enter_alternate_screen(&mut self) -> Result<(), Self::Error> {
230        Ok(())
231    }
232
233    fn leave_alternate_screen(&mut self) -> Result<(), Self::Error> {
234        Ok(())
235    }
236}
237
238/// Error type for test backend.
239#[derive(Debug, Clone, PartialEq, Eq)]
240pub enum TestBackendError {
241    /// Generic error
242    Generic(alloc::string::String),
243}
244
245impl fmt::Display for TestBackendError {
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        match self {
248            Self::Generic(msg) => write!(f, "{msg}"),
249        }
250    }
251}
252
253#[cfg(feature = "std")]
254impl std::error::Error for TestBackendError {}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn test_backend_size() {
262        let backend = TestBackend::new(80, 24);
263        let size = backend.size().unwrap();
264        assert_eq!(size.width, 80);
265        assert_eq!(size.height, 24);
266    }
267
268    #[test]
269    fn test_backend_cursor() {
270        let mut backend = TestBackend::new(80, 24);
271        backend.set_cursor(10, 5).unwrap();
272        let pos = backend.get_cursor().unwrap();
273        assert_eq!(pos.x, 10);
274        assert_eq!(pos.y, 5);
275    }
276
277    #[test]
278    fn test_backend_clear() {
279        let mut backend = TestBackend::new(10, 5);
280        backend.buffer_mut().set(0, 0, "X", Style::default());
281        backend.clear().unwrap();
282        assert_eq!(backend.buffer().get(0, 0).unwrap().symbol, " ");
283    }
284}