Skip to content

Commit

Permalink
feat(stream): Initial support for auto-adapting streams
Browse files Browse the repository at this point in the history
  • Loading branch information
epage committed Mar 7, 2023
1 parent 18f799f commit 6d4eaf6
Show file tree
Hide file tree
Showing 3 changed files with 245 additions and 1 deletion.
13 changes: 12 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions crates/anstyle-stream/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ edition.workspace = true
rust-version.workspace = true
include.workspace = true

[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]

[package.metadata.release]
pre-release-replacements = [
{file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1},
Expand All @@ -20,10 +24,15 @@ pre-release-replacements = [
{file="CHANGELOG.md", search="<!-- next-url -->", replace="<!-- next-url -->\n[Unreleased]: https://github.com/rust-cli/anstyle/compare/{{tag_name}}...HEAD", exactly=1},
]

[features]
default = ["auto"]
auto = ["dep:concolor-query", "dep:is-terminal"]

[dependencies]
anstyle = { version = "0.2.5", path = "../anstyle" }
anstyle-parse = { version = "0.1.0", path = "../anstyle-parse" }
concolor-query = { version = "0.2.0", optional = true }
is-terminal = { version = "0.4.4", optional = true }
utf8parse = "0.2.0"

[dev-dependencies]
Expand Down
224 changes: 224 additions & 0 deletions crates/anstyle-stream/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,213 @@
//! **Auto-adapting [`stdout`] / [`stderr`] streams**
//!
//! [`Stream`] always accepts [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code),
//! adapting to the user's terminal's capabilities.
//!
//! Benefits
//! - Allows the caller to not be concerned with the terminal's capabilities
//! - Semver safe way of passing styled text between crates as ANSI escape codes offer more
//! compatibility than most crate APIs.

#![cfg_attr(docsrs, feature(doc_auto_cfg))]

pub mod adapter;

/// Create an ANSI escape code compatible stdout
///
/// **Note:** Call [`Stream::lock`] in loops to avoid the performance hit of acquiring/releasing
/// from the implicit locking in each [`std::io::Write`] call
#[cfg(feature = "auto")]
pub fn stdout() -> Stream<std::io::Stdout> {
let stdout = std::io::stdout();
Stream::auto(stdout)
}

/// Create an ANSI escape code compatible stderr
///
/// **Note:** Call [`Stream::lock`] in loops to avoid the performance hit of acquiring/releasing
/// from the implicit locking in each [`std::io::Write`] call
#[cfg(feature = "auto")]
pub fn stderr() -> Stream<std::io::Stderr> {
let stderr = std::io::stderr();
Stream::auto(stderr)
}

/// Explicitly lock a [`std::io::Write`]able
pub trait Lockable {
type Locked;

/// Get exclusive access to the `Stream`
///
/// Why?
/// - Faster performance when writing in a loop
/// - Avoid other threads interleaving output with the current thread
fn lock(&self) -> Self::Locked;
}

impl Lockable for std::io::Stdout {
type Locked = std::io::StdoutLock<'static>;

#[inline]
fn lock(&self) -> Self::Locked {
self.lock()
}
}

impl Lockable for &'_ std::io::Stdout {
type Locked = std::io::StdoutLock<'static>;

#[inline]
fn lock(&self) -> Self::Locked {
(*self).lock()
}
}

impl Lockable for std::io::Stderr {
type Locked = std::io::StderrLock<'static>;

#[inline]
fn lock(&self) -> Self::Locked {
self.lock()
}
}

impl Lockable for &'_ std::io::Stderr {
type Locked = std::io::StderrLock<'static>;

#[inline]
fn lock(&self) -> Self::Locked {
(*self).lock()
}
}

/// [`std::io::Write`] that adapts ANSI escape codes to the underlying `Write`s capabilities
pub struct Stream<W> {
write: StreamInner<W>,
}

enum StreamInner<W> {
PassThrough(W),
Strip(StripStream<W>),
}

impl<W> Stream<W>
where
W: std::io::Write,
{
/// Force ANSI escape codes to be passed through as-is, no matter what the inner `Write`
/// supports.
#[inline]
pub fn always_ansi(write: W) -> Self {
let write = StreamInner::PassThrough(write);
Stream { write }
}

/// Only pass printable data to the inner `Write`.
#[inline]
pub fn never(write: W) -> Self {
let write = StreamInner::Strip(StripStream::new(write));
Stream { write }
}
}

impl<W> Stream<W>
where
W: Lockable,
{
/// Get exclusive access to the `Stream`
///
/// Why?
/// - Faster performance when writing in a loop
/// - Avoid other threads interleaving output with the current thread
#[inline]
pub fn lock(&self) -> <Self as Lockable>::Locked {
let write = match &self.write {
StreamInner::PassThrough(w) => StreamInner::PassThrough(w.lock()),
StreamInner::Strip(w) => StreamInner::Strip(w.lock()),
};
Stream { write }
}
}

#[cfg(feature = "auto")]
impl<W> Stream<W>
where
W: std::io::Write + is_terminal::IsTerminal,
{
#[cfg(feature = "auto")]
#[inline]
fn auto(write: W) -> Self {
if write.is_terminal() {
if concolor_query::windows::enable_ansi_colors().unwrap_or(true) {
Self::always_ansi(write)
} else {
Self::never(write)
}
} else {
Self::never(write)
}
}
}

impl<W> std::io::Write for Stream<W>
where
W: std::io::Write,
{
#[inline]
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
match &mut self.write {
StreamInner::PassThrough(w) => w.write(buf),
StreamInner::Strip(w) => w.write(buf),
}
}

#[inline]
fn flush(&mut self) -> std::io::Result<()> {
match &mut self.write {
StreamInner::PassThrough(w) => w.flush(),
StreamInner::Strip(w) => w.flush(),
}
}

// Provide explicit implementations of trait methods
// - To reduce bookkeeping
// - Avoid acquiring / releasing locks in a loop

#[inline]
fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
match &mut self.write {
StreamInner::PassThrough(w) => w.write_all(buf),
StreamInner::Strip(w) => w.write_all(buf),
}
}

// Not bothering with `write_fmt` as it just calls `write_all`
}

impl<W> Lockable for Stream<W>
where
W: Lockable,
{
type Locked = Stream<<W as Lockable>::Locked>;

#[inline]
fn lock(&self) -> Self::Locked {
self.lock()
}
}

impl<W> Lockable for &'_ Stream<W>
where
W: Lockable,
{
type Locked = Stream<<W as Lockable>::Locked>;

#[inline]
fn lock(&self) -> Self::Locked {
(*self).lock()
}
}

/// Only pass printable data to the inner `Write`
pub struct StripStream<W> {
write: W,
Expand Down Expand Up @@ -77,3 +285,19 @@ fn offset_to(total: &[u8], subslice: &[u8]) -> usize {
);
subslice as usize - total as usize
}

impl<W> Lockable for StripStream<W>
where
W: Lockable,
{
type Locked = StripStream<<W as Lockable>::Locked>;

#[inline]
fn lock(&self) -> Self::Locked {
Self::Locked {
write: self.write.lock(),
// WARNING: the state is not resumable after unlocking
state: self.state.clone_hack(),
}
}
}

0 comments on commit 6d4eaf6

Please sign in to comment.