diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67fab00..dced090 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,3 +25,5 @@ jobs: - run: cargo fmt --check --all - run: cargo clippy -- -D warnings - run: cargo run --example 256_colors + - run: cargo run --example hyperlink + - run: cargo run --example title diff --git a/examples/hyperlink.rs b/examples/hyperlink.rs new file mode 100644 index 0000000..3c17b76 --- /dev/null +++ b/examples/hyperlink.rs @@ -0,0 +1,15 @@ +use nu_ansi_term::Color; +mod may_sleep; +use may_sleep::{parse_cmd_args, sleep}; + +fn main() { + #[cfg(windows)] + nu_ansi_term::enable_ansi_support().unwrap(); + + let sleep_ms = parse_cmd_args(); + let mut link = Color::Blue.underline().paint("Link to example.com"); + link.hyperlink("https://example.com"); + + println!("{}", link); + sleep(sleep_ms); +} diff --git a/examples/may_sleep/mod.rs b/examples/may_sleep/mod.rs new file mode 100644 index 0000000..04724d4 --- /dev/null +++ b/examples/may_sleep/mod.rs @@ -0,0 +1,35 @@ +pub fn parse_cmd_args() -> Option { + let mut sleep_ms: Option = None; + let mut skip_next = false; + + for (i, arg) in std::env::args().skip(1).enumerate() { + if skip_next { + skip_next = false; + continue; + } + + match &arg[..] { + "-s" | "--sleep" => { + sleep_ms = std::env::args() + .nth(i + 2) // next is +2 because .skip(1) + .unwrap_or(String::from("5000u16")) + .parse::() + .ok() + .and_then(|parsed| { + skip_next = true; + Some(parsed) + }); + } + _ => {} + } + } + + sleep_ms +} + +pub fn sleep(sleep_ms: Option) { + if let Some(sleep_ms) = sleep_ms { + let sleep_ms = std::time::Duration::from_millis(sleep_ms as u64); + std::thread::sleep(sleep_ms); + } +} diff --git a/examples/title.rs b/examples/title.rs new file mode 100644 index 0000000..c043d87 --- /dev/null +++ b/examples/title.rs @@ -0,0 +1,19 @@ +use nu_ansi_term::AnsiGenericString; +mod may_sleep; +use may_sleep::{parse_cmd_args, sleep}; + +fn main() { + #[cfg(windows)] + nu_ansi_term::enable_ansi_support().unwrap(); + + let sleep_ms = parse_cmd_args(); + let title = AnsiGenericString::title("My Title"); + println!( + "{}Terminal title set for the next {:?} milliseconds", + title, sleep_ms + ); + + // sleep because often prompts change this before you can see + // the results + sleep(sleep_ms); +} diff --git a/src/ansi.rs b/src/ansi.rs index d615289..901e160 100644 --- a/src/ansi.rs +++ b/src/ansi.rs @@ -1,5 +1,5 @@ #![allow(missing_docs)] -use crate::style::{Color, OSControl, Style}; +use crate::style::{Color, Style}; use crate::write::AnyWrite; use std::fmt; @@ -13,95 +13,75 @@ impl Style { return Ok(()); } - if self.has_sgr() { - // Prefix everything with reset characters if needed - if self.with_reset { - write!(f, "\x1B[0m")? - } + // Prefix everything with reset characters if needed + if self.with_reset { + write!(f, "\x1B[0m")? + } - // "Specified Graphical Rendition" prefixes - // Write the codes’ prefix, then write numbers, separated by - // semicolons, for each text style we want to apply. - write!(f, "\x1B[")?; - let mut written_anything = false; - - { - let mut write_char = |c| { - if written_anything { - write!(f, ";")?; - } - written_anything = true; - #[cfg(feature = "gnu_legacy")] - write!(f, "0")?; - write!(f, "{}", c)?; - Ok(()) - }; - - if self.is_bold { - write_char('1')? - } - if self.is_dimmed { - write_char('2')? - } - if self.is_italic { - write_char('3')? - } - if self.is_underline { - write_char('4')? - } - if self.is_blink { - write_char('5')? - } - if self.is_reverse { - write_char('7')? - } - if self.is_hidden { - write_char('8')? - } - if self.is_strikethrough { - write_char('9')? - } - } + // Write the codes’ prefix, then write numbers, separated by + // semicolons, for each text style we want to apply. + write!(f, "\x1B[")?; + let mut written_anything = false; - // The foreground and background colors, if specified, need to be - // handled specially because the number codes are more complicated. - // (see `write_background_code` and `write_foreground_code`) - if let Some(bg) = self.background { + { + let mut write_char = |c| { if written_anything { write!(f, ";")?; } written_anything = true; - bg.write_background_code(f)?; + #[cfg(feature = "gnu_legacy")] + write!(f, "0")?; + write!(f, "{}", c)?; + Ok(()) + }; + + if self.is_bold { + write_char('1')? } - - if let Some(fg) = self.foreground { - if written_anything { - write!(f, ";")?; - } - fg.write_foreground_code(f)?; + if self.is_dimmed { + write_char('2')? } - - // All the SGR codes end with an `m`, because reasons. - write!(f, "m")?; - } - - // OS Control (OSC) prefixes - match self.oscontrol { - Some(OSControl::Hyperlink) => { - write!(f, "\x1B]8;;")?; + if self.is_italic { + write_char('3')? + } + if self.is_underline { + write_char('4')? + } + if self.is_blink { + write_char('5')? } - Some(OSControl::Title) => { - write!(f, "\x1B]2;")?; + if self.is_reverse { + write_char('7')? } - Some(OSControl::Icon) => { - write!(f, "\x1B]I;")?; + if self.is_hidden { + write_char('8')? } - Some(OSControl::Cwd) => { - write!(f, "\x1B]7;")?; + if self.is_strikethrough { + write_char('9')? + } + } + + // The foreground and background colors, if specified, need to be + // handled specially because the number codes are more complicated. + // (see `write_background_code` and `write_foreground_code`) + if let Some(bg) = self.background { + if written_anything { + write!(f, ";")?; + } + written_anything = true; + bg.write_background_code(f)?; + } + + if let Some(fg) = self.foreground { + if written_anything { + write!(f, ";")?; } - None => {} + fg.write_foreground_code(f)?; } + // All the codes end with an `m`, because reasons. + write!(f, "m")?; + Ok(()) } @@ -110,17 +90,7 @@ impl Style { if self.is_plain() { Ok(()) } else { - match self.oscontrol { - Some(OSControl::Hyperlink) => { - write!(f, "{}{}", HYPERLINK_RESET, RESET) - } - Some(OSControl::Title) | Some(OSControl::Icon) | Some(OSControl::Cwd) => { - write!(f, "{}", ST) - } - _ => { - write!(f, "{}", RESET) - } - } + write!(f, "{}", RESET) } } } @@ -128,10 +98,6 @@ impl Style { /// The code to send to reset all styles and return to `Style::default()`. pub static RESET: &str = "\x1B[0m"; -// The "String Termination" code. Used for OS Control (OSC) sequences. -static ST: &str = "\x1B\\"; -pub(crate) static HYPERLINK_RESET: &str = "\x1B]8;;\x1B\\"; - impl Color { fn write_foreground_code(&self, f: &mut W) -> Result<(), W::Error> { match self { @@ -396,33 +362,7 @@ impl fmt::Display for Infix { } Difference::Reset => { let f: &mut dyn fmt::Write = f; - - match (self.0, self.1) { - ( - Style { - oscontrol: Some(OSControl::Hyperlink), - .. - }, - Style { - oscontrol: None, .. - }, - ) => { - write!(f, "{}{}", HYPERLINK_RESET, self.1.prefix()) - } - ( - Style { - oscontrol: Some(_), .. - }, - Style { - oscontrol: None, .. - }, - ) => { - write!(f, "{}{}", ST, self.1.prefix()) - } - (_, _) => { - write!(f, "{}{}", RESET, self.1.prefix()) - } - } + write!(f, "{}{}", RESET, self.1.prefix()) } Difference::Empty => { Ok(()) // nothing to write diff --git a/src/difference.rs b/src/difference.rs index f1b5ce4..30a88f1 100644 --- a/src/difference.rs +++ b/src/difference.rs @@ -43,10 +43,6 @@ impl Difference { return Empty; } - if first.has_sgr() && !next.has_sgr() { - return Reset; - } - // Cannot un-bold, so must Reset. if first.is_bold && !next.is_bold { return Reset; @@ -91,10 +87,6 @@ impl Difference { return Reset; } - if first.oscontrol.is_some() && next.oscontrol.is_none() { - return Reset; - } - let mut extra_styles = Style::default(); if first.is_bold != next.is_bold { @@ -137,10 +129,6 @@ impl Difference { extra_styles.background = next.background; } - if first.oscontrol != next.oscontrol { - extra_styles.oscontrol = next.oscontrol; - } - ExtraStyles(extra_styles) } } diff --git a/src/display.rs b/src/display.rs index 9ff00f0..881fa4f 100644 --- a/src/display.rs +++ b/src/display.rs @@ -1,11 +1,32 @@ -use crate::ansi::{HYPERLINK_RESET, RESET}; +use crate::ansi::RESET; use crate::difference::Difference; -use crate::style::{Color, OSControl, Style}; +use crate::style::{Color, Style}; use crate::write::AnyWrite; use std::borrow::Cow; use std::fmt; use std::io; +#[derive(Eq, PartialEq, Debug)] +enum OSControl<'a, S: 'a + ToOwned + ?Sized> +where + ::Owned: fmt::Debug, +{ + Title, + Link { url: Cow<'a, S> }, +} + +impl<'a, S: 'a + ToOwned + ?Sized> Clone for OSControl<'a, S> +where + ::Owned: fmt::Debug, +{ + fn clone(&self) -> Self { + match self { + Self::Link { url: u } => Self::Link { url: u.clone() }, + Self::Title => Self::Title, + } + } +} + /// An `AnsiGenericString` includes a generic string type and a `Style` to /// display that string. `AnsiString` and `AnsiByteString` are aliases for /// this type on `str` and `\[u8]`, respectively. @@ -16,7 +37,7 @@ where { pub(crate) style: Style, pub(crate) string: Cow<'a, S>, - pub(crate) params: Option>, + oscontrol: Option>, } /// Cloning an `AnsiGenericString` will clone its underlying string. @@ -38,7 +59,7 @@ where AnsiGenericString { style: self.style, string: self.string.clone(), - params: self.params.clone(), + oscontrol: self.oscontrol.clone(), } } } @@ -100,7 +121,7 @@ where AnsiGenericString { string: input.into(), style: Style::default(), - params: None, + oscontrol: None, } } } @@ -109,83 +130,56 @@ impl<'a, S: 'a + ToOwned + ?Sized> AnsiGenericString<'a, S> where ::Owned: fmt::Debug, { + /// Directly access the style + pub const fn style_ref(&self) -> &Style { + &self.style + } + + /// Directly access the style mutably + pub fn style_ref_mut(&mut self) -> &mut Style { + &mut self.style + } + + /// Directly access the underlying string + pub fn as_str(&self) -> &S { + self.string.as_ref() + } + + // Instances that imply wrapping in OSC sequences + // and do not get displayed in the terminal text + // area. + // /// Produce an ANSI string that changes the title shown /// by the terminal emulator. /// /// # Examples /// /// ``` - /// use nu_ansi_term::AnsiString; - /// let title_string = AnsiString::title("My Title"); + /// use nu_ansi_term::AnsiGenericString; + /// let title_string = AnsiGenericString::title("My Title"); /// println!("{}", title_string); /// ``` /// Should produce an empty line but set the terminal title. - pub fn title(title: I) -> AnsiGenericString<'a, S> - where - I: Into>, - { - Self { - string: title.into(), - style: Style::title(), - params: None, - } - } - - /// Produce an ANSI string that notifies the terminal - /// emulator that the running application is better - /// represented by the icon found at a given path. - /// - /// # Examples - /// - /// ``` - /// use nu_ansi_term::AnsiString; - /// let icon_string = AnsiString::icon(std::path::Path::new("foo/bar.icn").to_string_lossy()); - /// println!("{}", icon_string); - /// ``` - /// Should produce an empty line but set the terminal icon. - /// Notice that we use std::path to be portable. - pub fn icon(path: I) -> AnsiGenericString<'a, S> + pub fn title(s: I) -> Self where I: Into>, { Self { - string: path.into(), - style: Style::icon(), - params: None, + style: Style::default(), + string: s.into(), + oscontrol: Some(OSControl::<'a, S>::Title), } } - /// Produce an ANSI string that notifies the terminal - /// emulator the current working directory has changed - /// to the given path. - /// - /// # Examples - /// - /// ``` - /// use nu_ansi_term::AnsiString; - /// let cwd_string = AnsiString::cwd(std::path::Path::new("/foo/bar").to_string_lossy()); - /// println!("{}", cwd_string); - /// ``` - /// Should produce an empty line but inform the terminal emulator - /// that the current directory is /foo/bar. - /// Notice that we use std::path to be portable. - pub fn cwd(path: I) -> AnsiGenericString<'a, S> - where - I: Into>, - { - Self { - string: path.into(), - style: Style::cwd(), - params: None, - } - } + // + // Annotations (OSC sequences that do more than wrap) + // /// Cause the styled ANSI string to link to the given URL /// /// # Examples /// /// ``` - /// use nu_ansi_term::AnsiString; /// use nu_ansi_term::Color::Red; /// /// let mut link_string = Red.paint("a red string"); @@ -198,23 +192,15 @@ where where I: Into>, { - self.style.hyperlink(); - self.params = Some(url.into()); - } - - /// Directly access the style - pub const fn style_ref(&self) -> &Style { - &self.style - } - - /// Directly access the style mutably - pub fn style_ref_mut(&mut self) -> &mut Style { - &mut self.style + self.oscontrol = Some(OSControl::Link { url: url.into() }); } - // Directly access the underlying string - pub fn as_str(&self) -> &S { - self.string.as_ref() + /// Get any URL associated with the string + pub fn url_string(&self) -> Option<&S> { + match &self.oscontrol { + Some(OSControl::Link { url: u }) => Some(u.as_ref()), + _ => None, + } } } @@ -259,7 +245,7 @@ impl Style { AnsiGenericString { string: input.into(), style: self, - params: None, + oscontrol: None, } } } @@ -282,7 +268,7 @@ impl Color { AnsiGenericString { string: input.into(), style: self.normal(), - params: None, + oscontrol: None, } } } @@ -310,13 +296,28 @@ where ::Owned: fmt::Debug, &'a S: AsRef<[u8]>, { + // write the part within the styling prefix and suffix + fn write_inner + ?Sized>(&self, w: &mut W) -> Result<(), W::Error> { + match &self.oscontrol { + Some(OSControl::Link { url: u }) => { + write!(w, "\x1B]8;;")?; + w.write_str(u.as_ref())?; + write!(w, "\x1B\x5C")?; + w.write_str(self.string.as_ref())?; + write!(w, "\x1B]8;;\x1B\x5C") + } + Some(OSControl::Title) => { + write!(w, "\x1B]2;")?; + w.write_str(self.string.as_ref())?; + write!(w, "\x1B\x5C") + } + None => w.write_str(self.string.as_ref()), + } + } + fn write_to_any + ?Sized>(&self, w: &mut W) -> Result<(), W::Error> { write!(w, "{}", self.style.prefix())?; - if let (Some(s), Some(_)) = (&self.params, self.style.oscontrol) { - w.write_str(s.as_ref())?; - write!(w, "\x1B\\")?; - } - w.write_str(self.string.as_ref())?; + self.write_inner(w)?; write!(w, "{}", self.style.suffix()) } } @@ -354,59 +355,16 @@ where }; write!(w, "{}", first.style.prefix())?; - if let (Some(s), Some(_)) = (&first.params, first.style.oscontrol) { - w.write_str(s.as_ref())?; - write!(w, "\x1B\\")?; - } - w.write_str(first.string.as_ref())?; + first.write_inner(w)?; for window in self.0.windows(2) { match Difference::between(&window[0].style, &window[1].style) { - ExtraStyles(style) => { - write!(w, "{}", style.prefix())?; - if let (Some(OSControl::Hyperlink), Some(s)) = - (style.oscontrol, &window[1].params) - { - w.write_str(s.as_ref())?; - write!(w, "\x1B\\")?; - } - } - Reset => match (&window[0].style, &window[1].style) { - ( - Style { - oscontrol: Some(OSControl::Hyperlink), - .. - }, - Style { - oscontrol: None, .. - }, - ) => { - write!( - w, - "{}{}{}", - HYPERLINK_RESET, - RESET, - window[1].style.prefix() - )?; - } - ( - Style { - oscontrol: Some(_), .. - }, - Style { - oscontrol: None, .. - }, - ) => { - write!(w, "\x1B\\{}", window[1].style.prefix())?; - } - (_, _) => { - write!(w, "{}{}", RESET, window[1].style.prefix())?; - } - }, + ExtraStyles(style) => write!(w, "{}", style.prefix())?, + Reset => write!(w, "{}{}", RESET, window[1].style.prefix())?, Empty => { /* Do nothing! */ } } - w.write_str(&window[1].string)?; + window[1].write_inner(w)?; } // Write the final reset string after all of the AnsiStrings have been @@ -414,17 +372,7 @@ where // have already been written by this point. if let Some(last) = self.0.last() { if !last.style.is_plain() { - match last.style.oscontrol { - Some(OSControl::Hyperlink) => { - write!(w, "{}{}", HYPERLINK_RESET, RESET)?; - } - Some(_) => { - write!(w, "\x1B\\")?; - } - _ => { - write!(w, "{}", RESET)?; - } - } + write!(w, "{}", RESET)?; } } @@ -506,25 +454,11 @@ mod tests { #[test] fn title() { - let title = Style::title().paint("Test Title"); + let title = AnsiGenericString::title("Test Title"); assert_eq!(title.clone().to_string(), "\x1B]2;Test Title\x1B\\"); idempotent(title) } - #[test] - fn icon() { - let icon = Style::icon().paint("/path/to/test.icn"); - assert_eq!(icon.to_string(), "\x1B]I;/path/to/test.icn\x1B\\"); - idempotent(icon) - } - - #[test] - fn cwd() { - let cwd = Style::cwd().paint("/path/to/test/"); - assert_eq!(cwd.to_string(), "\x1B]7;/path/to/test/\x1B\\"); - idempotent(cwd) - } - #[test] fn hyperlink() { let mut styled = Red.paint("Link to example.com."); diff --git a/src/style.rs b/src/style.rs index 89057ca..154028d 100644 --- a/src/style.rs +++ b/src/style.rs @@ -21,12 +21,6 @@ pub struct Style { /// The style's background color, if it has one. pub background: Option, - // The style's os control type, if it has one. - // Used by corresponding public API functions in - // AnsiGenericString. This allows us to keep the - // prefix and suffix bits in the Style definition. - pub(crate) oscontrol: Option, - /// Whether this style is bold. pub is_bold: bool, @@ -256,32 +250,6 @@ impl Style { } } - pub(crate) fn hyperlink(&mut self) -> &mut Style { - self.oscontrol = Some(OSControl::Hyperlink); - self - } - - pub(crate) fn title() -> Style { - Self { - oscontrol: Some(OSControl::Title), - ..Default::default() - } - } - - pub(crate) fn icon() -> Style { - Self { - oscontrol: Some(OSControl::Icon), - ..Default::default() - } - } - - pub(crate) fn cwd() -> Style { - Self { - oscontrol: Some(OSControl::Cwd), - ..Default::default() - } - } - /// Return true if this `Style` has no actual styles, and can be written /// without any control characters. /// @@ -296,21 +264,6 @@ impl Style { pub fn is_plain(self) -> bool { self == Style::default() } - - #[inline] - pub(crate) fn has_sgr(self) -> bool { - self.foreground.is_some() - || self.background.is_some() - || self.is_bold - || self.is_dimmed - || self.is_italic - || self.is_underline - || self.is_blink - || self.is_reverse - || self.is_hidden - || self.is_strikethrough - || self.with_reset - } } impl Default for Style { @@ -328,7 +281,6 @@ impl Default for Style { Style { foreground: None, background: None, - oscontrol: None, is_bold: false, is_dimmed: false, is_italic: false, @@ -666,21 +618,6 @@ impl From for Style { } } -#[non_exhaustive] -#[derive(Eq, PartialEq, Clone, Copy)] -#[cfg_attr( - feature = "derive_serde_style", - derive(serde::Deserialize, serde::Serialize) -)] -pub(crate) enum OSControl { - Hyperlink, - Title, - Icon, - Cwd, - //ScrollMarkerPromptBegin, // \e[?7711l - //ScrollMarkerPromptEnd, // \e[?7711h -} - #[cfg(test)] #[cfg(feature = "derive_serde_style")] mod serde_json_tests { @@ -722,6 +659,6 @@ mod serde_json_tests { fn style_serialization() { let style = Style::default(); - assert_eq!(serde_json::to_string(&style).unwrap(), "{\"foreground\":null,\"background\":null,\"oscontrol\":null,\"is_bold\":false,\"is_dimmed\":false,\"is_italic\":false,\"is_underline\":false,\"is_blink\":false,\"is_reverse\":false,\"is_hidden\":false,\"is_strikethrough\":false,\"with_reset\":false}".to_string()); + assert_eq!(serde_json::to_string(&style).unwrap(), "{\"foreground\":null,\"background\":null,\"is_bold\":false,\"is_dimmed\":false,\"is_italic\":false,\"is_underline\":false,\"is_blink\":false,\"is_reverse\":false,\"is_hidden\":false,\"is_strikethrough\":false,\"with_reset\":false}".to_string()); } } diff --git a/tests/style.rs b/tests/style.rs new file mode 100644 index 0000000..e0c5202 --- /dev/null +++ b/tests/style.rs @@ -0,0 +1,28 @@ +use nu_ansi_term::Style; + +#[test] +fn manual_instance_style() { + let s = Style { ..Style::default() }; + assert_eq!(Style::default(), s); + + let s = Style { + is_underline: false, + ..Style::default() + }; + assert_eq!(Style::default(), s); + + let s = Style { + foreground: None, + background: None, + is_bold: false, + is_dimmed: false, + is_italic: false, + is_underline: false, + is_blink: false, + is_reverse: false, + is_hidden: false, + is_strikethrough: false, + with_reset: false, + }; + assert_eq!(Style::default(), s); +}