Skip to main content

altui_core/backend/
crossterm.rs

1use crate::{
2    backend::Backend,
3    buffer::Cell,
4    layout::Rect,
5    style::{Color, Modifier},
6};
7use crossterm::{
8    cursor::{Hide, MoveTo, Show},
9    execute, queue,
10    style::{
11        Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
12        SetForegroundColor,
13    },
14    terminal::{self, Clear, ClearType},
15};
16use std::io::{self, Write};
17
18/// A [`Backend`] implementation based on the `crossterm` crate.
19///
20/// `CrosstermBackend` renders terminal output using Crossterm escape sequences
21/// and works on most platforms, including Windows, macOS, and Linux.
22///
23/// This backend is intended to be used together with [`Terminal`] and provides
24/// low-level drawing primitives such as cursor movement, color and attribute
25/// management, screen clearing, and buffer flushing.
26///
27/// ## Responsibilities
28///
29/// - render cells to the terminal using Crossterm commands
30/// - manage cursor visibility and position
31/// - query terminal size
32/// - clear and flush the terminal output
33///
34/// ## What this backend does *not* do
35///
36/// - enable or disable raw mode
37/// - enter or leave the alternate screen
38/// - handle mouse input
39/// - manage terminal lifetime
40///
41/// Terminal setup and teardown are expected to be handled externally
42/// (for example, by [`Altui`] or manual Crossterm calls).
43///
44/// ## Usage
45///
46/// In most applications, you should prefer using [`Altui`] instead of
47/// constructing a `CrosstermBackend` directly.
48///
49/// ### See also
50///
51/// - [`Altui`]
52/// - [`Terminal`]
53/// - [`backend::Backend`]
54pub struct CrosstermBackend<W: Write> {
55    buffer: W,
56}
57
58impl<W> CrosstermBackend<W>
59where
60    W: Write,
61{
62    /// Creates a new `CrosstermBackend` using the given output buffer.
63    ///
64    /// The buffer is typically `std::io::Stdout`, but any type implementing
65    /// [`Write`] may be used.
66    ///
67    /// # Parameters
68    ///
69    /// - `buffer`: the output target for terminal commands
70    ///
71    /// # Notes
72    ///
73    /// This function does not perform any terminal initialization.
74    /// Raw mode, alternate screen handling, and mouse capture must be managed
75    /// by the caller.
76    pub fn new(buffer: W) -> CrosstermBackend<W> {
77        CrosstermBackend { buffer }
78    }
79}
80
81/// `CrosstermBackend` forwards all `Write` calls to the underlying buffer.
82///
83/// This allows it to be used seamlessly with APIs that expect a writable
84/// output stream.
85impl<W> Write for CrosstermBackend<W>
86where
87    W: Write,
88{
89    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
90        self.buffer.write(buf)
91    }
92
93    fn flush(&mut self) -> io::Result<()> {
94        self.buffer.flush()
95    }
96}
97
98impl<W> Backend for CrosstermBackend<W>
99where
100    W: Write,
101{
102    /// Draws an iterator of cells to the terminal.
103    ///
104    /// The iterator yields `(x, y, &Cell)` tuples, which are rendered using
105    /// efficient cursor movement and minimal attribute changes.
106    ///
107    /// The backend internally tracks:
108    ///
109    /// - foreground color
110    /// - background color
111    /// - text modifiers
112    /// - cursor position
113    ///
114    /// to reduce the number of Crossterm commands sent to the terminal.
115    ///
116    /// # Errors
117    ///
118    /// Returns an error if writing to the output buffer fails.
119    fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
120    where
121        I: Iterator<Item = (u16, u16, &'a Cell)>,
122    {
123        let mut fg = Color::Reset;
124        let mut bg = Color::Reset;
125        let mut modifier = Modifier::empty();
126        let mut last_pos: Option<(u16, u16)> = None;
127        for (x, y, cell) in content {
128            // Move the cursor if the previous location was not (x - 1, y)
129            if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
130                queue!(self.buffer, MoveTo(x, y))?;
131            }
132            last_pos = Some((x, y));
133            if cell.modifier != modifier {
134                let diff = ModifierDiff {
135                    from: modifier,
136                    to: cell.modifier,
137                };
138                diff.queue(&mut self.buffer)?;
139                modifier = cell.modifier;
140            }
141            if cell.fg != fg {
142                let color = CColor::from(cell.fg);
143                queue!(self.buffer, SetForegroundColor(color))?;
144                fg = cell.fg;
145            }
146            if cell.bg != bg {
147                let color = CColor::from(cell.bg);
148                queue!(self.buffer, SetBackgroundColor(color))?;
149                bg = cell.bg;
150            }
151
152            queue!(self.buffer, Print(&cell.symbol))?;
153        }
154
155        queue!(
156            self.buffer,
157            SetForegroundColor(CColor::Reset),
158            SetBackgroundColor(CColor::Reset),
159            SetAttribute(CAttribute::Reset)
160        )
161    }
162
163    /// Hides the terminal cursor.
164    fn hide_cursor(&mut self) -> io::Result<()> {
165        execute!(self.buffer, Hide)
166    }
167
168    /// Shows the terminal cursor.
169    fn show_cursor(&mut self) -> io::Result<()> {
170        execute!(self.buffer, Show)
171    }
172
173    /// Returns the current cursor position.
174    ///
175    /// The position is reported in terminal coordinates.
176    fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
177        crossterm::cursor::position()
178            .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
179    }
180
181    /// Moves the cursor to the given position.
182    fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
183        execute!(self.buffer, MoveTo(x, y))
184    }
185
186    /// Clears the entire terminal screen.
187    fn clear(&mut self) -> io::Result<()> {
188        execute!(self.buffer, Clear(ClearType::All))
189    }
190
191    /// Returns the current terminal size.
192    ///
193    /// The returned [`Rect`] always starts at `(0, 0)`.
194    fn size(&self) -> io::Result<Rect> {
195        let (width, height) =
196            terminal::size().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
197
198        Ok(Rect::new(0, 0, width, height))
199    }
200
201    /// Flushes all queued commands to the terminal.
202    fn flush(&mut self) -> io::Result<()> {
203        self.buffer.flush()
204    }
205}
206
207impl From<Color> for CColor {
208    fn from(color: Color) -> Self {
209        match color {
210            Color::Reset => CColor::Reset,
211            Color::Black => CColor::Black,
212            Color::Red => CColor::DarkRed,
213            Color::Green => CColor::DarkGreen,
214            Color::Yellow => CColor::DarkYellow,
215            Color::Blue => CColor::DarkBlue,
216            Color::Magenta => CColor::DarkMagenta,
217            Color::Cyan => CColor::DarkCyan,
218            Color::Gray => CColor::Grey,
219            Color::DarkGray => CColor::DarkGrey,
220            Color::LightRed => CColor::Red,
221            Color::LightGreen => CColor::Green,
222            Color::LightBlue => CColor::Blue,
223            Color::LightYellow => CColor::Yellow,
224            Color::LightMagenta => CColor::Magenta,
225            Color::LightCyan => CColor::Cyan,
226            Color::White => CColor::White,
227            Color::Indexed(i) => CColor::AnsiValue(i),
228            Color::Rgb(r, g, b) => CColor::Rgb { r, g, b },
229        }
230    }
231}
232
233#[derive(Debug)]
234struct ModifierDiff {
235    pub from: Modifier,
236    pub to: Modifier,
237}
238
239impl ModifierDiff {
240    fn queue<W>(&self, mut w: W) -> io::Result<()>
241    where
242        W: io::Write,
243    {
244        //use crossterm::Attribute;
245        let removed = self.from - self.to;
246        if removed.contains(Modifier::REVERSED) {
247            queue!(w, SetAttribute(CAttribute::NoReverse))?;
248        }
249        if removed.contains(Modifier::BOLD) {
250            queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
251            if self.to.contains(Modifier::DIM) {
252                queue!(w, SetAttribute(CAttribute::Dim))?;
253            }
254        }
255        if removed.contains(Modifier::ITALIC) {
256            queue!(w, SetAttribute(CAttribute::NoItalic))?;
257        }
258        if removed.contains(Modifier::UNDERLINED) {
259            queue!(w, SetAttribute(CAttribute::NoUnderline))?;
260        }
261        if removed.contains(Modifier::DIM) {
262            queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
263        }
264        if removed.contains(Modifier::CROSSED_OUT) {
265            queue!(w, SetAttribute(CAttribute::NotCrossedOut))?;
266        }
267        if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
268            queue!(w, SetAttribute(CAttribute::NoBlink))?;
269        }
270
271        let added = self.to - self.from;
272        if added.contains(Modifier::REVERSED) {
273            queue!(w, SetAttribute(CAttribute::Reverse))?;
274        }
275        if added.contains(Modifier::BOLD) {
276            queue!(w, SetAttribute(CAttribute::Bold))?;
277        }
278        if added.contains(Modifier::ITALIC) {
279            queue!(w, SetAttribute(CAttribute::Italic))?;
280        }
281        if added.contains(Modifier::UNDERLINED) {
282            queue!(w, SetAttribute(CAttribute::Underlined))?;
283        }
284        if added.contains(Modifier::DIM) {
285            queue!(w, SetAttribute(CAttribute::Dim))?;
286        }
287        if added.contains(Modifier::CROSSED_OUT) {
288            queue!(w, SetAttribute(CAttribute::CrossedOut))?;
289        }
290        if added.contains(Modifier::SLOW_BLINK) {
291            queue!(w, SetAttribute(CAttribute::SlowBlink))?;
292        }
293        if added.contains(Modifier::RAPID_BLINK) {
294            queue!(w, SetAttribute(CAttribute::RapidBlink))?;
295        }
296
297        Ok(())
298    }
299}