From df0c2358fd0c1bbf0f380fcfb59b2ef909b033c3 Mon Sep 17 00:00:00 2001 From: Koxiaet Date: Sun, 3 Jan 2021 13:29:29 +0000 Subject: [PATCH] Inline ansi modules (#529) --- src/cursor.rs | 43 +++-- src/cursor/ansi.rs | 62 ------- src/event.rs | 25 ++- src/event/ansi.rs | 25 --- src/style.rs | 28 ++- src/style/ansi.rs | 341 ------------------------------------- src/style/types/color.rs | 68 ++++++++ src/style/types/colored.rs | 237 +++++++++++++++++++++++++- src/terminal.rs | 35 ++-- src/terminal/ansi.rs | 38 ----- 10 files changed, 391 insertions(+), 511 deletions(-) delete mode 100644 src/cursor/ansi.rs delete mode 100644 src/event/ansi.rs delete mode 100644 src/style/ansi.rs delete mode 100644 src/terminal/ansi.rs diff --git a/src/cursor.rs b/src/cursor.rs index 9491a1fa..ee93ed02 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -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)] diff --git a/src/cursor/ansi.rs b/src/cursor/ansi.rs deleted file mode 100644 index c3c6b91c..00000000 --- a/src/cursor/ansi.rs +++ /dev/null @@ -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"); diff --git a/src/event.rs b/src/event.rs index ebb12b19..2cce6574 100644 --- a/src/event.rs +++ b/src/event.rs @@ -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)] diff --git a/src/event/ansi.rs b/src/event/ansi.rs deleted file mode 100644 index b1259507..00000000 --- a/src/event/ansi.rs +++ /dev/null @@ -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"), -); diff --git a/src/style.rs b/src/style.rs index aba0b26c..25899a0d 100644 --- a/src/style.rs +++ b/src/style.rs @@ -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); 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) -> Option { + iter.next().and_then(|s| u8::from_str_radix(s, 10).ok()) +} diff --git a/src/style/ansi.rs b/src/style/ansi.rs deleted file mode 100644 index 8107125e..00000000 --- a/src/style/ansi.rs +++ /dev/null @@ -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) -> Option { - 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 [ 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 { - 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::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) -> Option { - let color = match parse_next_u8(values)? { - // 8 bit colors: `5;` - 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;;;` - 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"); - } -} diff --git a/src/style/types/color.rs b/src/style/types/color.rs index 9507289a..a98aa868 100644 --- a/src/style/types/color.rs +++ b/src/style/types/color.rs @@ -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::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) -> Option { + let color = match parse_next_u8(values)? { + // 8 bit colors: `5;` + 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;;;` + 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 = (); diff --git a/src/style/types/colored.rs b/src/style/types/colored.rs index d4e7aebf..f88486b8 100644 --- a/src/style/types/colored.rs +++ b/src/style/types/colored.rs @@ -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 [ 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 { + 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"); + } +} diff --git a/src/terminal.rs b/src/terminal.rs index 43a6c411..ea92302a 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -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(pub T); impl Command for SetTitle { 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)] diff --git a/src/terminal/ansi.rs b/src/terminal/ansi.rs deleted file mode 100644 index 7e091aab..00000000 --- a/src/terminal/ansi.rs +++ /dev/null @@ -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) -}