Skip to content

Commit

Permalink
Merge pull request #1 from rustadopt/pr61
Browse files Browse the repository at this point in the history
(ansi-term PR 61) Add hyperlink support

Resolves ogham/rust-ansi-term#60
  • Loading branch information
gierens authored Aug 29, 2023
2 parents de4aee5 + e83da1d commit 0352743
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 45 deletions.
68 changes: 48 additions & 20 deletions src/ansi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,24 +59,32 @@ impl Style {
// All the codes end with an `m`, because reasons.
write!(f, "m")?;

if let Some(url) = self.hyperlink_url.as_deref() {
write!(f, "\x1B]8;;{}\x1B\\", url)?;
}

Ok(())
}

/// Write any bytes that go *after* a piece of text to the given writer.
fn write_suffix<W: AnyWrite + ?Sized>(&self, f: &mut W) -> Result<(), W::Error> {
if self.is_plain() {
Ok(())
return Ok(());
}
else {
write!(f, "{}", RESET)
if self.hyperlink_url.is_some() {
write!(f, "{}", RESET_HYPERLINK)?;
}
write!(f, "{}", RESET)?;
Ok(())
}
}


/// The code to send to reset all styles and return to `Style::default()`.
pub static RESET: &str = "\x1B[0m";

/// The code to reset hyperlinks.
pub static RESET_HYPERLINK: &str = "\x1B]8;;\x1B\\";


impl Colour {
Expand Down Expand Up @@ -118,7 +126,7 @@ impl Colour {
/// `std::fmt` formatting without doing any extra allocation, and written to a
/// string with the `.to_string()` method. For examples, see
/// [`Style::prefix`](struct.Style.html#method.prefix).
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Debug)]
pub struct Prefix(Style);

/// Like `ANSIString`, but only displays the difference between two
Expand All @@ -128,7 +136,7 @@ pub struct Prefix(Style);
/// `std::fmt` formatting without doing any extra allocation, and written to a
/// string with the `.to_string()` method. For examples, see
/// [`Style::infix`](struct.Style.html#method.infix).
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Debug)]
pub struct Infix(Style, Style);

/// Like `ANSIString`, but only displays the style suffix.
Expand All @@ -137,7 +145,7 @@ pub struct Infix(Style, Style);
/// `std::fmt` formatting without doing any extra allocation, and written to a
/// string with the `.to_string()` method. For examples, see
/// [`Style::suffix`](struct.Style.html#method.suffix).
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Debug)]
pub struct Suffix(Style);


