Skip to content

Commit

Permalink
Add support for hyperlinks and other OSC codes (#43)
Browse files Browse the repository at this point in the history
Add support for producing colorized/stylized hyperlinks,
among a selection of other OS Control (OSC) codes such
as setting the window title, application/window icon,
and notifying the terminal about the  current working
directory.

There has already been some discussion and a change proposed
for handling hyperlinks in the dormant rust-ansi-term repo:
(See: ogham#61)
The above proposed change breaks the Copy trait for Style
and would require changing downstream projects that rely on
it.

These features aren't really about styling text so much as
adding more information for the terminal emulator to present
to the user outside of the typical area for rendered terminal
output.

So this change takes a different approach than taken
in the referenced pull request.

An enum describing the supported OSC codes, which is not
exposed outside the crate, is used to indicate that a Style
has additional terminal prefix and suffix output control
codes to take care of for hyperlinks, titles, etc. These
let us keep the prefix/suffix handling consistent.

However rather than library users using these enums
directly or calling externally visible functions on Style
or Color struct, AnsiGenericString uses them to implement
its hyperlink(), title(), etc. functions. These store the
hyperlink "src" string, title, etc. within the
AnsiGenericString rather than in the Style.

Style remains Copy-able, and, since it already stores
strings, AnsiGenericString traits are consistent with this
choice. The locations of the functions better reflect
what's happening because the supplied strings are not meant
to be rendered inline with the ANSI-styled output.

The OSControl enum also nicely describes the subset of OSC
codes the package currently supports.

Co-authored-by: Matt Helsley <matt.helsley+oss@gmail.com>
  • Loading branch information
mhelsley and Matt Helsley authored Jun 2, 2023
1 parent 313eac4 commit c32266c
Show file tree
Hide file tree
Showing 4 changed files with 478 additions and 65 deletions.
176 changes: 118 additions & 58 deletions src/ansi.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#![allow(missing_docs)]
use crate::style::{Color, Style};
use crate::style::{Color, OSControl, Style};
use crate::write::AnyWrite;
use std::fmt;

Expand All @@ -13,75 +13,95 @@ impl Style {
return Ok(());
}

// Prefix everything with reset characters if needed
if self.with_reset {
write!(f, "\x1B[0m")?
}
if self.has_sgr() {
// Prefix everything with reset characters if needed
if self.with_reset {
write!(f, "\x1B[0m")?
}

// 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;
// "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')?
}
}

{
let mut write_char = |c| {
// 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;
#[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')?
bg.write_background_code(f)?;
}
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, ";")?;
if let Some(fg) = self.foreground {
if written_anything {
write!(f, ";")?;
}
fg.write_foreground_code(f)?;
}
written_anything = true;
bg.write_background_code(f)?;

// All the SGR codes end with an `m`, because reasons.
write!(f, "m")?;
}

if let Some(fg) = self.foreground {
if written_anything {
write!(f, ";")?;
// OS Control (OSC) prefixes
match self.oscontrol {
Some(OSControl::Hyperlink) => {
write!(f, "\x1B]8;;")?;
}
Some(OSControl::Title) => {
write!(f, "\x1B]2;")?;
}
Some(OSControl::Icon) => {
write!(f, "\x1B]I;")?;
}
Some(OSControl::Cwd) => {
write!(f, "\x1B]7;")?;
}
fg.write_foreground_code(f)?;
None => {}
}

// All the codes end with an `m`, because reasons.
write!(f, "m")?;

Ok(())
}

Expand All @@ -90,14 +110,28 @@ impl Style {
if self.is_plain() {
Ok(())
} else {
write!(f, "{}", RESET)
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)
}
}
}
}
}

/// 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<W: AnyWrite + ?Sized>(&self, f: &mut W) -> Result<(), W::Error> {
match self {
Expand Down Expand Up @@ -362,7 +396,33 @@ impl fmt::Display for Infix {
}
Difference::Reset => {
let f: &mut dyn fmt::Write = f;
write!(f, "{}{}", RESET, self.1.prefix())

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())
}
}
}
Difference::Empty => {
Ok(()) // nothing to write
Expand Down
12 changes: 12 additions & 0 deletions src/difference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ 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;
Expand Down Expand Up @@ -87,6 +91,10 @@ 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 {
Expand Down Expand Up @@ -129,6 +137,10 @@ impl Difference {
extra_styles.background = next.background;
}

if first.oscontrol != next.oscontrol {
extra_styles.oscontrol = next.oscontrol;
}

ExtraStyles(extra_styles)
}
}
Expand Down
Loading

0 comments on commit c32266c

Please sign in to comment.