#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc(
html_logo_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/logo.png",
html_favicon_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/favicon.ico"
)]
#![warn(missing_docs)]
#![cfg_attr(feature = "document-features", doc = "\n## Features")]
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
use std::io::{self, Write};
pub use crossterm;
#[cfg(feature = "underline-color")]
use crossterm::style::SetUnderlineColor;
use crossterm::{
cursor::{Hide, MoveTo, Show},
execute, queue,
style::{
Attribute as CrosstermAttribute, Attributes as CrosstermAttributes,
Color as CrosstermColor, Colors as CrosstermColors, ContentStyle, Print, SetAttribute,
SetBackgroundColor, SetColors, SetForegroundColor,
},
terminal::{self, Clear},
};
use ratatui_core::{
backend::{Backend, ClearType, WindowSize},
buffer::Cell,
layout::{Position, Size},
style::{Color, Modifier, Style},
};
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct CrosstermBackend<W: Write> {
writer: W,
}
impl<W> CrosstermBackend<W>
where
W: Write,
{
pub const fn new(writer: W) -> Self {
Self { writer }
}
#[instability::unstable(
feature = "backend-writer",
issue = "https://github.com/ratatui/ratatui/pull/991"
)]
pub const fn writer(&self) -> &W {
&self.writer
}
#[instability::unstable(
feature = "backend-writer",
issue = "https://github.com/ratatui/ratatui/pull/991"
)]
pub fn writer_mut(&mut self) -> &mut W {
&mut self.writer
}
}
impl<W> Write for CrosstermBackend<W>
where
W: Write,
{
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.writer.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.writer.flush()
}
}
impl<W> Backend for CrosstermBackend<W>
where
W: Write,
{
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;
#[cfg(feature = "underline-color")]
let mut underline_color = Color::Reset;
let mut modifier = Modifier::empty();
let mut last_pos: Option<Position> = None;
for (x, y, cell) in content {
if !matches!(last_pos, Some(p) if x == p.x + 1 && y == p.y) {
queue!(self.writer, MoveTo(x, y))?;
}
last_pos = Some(Position { x, y });
if cell.modifier != modifier {
let diff = ModifierDiff {
from: modifier,
to: cell.modifier,
};
diff.queue(&mut self.writer)?;
modifier = cell.modifier;
}
if cell.fg != fg || cell.bg != bg {
queue!(
self.writer,
SetColors(CrosstermColors::new(
cell.fg.into_crossterm(),
cell.bg.into_crossterm(),
))
)?;
fg = cell.fg;
bg = cell.bg;
}
#[cfg(feature = "underline-color")]
if cell.underline_color != underline_color {
let color = cell.underline_color.into_crossterm();
queue!(self.writer, SetUnderlineColor(color))?;
underline_color = cell.underline_color;
}
queue!(self.writer, Print(cell.symbol()))?;
}
#[cfg(feature = "underline-color")]
return queue!(
self.writer,
SetForegroundColor(CrosstermColor::Reset),
SetBackgroundColor(CrosstermColor::Reset),
SetUnderlineColor(CrosstermColor::Reset),
SetAttribute(CrosstermAttribute::Reset),
);
#[cfg(not(feature = "underline-color"))]
return queue!(
self.writer,
SetForegroundColor(CrosstermColor::Reset),
SetBackgroundColor(CrosstermColor::Reset),
SetAttribute(CrosstermAttribute::Reset),
);
}
fn hide_cursor(&mut self) -> io::Result<()> {
execute!(self.writer, Hide)
}
fn show_cursor(&mut self) -> io::Result<()> {
execute!(self.writer, Show)
}
fn get_cursor_position(&mut self) -> io::Result<Position> {
crossterm::cursor::position()
.map(|(x, y)| Position { x, y })
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
}
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
let Position { x, y } = position.into();
execute!(self.writer, MoveTo(x, y))
}
fn clear(&mut self) -> io::Result<()> {
self.clear_region(ClearType::All)
}
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
execute!(
self.writer,
Clear(match clear_type {
ClearType::All => crossterm::terminal::ClearType::All,
ClearType::AfterCursor => crossterm::terminal::ClearType::FromCursorDown,
ClearType::BeforeCursor => crossterm::terminal::ClearType::FromCursorUp,
ClearType::CurrentLine => crossterm::terminal::ClearType::CurrentLine,
ClearType::UntilNewLine => crossterm::terminal::ClearType::UntilNewLine,
})
)
}
fn append_lines(&mut self, n: u16) -> io::Result<()> {
for _ in 0..n {
queue!(self.writer, Print("\n"))?;
}
self.writer.flush()
}
fn size(&self) -> io::Result<Size> {
let (width, height) = terminal::size()?;
Ok(Size { width, height })
}
fn window_size(&mut self) -> io::Result<WindowSize> {
let crossterm::terminal::WindowSize {
columns,
rows,
width,
height,
} = terminal::window_size()?;
Ok(WindowSize {
columns_rows: Size {
width: columns,
height: rows,
},
pixels: Size { width, height },
})
}
fn flush(&mut self) -> io::Result<()> {
self.writer.flush()
}
#[cfg(feature = "scrolling-regions")]
fn scroll_region_up(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
queue!(
self.writer,
ScrollUpInRegion {
first_row: region.start,
last_row: region.end.saturating_sub(1),
lines_to_scroll: amount,
}
)?;
self.writer.flush()
}
#[cfg(feature = "scrolling-regions")]
fn scroll_region_down(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
queue!(
self.writer,
ScrollDownInRegion {
first_row: region.start,
last_row: region.end.saturating_sub(1),
lines_to_scroll: amount,
}
)?;
self.writer.flush()
}
}
pub trait IntoCrossterm<C> {
fn into_crossterm(self) -> C;
}
pub trait FromCrossterm<C> {
fn from_crossterm(value: C) -> Self;
}
impl IntoCrossterm<CrosstermColor> for Color {
fn into_crossterm(self) -> CrosstermColor {
match self {
Self::Reset => CrosstermColor::Reset,
Self::Black => CrosstermColor::Black,
Self::Red => CrosstermColor::DarkRed,
Self::Green => CrosstermColor::DarkGreen,
Self::Yellow => CrosstermColor::DarkYellow,
Self::Blue => CrosstermColor::DarkBlue,
Self::Magenta => CrosstermColor::DarkMagenta,
Self::Cyan => CrosstermColor::DarkCyan,
Self::Gray => CrosstermColor::Grey,
Self::DarkGray => CrosstermColor::DarkGrey,
Self::LightRed => CrosstermColor::Red,
Self::LightGreen => CrosstermColor::Green,
Self::LightBlue => CrosstermColor::Blue,
Self::LightYellow => CrosstermColor::Yellow,
Self::LightMagenta => CrosstermColor::Magenta,
Self::LightCyan => CrosstermColor::Cyan,
Self::White => CrosstermColor::White,
Self::Indexed(i) => CrosstermColor::AnsiValue(i),
Self::Rgb(r, g, b) => CrosstermColor::Rgb { r, g, b },
}
}
}
impl FromCrossterm<CrosstermColor> for Color {
fn from_crossterm(value: CrosstermColor) -> Self {
match value {
CrosstermColor::Reset => Self::Reset,
CrosstermColor::Black => Self::Black,
CrosstermColor::DarkRed => Self::Red,
CrosstermColor::DarkGreen => Self::Green,
CrosstermColor::DarkYellow => Self::Yellow,
CrosstermColor::DarkBlue => Self::Blue,
CrosstermColor::DarkMagenta => Self::Magenta,
CrosstermColor::DarkCyan => Self::Cyan,
CrosstermColor::Grey => Self::Gray,
CrosstermColor::DarkGrey => Self::DarkGray,
CrosstermColor::Red => Self::LightRed,
CrosstermColor::Green => Self::LightGreen,
CrosstermColor::Blue => Self::LightBlue,
CrosstermColor::Yellow => Self::LightYellow,
CrosstermColor::Magenta => Self::LightMagenta,
CrosstermColor::Cyan => Self::LightCyan,
CrosstermColor::White => Self::White,
CrosstermColor::Rgb { r, g, b } => Self::Rgb(r, g, b),
CrosstermColor::AnsiValue(v) => Self::Indexed(v),
}
}
}
struct ModifierDiff {
pub from: Modifier,
pub to: Modifier,
}
impl ModifierDiff {
fn queue<W>(self, mut w: W) -> io::Result<()>
where
W: io::Write,
{
let removed = self.from - self.to;
if removed.contains(Modifier::REVERSED) {
queue!(w, SetAttribute(CrosstermAttribute::NoReverse))?;
}
if removed.contains(Modifier::BOLD) {
queue!(w, SetAttribute(CrosstermAttribute::NormalIntensity))?;
if self.to.contains(Modifier::DIM) {
queue!(w, SetAttribute(CrosstermAttribute::Dim))?;
}
}
if removed.contains(Modifier::ITALIC) {
queue!(w, SetAttribute(CrosstermAttribute::NoItalic))?;
}
if removed.contains(Modifier::UNDERLINED) {
queue!(w, SetAttribute(CrosstermAttribute::NoUnderline))?;
}
if removed.contains(Modifier::DIM) {
queue!(w, SetAttribute(CrosstermAttribute::NormalIntensity))?;
}
if removed.contains(Modifier::CROSSED_OUT) {
queue!(w, SetAttribute(CrosstermAttribute::NotCrossedOut))?;
}
if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
queue!(w, SetAttribute(CrosstermAttribute::NoBlink))?;
}
let added = self.to - self.from;
if added.contains(Modifier::REVERSED) {
queue!(w, SetAttribute(CrosstermAttribute::Reverse))?;
}
if added.contains(Modifier::BOLD) {
queue!(w, SetAttribute(CrosstermAttribute::Bold))?;
}
if added.contains(Modifier::ITALIC) {
queue!(w, SetAttribute(CrosstermAttribute::Italic))?;
}
if added.contains(Modifier::UNDERLINED) {
queue!(w, SetAttribute(CrosstermAttribute::Underlined))?;
}
if added.contains(Modifier::DIM) {
queue!(w, SetAttribute(CrosstermAttribute::Dim))?;
}
if added.contains(Modifier::CROSSED_OUT) {
queue!(w, SetAttribute(CrosstermAttribute::CrossedOut))?;
}
if added.contains(Modifier::SLOW_BLINK) {
queue!(w, SetAttribute(CrosstermAttribute::SlowBlink))?;
}
if added.contains(Modifier::RAPID_BLINK) {
queue!(w, SetAttribute(CrosstermAttribute::RapidBlink))?;
}
Ok(())
}
}
impl FromCrossterm<CrosstermAttribute> for Modifier {
fn from_crossterm(value: CrosstermAttribute) -> Self {
Self::from_crossterm(CrosstermAttributes::from(value))
}
}
impl FromCrossterm<CrosstermAttributes> for Modifier {
fn from_crossterm(value: CrosstermAttributes) -> Self {
let mut res = Self::empty();
if value.has(CrosstermAttribute::Bold) {
res |= Self::BOLD;
}
if value.has(CrosstermAttribute::Dim) {
res |= Self::DIM;
}
if value.has(CrosstermAttribute::Italic) {
res |= Self::ITALIC;
}
if value.has(CrosstermAttribute::Underlined)
|| value.has(CrosstermAttribute::DoubleUnderlined)
|| value.has(CrosstermAttribute::Undercurled)
|| value.has(CrosstermAttribute::Underdotted)
|| value.has(CrosstermAttribute::Underdashed)
{
res |= Self::UNDERLINED;
}
if value.has(CrosstermAttribute::SlowBlink) {
res |= Self::SLOW_BLINK;
}
if value.has(CrosstermAttribute::RapidBlink) {
res |= Self::RAPID_BLINK;
}
if value.has(CrosstermAttribute::Reverse) {
res |= Self::REVERSED;
}
if value.has(CrosstermAttribute::Hidden) {
res |= Self::HIDDEN;
}
if value.has(CrosstermAttribute::CrossedOut) {
res |= Self::CROSSED_OUT;
}
res
}
}
impl FromCrossterm<ContentStyle> for Style {
fn from_crossterm(value: ContentStyle) -> Self {
let mut sub_modifier = Modifier::empty();
if value.attributes.has(CrosstermAttribute::NoBold) {
sub_modifier |= Modifier::BOLD;
}
if value.attributes.has(CrosstermAttribute::NoItalic) {
sub_modifier |= Modifier::ITALIC;
}
if value.attributes.has(CrosstermAttribute::NotCrossedOut) {
sub_modifier |= Modifier::CROSSED_OUT;
}
if value.attributes.has(CrosstermAttribute::NoUnderline) {
sub_modifier |= Modifier::UNDERLINED;
}
if value.attributes.has(CrosstermAttribute::NoHidden) {
sub_modifier |= Modifier::HIDDEN;
}
if value.attributes.has(CrosstermAttribute::NoBlink) {
sub_modifier |= Modifier::RAPID_BLINK | Modifier::SLOW_BLINK;
}
if value.attributes.has(CrosstermAttribute::NoReverse) {
sub_modifier |= Modifier::REVERSED;
}
Self {
fg: value.foreground_color.map(FromCrossterm::from_crossterm),
bg: value.background_color.map(FromCrossterm::from_crossterm),
#[cfg(feature = "underline-color")]
underline_color: value.underline_color.map(FromCrossterm::from_crossterm),
add_modifier: Modifier::from_crossterm(value.attributes),
sub_modifier,
}
}
}
#[cfg(feature = "scrolling-regions")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ScrollUpInRegion {
pub first_row: u16,
pub last_row: u16,
pub lines_to_scroll: u16,
}
#[cfg(feature = "scrolling-regions")]
impl crate::crossterm::Command for ScrollUpInRegion {
fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
if self.lines_to_scroll != 0 {
write!(
f,
crate::crossterm::csi!("{};{}r"),
self.first_row.saturating_add(1),
self.last_row.saturating_add(1)
)?;
write!(f, crate::crossterm::csi!("{}S"), self.lines_to_scroll)?;
write!(f, crate::crossterm::csi!("r"))?;
}
Ok(())
}
#[cfg(windows)]
fn execute_winapi(&self) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"ScrollUpInRegion command not supported for winapi",
))
}
}
#[cfg(feature = "scrolling-regions")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ScrollDownInRegion {
pub first_row: u16,
pub last_row: u16,
pub lines_to_scroll: u16,
}
#[cfg(feature = "scrolling-regions")]
impl crate::crossterm::Command for ScrollDownInRegion {
fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
if self.lines_to_scroll != 0 {
write!(
f,
crate::crossterm::csi!("{};{}r"),
self.first_row.saturating_add(1),
self.last_row.saturating_add(1)
)?;
write!(f, crate::crossterm::csi!("{}T"), self.lines_to_scroll)?;
write!(f, crate::crossterm::csi!("r"))?;
}
Ok(())
}
#[cfg(windows)]
fn execute_winapi(&self) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"ScrollDownInRegion command not supported for winapi",
))
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case(CrosstermColor::Reset, Color::Reset)]
#[case(CrosstermColor::Black, Color::Black)]
#[case(CrosstermColor::DarkGrey, Color::DarkGray)]
#[case(CrosstermColor::Red, Color::LightRed)]
#[case(CrosstermColor::DarkRed, Color::Red)]
#[case(CrosstermColor::Green, Color::LightGreen)]
#[case(CrosstermColor::DarkGreen, Color::Green)]
#[case(CrosstermColor::Yellow, Color::LightYellow)]
#[case(CrosstermColor::DarkYellow, Color::Yellow)]
#[case(CrosstermColor::Blue, Color::LightBlue)]
#[case(CrosstermColor::DarkBlue, Color::Blue)]
#[case(CrosstermColor::Magenta, Color::LightMagenta)]
#[case(CrosstermColor::DarkMagenta, Color::Magenta)]
#[case(CrosstermColor::Cyan, Color::LightCyan)]
#[case(CrosstermColor::DarkCyan, Color::Cyan)]
#[case(CrosstermColor::White, Color::White)]
#[case(CrosstermColor::Grey, Color::Gray)]
#[case(CrosstermColor::Rgb { r: 0, g: 0, b: 0 }, Color::Rgb(0, 0, 0) )]
#[case(CrosstermColor::Rgb { r: 10, g: 20, b: 30 }, Color::Rgb(10, 20, 30) )]
#[case(CrosstermColor::AnsiValue(32), Color::Indexed(32))]
#[case(CrosstermColor::AnsiValue(37), Color::Indexed(37))]
fn from_crossterm_color(#[case] crossterm_color: CrosstermColor, #[case] color: Color) {
assert_eq!(Color::from_crossterm(crossterm_color), color);
}
mod modifier {
use super::*;
#[rstest]
#[case(CrosstermAttribute::Reset, Modifier::empty())]
#[case(CrosstermAttribute::Bold, Modifier::BOLD)]
#[case(CrosstermAttribute::NoBold, Modifier::empty())]
#[case(CrosstermAttribute::Italic, Modifier::ITALIC)]
#[case(CrosstermAttribute::NoItalic, Modifier::empty())]
#[case(CrosstermAttribute::Underlined, Modifier::UNDERLINED)]
#[case(CrosstermAttribute::NoUnderline, Modifier::empty())]
#[case(CrosstermAttribute::OverLined, Modifier::empty())]
#[case(CrosstermAttribute::NotOverLined, Modifier::empty())]
#[case(CrosstermAttribute::DoubleUnderlined, Modifier::UNDERLINED)]
#[case(CrosstermAttribute::Undercurled, Modifier::UNDERLINED)]
#[case(CrosstermAttribute::Underdotted, Modifier::UNDERLINED)]
#[case(CrosstermAttribute::Underdashed, Modifier::UNDERLINED)]
#[case(CrosstermAttribute::Dim, Modifier::DIM)]
#[case(CrosstermAttribute::NormalIntensity, Modifier::empty())]
#[case(CrosstermAttribute::CrossedOut, Modifier::CROSSED_OUT)]
#[case(CrosstermAttribute::NotCrossedOut, Modifier::empty())]
#[case(CrosstermAttribute::NoUnderline, Modifier::empty())]
#[case(CrosstermAttribute::SlowBlink, Modifier::SLOW_BLINK)]
#[case(CrosstermAttribute::RapidBlink, Modifier::RAPID_BLINK)]
#[case(CrosstermAttribute::Hidden, Modifier::HIDDEN)]
#[case(CrosstermAttribute::NoHidden, Modifier::empty())]
#[case(CrosstermAttribute::Reverse, Modifier::REVERSED)]
#[case(CrosstermAttribute::NoReverse, Modifier::empty())]
fn from_crossterm_attribute(
#[case] crossterm_attribute: CrosstermAttribute,
#[case] ratatui_modifier: Modifier,
) {
assert_eq!(
Modifier::from_crossterm(crossterm_attribute),
ratatui_modifier
);
}
#[rstest]
#[case(&[CrosstermAttribute::Bold], Modifier::BOLD)]
#[case(&[CrosstermAttribute::Bold, CrosstermAttribute::Italic], Modifier::BOLD | Modifier::ITALIC)]
#[case(&[CrosstermAttribute::Bold, CrosstermAttribute::NotCrossedOut], Modifier::BOLD)]
#[case(&[CrosstermAttribute::Dim, CrosstermAttribute::Underdotted], Modifier::DIM | Modifier::UNDERLINED)]
#[case(&[CrosstermAttribute::Dim, CrosstermAttribute::SlowBlink, CrosstermAttribute::Italic], Modifier::DIM | Modifier::SLOW_BLINK | Modifier::ITALIC)]
#[case(&[CrosstermAttribute::Hidden, CrosstermAttribute::NoUnderline, CrosstermAttribute::NotCrossedOut], Modifier::HIDDEN)]
#[case(&[CrosstermAttribute::Reverse], Modifier::REVERSED)]
#[case(&[CrosstermAttribute::Reset], Modifier::empty())]
#[case(&[CrosstermAttribute::RapidBlink, CrosstermAttribute::CrossedOut], Modifier::RAPID_BLINK | Modifier::CROSSED_OUT)]
fn from_crossterm_attributes(
#[case] crossterm_attributes: &[CrosstermAttribute],
#[case] ratatui_modifier: Modifier,
) {
assert_eq!(
Modifier::from_crossterm(CrosstermAttributes::from(crossterm_attributes)),
ratatui_modifier
);
}
}
#[rstest]
#[case(ContentStyle::default(), Style::default())]
#[case(
ContentStyle {
foreground_color: Some(CrosstermColor::DarkYellow),
..Default::default()
},
Style::default().fg(Color::Yellow)
)]
#[case(
ContentStyle {
background_color: Some(CrosstermColor::DarkYellow),
..Default::default()
},
Style::default().bg(Color::Yellow)
)]
#[case(
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::Bold),
..Default::default()
},
Style::default().add_modifier(Modifier::BOLD)
)]
#[case(
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::NoBold),
..Default::default()
},
Style::default().remove_modifier(Modifier::BOLD)
)]
#[case(
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::Italic),
..Default::default()
},
Style::default().add_modifier(Modifier::ITALIC)
)]
#[case(
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::NoItalic),
..Default::default()
},
Style::default().remove_modifier(Modifier::ITALIC)
)]
#[case(
ContentStyle {
attributes: CrosstermAttributes::from(
[CrosstermAttribute::Bold, CrosstermAttribute::Italic].as_ref()
),
..Default::default()
},
Style::default()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::ITALIC)
)]
#[case(
ContentStyle {
attributes: CrosstermAttributes::from(
[CrosstermAttribute::NoBold, CrosstermAttribute::NoItalic].as_ref()
),
..Default::default()
},
Style::default()
.remove_modifier(Modifier::BOLD)
.remove_modifier(Modifier::ITALIC)
)]
fn from_crossterm_content_style(#[case] content_style: ContentStyle, #[case] style: Style) {
assert_eq!(Style::from_crossterm(content_style), style);
}
#[test]
#[cfg(feature = "underline-color")]
fn from_crossterm_content_style_underline() {
let content_style = ContentStyle {
underline_color: Some(CrosstermColor::DarkRed),
..Default::default()
};
assert_eq!(
Style::from_crossterm(content_style),
Style::default().underline_color(Color::Red)
);
}
}