Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: BurntSushi/termcolor
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 1.2.0
Choose a base ref
...
head repository: BurntSushi/termcolor
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 1.3.0
Choose a head ref
  • 5 commits
  • 5 files changed
  • 2 contributors

Commits on Jan 15, 2023

  1. Copy the full SHA
    04966b4 View commit details

Commits on May 24, 2023

  1. Copy the full SHA
    7f9c030 View commit details

Commits on Sep 19, 2023

  1. doc: remove recommendation for atty

    We replace it with a recommendation to use the standard
    library.
    
    Fixes #73
    BurntSushi committed Sep 19, 2023
    Copy the full SHA
    11b93e2 View commit details
  2. termcolor: add hyperlink support

    This commit adds a new type `HyperlinkSpec` and two new default methods
    to the `WriteColor` trait: `set_hyperlink` and `supports_hyperlinks`.
    The former is the analog to `set_color`, but for hyperlinks. The latter
    is the analog to `supports_color`. By default, the former is a no-op and
    the latter always returns `false`. But all ANSI-supported impls of
    `WriteColor` in this trait now support it in accordance with OSC8[1].
    
    Closes #65
    
    [1]: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
    ltrzesniewski authored and BurntSushi committed Sep 19, 2023
    Copy the full SHA
    882b662 View commit details
  3. 1.3.0

    BurntSushi committed Sep 19, 2023
    Copy the full SHA
    8bcf6a2 View commit details
Showing with 249 additions and 31 deletions.
  1. +15 −20 .github/workflows/ci.yml
  2. +1 −0 .gitignore
  3. +1 −1 Cargo.toml
  4. +1 −1 README.md
  5. +231 −9 src/lib.rs
35 changes: 15 additions & 20 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -23,56 +23,51 @@ jobs:
- win-gnu
include:
- build: pinned
os: ubuntu-18.04
os: ubuntu-latest
rust: 1.34.0
- build: pinned-win
os: windows-2019
os: windows-latest
rust: 1.34.0
- build: stable
os: ubuntu-18.04
os: ubuntu-latest
rust: stable
- build: beta
os: ubuntu-18.04
os: ubuntu-latest
rust: beta
- build: nightly
os: ubuntu-18.04
os: ubuntu-latest
rust: nightly
- build: macos
os: macos-latest
rust: stable
- build: win-msvc
os: windows-2019
os: windows-latest
rust: stable
- build: win-gnu
os: windows-2019
os: windows-latest
rust: stable-x86_64-gnu
steps:
- name: Checkout repository
uses: actions/checkout@v1
with:
fetch-depth: 1
uses: actions/checkout@v3
- name: Install Rust
uses: hecrj/setup-rust-action@v1
uses: dtolnay/rust-toolchain@master
with:
rust-version: ${{ matrix.rust }}
toolchain: ${{ matrix.rust }}
- run: cargo build --verbose
- run: cargo doc --verbose
- run: cargo test --verbose

