altui-core 0.2.0

A library to build rich terminal user interfaces or dashboards
Documentation
use crate::{
    backend::Backend,
    buffer::Cell,
    layout::Rect,
    style::{Color, Modifier},
};
use crossterm::{
    cursor::{Hide, MoveTo, Show},
    execute, queue,
    style::{
        Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
        SetForegroundColor,
    },
    terminal::{self, Clear, ClearType},
};
use std::io::{self, Write};

/// A [`Backend`] implementation based on the `crossterm` crate.
///
/// `CrosstermBackend` renders terminal output using Crossterm escape sequences
/// and works on most platforms, including Windows, macOS, and Linux.
///
/// This backend is intended to be used together with [`Terminal`] and provides
/// low-level drawing primitives such as cursor movement, color and attribute
/// management, screen clearing, and buffer flushing.
///
/// ## Responsibilities
///
/// - render cells to the terminal using Crossterm commands
/// - manage cursor visibility and position
/// - query terminal size
/// - clear and flush the terminal output
///
/// ## What this backend does *not* do
///
/// - enable or disable raw mode
/// - enter or leave the alternate screen
/// - handle mouse input
/// - manage terminal lifetime
///
/// Terminal setup and teardown are expected to be handled externally
/// (for example, by [`Altui`] or manual Crossterm calls).
///
/// ## Usage
///
/// In most applications, you should prefer using [`Altui`] instead of
/// constructing a `CrosstermBackend` directly.
///
/// ### See also
///
/// - [`Altui`]
/// - [`Terminal`]
/// - [`backend::Backend`]
pub struct CrosstermBackend<W: Write> {
    buffer: W,
}

impl<W> CrosstermBackend<W>
where
    W: Write,
{
    /// Creates a new `CrosstermBackend` using the given output buffer.
    ///
    /// The buffer is typically `std::io::Stdout`, but any type implementing
    /// [`Write`] may be used.
    ///
    /// # Parameters
    ///
    /// - `buffer`: the output target for terminal commands
    ///
    /// # Notes
    ///
    /// This function does not perform any terminal initialization.
    /// Raw mode, alternate screen handling, and mouse capture must be managed
    /// by the caller.
    pub fn new(buffer: W) -> CrosstermBackend<W> {
        CrosstermBackend { buffer }
    }
}

/// `CrosstermBackend` forwards all `Write` calls to the underlying buffer.
///
/// This allows it to be used seamlessly with APIs that expect a writable
/// output stream.
impl<W> Write for CrosstermBackend<W>
where
    W: Write,
{
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        self.buffer.write(buf)
    }

    fn flush(&mut self) -> io::Result<()> {
        self.buffer.flush()
    }
}

impl<W> Backend for CrosstermBackend<W>
where
    W: Write,
{
    /// Draws an iterator of cells to the terminal.
    ///
    /// The iterator yields `(x, y, &Cell)` tuples, which are rendered using
    /// efficient cursor movement and minimal attribute changes.
    ///
    /// The backend internally tracks:
    ///
    /// - foreground color
    /// - background color
    /// - text modifiers
    /// - cursor position
    ///
    /// to reduce the number of Crossterm commands sent to the terminal.
    ///
    /// # Errors
    ///
    /// Returns an error if writing to the output buffer fails.
    fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
    where
        I: Iterator<Item = (u16, u16, &'a Cell)>,
    {
        let mut fg = Color::Reset;
        let mut bg = Color::Reset;
        let mut modifier = Modifier::empty();
        let mut last_pos: Option<(u16, u16)> = None;
        for (x, y, cell) in content {
            // Move the cursor if the previous location was not (x - 1, y)
            if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
                queue!(self.buffer, MoveTo(x, y))?;
            }
            last_pos = Some((x, y));
            if cell.modifier != modifier {
                let diff = ModifierDiff {
                    from: modifier,
                    to: cell.modifier,
                };
                diff.queue(&mut self.buffer)?;
                modifier = cell.modifier;
            }
            if cell.fg != fg {
                let color = CColor::from(cell.fg);
                queue!(self.buffer, SetForegroundColor(color))?;
                fg = cell.fg;
            }
            if cell.bg != bg {
                let color = CColor::from(cell.bg);
                queue!(self.buffer, SetBackgroundColor(color))?;
                bg = cell.bg;
            }

            queue!(self.buffer, Print(&cell.symbol))?;
        }

        queue!(
            self.buffer,
            SetForegroundColor(CColor::Reset),
            SetBackgroundColor(CColor::Reset),
            SetAttribute(CAttribute::Reset)
        )
    }

    /// Hides the terminal cursor.
    fn hide_cursor(&mut self) -> io::Result<()> {
        execute!(self.buffer, Hide)
    }

    /// Shows the terminal cursor.
    fn show_cursor(&mut self) -> io::Result<()> {
        execute!(self.buffer, Show)
    }

    /// Returns the current cursor position.
    ///
    /// The position is reported in terminal coordinates.
    fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
        crossterm::cursor::position()
            .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
    }

    /// Moves the cursor to the given position.
    fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
        execute!(self.buffer, MoveTo(x, y))
    }

    /// Clears the entire terminal screen.
    fn clear(&mut self) -> io::Result<()> {
        execute!(self.buffer, Clear(ClearType::All))
    }

    /// Returns the current terminal size.
    ///
    /// The returned [`Rect`] always starts at `(0, 0)`.
    fn size(&self) -> io::Result<Rect> {
        let (width, height) =
            terminal::size().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;

        Ok(Rect::new(0, 0, width, height))
    }

    /// Flushes all queued commands to the terminal.
    fn flush(&mut self) -> io::Result<()> {
        self.buffer.flush()
    }
}

