Inline ansi modules (#529)

This commit is contained in:
Koxiaet 2021-01-03 13:29:29 +00:00 committed by GitHub
parent b442de72be
commit df0c2358fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 391 additions and 511 deletions

View File

@ -46,11 +46,10 @@ use std::fmt;
#[cfg(windows)]
use crate::Result;
use crate::{impl_display, Command};
use crate::{csi, impl_display, Command};
pub use sys::position;
mod ansi;
pub(crate) mod sys;
/// A command that moves the terminal cursor to the given position (column, row).
@ -64,7 +63,7 @@ pub struct MoveTo(pub u16, pub u16);
impl Command for MoveTo {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
ansi::move_to_csi_sequence(f, self.0, self.1)
write!(f, csi!("{};{}H"), self.1 + 1, self.0 + 1)
}
#[cfg(windows)]
@ -84,7 +83,7 @@ pub struct MoveToNextLine(pub u16);
impl Command for MoveToNextLine {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
ansi::move_to_next_line_csi_sequence(f, self.0)
write!(f, csi!("{}E"), self.0)
}
#[cfg(windows)]
@ -104,7 +103,7 @@ pub struct MoveToPreviousLine(pub u16);
impl Command for MoveToPreviousLine {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
ansi::move_to_previous_line_csi_sequence(f, self.0)
write!(f, csi!("{}F"), self.0)
}
#[cfg(windows)]
@ -123,7 +122,7 @@ pub struct MoveToColumn(pub u16);
impl Command for MoveToColumn {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
ansi::move_to_column_csi_sequence(f, self.0)
write!(f, csi!("{}G"), self.0)
}
#[cfg(windows)]
@ -142,7 +141,10 @@ pub struct MoveUp(pub u16);
impl Command for MoveUp {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
ansi::move_up_csi_sequence(f, self.0)
if self.0 != 0 {
write!(f, csi!("{}A"), self.0)?;
}
Ok(())
}
#[cfg(windows)]
@ -161,7 +163,10 @@ pub struct MoveRight(pub u16);
impl Command for MoveRight {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
ansi::move_right_csi_sequence(f, self.0)
if self.0 != 0 {
write!(f, csi!("{}C"), self.0)?;
}
Ok(())
}
#[cfg(windows)]
@ -180,7 +185,10 @@ pub struct MoveDown(pub u16);
impl Command for MoveDown {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
ansi::move_down_csi_sequence(f, self.0)
if self.0 != 0 {
write!(f, csi!("{}B"), self.0)?;
}
Ok(())
}
#[cfg(windows)]
@ -199,7 +207,10 @@ pub struct MoveLeft(pub u16);
impl Command for MoveLeft {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
ansi::move_left_csi_sequence(f, self.0)
if self.0 != 0 {
write!(f, csi!("{}D"), self.0)?;
}
Ok(())
}
#[cfg(windows)]
@ -221,7 +232,7 @@ pub struct SavePosition;
impl Command for SavePosition {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
f.write_str(ansi::SAVE_POSITION_CSI_SEQUENCE)
f.write_str("\x1B7")
}
#[cfg(windows)]
@ -243,7 +254,7 @@ pub struct RestorePosition;
impl Command for RestorePosition {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
f.write_str(ansi::RESTORE_POSITION_CSI_SEQUENCE)
f.write_str("\x1B8")
}
#[cfg(windows)]
@ -262,7 +273,7 @@ pub struct Hide;
impl Command for Hide {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
f.write_str(ansi::HIDE_CSI_SEQUENCE)
f.write_str(csi!("?25l"))
}
#[cfg(windows)]
@ -281,7 +292,7 @@ pub struct Show;
impl Command for Show {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
f.write_str(ansi::SHOW_CSI_SEQUENCE)
f.write_str(csi!("?25h"))
}
#[cfg(windows)]
@ -301,7 +312,7 @@ pub struct EnableBlinking;
impl Command for EnableBlinking {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
f.write_str(ansi::ENABLE_BLINKING_CSI_SEQUENCE)
f.write_str(csi!("?12h"))
}
#[cfg(windows)]
@ -321,7 +332,7 @@ pub struct DisableBlinking;
impl Command for DisableBlinking {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
f.write_str(ansi::DISABLE_BLINKING_CSI_SEQUENCE)
f.write_str(csi!("?12l"))
}
#[cfg(windows)]

View File

@ -1,62 +0,0 @@
//! This module provides cursor related ANSI escape codes.
use crate::csi;
use std::fmt;
pub(crate) fn move_to_csi_sequence(f: &mut impl fmt::Write, x: u16, y: u16) -> fmt::Result {
write!(f, csi!("{};{}H"), y + 1, x + 1)
}
pub(crate) fn move_up_csi_sequence(f: &mut impl fmt::Write, count: u16) -> fmt::Result {
if count != 0 {
write!(f, csi!("{}A"), count)
} else {
Ok(())
}
}
pub(crate) fn move_right_csi_sequence(f: &mut impl fmt::Write, count: u16) -> fmt::Result {
if count != 0 {
write!(f, csi!("{}C"), count)
} else {
Ok(())
}
}
pub(crate) fn move_down_csi_sequence(f: &mut impl fmt::Write, count: u16) -> fmt::Result {
if count != 0 {
write!(f, csi!("{}B"), count)
} else {
Ok(())
}
}
pub(crate) fn move_left_csi_sequence(f: &mut impl fmt::Write, count: u16) -> fmt::Result {
if count != 0 {
write!(f, csi!("{}D"), count)
} else {
Ok(())
}
}
pub(crate) fn move_to_column_csi_sequence(f: &mut impl fmt::Write, count: u16) -> fmt::Result {
write!(f, csi!("{}G"), count)
}
pub(crate) fn move_to_previous_line_csi_sequence(
f: &mut impl fmt::Write,
count: u16,
) -> fmt::Result {
write!(f, csi!("{}F"), count)
}
pub(crate) fn move_to_next_line_csi_sequence(f: &mut impl fmt::Write, count: u16) -> fmt::Result {
write!(f, csi!("{}E"), count)
}
pub(crate) const SAVE_POSITION_CSI_SEQUENCE: &str = "\x1B7";
pub(crate) const RESTORE_POSITION_CSI_SEQUENCE: &str = "\x1B8";
pub(crate) const HIDE_CSI_SEQUENCE: &str = csi!("?25l");
pub(crate) const SHOW_CSI_SEQUENCE: &str = csi!("?25h");
pub(crate) const ENABLE_BLINKING_CSI_SEQUENCE: &str = csi!("?12h");
pub(crate) const DISABLE_BLINKING_CSI_SEQUENCE: &str = csi!("?12l");

View File

@ -82,14 +82,13 @@ use serde::{Deserialize, Serialize};
use bitflags::bitflags;
use lazy_static::lazy_static;
use crate::{Command, Result};
use crate::{csi, Command, Result};
use filter::{EventFilter, Filter};
#[cfg(feature = "event-stream")]
pub use stream::EventStream;
use timeout::PollTimeout;
mod ansi;
pub(crate) mod filter;
mod read;
mod source;
@ -230,7 +229,18 @@ pub struct EnableMouseCapture;
impl Command for EnableMouseCapture {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
f.write_str(ansi::ENABLE_MOUSE_MODE_CSI_SEQUENCE)
f.write_str(concat!(
// Normal tracking: Send mouse X & Y on button press and release
csi!("?1000h"),
// Button-event tracking: Report button motion events (dragging)
csi!("?1002h"),
// Any-event tracking: Report all motion events
csi!("?1003h"),
// RXVT mouse mode: Allows mouse coordinates of >223
csi!("?1015h"),
// SGR mouse mode: Allows mouse coordinates of >223, preferred over RXVT mode
csi!("?1006h"),
))
}
#[cfg(windows)]
@ -252,7 +262,14 @@ pub struct DisableMouseCapture;
impl Command for DisableMouseCapture {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
f.write_str(ansi::DISABLE_MOUSE_MODE_CSI_SEQUENCE)
f.write_str(concat!(
// The inverse commands of EnableMouseCapture, in reverse order.
csi!("?1006l"),
csi!("?1015l"),
csi!("?1003l"),
csi!("?1002l"),
csi!("?1000l"),
))
}
#[cfg(windows)]

View File

@ -1,25 +0,0 @@
//! This module provides input related ANSI escape codes.
use crate::csi;
pub(crate) const ENABLE_MOUSE_MODE_CSI_SEQUENCE: &str = concat!(
// Normal tracking: Send mouse X & Y on button press and release
csi!("?1000h"),
// Button-event tracking: Report button motion events (dragging)
csi!("?1002h"),
// Any-event tracking: Report all motion events
csi!("?1003h"),
// RXVT mouse mode: Allows mouse coordinates of >223
csi!("?1015h"),
// SGR mouse mode: Allows mouse coordinates of >223, preferred over RXVT mode
csi!("?1006h"),
);
pub(crate) const DISABLE_MOUSE_MODE_CSI_SEQUENCE: &str = concat!(
// The above, in reverse order.
csi!("?1006l"),
csi!("?1015l"),
csi!("?1003l"),
csi!("?1002l"),
csi!("?1000l"),
);

View File

@ -117,7 +117,7 @@ use std::{
#[cfg(windows)]
use crate::Result;
use crate::{impl_display, Command};
use crate::{csi, impl_display, Command};
pub use self::{
attributes::Attributes,
@ -129,7 +129,6 @@ pub use self::{
#[macro_use]
mod macros;
mod ansi;
mod attributes;
mod content_style;
mod styled_content;
@ -205,7 +204,7 @@ pub struct SetForegroundColor(pub Color);
impl Command for SetForegroundColor {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
ansi::set_fg_csi_sequence(f, self.0)
write!(f, csi!("{}m"), Colored::ForegroundColor(self.0))
}
#[cfg(windows)]
@ -229,7 +228,7 @@ pub struct SetBackgroundColor(pub Color);
impl Command for SetBackgroundColor {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
ansi::set_bg_csi_sequence(f, self.0)
write!(f, csi!("{}m"), Colored::BackgroundColor(self.0))
}
#[cfg(windows)]
@ -265,10 +264,10 @@ pub struct SetColors(pub Colors);
impl Command for SetColors {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
if let Some(color) = self.0.foreground {
ansi::set_fg_csi_sequence(f, color)?;
SetForegroundColor(color).write_ansi(f)?;
}
if let Some(color) = self.0.background {
ansi::set_bg_csi_sequence(f, color)?;
SetBackgroundColor(color).write_ansi(f)?;
}
Ok(())
}
@ -297,7 +296,7 @@ pub struct SetAttribute(pub Attribute);
impl Command for SetAttribute {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
ansi::set_attr_csi_sequence(f, self.0)
write!(f, csi!("{}m"), self.0.sgr())
}
#[cfg(windows)]
@ -319,7 +318,12 @@ pub struct SetAttributes(pub Attributes);
impl Command for SetAttributes {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
ansi::set_attrs_csi_sequence(f, self.0)
for attr in Attribute::iterator() {
if self.0.has(attr) {
SetAttribute(attr).write_ansi(f)?;
}
}
Ok(())
}
#[cfg(windows)]
@ -360,7 +364,7 @@ pub struct ResetColor;
impl Command for ResetColor {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
f.write_str(ansi::RESET_CSI_SEQUENCE)
f.write_str(csi!("0m"))
}
#[cfg(windows)]
@ -399,3 +403,9 @@ impl_display!(for SetAttribute);
impl_display!(for PrintStyledContent<String>);
impl_display!(for PrintStyledContent<&'static str>);
impl_display!(for ResetColor);
/// Utility function for ANSI parsing in Color and Colored.
/// Gets the next element of `iter` and tries to parse it as a u8.
fn parse_next_u8<'a>(iter: &mut impl Iterator<Item = &'a str>) -> Option<u8> {
iter.next().and_then(|s| u8::from_str_radix(s, 10).ok())
}

View File

@ -1,341 +0,0 @@
//! This is a ANSI specific implementation for styling related action.
//! This module is used for Windows 10 terminals and Unix terminals by default.
use std::fmt::{self, Formatter};
use crate::{
csi,
style::{Attribute, Attributes, Color, Colored},
};
pub(crate) fn set_fg_csi_sequence(f: &mut impl fmt::Write, fg_color: Color) -> fmt::Result {
write!(f, csi!("{}m"), Colored::ForegroundColor(fg_color))
}
pub(crate) fn set_bg_csi_sequence(f: &mut impl fmt::Write, bg_color: Color) -> fmt::Result {
write!(f, csi!("{}m"), Colored::BackgroundColor(bg_color))
}
pub(crate) fn set_attr_csi_sequence(f: &mut impl fmt::Write, attribute: Attribute) -> fmt::Result {
write!(f, csi!("{}m"), attribute.sgr())
}
pub(crate) fn set_attrs_csi_sequence(
f: &mut impl fmt::Write,
attributes: Attributes,
) -> fmt::Result {
for attr in Attribute::iterator() {
if attributes.has(attr) {
write!(f, csi!("{}m"), attr.sgr())?;
}
}
Ok(())
}
pub(crate) const RESET_CSI_SEQUENCE: &str = csi!("0m");
impl fmt::Display for Colored {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let color;
match *self {
Colored::ForegroundColor(new_color) => {
if new_color == Color::Reset {
return f.write_str("39");
} else {
f.write_str("38;")?;
color = new_color;
}
}
Colored::BackgroundColor(new_color) => {
if new_color == Color::Reset {
return f.write_str("49");
} else {
f.write_str("48;")?;
color = new_color;
}
}
}
match color {
Color::Black => f.write_str("5;0"),
Color::DarkGrey => f.write_str("5;8"),
Color::Red => f.write_str("5;9"),
Color::DarkRed => f.write_str("5;1"),
Color::Green => f.write_str("5;10"),
Color::DarkGreen => f.write_str("5;2"),
Color::Yellow => f.write_str("5;11"),
Color::DarkYellow => f.write_str("5;3"),
Color::Blue => f.write_str("5;12"),
Color::DarkBlue => f.write_str("5;4"),
Color::Magenta => f.write_str("5;13"),
Color::DarkMagenta => f.write_str("5;5"),
Color::Cyan => f.write_str("5;14"),
Color::DarkCyan => f.write_str("5;6"),
Color::White => f.write_str("5;15"),
Color::Grey => f.write_str("5;7"),
Color::Rgb { r, g, b } => write!(f, "2;{};{};{}", r, g, b),
Color::AnsiValue(val) => write!(f, "5;{}", val),
_ => Ok(()),
}
}
}
/// Utility function for ANSI parsing in Color and Colored.
/// Gets the next element of `iter` and tries to parse it as a u8.
fn parse_next_u8<'a>(iter: &mut impl Iterator<Item = &'a str>) -> Option<u8> {
iter.next()
.and_then(|s| u8::from_str_radix(s, 10).map(Some).unwrap_or(None))
}
impl Colored {
/// Parse an ANSI foreground or background color.
/// This is the string that would appear within an `ESC [ <str> m` escape sequence, as found in
/// various configuration files.
///
/// For example:
/// * `38;5;0 -> ForegroundColor(Black)`,
/// * `38;5;26 -> ForegroundColor(AnsiValue(26))`
/// * `48;2;50;60;70 -> BackgroundColor(Rgb(50, 60, 70))`
/// * `49 -> BackgroundColor(Reset)`
/// Invalid sequences map to `None`.
///
/// Currently, 3/4 bit color values aren't supported so return `None`.
///
/// See also: [Color::parse_ansi](enum.Color.html#method.parse_ansi)
pub fn parse_ansi(ansi: &str) -> Option<Self> {
use Colored::{BackgroundColor, ForegroundColor};
let values = &mut ansi.split(';');
let output = match parse_next_u8(values)? {
38 => return Color::parse_ansi_iter(values).map(ForegroundColor),
48 => return Color::parse_ansi_iter(values).map(BackgroundColor),
39 => ForegroundColor(Color::Reset),
49 => BackgroundColor(Color::Reset),
_ => return None,
};
if values.next().is_some() {
return None;
}
Some(output)
}
}
impl<'a> Color {
/// Parses an ANSI color sequence.
/// For example:
/// * `5;0 -> Black`,
/// * `5;26 -> AnsiValue(26)`,
/// * `2;50;60;70 -> Rgb(50, 60, 70)`.
/// Invalid sequences map to `None`.
///
/// Currently, 3/4 bit color values aren't supported so return `None`.
///
/// See also: [Colored::parse_ansi](enum.Colored.html#method.parse_ansi)
pub fn parse_ansi(ansi: &str) -> Option<Self> {
Self::parse_ansi_iter(&mut ansi.split(';'))
}
/// The logic for parse_ansi, takes an iterator of the sequences terms (the numbers between the
/// ';'). It's a separate function so it can be used by both Color::parse_ansi and
/// colored::parse_ansi.
/// Tested in Colored tests.
fn parse_ansi_iter(values: &mut impl Iterator<Item = &'a str>) -> Option<Self> {
let color = match parse_next_u8(values)? {
// 8 bit colors: `5;<n>`
5 => {
let n = parse_next_u8(values)?;
use Color::*;
[
Black, // 0
DarkRed, // 1
DarkGreen, // 2
DarkYellow, // 3
DarkBlue, // 4
DarkMagenta, // 5
DarkCyan, // 6
Grey, // 7
DarkGrey, // 8
Red, // 9
Green, // 10
Yellow, // 11
Blue, // 12
Magenta, // 13
Cyan, // 14
White, // 15
]
.get(n as usize)
.copied()
.unwrap_or(Color::AnsiValue(n))
}
// 24 bit colors: `2;<r>;<g>;<b>`
2 => Color::Rgb {
r: parse_next_u8(values)?,
g: parse_next_u8(values)?,
b: parse_next_u8(values)?,
},
_ => return None,
};
// If there's another value, it's unexpected so return None.
if values.next().is_some() {
return None;
}
Some(color)
}
}
#[cfg(test)]
mod tests {
use crate::style::{Color, Colored};
#[test]
fn test_format_fg_color() {
let colored = Colored::ForegroundColor(Color::Red);
assert_eq!(colored.to_string(), "38;5;9");
}
#[test]
fn test_format_bg_color() {
let colored = Colored::BackgroundColor(Color::Red);
assert_eq!(colored.to_string(), "48;5;9");
}
#[test]
fn test_format_reset_fg_color() {
let colored = Colored::ForegroundColor(Color::Reset);
assert_eq!(colored.to_string(), "39");
}
#[test]
fn test_format_reset_bg_color() {
let colored = Colored::BackgroundColor(Color::Reset);
assert_eq!(colored.to_string(), "49");
}
#[test]
fn test_format_fg_rgb_color() {
let colored = Colored::BackgroundColor(Color::Rgb { r: 1, g: 2, b: 3 });
assert_eq!(colored.to_string(), "48;2;1;2;3");
}
#[test]
fn test_format_fg_ansi_color() {
let colored = Colored::ForegroundColor(Color::AnsiValue(255));
assert_eq!(colored.to_string(), "38;5;255");
}
#[test]
fn test_parse_ansi_fg() {
test_parse_ansi(Colored::ForegroundColor)
}
#[test]
fn test_parse_ansi_bg() {
test_parse_ansi(Colored::ForegroundColor)
}
/// Used for test_parse_ansi_fg and test_parse_ansi_bg
fn test_parse_ansi(bg_or_fg: impl Fn(Color) -> Colored) {
/// Formats a re-parses `color` to check the result.
macro_rules! test {
($color:expr) => {
let colored = bg_or_fg($color);
assert_eq!(Colored::parse_ansi(&format!("{}", colored)), Some(colored));
};
}
use Color::*;
test!(Reset);
test!(Black);
test!(DarkGrey);
test!(Red);
test!(DarkRed);
test!(Green);
test!(DarkGreen);
test!(Yellow);
test!(DarkYellow);
test!(Blue);
test!(DarkBlue);
test!(Magenta);
test!(DarkMagenta);
test!(Cyan);
test!(DarkCyan);
test!(White);
test!(Grey);
// n in 0..=15 will give us the color values above back.
for n in 16..=255 {
test!(AnsiValue(n));
}
for r in 0..=255 {
for g in [0, 2, 18, 19, 60, 100, 200, 250, 254, 255].iter().copied() {
for b in [0, 12, 16, 99, 100, 161, 200, 255].iter().copied() {
test!(Rgb { r, g, b });
}
}
}
}
#[test]
fn test_parse_invalid_ansi_color() {
/// Checks that trying to parse `s` yields None.
fn test(s: &str) {
assert_eq!(Colored::parse_ansi(s), None);
}
test("");
test(";");
test(";;");
test(";;");
test("0");
test("1");
test("12");
test("100");
test("100048949345");
test("39;");
test("49;");
test("39;2");
test("49;2");
test("38");
test("38;");
test("38;0");
test("38;5");
test("38;5;0;");
test("38;5;0;2");
test("38;5;80;");
test("38;5;80;2");
test("38;5;257");
test("38;2");
test("38;2;");
test("38;2;0");
test("38;2;0;2");
test("38;2;0;2;257");
test("38;2;0;2;25;");
test("38;2;0;2;25;3");
test("48");
test("48;");
test("48;0");
test("48;5");
test("48;5;0;");
test("48;5;0;2");
test("48;5;80;");
test("48;5;80;2");
test("48;5;257");
test("48;2");
test("48;2;");
test("48;2;0");
test("48;2;0;2");
test("48;2;0;2;257");
test("48;2;0;2;25;");
test("48;2;0;2;25;3");
}
}

View File

@ -3,6 +3,8 @@ use std::{convert::AsRef, convert::TryFrom, result::Result, str::FromStr};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::style::parse_next_u8;
/// Represents a color.
///
/// # Platform-specific Notes
@ -90,6 +92,72 @@ pub enum Color {
AnsiValue(u8),
}
impl Color {
/// Parses an ANSI color sequence.
/// For example:
/// * `5;0 -> Black`,
/// * `5;26 -> AnsiValue(26)`,
/// * `2;50;60;70 -> Rgb(50, 60, 70)`.
/// Invalid sequences map to `None`.
///
/// Currently, 3/4 bit color values aren't supported so return `None`.
///
/// See also: [`Colored::parse_ansi`](crate::style::Colored::parse_ansi).
pub fn parse_ansi(ansi: &str) -> Option<Self> {
Self::parse_ansi_iter(&mut ansi.split(';'))
}
/// The logic for parse_ansi, takes an iterator of the sequences terms (the numbers between the
/// ';'). It's a separate function so it can be used by both Color::parse_ansi and
/// colored::parse_ansi.
/// Tested in Colored tests.
pub(crate) fn parse_ansi_iter<'a>(values: &mut impl Iterator<Item = &'a str>) -> Option<Self> {
let color = match parse_next_u8(values)? {
// 8 bit colors: `5;<n>`
5 => {
let n = parse_next_u8(values)?;
use Color::*;
[
Black, // 0
DarkRed, // 1
DarkGreen, // 2
DarkYellow, // 3
DarkBlue, // 4
DarkMagenta, // 5
DarkCyan, // 6
Grey, // 7
DarkGrey, // 8
Red, // 9
Green, // 10
Yellow, // 11
Blue, // 12
Magenta, // 13
Cyan, // 14
White, // 15
]
.get(n as usize)
.copied()
.unwrap_or(Color::AnsiValue(n))
}
// 24 bit colors: `2;<r>;<g>;<b>`
2 => Color::Rgb {
r: parse_next_u8(values)?,
g: parse_next_u8(values)?,
b: parse_next_u8(values)?,
},
_ => return None,
};
// If there's another value, it's unexpected so return None.
if values.next().is_some() {
return None;
}
Some(color)
}
}
impl TryFrom<&str> for Color {
type Error = ();

View File

@ -1,7 +1,9 @@
use std::fmt::{self, Formatter};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::style::Color;
use crate::style::{parse_next_u8, Color};
/// Represents a foreground or background color.
///
@ -15,3 +17,236 @@ pub enum Colored {
/// A background color.
BackgroundColor(Color),
}
impl Colored {
/// Parse an ANSI foreground or background color.
/// This is the string that would appear within an `ESC [ <str> m` escape sequence, as found in
/// various configuration files.
///
/// For example:
/// * `38;5;0 -> ForegroundColor(Black)`,
/// * `38;5;26 -> ForegroundColor(AnsiValue(26))`
/// * `48;2;50;60;70 -> BackgroundColor(Rgb(50, 60, 70))`
/// * `49 -> BackgroundColor(Reset)`
/// Invalid sequences map to `None`.
///
/// Currently, 3/4 bit color values aren't supported so return `None`.
///
/// See also: [Color::parse_ansi](enum.Color.html#method.parse_ansi)
pub fn parse_ansi(ansi: &str) -> Option<Self> {
use Colored::{BackgroundColor, ForegroundColor};
let values = &mut ansi.split(';');
let output = match parse_next_u8(values)? {
38 => return Color::parse_ansi_iter(values).map(ForegroundColor),
48 => return Color::parse_ansi_iter(values).map(BackgroundColor),
39 => ForegroundColor(Color::Reset),
49 => BackgroundColor(Color::Reset),
_ => return None,
};
if values.next().is_some() {
return None;
}
Some(output)
}
}
impl fmt::Display for Colored {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let color;
match *self {
Colored::ForegroundColor(new_color) => {
if new_color == Color::Reset {
return f.write_str("39");
} else {
f.write_str("38;")?;
color = new_color;
}
}
Colored::BackgroundColor(new_color) => {
if new_color == Color::Reset {
return f.write_str("49");
} else {
f.write_str("48;")?;
color = new_color;
}
}
}
match color {
Color::Black => f.write_str("5;0"),
Color::DarkGrey => f.write_str("5;8"),
Color::Red => f.write_str("5;9"),
Color::DarkRed => f.write_str("5;1"),
Color::Green => f.write_str("5;10"),
Color::DarkGreen => f.write_str("5;2"),
Color::Yellow => f.write_str("5;11"),
Color::DarkYellow => f.write_str("5;3"),
Color::Blue => f.write_str("5;12"),
Color::DarkBlue => f.write_str("5;4"),
Color::Magenta => f.write_str("5;13"),
Color::DarkMagenta => f.write_str("5;5"),
Color::Cyan => f.write_str("5;14"),
Color::DarkCyan => f.write_str("5;6"),
Color::White => f.write_str("5;15"),
Color::Grey => f.write_str("5;7"),
Color::Rgb { r, g, b } => write!(f, "2;{};{};{}", r, g, b),
Color::AnsiValue(val) => write!(f, "5;{}", val),
_ => Ok(()),
}
}
}
#[cfg(test)]
mod tests {
use crate::style::{Color, Colored};
#[test]
fn test_format_fg_color() {
let colored = Colored::ForegroundColor(Color::Red);
assert_eq!(colored.to_string(), "38;5;9");
}
#[test]
fn test_format_bg_color() {
let colored = Colored::BackgroundColor(Color::Red);
assert_eq!(colored.to_string(), "48;5;9");
}
#[test]
fn test_format_reset_fg_color() {
let colored = Colored::ForegroundColor(Color::Reset);
assert_eq!(colored.to_string(), "39");
}
#[test]
fn test_format_reset_bg_color() {
let colored = Colored::BackgroundColor(Color::Reset);
assert_eq!(colored.to_string(), "49");
}
#[test]
fn test_format_fg_rgb_color() {
let colored = Colored::BackgroundColor(Color::Rgb { r: 1, g: 2, b: 3 });
assert_eq!(colored.to_string(), "48;2;1;2;3");
}
#[test]
fn test_format_fg_ansi_color() {
let colored = Colored::ForegroundColor(Color::AnsiValue(255));
assert_eq!(colored.to_string(), "38;5;255");
}
#[test]
fn test_parse_ansi_fg() {
test_parse_ansi(Colored::ForegroundColor)
}
#[test]
fn test_parse_ansi_bg() {
test_parse_ansi(Colored::ForegroundColor)
}
/// Used for test_parse_ansi_fg and test_parse_ansi_bg
fn test_parse_ansi(bg_or_fg: impl Fn(Color) -> Colored) {
/// Formats a re-parses `color` to check the result.
macro_rules! test {
($color:expr) => {
let colored = bg_or_fg($color);
assert_eq!(Colored::parse_ansi(&format!("{}", colored)), Some(colored));
};
}
use Color::*;
test!(Reset);
test!(Black);
test!(DarkGrey);
test!(Red);
test!(DarkRed);
test!(Green);
test!(DarkGreen);
test!(Yellow);
test!(DarkYellow);
test!(Blue);
test!(DarkBlue);
test!(Magenta);
test!(DarkMagenta);
test!(Cyan);
test!(DarkCyan);
test!(White);
test!(Grey);
// n in 0..=15 will give us the color values above back.
for n in 16..=255 {
test!(AnsiValue(n));
}
for r in 0..=255 {
for g in [0, 2, 18, 19, 60, 100, 200, 250, 254, 255].iter().copied() {
for b in [0, 12, 16, 99, 100, 161, 200, 255].iter().copied() {
test!(Rgb { r, g, b });
}
}
}
}
#[test]
fn test_parse_invalid_ansi_color() {
/// Checks that trying to parse `s` yields None.
fn test(s: &str) {
assert_eq!(Colored::parse_ansi(s), None);
}
test("");
test(";");
test(";;");
test(";;");
test("0");
test("1");
test("12");
test("100");
test("100048949345");
test("39;");
test("49;");
test("39;2");
test("49;2");
test("38");
test("38;");
test("38;0");
test("38;5");
test("38;5;0;");
test("38;5;0;2");
test("38;5;80;");
test("38;5;80;2");
test("38;5;257");
test("38;2");
test("38;2;");
test("38;2;0");
test("38;2;0;2");
test("38;2;0;2;257");
test("38;2;0;2;25;");
test("38;2;0;2;25;3");
test("48");
test("48;");
test("48;0");
test("48;5");
test("48;5;0;");
test("48;5;0;2");
test("48;5;80;");
test("48;5;80;2");
test("48;5;257");
test("48;2");
test("48;2;");
test("48;2;0");
test("48;2;0;2");
test("48;2;0;2;257");
test("48;2;0;2;25;");
test("48;2;0;2;25;3");
}
}

