use crate::caps::{Capabilities, ColorLevel};
use crate::cell::{AttributeChange, Blink, CellAttributes, Intensity, Underline};
use crate::color::{ColorAttribute, ColorSpec};
use crate::escape::csi::{Cursor, Edit, EraseInDisplay, EraseInLine, Sgr, CSI};
use crate::escape::osc::{ITermDimension, ITermFileData, ITermProprietary, OperatingSystemCommand};
use crate::escape::OneBased;
use crate::image::TextureCoordinate;
use crate::render::RenderTty;
use crate::surface::{Change, CursorShape, Position};
use std::io::Write;
use terminfo::{capability as cap, Capability as TermInfoCapability};
pub struct TerminfoRenderer {
caps: Capabilities,
current_attr: CellAttributes,
pending_attr: Option<CellAttributes>,
}
impl TerminfoRenderer {
pub fn new(caps: Capabilities) -> Self {
Self {
caps,
current_attr: CellAttributes::default(),
pending_attr: None,
}
}
fn get_capability<'a, T: TermInfoCapability<'a>>(&'a self) -> Option<T> {
self.caps.terminfo_db().and_then(|db| db.get::<T>())
}
fn attr_apply<F: FnOnce(&mut CellAttributes)>(&mut self, func: F) {
self.pending_attr = Some(match self.pending_attr.take() {
Some(mut attr) => {
func(&mut attr);
attr
}
None => {
let mut attr = self.current_attr.clone();
func(&mut attr);
attr
}
});
}
#[cfg_attr(feature = "cargo-clippy", allow(clippy::cognitive_complexity))]
fn flush_pending_attr<W: RenderTty + Write>(&mut self, out: &mut W) -> anyhow::Result<()> {
macro_rules! attr_on {
($cap:ident, $sgr:expr) => {
if let Some(attr) = self.get_capability::<cap::$cap>() {
attr.expand().to(out.by_ref())?;
} else {
write!(out, "{}", CSI::Sgr($sgr))?;
}
};
($sgr:expr) => {
write!(out, "{}", CSI::Sgr($sgr))?;
};
}
if let Some(attr) = self.pending_attr.take() {
let mut current_foreground = self.current_attr.foreground;
let mut current_background = self.current_attr.background;
if !attr.attribute_bits_equal(&self.current_attr) {
current_foreground = ColorAttribute::Default;
current_background = ColorAttribute::Default;
if let Some(sgr) = self.get_capability::<cap::SetAttributes>() {
sgr.expand()
.bold(attr.intensity() == Intensity::Bold)
.dim(attr.intensity() == Intensity::Half)
.underline(attr.underline() == Underline::Single)
.blink(attr.blink() == Blink::Slow)
.reverse(attr.reverse())
.invisible(attr.invisible())
.to(out.by_ref())?;
} else {
attr_on!(ExitAttributeMode, Sgr::Reset);
match attr.intensity() {
Intensity::Bold => attr_on!(EnterBoldMode, Sgr::Intensity(Intensity::Bold)),
Intensity::Half => attr_on!(EnterDimMode, Sgr::Intensity(Intensity::Half)),
_ => {}
}
if attr.underline() == Underline::Single {
attr_on!(Sgr::Underline(Underline::Single));
}
if attr.blink() == Blink::Slow {
attr_on!(Sgr::Blink(Blink::Slow));
}
if attr.reverse() {
attr_on!(EnterReverseMode, Sgr::Inverse(true));
}
if attr.invisible() {
attr_on!(Sgr::Invisible(true));
}
}
if attr.underline() == Underline::Double {
attr_on!(Sgr::Underline(Underline::Double));
}
if attr.blink() == Blink::Rapid {
attr_on!(Sgr::Blink(Blink::Rapid));
}
if attr.italic() {
attr_on!(EnterItalicsMode, Sgr::Italic(true));
}
if attr.strikethrough() {
attr_on!(Sgr::StrikeThrough(true));
}
}
let has_true_color = self.caps.color_level() == ColorLevel::TrueColor;
if attr.foreground != current_foreground {
match (has_true_color, attr.foreground) {
(true, ColorAttribute::TrueColorWithPaletteFallback(tc, _))
| (true, ColorAttribute::TrueColorWithDefaultFallback(tc)) => {
write!(
out,
"{}",
CSI::Sgr(Sgr::Foreground(ColorSpec::TrueColor(tc)))
)?;
}
(false, ColorAttribute::TrueColorWithDefaultFallback(_))
| (_, ColorAttribute::Default) => {
write!(out, "{}", CSI::Sgr(Sgr::Foreground(ColorSpec::Default)))?;
}
(false, ColorAttribute::TrueColorWithPaletteFallback(_, idx))
| (_, ColorAttribute::PaletteIndex(idx)) => {
if let Some(set) = self.get_capability::<cap::SetAForeground>() {
set.expand().color(idx).to(out.by_ref())?;
} else {
write!(
out,
"{}",
CSI::Sgr(Sgr::Foreground(ColorSpec::PaletteIndex(idx)))
)?;
}
}
}
}
if attr.background != current_background {
match (has_true_color, attr.background) {
(true, ColorAttribute::TrueColorWithPaletteFallback(tc, _))
| (true, ColorAttribute::TrueColorWithDefaultFallback(tc)) => {
write!(
out,
"{}",
CSI::Sgr(Sgr::Background(ColorSpec::TrueColor(tc)))
)?;
}
(false, ColorAttribute::TrueColorWithDefaultFallback(_))
| (_, ColorAttribute::Default) => {
write!(out, "{}", CSI::Sgr(Sgr::Background(ColorSpec::Default)))?;
}
(false, ColorAttribute::TrueColorWithPaletteFallback(_, idx))
| (_, ColorAttribute::PaletteIndex(idx)) => {
if let Some(set) = self.get_capability::<cap::SetABackground>() {
set.expand().color(idx).to(out.by_ref())?;
} else {
write!(
out,
"{}",
CSI::Sgr(Sgr::Background(ColorSpec::PaletteIndex(idx)))
)?;
}
}
}
}
if self.caps.hyperlinks() {
if let Some(link) = attr.hyperlink.as_ref() {
let osc = OperatingSystemCommand::SetHyperlink(Some((**link).clone()));
write!(out, "{}", osc)?;
} else if self.current_attr.hyperlink.is_some() {
let osc = OperatingSystemCommand::SetHyperlink(None);
write!(out, "{}", osc)?;
}
}
self.current_attr = attr;
}
Ok(())
}
fn cursor_up<W: RenderTty + Write>(&mut self, n: u32, out: &mut W) -> anyhow::Result<()> {
if n > 0 {
if let Some(attr) = self.get_capability::<cap::ParmUpCursor>() {
attr.expand().count(n).to(out.by_ref())?;
} else {
write!(out, "{}", CSI::Cursor(Cursor::Up(n)))?;
}
}
Ok(())
}
fn cursor_down<W: RenderTty + Write>(&mut self, n: u32, out: &mut W) -> anyhow::Result<()> {
if n > 0 {
if let Some(attr) = self.get_capability::<cap::ParmDownCursor>() {
attr.expand().count(n).to(out.by_ref())?;
} else {
write!(out, "{}", CSI::Cursor(Cursor::Down(n)))?;
}
}
Ok(())
}
fn cursor_y_relative<W: RenderTty + Write>(
&mut self,
y: isize,
out: &mut W,
) -> anyhow::Result<()> {
if y > 0 {
self.cursor_down(y as u32, out)
} else {
self.cursor_up(-y as u32, out)
}
}
fn cursor_left<W: RenderTty + Write>(&mut self, n: u32, out: &mut W) -> anyhow::Result<()> {
if n > 0 {
if let Some(attr) = self.get_capability::<cap::ParmLeftCursor>() {
attr.expand().count(n).to(out.by_ref())?;
} else {
write!(out, "{}", CSI::Cursor(Cursor::Left(n)))?;
}
}
Ok(())
}
fn cursor_right<W: RenderTty + Write>(&mut self, n: u32, out: &mut W) -> anyhow::Result<()> {
if n > 0 {
if let Some(attr) = self.get_capability::<cap::ParmRightCursor>() {
attr.expand().count(n).to(out.by_ref())?;
} else {
write!(out, "{}", CSI::Cursor(Cursor::Right(n)))?;
}
}
Ok(())
}
fn cursor_x_relative<W: RenderTty + Write>(
&mut self,
x: isize,
out: &mut W,
) -> anyhow::Result<()> {
if x > 0 {
self.cursor_right(x as u32, out)
} else {
self.cursor_left(-x as u32, out)
}
}
fn move_cursor_absolute<W: RenderTty + Write>(
&mut self,
x: u32,
y: u32,
out: &mut W,
) -> anyhow::Result<()> {
if x == 0 && y == 0 {
if let Some(attr) = self.get_capability::<cap::CursorHome>() {
attr.expand().to(out.by_ref())?;
return Ok(());
}
}
if let Some(attr) = self.get_capability::<cap::CursorAddress>() {
attr.expand().x(x).y(y).to(out.by_ref())?;
} else {
write!(
out,
"{}",
CSI::Cursor(Cursor::Position {
line: OneBased::from_zero_based(x),
col: OneBased::from_zero_based(y),
})
)?;
}
Ok(())
}
#[cfg_attr(
feature = "cargo-clippy",
allow(clippy::cyclomatic_complexity, clippy::cognitive_complexity)
)]
pub fn render_to<W: RenderTty + Write>(
&mut self,
changes: &[Change],
out: &mut W,
) -> anyhow::Result<()> {
macro_rules! record {
($accesor:ident, $value:expr) => {
self.attr_apply(|attr| {
attr.$accesor(*$value);
});
};
}
for change in changes {
match change {
Change::ClearScreen(color) => {
let defaults = CellAttributes::default()
.set_background(color.clone())
.clone();
if self.current_attr != defaults {
self.pending_attr = Some(defaults);
self.flush_pending_attr(out)?;
}
self.pending_attr = None;
if self.current_attr.background == ColorAttribute::Default || self.caps.bce() {
if let Some(clr) = self.get_capability::<cap::ClearScreen>() {
clr.expand().to(out.by_ref())?;
} else {
self.move_cursor_absolute(0, 0, out)?;
write!(
out,
"{}",
CSI::Edit(Edit::EraseInDisplay(EraseInDisplay::EraseDisplay))
)?;
}
} else {
self.move_cursor_absolute(0, 0, out)?;
let (cols, rows) = out.get_size_in_cells()?;
let num_spaces = cols * rows;
let mut buf = Vec::with_capacity(num_spaces);
buf.resize(num_spaces, b' ');
out.write_all(buf.as_slice())?;
}
}
Change::ClearToEndOfLine(color) => {
let defaults = CellAttributes::default()
.set_background(color.clone())
.clone();
if self.current_attr != defaults {
self.pending_attr = Some(defaults);
self.flush_pending_attr(out)?;
}
self.pending_attr = None;
if let Some(clr) = self.get_capability::<cap::ClrEol>() {
clr.expand().to(out.by_ref())?;
} else {
write!(
out,
"{}",
CSI::Edit(Edit::EraseInLine(EraseInLine::EraseToEndOfLine))
)?;
}
}
Change::ClearToEndOfScreen(color) => {
let defaults = CellAttributes::default()
.set_background(color.clone())
.clone();
if self.current_attr != defaults {
self.pending_attr = Some(defaults);
self.flush_pending_attr(out)?;
}
self.pending_attr = None;
if let Some(clr) = self.get_capability::<cap::ClrEos>() {
clr.expand().to(out.by_ref())?;
} else {
write!(
out,
"{}",
CSI::Edit(Edit::EraseInDisplay(EraseInDisplay::EraseToEndOfDisplay))
)?;
}
}
Change::Attribute(AttributeChange::Intensity(value)) => {
record!(set_intensity, value);
}
Change::Attribute(AttributeChange::Italic(value)) => {
record!(set_italic, value);
}
Change::Attribute(AttributeChange::Reverse(value)) => {
record!(set_reverse, value);
}
Change::Attribute(AttributeChange::StrikeThrough(value)) => {
record!(set_strikethrough, value);
}
Change::Attribute(AttributeChange::Blink(value)) => {
record!(set_blink, value);
}
Change::Attribute(AttributeChange::Invisible(value)) => {
record!(set_invisible, value);
}
Change::Attribute(AttributeChange::Underline(value)) => {
record!(set_underline, value);
}
Change::Attribute(AttributeChange::Foreground(col)) => {
self.attr_apply(|attr| attr.foreground = *col);
}
Change::Attribute(AttributeChange::Background(col)) => {
self.attr_apply(|attr| attr.background = *col);
}
Change::Attribute(AttributeChange::Hyperlink(link)) => {
self.attr_apply(|attr| attr.hyperlink = link.clone());
}
Change::AllAttributes(all) => {
self.pending_attr = Some(all.clone());
}
Change::Text(text) => {
self.flush_pending_attr(out)?;
out.by_ref().write_all(text.as_bytes())?;
}
Change::CursorPosition { x, y } => {
match (x, y) {
(Position::Relative(0), Position::Relative(0)) => {}
(Position::Absolute(0), Position::Relative(1)) => {
out.by_ref().write_all(b"\r\n")?;
}
(Position::Absolute(x), Position::Relative(y)) => {
self.cursor_y_relative(*y, out)?;
out.by_ref().write_all(b"\r")?;
self.cursor_right(*x as u32, out)?;
}
(Position::Relative(x), Position::EndRelative(y)) => {
let (_cols, rows) = out.get_size_in_cells()?;
self.cursor_up(rows as u32, out)?;
self.cursor_down(rows.saturating_sub(y + 1) as u32, out)?;
self.cursor_x_relative(*x, out)?;
}
(Position::Relative(x), Position::Relative(y)) => {
self.cursor_y_relative(*y, out)?;
self.cursor_x_relative(*x, out)?;
}
(Position::EndRelative(x), Position::Relative(y)) => {
self.cursor_y_relative(*y, out)?;
let (cols, _rows) = out.get_size_in_cells()?;
out.by_ref().write_all(b"\r")?;
self.cursor_right(cols.saturating_sub(x + 1) as u32, out)?;
}
(Position::Absolute(x), Position::Absolute(y)) => {
self.move_cursor_absolute(*x as u32, *y as u32, out)?;
}
(Position::Absolute(x), Position::EndRelative(y)) => {
let (_cols, rows) = out.get_size_in_cells()?;
self.move_cursor_absolute(
*x as u32,
rows.saturating_sub(y + 1) as u32,
out,
)?;
}
(Position::EndRelative(x), Position::EndRelative(y)) => {
let (cols, rows) = out.get_size_in_cells()?;
self.move_cursor_absolute(
cols.saturating_sub(x + 1) as u32,
rows.saturating_sub(y + 1) as u32,
out,
)?;
}
(Position::Relative(x), Position::Absolute(y)) => {
let (_cols, rows) = out.get_size_in_cells()?;
self.cursor_up(rows as u32, out)?;
self.cursor_down(*y as u32, out)?;
self.cursor_x_relative(*x, out)?;
}
(Position::EndRelative(x), Position::Absolute(y)) => {
let (cols, _rows) = out.get_size_in_cells()?;
self.move_cursor_absolute(
cols.saturating_sub(x + 1) as u32,
*y as u32,
out,
)?;
}
}
}
Change::CursorColor(_color) => {
}
Change::CursorShape(shape) => match shape {
CursorShape::Default => {
if let Some(normal) = self.get_capability::<cap::CursorNormal>() {
normal.expand().to(out.by_ref())?;
} else {
if let Some(show) = self.get_capability::<cap::CursorVisible>() {
show.expand().to(out.by_ref())?;
}
}
if let Some(reset) = self.get_capability::<cap::ResetCursorStyle>() {
reset.expand().to(out.by_ref())?;
}
}
CursorShape::Hidden => {
if let Some(hide) = self.get_capability::<cap::CursorInvisible>() {
hide.expand().to(out.by_ref())?;
}
}
_ => {
if let Some(show) = self.get_capability::<cap::CursorVisible>() {
show.expand().to(out.by_ref())?;
}
let param = match shape {
CursorShape::Default | CursorShape::Hidden => unreachable!(),
CursorShape::BlinkingBlock => 1,
CursorShape::SteadyBlock => 2,
CursorShape::BlinkingUnderline => 3,
CursorShape::SteadyUnderline => 4,
CursorShape::BlinkingBar => 5,
CursorShape::SteadyBar => 6,
};
if let Some(set) = self.get_capability::<cap::SetCursorStyle>() {
set.expand().kind(param).to(out.by_ref())?;
}
}
},
Change::Image(image) => {
if self.caps.iterm2_image() {
let data = if image.top_left == TextureCoordinate::new_f32(0.0, 0.0)
&& image.bottom_right == TextureCoordinate::new_f32(1.0, 1.0)
{
image.image.data().to_vec()
} else {
unimplemented!();
};
let file = ITermFileData {
name: None,
size: Some(data.len()),
width: ITermDimension::Cells(image.width as i64),
height: ITermDimension::Cells(image.height as i64),
preserve_aspect_ratio: true,
inline: true,
data,
};
let osc = OperatingSystemCommand::ITermProprietary(ITermProprietary::File(
Box::new(file),
));
write!(out, "{}", osc)?;
} else {
for y in 0..image.height {
for _ in 0..image.width {
write!(out, " ")?;
}
if y != image.height - 1 {
writeln!(out)?;
self.cursor_left(image.width as u32, out)?;
}
}
self.cursor_up(image.height as u32, out)?;
}
}
Change::ScrollRegionUp {
first_row,
region_size,
scroll_count,
} => {
if *region_size > 0 {
if let Some(csr) = self.get_capability::<cap::ChangeScrollRegion>() {
let top = *first_row as u32;
let bottom = (*first_row + *region_size - 1) as u32;
let scroll_count = *scroll_count as u32;
csr.expand().top(top).bottom(bottom).to(out.by_ref())?;
if scroll_count > 0 {
if let Some(scroll) = self.get_capability::<cap::ParmIndex>() {
scroll.expand().count(scroll_count).to(out.by_ref())?
} else {
let scroll = self.get_capability::<cap::ScrollForward>();
let set_position = self.get_capability::<cap::CursorAddress>();
if let (Some(scroll), Some(set_position)) =
(scroll, set_position)
{
set_position.expand().x(0).y(bottom).to(out.by_ref())?;
for _ in 0..scroll_count {
scroll.expand().to(out.by_ref())?
}
}
}
}
}
}
}
Change::ScrollRegionDown {
first_row,
region_size,
scroll_count,
} => {
if *region_size > 0 {
if let Some(csr) = self.get_capability::<cap::ChangeScrollRegion>() {
let top = *first_row as u32;
let bottom = (*first_row + *region_size - 1) as u32;
let scroll_count = *scroll_count as u32;
csr.expand().top(top).bottom(bottom).to(out.by_ref())?;
if scroll_count > 0 {
if let Some(scroll) = self.get_capability::<cap::ParmRindex>() {
scroll.expand().count(scroll_count).to(out.by_ref())?
} else {
let scroll = self.get_capability::<cap::ScrollReverse>();
let set_position = self.get_capability::<cap::CursorAddress>();
if let (Some(scroll), Some(set_position)) =
(scroll, set_position)
{
set_position.expand().x(0).y(top).to(out.by_ref())?;
for _ in 0..scroll_count {
scroll.expand().to(out.by_ref())?
}
}
}
}
}
}
}
Change::Title(_text) => {
}
}
}
self.flush_pending_attr(out)?;
out.flush()?;
Ok(())
}
}
#[cfg(all(test, unix))]
mod test {
use super::*;
use crate::caps::ProbeHints;
use crate::color::{AnsiColor, ColorAttribute, RgbColor};
use crate::escape::parser::Parser;
use crate::escape::{Action, Esc, EscCode};
use crate::input::InputEvent;
use crate::terminal::unix::{Purge, SetAttributeWhen, UnixTty};
use crate::terminal::ScreenSize;
use crate::terminal::{cast, Terminal, TerminalWaker};
use anyhow::bail;
use libc::winsize;
use std::io::{Error as IoError, ErrorKind, Read, Result as IoResult, Write};
use std::mem;
use std::time::Duration;
use terminfo;
use termios::Termios;
fn xterm_terminfo() -> Capabilities {
let data = include_bytes!("../../data/xterm-256color");
Capabilities::new_with_hints(ProbeHints::default().terminfo_db(Some(
terminfo::Database::from_buffer(data.as_ref()).unwrap(),
)))
.unwrap()
}
fn no_terminfo_all_enabled() -> Capabilities {
Capabilities::new_with_hints(ProbeHints::default().color_level(Some(ColorLevel::TrueColor)))
.unwrap()
}
struct FakeTty {
buf: Vec<u8>,
size: winsize,
termios: Termios,
}
impl FakeTty {
fn new_with_size(width: usize, height: usize) -> Self {
let size = winsize {
ws_col: cast(width).unwrap(),
ws_row: cast(height).unwrap(),
ws_xpixel: 0,
ws_ypixel: 0,
};
let buf = Vec::new();
Self {
size,
buf,
termios: unsafe { mem::zeroed() },
}
}
}
impl RenderTty for FakeTty {
fn get_size_in_cells(&mut self) -> anyhow::Result<(usize, usize)> {
Ok((self.size.ws_col as usize, self.size.ws_row as usize))
}
}
impl UnixTty for FakeTty {
fn get_size(&mut self) -> anyhow::Result<winsize> {
Ok(self.size.clone())
}
fn set_size(&mut self, size: winsize) -> anyhow::Result<()> {
self.size = size.clone();
Ok(())
}
fn get_termios(&mut self) -> anyhow::Result<Termios> {
Ok(self.termios.clone())
}
fn set_termios(
&mut self,
termios: &Termios,
_when: SetAttributeWhen,
) -> anyhow::Result<()> {
self.termios = termios.clone();
Ok(())
}
fn drain(&mut self) -> anyhow::Result<()> {
Ok(())
}
fn purge(&mut self, _purge: Purge) -> anyhow::Result<()> {
Ok(())
}
}
impl Read for FakeTty {
fn read(&mut self, _buf: &mut [u8]) -> Result<usize, IoError> {
Err(IoError::new(ErrorKind::Other, "not implemented"))
}
}
impl Write for FakeTty {
fn write(&mut self, buf: &[u8]) -> IoResult<usize> {
self.buf.write(buf)
}
fn flush(&mut self) -> IoResult<()> {
self.buf.flush()
}
}
struct FakeTerm {
write: FakeTty,
renderer: TerminfoRenderer,
}
impl FakeTerm {
fn new(caps: Capabilities) -> Self {
Self::new_with_size(caps, 80, 24)
}
fn new_with_size(caps: Capabilities, width: usize, height: usize) -> Self {
let write = FakeTty::new_with_size(width, height);
let renderer = TerminfoRenderer::new(caps);
Self { write, renderer }
}
fn parse(&self) -> Vec<Action> {
let mut p = Parser::new();
p.parse_as_vec(&self.write.buf)
}
}
impl Terminal for FakeTerm {
fn set_raw_mode(&mut self) -> anyhow::Result<()> {
bail!("not implemented");
}
fn set_cooked_mode(&mut self) -> anyhow::Result<()> {
bail!("not implemented");
}
fn enter_alternate_screen(&mut self) -> anyhow::Result<()> {
bail!("not implemented");
}
fn exit_alternate_screen(&mut self) -> anyhow::Result<()> {
bail!("not implemented");
}
fn render(&mut self, changes: &[Change]) -> anyhow::Result<()> {
self.renderer.render_to(changes, &mut self.write)
}
fn get_screen_size(&mut self) -> anyhow::Result<ScreenSize> {
let size = self.write.get_size()?;
Ok(ScreenSize {
rows: cast(size.ws_row)?,
cols: cast(size.ws_col)?,
xpixel: cast(size.ws_xpixel)?,
ypixel: cast(size.ws_ypixel)?,
})
}
fn set_screen_size(&mut self, size: ScreenSize) -> anyhow::Result<()> {
let size = winsize {
ws_row: cast(size.rows)?,
ws_col: cast(size.cols)?,
ws_xpixel: cast(size.xpixel)?,
ws_ypixel: cast(size.ypixel)?,
};
self.write.set_size(size)
}
fn flush(&mut self) -> anyhow::Result<()> {
Ok(())
}
fn poll_input(&mut self, _wait: Option<Duration>) -> anyhow::Result<Option<InputEvent>> {
bail!("not implemented");
}
fn waker(&self) -> TerminalWaker {
unimplemented!();
}
}
#[test]
fn empty_render() {
let mut out = FakeTerm::new(xterm_terminfo());
out.render(&[]).unwrap();
assert_eq!("", String::from_utf8(out.write.buf).unwrap());
assert_eq!(out.renderer.current_attr, CellAttributes::default());
}
#[test]
fn basic_text() {
let mut out = FakeTerm::new(xterm_terminfo());
out.render(&[Change::Text("foo".into())]).unwrap();
assert_eq!("foo", String::from_utf8(out.write.buf).unwrap());
assert_eq!(out.renderer.current_attr, CellAttributes::default());
}
#[test]
fn bold_text() {
let mut out = FakeTerm::new(xterm_terminfo());
out.render(&[
Change::Text("not ".into()),
Change::Attribute(AttributeChange::Intensity(Intensity::Bold)),
Change::Text("foo".into()),
])
.unwrap();
let result = out.parse();
assert_eq!(
result,
vec![
Action::Print('n'),
Action::Print('o'),
Action::Print('t'),
Action::Print(' '),
Action::Esc(Esc::Code(EscCode::AsciiCharacterSet)),
Action::CSI(CSI::Sgr(Sgr::Reset)),
Action::CSI(CSI::Sgr(Sgr::Intensity(Intensity::Bold))),
Action::Print('f'),
Action::Print('o'),
Action::Print('o'),
]
);
assert_eq!(
out.renderer.current_attr,
CellAttributes::default()
.set_intensity(Intensity::Bold)
.clone()
);
}
#[test]
fn clear_screen() {
let mut out = FakeTerm::new_with_size(xterm_terminfo(), 4, 3);
out.render(&[Change::ClearScreen(ColorAttribute::default())])
.unwrap();
let result = out.parse();
assert_eq!(
result,
vec![
Action::CSI(CSI::Cursor(Cursor::Position {
line: OneBased::new(1),
col: OneBased::new(1)
})),
Action::CSI(CSI::Edit(Edit::EraseInDisplay(
EraseInDisplay::EraseDisplay,
))),
]
);
assert_eq!(out.renderer.current_attr, CellAttributes::default());
}
#[test]
fn clear_screen_bce() {
let mut out = FakeTerm::new_with_size(xterm_terminfo(), 4, 3);
out.render(&[Change::ClearScreen(AnsiColor::Maroon.into())])
.unwrap();
let result = out.parse();
assert_eq!(
result,
vec![
Action::CSI(CSI::Sgr(Sgr::Background(AnsiColor::Maroon.into()))),
Action::CSI(CSI::Cursor(Cursor::Position {
line: OneBased::new(1),
col: OneBased::new(1)
})),
Action::CSI(CSI::Edit(Edit::EraseInDisplay(
EraseInDisplay::EraseDisplay,
))),
]
);
assert_eq!(
out.renderer.current_attr,
CellAttributes::default()
.set_background(AnsiColor::Maroon)
.clone()
);
}
#[test]
fn clear_screen_no_terminfo() {
let mut out = FakeTerm::new_with_size(no_terminfo_all_enabled(), 4, 3);
out.render(&[Change::ClearScreen(ColorAttribute::default())])
.unwrap();
let result = out.parse();
assert_eq!(
result,
vec![
Action::CSI(CSI::Cursor(Cursor::Position {
line: OneBased::new(1),
col: OneBased::new(1)
})),
Action::CSI(CSI::Edit(Edit::EraseInDisplay(
EraseInDisplay::EraseDisplay,
))),
]
);
assert_eq!(out.renderer.current_attr, CellAttributes::default());
}
#[test]
fn clear_screen_bce_no_terminfo() {
let mut out = FakeTerm::new_with_size(no_terminfo_all_enabled(), 4, 3);
out.render(&[Change::ClearScreen(AnsiColor::Maroon.into())])
.unwrap();
let result = out.parse();
assert_eq!(
result,
vec![
Action::CSI(CSI::Sgr(Sgr::Background(AnsiColor::Maroon.into()))),
Action::CSI(CSI::Cursor(Cursor::Position {
line: OneBased::new(1),
col: OneBased::new(1)
})),
Action::Print(' '),
Action::Print(' '),
Action::Print(' '),
Action::Print(' '),
Action::Print(' '),
Action::Print(' '),
Action::Print(' '),
Action::Print(' '),
Action::Print(' '),
Action::Print(' '),
Action::Print(' '),
Action::Print(' '),
]
);
assert_eq!(
out.renderer.current_attr,
CellAttributes::default()
.set_background(AnsiColor::Maroon)
.clone()
);
}
#[test]
fn bold_text_no_terminfo() {
let mut out = FakeTerm::new(no_terminfo_all_enabled());
out.render(&[
Change::Text("not ".into()),
Change::Attribute(AttributeChange::Intensity(Intensity::Bold)),
Change::Text("foo".into()),
])
.unwrap();
let result = out.parse();
assert_eq!(
result,
vec![
Action::Print('n'),
Action::Print('o'),
Action::Print('t'),
Action::Print(' '),
Action::CSI(CSI::Sgr(Sgr::Reset)),
Action::CSI(CSI::Sgr(Sgr::Intensity(Intensity::Bold))),
Action::Print('f'),
Action::Print('o'),
Action::Print('o'),
]
);
assert_eq!(
out.renderer.current_attr,
CellAttributes::default()
.set_intensity(Intensity::Bold)
.clone()
);
}
#[test]
fn red_bold_text() {
let mut out = FakeTerm::new(xterm_terminfo());
out.render(&[
Change::Attribute(AttributeChange::Foreground(AnsiColor::Maroon.into())),
Change::Attribute(AttributeChange::Intensity(Intensity::Bold)),
Change::Text("red".into()),
Change::Attribute(AttributeChange::Foreground(AnsiColor::Red.into())),
])
.unwrap();
let result = out.parse();
assert_eq!(
result,
vec![
Action::Esc(Esc::Code(EscCode::AsciiCharacterSet)),
Action::CSI(CSI::Sgr(Sgr::Reset)),
Action::CSI(CSI::Sgr(Sgr::Intensity(Intensity::Bold))),
Action::CSI(CSI::Sgr(Sgr::Foreground(AnsiColor::Maroon.into()))),
Action::Print('r'),
Action::Print('e'),
Action::Print('d'),
Action::CSI(CSI::Sgr(Sgr::Foreground(AnsiColor::Red.into()))),
]
);
assert_eq!(
out.renderer.current_attr,
CellAttributes::default()
.set_intensity(Intensity::Bold)
.set_foreground(AnsiColor::Red)
.clone()
);
}
#[test]
fn red_bold_text_no_terminfo() {
let mut out = FakeTerm::new(no_terminfo_all_enabled());
out.render(&[
Change::Attribute(AttributeChange::Foreground(AnsiColor::Maroon.into())),
Change::Attribute(AttributeChange::Intensity(Intensity::Bold)),
Change::Text("red".into()),
Change::Attribute(AttributeChange::Foreground(AnsiColor::Red.into())),
])
.unwrap();
let result = out.parse();
assert_eq!(
result,
vec![
Action::CSI(CSI::Sgr(Sgr::Reset)),
Action::CSI(CSI::Sgr(Sgr::Intensity(Intensity::Bold))),
Action::CSI(CSI::Sgr(Sgr::Foreground(AnsiColor::Maroon.into()))),
Action::Print('r'),
Action::Print('e'),
Action::Print('d'),
Action::CSI(CSI::Sgr(Sgr::Foreground(AnsiColor::Red.into()))),
]
);
assert_eq!(
out.renderer.current_attr,
CellAttributes::default()
.set_intensity(Intensity::Bold)
.set_foreground(AnsiColor::Red)
.clone()
);
}
#[test]
fn color_after_attribute_change() {
let mut out = FakeTerm::new(xterm_terminfo());
out.render(&[
Change::Attribute(AttributeChange::Foreground(AnsiColor::Maroon.into())),
Change::Attribute(AttributeChange::Intensity(Intensity::Bold)),
Change::Text("red".into()),
Change::Attribute(AttributeChange::Intensity(Intensity::Normal)),
Change::Text("2".into()),
])
.unwrap();
let result = out.parse();
assert_eq!(
result,
vec![
Action::Esc(Esc::Code(EscCode::AsciiCharacterSet)),
Action::CSI(CSI::Sgr(Sgr::Reset)),
Action::CSI(CSI::Sgr(Sgr::Intensity(Intensity::Bold))),
Action::CSI(CSI::Sgr(Sgr::Foreground(AnsiColor::Maroon.into()))),
Action::Print('r'),
Action::Print('e'),
Action::Print('d'),
Action::Esc(Esc::Code(EscCode::AsciiCharacterSet)),
Action::CSI(CSI::Sgr(Sgr::Reset)),
Action::CSI(CSI::Sgr(Sgr::Foreground(AnsiColor::Maroon.into()))),
Action::Print('2'),
]
);
assert_eq!(
out.renderer.current_attr,
CellAttributes::default()
.set_foreground(AnsiColor::Maroon)
.clone()
);
}
#[test]
fn truecolor() {
let mut out = FakeTerm::new(xterm_terminfo());
out.render(&[
Change::Attribute(AttributeChange::Foreground(
ColorSpec::TrueColor(RgbColor::new(255, 128, 64)).into(),
)),
Change::Text("A".into()),
])
.unwrap();
let result = out.parse();
assert_eq!(
result,
vec![
Action::CSI(CSI::Sgr(Sgr::Foreground(
ColorSpec::TrueColor(RgbColor::new(255, 128, 64)).into(),
))),
Action::Print('A'),
]
);
}
#[test]
fn truecolor_no_terminfo() {
let mut out = FakeTerm::new(no_terminfo_all_enabled());
out.render(&[
Change::Attribute(AttributeChange::Foreground(
ColorSpec::TrueColor(RgbColor::new(255, 128, 64)).into(),
)),
Change::Text("A".into()),
])
.unwrap();
let result = out.parse();
assert_eq!(
result,
vec![
Action::CSI(CSI::Sgr(Sgr::Foreground(
ColorSpec::TrueColor(RgbColor::new(255, 128, 64)).into(),
))),
Action::Print('A'),
]
);
}
}