tuxtui_crossterm/
lib.rs

1//! # tuxtui-crossterm
2//!
3//! Crossterm backend implementation for tuxtui.
4//!
5//! This crate provides a backend implementation using the `crossterm` crate
6//! for cross-platform terminal manipulation.
7//!
8//! ## Features
9//!
10//! - `crossterm_0_28`: Use crossterm 0.28
11//! - `crossterm_0_29` (default): Use crossterm 0.29
12//! - `serde`: Enable serialization support
13//! - `underline-color`: Enable colored underlines
14//! - `scrolling-regions`: Enable scrolling region support
15//! - `unstable`: Enable unstable features
16//! - `unstable-backend-writer`: Enable unstable backend writer API
17//!
18//! ## Example
19//!
20//! ```no_run
21//! use tuxtui_crossterm::CrosstermBackend;
22//! use tuxtui_core::terminal::Terminal;
23//! use std::io::stdout;
24//!
25//! let backend = CrosstermBackend::new(stdout());
26//! let mut terminal = Terminal::new(backend).unwrap();
27//! ```
28
29#![forbid(unsafe_code)]
30#![warn(missing_docs)]
31
32use crossterm::{
33    cursor, execute, queue,
34    style::{
35        self, Attribute, Color as CColor, SetAttribute, SetBackgroundColor, SetForegroundColor,
36    },
37    terminal::{self, Clear, ClearType},
38};
39use std::io::{self, Write};
40use tuxtui_core::backend::Backend;
41use tuxtui_core::buffer::Cell;
42use tuxtui_core::geometry::{Position, Rect};
43use tuxtui_core::style::{Color, Modifier, Style};
44
45/// Crossterm backend.
46///
47/// Wraps a writer (typically stdout) and uses crossterm for terminal operations.
48pub struct CrosstermBackend<W: Write> {
49    writer: W,
50}
51
52impl<W: Write> CrosstermBackend<W> {
53    /// Create a new crossterm backend.
54    ///
55    /// # Example
56    ///
57    /// ```no_run
58    /// use tuxtui_crossterm::CrosstermBackend;
59    /// use std::io::stdout;
60    ///
61    /// let backend = CrosstermBackend::new(stdout());
62    /// ```
63    pub fn new(writer: W) -> Self {
64        Self { writer }
65    }
66
67    /// Get a reference to the writer.
68    pub fn writer(&self) -> &W {
69        &self.writer
70    }
71
72    /// Get a mutable reference to the writer.
73    pub fn writer_mut(&mut self) -> &mut W {
74        &mut self.writer
75    }
76
77    fn convert_color(color: Color) -> CColor {
78        match color {
79            Color::Reset => CColor::Reset,
80            Color::Black => CColor::Black,
81            Color::Red => CColor::DarkRed,
82            Color::Green => CColor::DarkGreen,
83            Color::Yellow => CColor::DarkYellow,
84            Color::Blue => CColor::DarkBlue,
85            Color::Magenta => CColor::DarkMagenta,
86            Color::Cyan => CColor::DarkCyan,
87            Color::White => CColor::Grey,
88            Color::Gray => CColor::DarkGrey,
89            Color::LightRed => CColor::Red,
90            Color::LightGreen => CColor::Green,
91            Color::LightYellow => CColor::Yellow,
92            Color::LightBlue => CColor::Blue,
93            Color::LightMagenta => CColor::Magenta,
94            Color::LightCyan => CColor::Cyan,
95            Color::LightGray => CColor::White,
96            Color::Indexed(i) => CColor::AnsiValue(i),
97            Color::Rgb(r, g, b) => CColor::Rgb { r, g, b },
98        }
99    }
100
101    fn apply_modifiers(&mut self, modifiers: Modifier) -> io::Result<()> {
102        if modifiers.contains(Modifier::BOLD) {
103            queue!(self.writer, SetAttribute(Attribute::Bold))?;
104        }
105        if modifiers.contains(Modifier::DIM) {
106            queue!(self.writer, SetAttribute(Attribute::Dim))?;
107        }
108        if modifiers.contains(Modifier::ITALIC) {
109            queue!(self.writer, SetAttribute(Attribute::Italic))?;
110        }
111        if modifiers.contains(Modifier::UNDERLINED) {
112            queue!(self.writer, SetAttribute(Attribute::Underlined))?;
113        }
114        if modifiers.contains(Modifier::SLOW_BLINK) {
115            queue!(self.writer, SetAttribute(Attribute::SlowBlink))?;
116        }
117        if modifiers.contains(Modifier::RAPID_BLINK) {
118            queue!(self.writer, SetAttribute(Attribute::RapidBlink))?;
119        }
120        if modifiers.contains(Modifier::REVERSED) {
121            queue!(self.writer, SetAttribute(Attribute::Reverse))?;
122        }
123        if modifiers.contains(Modifier::HIDDEN) {
124            queue!(self.writer, SetAttribute(Attribute::Hidden))?;
125        }
126        if modifiers.contains(Modifier::CROSSED_OUT) {
127            queue!(self.writer, SetAttribute(Attribute::CrossedOut))?;
128        }
129        Ok(())
130    }
131}
132
133impl<W: Write> Backend for CrosstermBackend<W> {
134    type Error = io::Error;
135
136    fn size(&self) -> Result<Rect, Self::Error> {
137        let (width, height) = terminal::size()?;
138        Ok(Rect::new(0, 0, width, height))
139    }
140
141    fn clear(&mut self) -> Result<(), Self::Error> {
142        execute!(self.writer, Clear(ClearType::All))
143    }
144
145    fn clear_region(&mut self, region: Rect) -> Result<(), Self::Error> {
146        for y in region.top()..region.bottom() {
147            queue!(self.writer, cursor::MoveTo(region.left(), y))?;
148            for _ in region.left()..region.right() {
149                queue!(self.writer, style::Print(" "))?;
150            }
151        }
152        Ok(())
153    }
154
155    fn hide_cursor(&mut self) -> Result<(), Self::Error> {
156        execute!(self.writer, cursor::Hide)
157    }
158
159    fn show_cursor(&mut self) -> Result<(), Self::Error> {
160        execute!(self.writer, cursor::Show)
161    }
162
163    fn get_cursor(&mut self) -> Result<Position, Self::Error> {
164        // Crossterm doesn't have a simple position() function
165        // We'll return a default for now
166        Ok(Position::new(0, 0))
167    }
168
169    fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), Self::Error> {
170        queue!(self.writer, cursor::MoveTo(x, y))?;
171        Ok(())
172    }
173
174    fn draw_cell(&mut self, x: u16, y: u16, cell: &Cell) -> Result<(), Self::Error> {
175        if cell.skip {
176            return Ok(());
177        }
178
179        queue!(self.writer, cursor::MoveTo(x, y))?;
180
181        if let Some(fg) = cell.style.fg {
182            queue!(self.writer, SetForegroundColor(Self::convert_color(fg)))?;
183        }
184        if let Some(bg) = cell.style.bg {
185            queue!(self.writer, SetBackgroundColor(Self::convert_color(bg)))?;
186        }
187
188        self.apply_modifiers(cell.style.add_modifier)?;
189
190        queue!(self.writer, style::Print(&cell.symbol))?;
191
192        // Reset if we applied any modifiers
193        if !cell.style.add_modifier.is_empty() || cell.style.fg.is_some() || cell.style.bg.is_some()
194        {
195            queue!(self.writer, SetAttribute(Attribute::Reset))?;
196        }
197
198        Ok(())
199    }
200
201    fn set_style(&mut self, style: Style) -> Result<(), Self::Error> {
202        if let Some(fg) = style.fg {
203            queue!(self.writer, SetForegroundColor(Self::convert_color(fg)))?;
204        }
205        if let Some(bg) = style.bg {
206            queue!(self.writer, SetBackgroundColor(Self::convert_color(bg)))?;
207        }
208        self.apply_modifiers(style.add_modifier)?;
209        Ok(())
210    }
211
212    fn reset_style(&mut self) -> Result<(), Self::Error> {
213        queue!(self.writer, SetAttribute(Attribute::Reset))?;
214        Ok(())
215    }
216
217    fn flush(&mut self) -> Result<(), Self::Error> {
218        self.writer.flush()
219    }
220
221    fn enable_raw_mode(&mut self) -> Result<(), Self::Error> {
222        terminal::enable_raw_mode()
223    }
224
225    fn disable_raw_mode(&mut self) -> Result<(), Self::Error> {
226        terminal::disable_raw_mode()
227    }
228
229    fn enter_alternate_screen(&mut self) -> Result<(), Self::Error> {
230        execute!(self.writer, terminal::EnterAlternateScreen)
231    }
232
233    fn leave_alternate_screen(&mut self) -> Result<(), Self::Error> {
234        execute!(self.writer, terminal::LeaveAlternateScreen)
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241    use std::io::Cursor;
242
243    #[test]
244    fn test_backend_creation() {
245        let cursor = Cursor::new(Vec::new());
246        let backend = CrosstermBackend::new(cursor);
247        assert!(backend.writer().get_ref().is_empty());
248    }
249
250    #[test]
251    fn test_color_conversion() {
252        assert!(matches!(
253            CrosstermBackend::<Vec<u8>>::convert_color(Color::Red),
254            CColor::DarkRed
255        ));
256        assert!(matches!(
257            CrosstermBackend::<Vec<u8>>::convert_color(Color::Rgb(255, 128, 0)),
258            CColor::Rgb {
259                r: 255,
260                g: 128,
261                b: 0
262            }
263        ));
264    }
265}