View File

@ -92,9 +92,8 @@ use winapi::um::wincon::ENABLE_WRAP_AT_EOL_OUTPUT;
#[doc(no_inline)]
use crate::Command;
use crate::{impl_display, Result};
use crate::{csi, impl_display, Result};
mod ansi;
pub(crate) mod sys;
/// Enables raw mode.
@ -124,7 +123,7 @@ pub struct DisableLineWrap;
impl Command for DisableLineWrap {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
f.write_str(ansi::DISABLE_LINE_WRAP_CSI_SEQUENCE)
f.write_str(csi!("?7l"))
}
#[cfg(windows)]
@ -143,7 +142,7 @@ pub struct EnableLineWrap;
impl Command for EnableLineWrap {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
f.write_str(ansi::ENABLE_LINE_WRAP_CSI_SEQUENCE)
f.write_str(csi!("?7h"))
}
#[cfg(windows)]
@ -183,7 +182,7 @@ pub struct EnterAlternateScreen;
impl Command for EnterAlternateScreen {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
f.write_str(ansi::ENTER_ALTERNATE_SCREEN_CSI_SEQUENCE)
f.write_str(csi!("?1049h"))
}
#[cfg(windows)]
@ -221,7 +220,7 @@ pub struct LeaveAlternateScreen;
impl Command for LeaveAlternateScreen {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
f.write_str(ansi::LEAVE_ALTERNATE_SCREEN_CSI_SEQUENCE)
f.write_str(csi!("?1049l"))
}
#[cfg(windows)]
@ -258,7 +257,10 @@ pub struct ScrollUp(pub u16);
impl Command for ScrollUp {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
ansi::scroll_up_csi_sequence(f, self.0)
if self.0 != 0 {
write!(f, csi!("{}S"), self.0)?;
}
Ok(())
}
#[cfg(windows)]
@ -277,7 +279,10 @@ pub struct ScrollDown(pub u16);
impl Command for ScrollDown {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
ansi::scroll_down_csi_sequence(f, self.0)
if self.0 != 0 {
write!(f, csi!("{}T"), self.0)?;
}
Ok(())
}
#[cfg(windows)]
@ -299,11 +304,11 @@ pub struct Clear(pub ClearType);
impl Command for Clear {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
f.write_str(match self.0 {
ClearType::All => ansi::CLEAR_ALL_CSI_SEQUENCE,
ClearType::FromCursorDown => ansi::CLEAR_FROM_CURSOR_DOWN_CSI_SEQUENCE,
ClearType::FromCursorUp => ansi::CLEAR_FROM_CURSOR_UP_CSI_SEQUENCE,
ClearType::CurrentLine => ansi::CLEAR_FROM_CURRENT_LINE_CSI_SEQUENCE,
ClearType::UntilNewLine => ansi::CLEAR_UNTIL_NEW_LINE_CSI_SEQUENCE,
ClearType::All => csi!("2J"),
ClearType::FromCursorDown => csi!("J"),
ClearType::FromCursorUp => csi!("1J"),
ClearType::CurrentLine => csi!("2K"),
ClearType::UntilNewLine => csi!("K"),
})
}
@ -323,7 +328,7 @@ pub struct SetSize(pub u16, pub u16);
impl Command for SetSize {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
ansi::set_size_csi_sequence(f, self.0, self.1)
write!(f, csi!("8;{};{}t"), self.1, self.0)
}
#[cfg(windows)]
@ -342,7 +347,7 @@ pub struct SetTitle<T>(pub T);
impl<T: fmt::Display> Command for SetTitle<T> {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
ansi::set_title_ansi_sequence(f, &self.0)
write!(f, "\x1B]0;{}\x07", &self.0)
}
#[cfg(windows)]