impl From<Color> for CColor {
    fn from(color: Color) -> Self {
        match color {
            Color::Reset => CColor::Reset,
            Color::Black => CColor::Black,
            Color::Red => CColor::DarkRed,
            Color::Green => CColor::DarkGreen,
            Color::Yellow => CColor::DarkYellow,
            Color::Blue => CColor::DarkBlue,
            Color::Magenta => CColor::DarkMagenta,
            Color::Cyan => CColor::DarkCyan,
            Color::Gray => CColor::Grey,
            Color::DarkGray => CColor::DarkGrey,
            Color::LightRed => CColor::Red,
            Color::LightGreen => CColor::Green,
            Color::LightBlue => CColor::Blue,
            Color::LightYellow => CColor::Yellow,
            Color::LightMagenta => CColor::Magenta,
            Color::LightCyan => CColor::Cyan,
            Color::White => CColor::White,
            Color::Indexed(i) => CColor::AnsiValue(i),
            Color::Rgb(r, g, b) => CColor::Rgb { r, g, b },
        }
    }
}

#[derive(Debug)]
struct ModifierDiff {
    pub from: Modifier,
    pub to: Modifier,
}

impl ModifierDiff {
    fn queue<W>(&self, mut w: W) -> io::Result<()>
    where
        W: io::Write,
    {
        //use crossterm::Attribute;
        let removed = self.from - self.to;
        if removed.contains(Modifier::REVERSED) {
            queue!(w, SetAttribute(CAttribute::NoReverse))?;
        }
        if removed.contains(Modifier::BOLD) {
            queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
            if self.to.contains(Modifier::DIM) {
                queue!(w, SetAttribute(CAttribute::Dim))?;
            }
        }
        if removed.contains(Modifier::ITALIC) {
            queue!(w, SetAttribute(CAttribute::NoItalic))?;
        }
        if removed.contains(Modifier::UNDERLINED) {
            queue!(w, SetAttribute(CAttribute::NoUnderline))?;
        }
        if removed.contains(Modifier::DIM) {
            queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
        }
        if removed.contains(Modifier::CROSSED_OUT) {
            queue!(w, SetAttribute(CAttribute::NotCrossedOut))?;
        }
        if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
            queue!(w, SetAttribute(CAttribute::NoBlink))?;
        }

        let added = self.to - self.from;
        if added.contains(Modifier::REVERSED) {
            queue!(w, SetAttribute(CAttribute::Reverse))?;
        }
        if added.contains(Modifier::BOLD) {
            queue!(w, SetAttribute(CAttribute::Bold))?;
        }
        if added.contains(Modifier::ITALIC) {
            queue!(w, SetAttribute(CAttribute::Italic))?;
        }
        if added.contains(Modifier::UNDERLINED) {
            queue!(w, SetAttribute(CAttribute::Underlined))?;
        }
        if added.contains(Modifier::DIM) {
            queue!(w, SetAttribute(CAttribute::Dim))?;
        }
        if added.contains(Modifier::CROSSED_OUT) {
            queue!(w, SetAttribute(CAttribute::CrossedOut))?;
        }
        if added.contains(Modifier::SLOW_BLINK) {
            queue!(w, SetAttribute(CAttribute::SlowBlink))?;
        }
        if added.contains(Modifier::RAPID_BLINK) {
            queue!(w, SetAttribute(CAttribute::RapidBlink))?;
        }

        Ok(())
    }
}