rustfmt:
name: rustfmt
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v1
with:
fetch-depth: 1
uses: actions/checkout@v3
- name: Install Rust
uses: hecrj/setup-rust-action@v1
uses: dtolnay/rust-toolchain@master
with:
rust-version: stable
- name: Install rustfmt
run: rustup component add rustfmt
toolchain: stable
components: rustfmt
- name: Check formatting
run: |
cargo fmt --all -- --check
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -3,3 +3,4 @@ tags
target
/Cargo.lock
/wincolor/Cargo.lock
/.idea
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "termcolor"
version = "1.2.0" #:version
version = "1.3.0" #:version
authors = ["Andrew Gallant <jamslam@gmail.com>"]
description = """
A simple cross platform library for writing colored text to a terminal.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@ Add this to your `Cargo.toml`:

```toml
[dependencies]
termcolor = "1.1"
termcolor = "1.2"
```

### Organization
240 changes: 231 additions & 9 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -10,6 +10,8 @@ to an in memory buffer. While this is easy to do with ANSI escape sequences
(because they are in the buffer themselves), it is trickier to do with the
Windows console API, which requires synchronous communication.
In ANSI mode, this crate also provides support for writing hyperlinks.
# Organization
The `WriteColor` trait extends the `io::Write` trait with methods for setting
@@ -85,21 +87,21 @@ is via libc's
[`isatty`](https://man7.org/linux/man-pages/man3/isatty.3.html)
function.
Unfortunately, this notoriously does not work well in Windows environments. To
work around that, the currently recommended solution is to use the
[`atty`](https://crates.io/crates/atty)
crate, which goes out of its way to get this as right as possible in Windows
environments.
work around that, the recommended solution is to use the standard library's
[`IsTerminal`](https://doc.rust-lang.org/std/io/trait.IsTerminal.html) trait.
It goes out of its way to get it as right as possible in Windows environments.
For example, in a command line application that exposes a `--color` flag,
your logic for how to enable colors might look like this:
```rust,ignore
use atty;
```ignore
use std::io::IsTerminal;
use termcolor::{ColorChoice, StandardStream};
let preference = argv.get_flag("color").unwrap_or("auto");
let mut choice = preference.parse::<ColorChoice>()?;
if choice == ColorChoice::Auto && !atty::is(atty::Stream::Stdout) {
if choice == ColorChoice::Auto && !std::io::stdin().is_terminal() {
choice = ColorChoice::Never;
}
let stdout = StandardStream::stdout(choice);
@@ -146,6 +148,10 @@ pub trait WriteColor: io::Write {
///
/// If there was a problem resetting the color settings, then an error is
/// returned.
///
/// Note that this does not reset hyperlinks. Those need to be
/// reset on their own, e.g., by calling `set_hyperlink` with
/// [`HyperlinkSpec::none`].
fn reset(&mut self) -> io::Result<()>;

/// Returns true if and only if the underlying writer must synchronously
@@ -162,15 +168,49 @@ pub trait WriteColor: io::Write {
fn is_synchronous(&self) -> bool {
false
}

/// Set the current hyperlink of the writer.
///
/// The typical way to use this is to first call it with a
/// [`HyperlinkSpec::open`] to write the actual URI to a tty that supports
/// [OSC-8]. At this point, the caller can now write the label for the
/// hyperlink. This may include coloring or other styles. Once the caller
/// has finished writing the label, one should call this method again with
/// [`HyperlinkSpec::close`].
///
/// If there was a problem setting the hyperlink, then an error is
/// returned.
///
/// This defaults to doing nothing.
///
/// [OSC8]: https://github.com/Alhadis/OSC8-Adoption/
fn set_hyperlink(&mut self, _link: &HyperlinkSpec) -> io::Result<()> {
Ok(())
}

/// Returns true if and only if the underlying writer supports hyperlinks.
///
/// This can be used to avoid generating hyperlink URIs unnecessarily.
///
/// This defaults to `false`.
fn supports_hyperlinks(&self) -> bool {
false
}
}

impl<'a, T: ?Sized + WriteColor> WriteColor for &'a mut T {
fn supports_color(&self) -> bool {
(&**self).supports_color()
}
fn supports_hyperlinks(&self) -> bool {
(&**self).supports_hyperlinks()
}
fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> {
(&mut **self).set_color(spec)
}
fn set_hyperlink(&mut self, link: &HyperlinkSpec) -> io::Result<()> {
(&mut **self).set_hyperlink(link)
}
fn reset(&mut self) -> io::Result<()> {
(&mut **self).reset()
}
@@ -183,9 +223,15 @@ impl<T: ?Sized + WriteColor> WriteColor for Box<T> {
fn supports_color(&self) -> bool {
(&**self).supports_color()
}
fn supports_hyperlinks(&self) -> bool {
(&**self).supports_hyperlinks()
}
fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> {
(&mut **self).set_color(spec)
}
fn set_hyperlink(&mut self, link: &HyperlinkSpec) -> io::Result<()> {
(&mut **self).set_hyperlink(link)
}
fn reset(&mut self) -> io::Result<()> {
(&mut **self).reset()
}
@@ -668,11 +714,21 @@ impl WriteColor for StandardStream {
self.wtr.supports_color()
}

#[inline]
fn supports_hyperlinks(&self) -> bool {
self.wtr.supports_hyperlinks()
}

#[inline]
fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> {
self.wtr.set_color(spec)
}

#[inline]
fn set_hyperlink(&mut self, link: &HyperlinkSpec) -> io::Result<()> {
self.wtr.set_hyperlink(link)
}

#[inline]
fn reset(&mut self) -> io::Result<()> {
self.wtr.reset()
@@ -702,11 +758,21 @@ impl<'a> WriteColor for StandardStreamLock<'a> {
self.wtr.supports_color()
}

#[inline]
fn supports_hyperlinks(&self) -> bool {
self.wtr.supports_hyperlinks()
}

#[inline]
fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> {
self.wtr.set_color(spec)
}

#[inline]
fn set_hyperlink(&mut self, link: &HyperlinkSpec) -> io::Result<()> {
self.wtr.set_hyperlink(link)
}

#[inline]
fn reset(&mut self) -> io::Result<()> {
self.wtr.reset()
@@ -736,6 +802,11 @@ impl WriteColor for BufferedStandardStream {
self.wtr.supports_color()
}

#[inline]
fn supports_hyperlinks(&self) -> bool {
self.wtr.supports_hyperlinks()
}

#[inline]
fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> {
if self.is_synchronous() {
@@ -744,6 +815,14 @@ impl WriteColor for BufferedStandardStream {
self.wtr.set_color(spec)
}

#[inline]
fn set_hyperlink(&mut self, link: &HyperlinkSpec) -> io::Result<()> {
if self.is_synchronous() {
self.wtr.flush()?;
}
self.wtr.set_hyperlink(link)
}

#[inline]
fn reset(&mut self) -> io::Result<()> {
self.wtr.reset()
@@ -787,6 +866,15 @@ impl<W: io::Write> WriteColor for WriterInner<W> {
}
}

fn supports_hyperlinks(&self) -> bool {
match *self {
WriterInner::NoColor(_) => false,
WriterInner::Ansi(_) => true,
#[cfg(windows)]
WriterInner::Windows { .. } => false,
}
}

fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> {
match *self {
WriterInner::NoColor(ref mut wtr) => wtr.set_color(spec),
@@ -800,6 +888,15 @@ impl<W: io::Write> WriteColor for WriterInner<W> {
}
}

fn set_hyperlink(&mut self, link: &HyperlinkSpec) -> io::Result<()> {
match *self {
WriterInner::NoColor(ref mut wtr) => wtr.set_hyperlink(link),
WriterInner::Ansi(ref mut wtr) => wtr.set_hyperlink(link),
#[cfg(windows)]
WriterInner::Windows { .. } => Ok(()),
}
}

fn reset(&mut self) -> io::Result<()> {
match *self {
WriterInner::NoColor(ref mut wtr) => wtr.reset(),
@@ -856,6 +953,16 @@ impl<'a, W: io::Write> WriteColor for WriterInnerLock<'a, W> {
}
}

fn supports_hyperlinks(&self) -> bool {
match *self {
WriterInnerLock::Unreachable(_) => unreachable!(),
WriterInnerLock::NoColor(_) => false,
WriterInnerLock::Ansi(_) => true,
#[cfg(windows)]
WriterInnerLock::Windows { .. } => false,
}
}

fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> {
match *self {
WriterInnerLock::Unreachable(_) => unreachable!(),
@@ -869,6 +976,16 @@ impl<'a, W: io::Write> WriteColor for WriterInnerLock<'a, W> {
}
}

fn set_hyperlink(&mut self, link: &HyperlinkSpec) -> io::Result<()> {
match *self {
WriterInnerLock::Unreachable(_) => unreachable!(),
WriterInnerLock::NoColor(ref mut wtr) => wtr.set_hyperlink(link),
WriterInnerLock::Ansi(ref mut wtr) => wtr.set_hyperlink(link),
#[cfg(windows)]
WriterInnerLock::Windows { .. } => Ok(()),
}
}

fn reset(&mut self) -> io::Result<()> {
match *self {
WriterInnerLock::Unreachable(_) => unreachable!(),
@@ -1219,6 +1336,16 @@ impl WriteColor for Buffer {
}
}

#[inline]
fn supports_hyperlinks(&self) -> bool {
match self.0 {
BufferInner::NoColor(_) => false,
BufferInner::Ansi(_) => true,
#[cfg(windows)]
BufferInner::Windows(_) => false,
}
}

#[inline]
fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> {
match self.0 {
@@ -1229,6 +1356,16 @@ impl WriteColor for Buffer {
}
}

#[inline]
fn set_hyperlink(&mut self, link: &HyperlinkSpec) -> io::Result<()> {
match self.0 {
BufferInner::NoColor(ref mut w) => w.set_hyperlink(link),
BufferInner::Ansi(ref mut w) => w.set_hyperlink(link),
#[cfg(windows)]
BufferInner::Windows(ref mut w) => w.set_hyperlink(link),
}
}

#[inline]
fn reset(&mut self) -> io::Result<()> {
match self.0 {
@@ -1290,11 +1427,21 @@ impl<W: io::Write> WriteColor for NoColor<W> {
false
}

#[inline]
fn supports_hyperlinks(&self) -> bool {
false
}

#[inline]
fn set_color(&mut self, _: &ColorSpec) -> io::Result<()> {
Ok(())
}

#[inline]
fn set_hyperlink(&mut self, _: &HyperlinkSpec) -> io::Result<()> {
Ok(())
}

#[inline]
fn reset(&mut self) -> io::Result<()> {
Ok(())
@@ -1362,6 +1509,11 @@ impl<W: io::Write> WriteColor for Ansi<W> {
true
}

#[inline]
fn supports_hyperlinks(&self) -> bool {
true
}

#[inline]
fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> {
if spec.reset {
@@ -1391,6 +1543,15 @@ impl<W: io::Write> WriteColor for Ansi<W> {
Ok(())
}

#[inline]
fn set_hyperlink(&mut self, link: &HyperlinkSpec) -> io::Result<()> {
self.write_str("\x1B]8;;")?;
if let Some(uri) = link.uri() {
self.write_all(uri)?;
}
self.write_str("\x1B\\")
}

#[inline]
fn reset(&mut self) -> io::Result<()> {
self.write_str("\x1B[0m")
@@ -1522,10 +1683,18 @@ impl WriteColor for io::Sink {
false
}

fn supports_hyperlinks(&self) -> bool {
false
}

fn set_color(&mut self, _: &ColorSpec) -> io::Result<()> {
Ok(())
}

fn set_hyperlink(&mut self, _: &HyperlinkSpec) -> io::Result<()> {
Ok(())
}

fn reset(&mut self) -> io::Result<()> {
Ok(())
}
@@ -1624,12 +1793,22 @@ impl WriteColor for WindowsBuffer {
true
}

#[inline]
fn supports_hyperlinks(&self) -> bool {
false
}

#[inline]
fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> {
self.push(Some(spec.clone()));
Ok(())
}

#[inline]
fn set_hyperlink(&mut self, _: &HyperlinkSpec) -> io::Result<()> {
Ok(())
}

#[inline]
fn reset(&mut self) -> io::Result<()> {
self.push(None);
@@ -2085,6 +2264,29 @@ impl FromStr for Color {
}
}

/// A hyperlink specification.
#[derive(Clone, Debug)]
pub struct HyperlinkSpec<'a> {
uri: Option<&'a [u8]>,
}

impl<'a> HyperlinkSpec<'a> {
/// Creates a new hyperlink specification.
pub fn open(uri: &'a [u8]) -> HyperlinkSpec<'a> {
HyperlinkSpec { uri: Some(uri) }
}

/// Creates a hyperlink specification representing no hyperlink.
pub fn close() -> HyperlinkSpec<'a> {
HyperlinkSpec { uri: None }
}

/// Returns the URI of the hyperlink if one is attached to this spec.
pub fn uri(&self) -> Option<&'a [u8]> {
self.uri
}
}

#[derive(Debug)]
struct LossyStandardStream<W> {
wtr: W,
@@ -2124,9 +2326,15 @@ impl<W: WriteColor> WriteColor for LossyStandardStream<W> {
fn supports_color(&self) -> bool {
self.wtr.supports_color()
}
fn supports_hyperlinks(&self) -> bool {
self.wtr.supports_hyperlinks()
}
fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> {
self.wtr.set_color(spec)
}
fn set_hyperlink(&mut self, link: &HyperlinkSpec) -> io::Result<()> {
self.wtr.set_hyperlink(link)
}
fn reset(&mut self) -> io::Result<()> {
self.wtr.reset()
}
@@ -2170,8 +2378,8 @@ fn write_lossy_utf8<W: io::Write>(mut w: W, buf: &[u8]) -> io::Result<usize> {
#[cfg(test)]
mod tests {
use super::{
Ansi, Color, ColorSpec, ParseColorError, ParseColorErrorKind,
StandardStream, WriteColor,
Ansi, Color, ColorSpec, HyperlinkSpec, ParseColorError,
ParseColorErrorKind, StandardStream, WriteColor,
};

fn assert_is_send<T: Send>() {}
@@ -2347,4 +2555,18 @@ mod tests {
assert!(color1.is_none(), "{:?} => {:?}", color, color1);
}
}

#[test]
fn test_ansi_hyperlink() {
let mut buf = Ansi::new(vec![]);
buf.set_hyperlink(&HyperlinkSpec::open(b"https://example.com"))
.unwrap();
buf.write_str("label").unwrap();
buf.set_hyperlink(&HyperlinkSpec::close()).unwrap();

assert_eq!(
buf.0,
b"\x1B]8;;https://example.com\x1B\\label\x1B]8;;\x1B\\".to_vec()
);
}
}