Expand All @@ -163,8 +171,8 @@ impl Style {
/// assert_eq!("",
/// style.prefix().to_string());
/// ```
pub fn prefix(self) -> Prefix {
Prefix(self)
pub fn prefix(&self) -> Prefix {
Prefix(self.clone())
}

/// The infix bytes between this style and `next` style. These are the bytes
Expand All @@ -178,18 +186,18 @@ impl Style {
///
/// let style = Style::default().bold();
/// assert_eq!("\x1b[32m",
/// style.infix(Green.bold()).to_string());
/// style.infix(&Green.bold()).to_string());
///
/// let style = Green.normal();
/// assert_eq!("\x1b[1m",
/// style.infix(Green.bold()).to_string());
/// style.infix(&Green.bold()).to_string());
///
/// let style = Style::default();
/// assert_eq!("",
/// style.infix(style).to_string());
/// style.infix(&style).to_string());
/// ```
pub fn infix(self, next: Style) -> Infix {
Infix(self, next)
pub fn infix(&self, next: &Style) -> Infix {
Infix(self.clone(), next.clone())
}

/// The suffix for this style. These are the bytes that tell the terminal
Expand All @@ -212,8 +220,8 @@ impl Style {
/// assert_eq!("",
/// style.suffix().to_string());
/// ```
pub fn suffix(self) -> Suffix {
Suffix(self)
pub fn suffix(&self) -> Suffix {
Suffix(self.clone())
}
}

Expand Down Expand Up @@ -295,6 +303,10 @@ impl fmt::Display for Infix {
let f: &mut fmt::Write = f;
write!(f, "{}{}", RESET, self.1.prefix())
},
Difference::ResetHyperlink => {
let f: &mut fmt::Write = f;
write!(f, "{}{}{}", RESET_HYPERLINK, RESET, self.1.prefix())
},
Difference::NoDifference => {
Ok(()) // nothing to write
},
Expand Down Expand Up @@ -362,13 +374,29 @@ mod test {
test!(reverse: Style::new().reverse(); "hi" => "\x1B[7mhi\x1B[0m");
test!(hidden: Style::new().hidden(); "hi" => "\x1B[8mhi\x1B[0m");
test!(stricken: Style::new().strikethrough(); "hi" => "\x1B[9mhi\x1B[0m");
test!(hyperlink_plain: Style::new().hyperlink("url"); "hi" => "\x1B[m\x1B]8;;url\x1B\\hi\x1B]8;;\x1B\\\x1B[0m");
test!(hyperlink_color: Blue.hyperlink("url"); "hi" => "\x1B[34m\x1B]8;;url\x1B\\hi\x1B]8;;\x1B\\\x1B[0m");
test!(hyperlink_style: Blue.underline().hyperlink("url"); "hi" => "\x1B[4;34m\x1B]8;;url\x1B\\hi\x1B]8;;\x1B\\\x1B[0m");

#[test]
fn test_infix() {
assert_eq!(Style::new().dimmed().infix(Style::new()).to_string(), "\x1B[0m");
assert_eq!(White.dimmed().infix(White.normal()).to_string(), "\x1B[0m\x1B[37m");
assert_eq!(White.normal().infix(White.bold()).to_string(), "\x1B[1m");
assert_eq!(White.normal().infix(Blue.normal()).to_string(), "\x1B[34m");
assert_eq!(Blue.bold().infix(Blue.bold()).to_string(), "");
assert_eq!(Style::new().dimmed().infix(&Style::new()).to_string(), "\x1B[0m");
assert_eq!(White.dimmed().infix(&White.normal()).to_string(), "\x1B[0m\x1B[37m");
assert_eq!(White.normal().infix(&White.bold()).to_string(), "\x1B[1m");
assert_eq!(White.normal().infix(&Blue.normal()).to_string(), "\x1B[34m");
assert_eq!(Blue.bold().infix(&Blue.bold()).to_string(), "");
}

#[test]
fn test_infix_hyperlink() {
assert_eq!(Blue.hyperlink("url1").infix(&Style::new()).to_string(), "\x1B]8;;\x1B\\\x1B[0m");
assert_eq!(Blue.hyperlink("url1").infix(&Red.normal()).to_string(), "\x1B]8;;\x1B\\\x1B[0m\x1B[31m");
assert_eq!(Blue.normal().infix(&Red.hyperlink("url2")).to_string(), "\x1B[31m\x1B]8;;url2\x1B\\");
assert_eq!(Blue.hyperlink("url1").infix(&Red.hyperlink("url2")).to_string(), "\x1B[31m\x1B]8;;url2\x1B\\");
assert_eq!(Blue.underline().hyperlink("url1").infix(&Red.italic().hyperlink("url2")).to_string(), "\x1B[0m\x1B[3;31m\x1B]8;;url2\x1B\\");

assert_eq!(Style::new().hyperlink("url1").infix(&Style::new().hyperlink("url1")).to_string(), "");
assert_eq!(Blue.hyperlink("url1").infix(&Red.hyperlink("url1")).to_string(), "\x1B[31m");
assert_eq!(Blue.underline().hyperlink("url1").infix(&Red.underline().hyperlink("url1")).to_string(), "\x1B[31m");
}
}
19 changes: 18 additions & 1 deletion src/difference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use super::Style;

/// When printing out one coloured string followed by another, use one of
/// these rules to figure out which *extra* control codes need to be sent.
#[derive(PartialEq, Clone, Copy, Debug)]
#[derive(PartialEq, Clone, Debug)]
pub enum Difference {

/// Print out the control codes specified by this style to end up looking
Expand All @@ -14,6 +14,11 @@ pub enum Difference {
/// command and then the second string's styles.
Reset,

/// Converting between these two is impossible, and the first includes a
/// hyperlink, so send a reset and a hyperlink reset, then the second
/// string's styles.
ResetHyperlink,

/// The before style is exactly the same as the after style, so no further
/// control codes need to be printed.
NoDifference,
Expand Down Expand Up @@ -47,6 +52,10 @@ impl Difference {
return NoDifference;
}

if first.hyperlink_url.is_some() && next.hyperlink_url.is_none() {
return ResetHyperlink;
}

// Cannot un-bold, so must Reset.
if first.is_bold && !next.is_bold {
return Reset;
Expand Down Expand Up @@ -133,6 +142,10 @@ impl Difference {
extra_styles.background = next.background;
}

if first.hyperlink_url != next.hyperlink_url {
extra_styles.hyperlink_url = next.hyperlink_url.clone();
}

ExtraStyles(extra_styles)
}
}
Expand Down Expand Up @@ -170,10 +183,14 @@ mod test {
test!(addition_of_hidden: style(); style().hidden() => ExtraStyles(style().hidden()));
test!(addition_of_reverse: style(); style().reverse() => ExtraStyles(style().reverse()));
test!(addition_of_strikethrough: style(); style().strikethrough() => ExtraStyles(style().strikethrough()));
test!(addition_of_hyperlink: style(); style().hyperlink("x") => ExtraStyles(style().hyperlink("x")));

test!(removal_of_strikethrough: style().strikethrough(); style() => Reset);
test!(removal_of_reverse: style().reverse(); style() => Reset);
test!(removal_of_hidden: style().hidden(); style() => Reset);
test!(removal_of_dimmed: style().dimmed(); style() => Reset);
test!(removal_of_blink: style().blink(); style() => Reset);
test!(removal_of_hyperlink: style().hyperlink("x"); style() => ResetHyperlink);

test!(change_of_hyperlink: style().hyperlink("url1"); style().hyperlink("url2") => ExtraStyles(style().hyperlink("url2")));
}
20 changes: 10 additions & 10 deletions src/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::fmt;
use std::io;
use std::ops::Deref;

use ansi::RESET;
use ansi::{RESET, RESET_HYPERLINK};
use difference::Difference;
use style::{Style, Colour};
use write::AnyWrite;
Expand Down Expand Up @@ -35,7 +35,7 @@ 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,
style: self.style.clone(),
string: self.string.clone(),
}
}
Expand Down Expand Up @@ -161,12 +161,12 @@ impl Style {

/// Paints the given text with this colour, returning an ANSI string.
#[must_use]
pub fn paint<'a, I, S: 'a + ToOwned + ?Sized>(self, input: I) -> ANSIGenericString<'a, S>
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,
style: self.clone(),
}
}
}
Expand Down Expand Up @@ -258,19 +258,19 @@ where <S as ToOwned>::Owned: fmt::Debug, &'a S: AsRef<[u8]> {
match Difference::between(&window[0].style, &window[1].style) {
ExtraStyles(style) => write!(w, "{}", style.prefix())?,
Reset => write!(w, "{}{}", RESET, window[1].style.prefix())?,
ResetHyperlink => {
write!(w, "{}{}{}", RESET_HYPERLINK, RESET, window[1].style.prefix())?;
}
NoDifference => {/* Do nothing! */},
}

w.write_str(&window[1].string)?;
}

// Write the final reset string after all of the ANSIStrings have been
// written, *except* if the last one has no styles, because it would
// have already been written by this point.
// Write any final reset strings needed after all of the ANSIStrings
// have been written.
if let Some(last) = self.0.last() {
if !last.style.is_plain() {
write!(w, "{}", RESET)?;
}
write!(w, "{}", last.style.suffix())?;
}

Ok(())
Expand Down
Loading

0 comments on commit 0352743

Please sign in to comment.