View File

@ -1,38 +0,0 @@
//! This module provides terminal related ANSI escape codes.
use std::fmt;
use crate::csi;
pub(crate) const CLEAR_ALL_CSI_SEQUENCE: &str = csi!("2J");
pub(crate) const CLEAR_FROM_CURSOR_DOWN_CSI_SEQUENCE: &str = csi!("J");
pub(crate) const CLEAR_FROM_CURSOR_UP_CSI_SEQUENCE: &str = csi!("1J");
pub(crate) const CLEAR_FROM_CURRENT_LINE_CSI_SEQUENCE: &str = csi!("2K");
pub(crate) const CLEAR_UNTIL_NEW_LINE_CSI_SEQUENCE: &str = csi!("K");
pub(crate) const ENTER_ALTERNATE_SCREEN_CSI_SEQUENCE: &str = csi!("?1049h");
pub(crate) const LEAVE_ALTERNATE_SCREEN_CSI_SEQUENCE: &str = csi!("?1049l");
pub(crate) const DISABLE_LINE_WRAP_CSI_SEQUENCE: &str = csi!("?7l");
pub(crate) const ENABLE_LINE_WRAP_CSI_SEQUENCE: &str = csi!("?7h");
pub(crate) fn scroll_up_csi_sequence(f: &mut impl fmt::Write, count: u16) -> fmt::Result {
write!(f, csi!("{}S"), count)
}
pub(crate) fn scroll_down_csi_sequence(f: &mut impl fmt::Write, count: u16) -> fmt::Result {
write!(f, csi!("{}T"), count)
}
pub(crate) fn set_size_csi_sequence(
f: &mut impl fmt::Write,
width: u16,
height: u16,
) -> fmt::Result {
write!(f, csi!("8;{};{}t"), height, width)
}
pub(crate) fn set_title_ansi_sequence(
f: &mut impl fmt::Write,
title: impl fmt::Display,
) -> fmt::Result {
write!(f, "\x1B]0;{}\x07", title)
}