use crate::ansi::RESET;
use crate::difference::Difference;
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
<S as ToOwned>::Owned: fmt::Debug,
{
Title,
Link { url: Cow<'a, S> },
}
impl<'a, S: 'a + ToOwned + ?Sized> Clone for OSControl<'a, S>
where
<S as ToOwned>::Owned: fmt::Debug,
{
fn clone(&self) -> Self {
match self {
Self::Link { url: u } => Self::Link { url: u.clone() },
Self::Title => Self::Title,
}
}
}
#[derive(Eq, PartialEq, Debug)]
pub struct AnsiGenericString<'a, S: 'a + ToOwned + ?Sized>
where
<S as ToOwned>::Owned: fmt::Debug,
{
pub(crate) style: Style,
pub(crate) string: Cow<'a, S>,
oscontrol: Option<OSControl<'a, S>>,
}
impl<'a, S: 'a + ToOwned + ?Sized> Clone for AnsiGenericString<'a, S>
where
<S as ToOwned>::Owned: fmt::Debug,
{
fn clone(&self) -> AnsiGenericString<'a, S> {
AnsiGenericString {
style: self.style,
string: self.string.clone(),
oscontrol: self.oscontrol.clone(),
}
}
}
pub type AnsiString<'a> = AnsiGenericString<'a, str>;
pub type AnsiByteString<'a> = AnsiGenericString<'a, [u8]>;
impl<'a, I, S: 'a + ToOwned + ?Sized> From<I> for AnsiGenericString<'a, S>
where
I: Into<Cow<'a, S>>,
<S as ToOwned>::Owned: fmt::Debug,
{
fn from(input: I) -> AnsiGenericString<'a, S> {
AnsiGenericString {
string: input.into(),
style: Style::default(),
oscontrol: None,
}
}
}
impl<'a, S: 'a + ToOwned + ?Sized> AnsiGenericString<'a, S>
where
<S as ToOwned>::Owned: fmt::Debug,
{
pub const fn style_ref(&self) -> &Style {
&self.style
}
pub fn style_ref_mut(&mut self) -> &mut Style {
&mut self.style
}
pub fn as_str(&self) -> &S {
self.string.as_ref()
}
pub fn title<I>(s: I) -> Self
where
I: Into<Cow<'a, S>>,
{
Self {
style: Style::default(),
string: s.into(),
oscontrol: Some(OSControl::<'a, S>::Title),
}
}
pub fn hyperlink<I>(mut self, url: I) -> Self
where
I: Into<Cow<'a, S>>,
{
self.oscontrol = Some(OSControl::Link { url: url.into() });
self
}
pub fn url_string(&self) -> Option<&S> {
match &self.oscontrol {
Some(OSControl::Link { url: u }) => Some(u.as_ref()),
_ => None,
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct AnsiGenericStrings<'a, S: 'a + ToOwned + ?Sized>(pub &'a [AnsiGenericString<'a, S>])
where
<S as ToOwned>::Owned: fmt::Debug,
S: PartialEq;
pub type AnsiStrings<'a> = AnsiGenericStrings<'a, str>;
#[allow(non_snake_case)]
pub const fn AnsiStrings<'a>(arg: &'a [AnsiString<'a>]) -> AnsiStrings<'a> {
AnsiGenericStrings(arg)
}
pub type AnsiByteStrings<'a> = AnsiGenericStrings<'a, [u8]>;
#[allow(non_snake_case)]
pub const fn AnsiByteStrings<'a>(arg: &'a [AnsiByteString<'a>]) -> AnsiByteStrings<'a> {
AnsiGenericStrings(arg)
}
impl Style {
#[must_use]
pub fn paint<'a, I, S: 'a + ToOwned + ?Sized>(self, input: I) -> AnsiGenericString<'a, S>
where
I: Into<Cow<'a, S>>,
<S as ToOwned>::Owned: fmt::Debug,
{
AnsiGenericString {
string: input.into(),
style: self,
oscontrol: None,
}
}
}
impl Color {
#[must_use]
pub fn paint<'a, I, S: 'a + ToOwned + ?Sized>(self, input: I) -> AnsiGenericString<'a, S>
where
I: Into<Cow<'a, S>>,
<S as ToOwned>::Owned: fmt::Debug,
{
AnsiGenericString {
string: input.into(),
style: self.normal(),
oscontrol: None,
}
}
}
impl<'a> fmt::Display for AnsiString<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let w: &mut dyn fmt::Write = f;
self.write_to_any(w)
}
}
impl<'a> AnsiByteString<'a> {
pub fn write_to<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
let w: &mut dyn io::Write = w;
self.write_to_any(w)
}
}
impl<'a, S: 'a + ToOwned + ?Sized> AnsiGenericString<'a, S>
where
<S as ToOwned>::Owned: fmt::Debug,
&'a S: AsRef<[u8]>,
{
fn write_inner<W: AnyWrite<Wstr = S> + ?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<W: AnyWrite<Wstr = S> + ?Sized>(&self, w: &mut W) -> Result<(), W::Error> {
write!(w, "{}", self.style.prefix())?;
self.write_inner(w)?;
write!(w, "{}", self.style.suffix())
}
}
impl<'a> fmt::Display for AnsiStrings<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let f: &mut dyn fmt::Write = f;
self.write_to_any(f)
}
}
impl<'a> AnsiByteStrings<'a> {
pub fn write_to<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
let w: &mut dyn io::Write = w;
self.write_to_any(w)
}
}
impl<'a, S: 'a + ToOwned + ?Sized + PartialEq> AnsiGenericStrings<'a, S>
where
<S as ToOwned>::Owned: fmt::Debug,
&'a S: AsRef<[u8]>,
{
fn write_to_any<W: AnyWrite<Wstr = S> + ?Sized>(&self, w: &mut W) -> Result<(), W::Error> {
use self::Difference::*;
let first = match self.0.first() {
None => return Ok(()),
Some(f) => f,
};
write!(w, "{}", first.style.prefix())?;
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())?,
Reset => write!(w, "{}{}", RESET, window[1].style.prefix())?,
Empty => { }
}
window[1].write_inner(w)?;
}
if let Some(last) = self.0.last() {
if !last.style.is_plain() {
write!(w, "{}", RESET)?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
pub use super::super::{AnsiGenericString, AnsiStrings};
pub use crate::style::Color::*;
pub use crate::style::Style;
#[test]
fn no_control_codes_for_plain() {
let one = Style::default().paint("one");
let two = Style::default().paint("two");
let output = AnsiStrings(&[one, two]).to_string();
assert_eq!(output, "onetwo");
}
fn idempotent(unstyled: AnsiGenericString<'_, str>) {
let before_g = Green.paint("Before is Green. ");
let before = Style::default().paint("Before is Plain. ");
let after_g = Green.paint(" After is Green.");
let after = Style::default().paint(" After is Plain.");
let unstyled_s = unstyled.clone().to_string();
let joined = AnsiStrings(&[before_g.clone(), unstyled.clone()]).to_string();
assert!(joined.starts_with("\x1B[32mBefore is Green. \x1B[0m"));
assert!(
joined.ends_with(unstyled_s.as_str()),
"{:?} does not end with {:?}",
joined,
unstyled_s
);
let joined = AnsiStrings(&[unstyled.clone(), after_g.clone()]).to_string();
assert!(
joined.starts_with(unstyled_s.as_str()),
"{:?} does not start with {:?}",
joined,
unstyled_s
);
assert!(joined.ends_with("\x1B[32m After is Green.\x1B[0m"));
let joined = AnsiStrings(&[unstyled.clone()]).to_string();
assert!(
!joined.contains("\x1B["),
"{:?} does contain \\x1B[",
joined
);
let joined = AnsiStrings(&[before.clone(), unstyled.clone()]).to_string();
assert!(
!joined.contains("\x1B["),
"{:?} does contain \\x1B[",
joined
);
let joined = AnsiStrings(&[before.clone(), unstyled.clone(), after.clone()]).to_string();
assert!(
!joined.contains("\x1B["),
"{:?} does contain \\x1B[",
joined
);
let joined = AnsiStrings(&[unstyled.clone(), after.clone()]).to_string();
assert!(
!joined.contains("\x1B["),
"{:?} does contain \\x1B[",
joined
);
}
#[test]
fn title() {
let title = AnsiGenericString::title("Test Title");
assert_eq!(title.clone().to_string(), "\x1B]2;Test Title\x1B\\");
idempotent(title)
}
#[test]
fn hyperlink() {
let styled = Red
.paint("Link to example.com.")
.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"
);
}
#[test]
fn hyperlinks() {
let before = Green.paint("Before link. ");
let link = Blue
.underline()
.paint("Link to example.com.")
.hyperlink("https://example.com");
let after = Green.paint(" After link.");
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"));
#[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"));
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"));
#[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"));
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"));
#[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"));
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"));
#[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"));
}
}