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..7ea9592 100644 --- a/src/display.rs +++ b/src/display.rs @@ -1,11 +1,36 @@ -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, + Icon, + Cwd, + 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, + Self::Icon => Self::Icon, + Self::Cwd => Self::Cwd, + } + } +} + /// 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 +41,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 +63,7 @@ where AnsiGenericString { style: self.style, string: self.string.clone(), - params: self.params.clone(), + oscontrol: self.oscontrol.clone(), } } } @@ -100,7 +125,7 @@ where AnsiGenericString { string: input.into(), style: Style::default(), - params: None, + oscontrol: None, } } } @@ -109,25 +134,44 @@ 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> + pub fn title(s: I) -> Self where I: Into>, { Self { - string: title.into(), - style: Style::title(), - params: None, + style: Style::default(), + string: s.into(), + oscontrol: Some(OSControl::<'a, S>::Title), } } @@ -138,20 +182,21 @@ where /// # Examples /// /// ``` - /// use nu_ansi_term::AnsiString; - /// let icon_string = AnsiString::icon(std::path::Path::new("foo/bar.icn").to_string_lossy()); + /// use nu_ansi_term::AnsiGenericString; + /// use std::path::Path; + /// let icon_string = AnsiGenericString::icon(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 icon(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>::Icon), } } @@ -162,30 +207,34 @@ where /// # Examples /// /// ``` - /// use nu_ansi_term::AnsiString; - /// let cwd_string = AnsiString::cwd(std::path::Path::new("/foo/bar").to_string_lossy()); + /// use nu_ansi_term::AnsiGenericString; + /// use std::path::Path; + /// let cwd_string = AnsiGenericString::cwd(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> + pub fn cwd(s: I) -> Self where I: Into>, { Self { - string: path.into(), - style: Style::cwd(), - params: None, + style: Style::default(), + string: s.into(), + oscontrol: Some(OSControl::<'a, S>::Cwd), } } + // + // 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 +247,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 +300,7 @@ impl Style { AnsiGenericString { string: input.into(), style: self, - params: None, + oscontrol: None, } } } @@ -282,7 +323,7 @@ impl Color { AnsiGenericString { string: input.into(), style: self.normal(), - params: None, + oscontrol: None, } } } @@ -310,13 +351,39 @@ 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> { + // \x01 .. \x02 are used to wrap zero-width portions of output + match &self.oscontrol { + Some(OSControl::Link { url: u }) => { + write!(w, "\x01\x1B]8;;")?; + w.write_str(u.as_ref())?; + write!(w, "\x1B\x5C\x02")?; + w.write_str(self.string.as_ref())?; + write!(w, "\x01\x1B]8;;\x1B\x5C\x02") + } + Some(OSControl::Title) => { + write!(w, "\x01\x1B]2;")?; + w.write_str(self.string.as_ref())?; + write!(w, "\x1B\x5C\x02") + } + Some(OSControl::Icon) => { + write!(w, "\x01\x1B]I;")?; + w.write_str(self.string.as_ref())?; + write!(w, "\x1B\x5C\x02") + } + Some(OSControl::Cwd) => { + write!(w, "\x01\x1B]7;")?; + w.write_str(self.string.as_ref())?; + write!(w, "\x1B\x5C\x02") + } + 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 +421,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 +438,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,22 +520,22 @@ mod tests { #[test] fn title() { - let title = Style::title().paint("Test Title"); - assert_eq!(title.clone().to_string(), "\x1B]2;Test Title\x1B\\"); + let title = AnsiGenericString::title("Test Title"); + assert_eq!(title.clone().to_string(), "\x01\x1B]2;Test Title\x1B\\\x02"); 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\\"); + let icon = AnsiGenericString::icon("/path/to/test.icn"); + assert_eq!(icon.to_string(), "\x01\x1B]I;/path/to/test.icn\x1B\\\x02"); idempotent(icon) } #[test] fn cwd() { - let cwd = Style::cwd().paint("/path/to/test/"); - assert_eq!(cwd.to_string(), "\x1B]7;/path/to/test/\x1B\\"); + let cwd = AnsiGenericString::cwd("/path/to/test/"); + assert_eq!(cwd.to_string(), "\x01\x1B]7;/path/to/test/\x1B\\\x02"); idempotent(cwd) } @@ -531,7 +545,7 @@ mod tests { styled.hyperlink("https://example.com"); assert_eq!( styled.to_string(), - "\x1B[31m\x1B]8;;https://example.com\x1B\\Link to example.com.\x1B]8;;\x1B\\\x1B[0m" + "\x1B[31m\x01\x1B]8;;https://example.com\x1B\\\x02Link to example.com.\x01\x1B]8;;\x1B\\\x02\x1B[0m" ); } @@ -545,29 +559,29 @@ mod tests { // Assemble with link by itself let joined = AnsiStrings(&[link.clone()]).to_string(); #[cfg(feature = "gnu_legacy")] - assert_eq!(joined, format!("\x1B[04;34m\x1B]8;;https://example.com\x1B\\Link to example.com.\x1B]8;;\x1B\\\x1B[0m")); + assert_eq!(joined, format!("\x1B[04;34m\x01\x1B]8;;https://example.com\x1B\\\x02Link to example.com.\x01\x1B]8;;\x1B\\\x02\x1B[0m")); #[cfg(not(feature = "gnu_legacy"))] - assert_eq!(joined, format!("\x1B[4;34m\x1B]8;;https://example.com\x1B\\Link to example.com.\x1B]8;;\x1B\\\x1B[0m")); + assert_eq!(joined, format!("\x1B[4;34m\x01\x1B]8;;https://example.com\x1B\\\x02Link to example.com.\x01\x1B]8;;\x1B\\\x02\x1B[0m")); // Assemble with link in the middle let joined = AnsiStrings(&[before.clone(), link.clone(), after.clone()]).to_string(); #[cfg(feature = "gnu_legacy")] - assert_eq!(joined, format!("\x1B[32mBefore link. \x1B[04;34m\x1B]8;;https://example.com\x1B\\Link to example.com.\x1B]8;;\x1B\\\x1B[0m\x1B[32m After link.\x1B[0m")); + assert_eq!(joined, format!("\x1B[32mBefore link. \x1B[04;34m\x01\x1B]8;;https://example.com\x1B\\\x02Link to example.com.\x01\x1B]8;;\x1B\\\x02\x1B[0m\x1B[32m After link.\x1B[0m")); #[cfg(not(feature = "gnu_legacy"))] - assert_eq!(joined, format!("\x1B[32mBefore link. \x1B[4;34m\x1B]8;;https://example.com\x1B\\Link to example.com.\x1B]8;;\x1B\\\x1B[0m\x1B[32m After link.\x1B[0m")); + assert_eq!(joined, format!("\x1B[32mBefore link. \x1B[4;34m\x01\x1B]8;;https://example.com\x1B\\\x02Link to example.com.\x01\x1B]8;;\x1B\\\x02\x1B[0m\x1B[32m After link.\x1B[0m")); // Assemble with link first let joined = AnsiStrings(&[link.clone(), after.clone()]).to_string(); #[cfg(feature = "gnu_legacy")] - assert_eq!(joined, format!("\x1B[04;34m\x1B]8;;https://example.com\x1B\\Link to example.com.\x1B]8;;\x1B\\\x1B[0m\x1B[32m After link.\x1B[0m")); + assert_eq!(joined, format!("\x1B[04;34m\x01\x1B]8;;https://example.com\x1B\\\x02Link to example.com.\x01\x1B]8;;\x1B\\\x02\x1B[0m\x1B[32m After link.\x1B[0m")); #[cfg(not(feature = "gnu_legacy"))] - assert_eq!(joined, format!("\x1B[4;34m\x1B]8;;https://example.com\x1B\\Link to example.com.\x1B]8;;\x1B\\\x1B[0m\x1B[32m After link.\x1B[0m")); + assert_eq!(joined, format!("\x1B[4;34m\x01\x1B]8;;https://example.com\x1B\\\x02Link to example.com.\x01\x1B]8;;\x1B\\\x02\x1B[0m\x1B[32m After link.\x1B[0m")); // Assemble with link at the end let joined = AnsiStrings(&[before.clone(), link.clone()]).to_string(); #[cfg(feature = "gnu_legacy")] - assert_eq!(joined, format!("\x1B[32mBefore link. \x1B[04;34m\x1B]8;;https://example.com\x1B\\Link to example.com.\x1B]8;;\x1B\\\x1B[0m")); + assert_eq!(joined, format!("\x1B[32mBefore link. \x1B[04;34m\x01\x1B]8;;https://example.com\x1B\\\x02Link to example.com.\x01\x1B]8;;\x1B\\\x02\x1B[0m")); #[cfg(not(feature = "gnu_legacy"))] - assert_eq!(joined, format!("\x1B[32mBefore link. \x1B[4;34m\x1B]8;;https://example.com\x1B\\Link to example.com.\x1B]8;;\x1B\\\x1B[0m")); + assert_eq!(joined, format!("\x1B[32mBefore link. \x1B[4;34m\x01\x1B]8;;https://example.com\x1B\\\x02Link to example.com.\x01\x1B]8;;\x1B\\\x02\x1B[0m")); } } 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()); } }