diff --git a/crates/anstream/benches/stream.rs b/crates/anstream/benches/stream.rs index 55c20b89..63a9d5ef 100644 --- a/crates/anstream/benches/stream.rs +++ b/crates/anstream/benches/stream.rs @@ -37,8 +37,7 @@ fn stream(c: &mut Criterion) { group.bench_function("WinconStream", |b| { b.iter(|| { let buffer = anstream::Buffer::with_capacity(content.len()); - let mut stream = - anstream::WinconStream::new(anstyle_wincon::Console::new(buffer).unwrap()); + let mut stream = anstream::WinconStream::new(buffer); stream.write_all(content).unwrap(); diff --git a/crates/anstream/src/auto.rs b/crates/anstream/src/auto.rs index 969a8da9..44ed0d4b 100644 --- a/crates/anstream/src/auto.rs +++ b/crates/anstream/src/auto.rs @@ -102,9 +102,8 @@ where fn wincon(raw: S) -> Result { #[cfg(all(windows, feature = "wincon"))] { - let console = anstyle_wincon::Console::new(raw)?; Ok(Self { - inner: StreamInner::Wincon(WinconStream::new(console)), + inner: StreamInner::Wincon(WinconStream::new(raw)), }) } #[cfg(not(all(windows, feature = "wincon")))] @@ -120,7 +119,7 @@ where StreamInner::PassThrough(w) => w, StreamInner::Strip(w) => w.into_inner(), #[cfg(all(windows, feature = "wincon"))] - StreamInner::Wincon(w) => w.into_inner().into_inner(), + StreamInner::Wincon(w) => w.into_inner(), } } diff --git a/crates/anstream/src/buffer.rs b/crates/anstream/src/buffer.rs index e635687c..6f359283 100644 --- a/crates/anstream/src/buffer.rs +++ b/crates/anstream/src/buffer.rs @@ -50,28 +50,12 @@ impl std::io::Write for Buffer { #[cfg(all(windows, feature = "wincon"))] impl anstyle_wincon::WinconStream for Buffer { - fn set_colors( + fn write_colored( &mut self, fg: Option, bg: Option, - ) -> std::io::Result<()> { - use std::io::Write as _; - - if let Some(fg) = fg { - write!(self, "{}", fg.render_fg())?; - } - if let Some(bg) = bg { - write!(self, "{}", bg.render_bg())?; - } - if fg.is_none() && bg.is_none() { - write!(self, "{}", anstyle::Reset.render())?; - } - Ok(()) - } - - fn get_colors( - &self, - ) -> std::io::Result<(Option, Option)> { - Ok((None, None)) + data: &[u8], + ) -> std::io::Result { + self.0.write_colored(fg, bg, data) } } diff --git a/crates/anstream/src/is_terminal.rs b/crates/anstream/src/is_terminal.rs index 840f851d..e7ece1b8 100644 --- a/crates/anstream/src/is_terminal.rs +++ b/crates/anstream/src/is_terminal.rs @@ -30,38 +30,6 @@ impl IsTerminal for std::io::StderrLock<'static> { } } -#[cfg(all(windows, feature = "wincon"))] -impl IsTerminal for anstyle_wincon::Console { - #[inline] - fn is_terminal(&self) -> bool { - self.is_terminal() - } -} - -#[cfg(all(windows, feature = "wincon"))] -impl IsTerminal for anstyle_wincon::Console> { - #[inline] - fn is_terminal(&self) -> bool { - self.is_terminal() - } -} - -#[cfg(all(windows, feature = "wincon"))] -impl IsTerminal for anstyle_wincon::Console { - #[inline] - fn is_terminal(&self) -> bool { - self.is_terminal() - } -} - -#[cfg(all(windows, feature = "wincon"))] -impl IsTerminal for anstyle_wincon::Console> { - #[inline] - fn is_terminal(&self) -> bool { - self.is_terminal() - } -} - impl IsTerminal for std::fs::File { #[inline] fn is_terminal(&self) -> bool { diff --git a/crates/anstream/src/lockable.rs b/crates/anstream/src/lockable.rs index 19d06a01..51568f84 100644 --- a/crates/anstream/src/lockable.rs +++ b/crates/anstream/src/lockable.rs @@ -29,23 +29,3 @@ impl Lockable for std::io::Stderr { (&self).lock() } } - -#[cfg(all(windows, feature = "wincon"))] -impl Lockable for anstyle_wincon::Console { - type Locked = anstyle_wincon::Console>; - - #[inline] - fn lock(self) -> Self::Locked { - self.lock() - } -} - -#[cfg(all(windows, feature = "wincon"))] -impl Lockable for anstyle_wincon::Console { - type Locked = anstyle_wincon::Console>; - - #[inline] - fn lock(self) -> Self::Locked { - self.lock() - } -} diff --git a/crates/anstream/src/wincon.rs b/crates/anstream/src/wincon.rs index 8983db86..44bb512f 100644 --- a/crates/anstream/src/wincon.rs +++ b/crates/anstream/src/wincon.rs @@ -10,7 +10,7 @@ pub struct WinconStream where S: RawStream, { - console: anstyle_wincon::Console, + raw: S, // `WinconBytes` is especially large compared to other variants of `AutoStream`, so boxing it // here so `AutoStream` doesn't have to discard one allocation and create another one when // calling `AutoStream::lock` @@ -23,24 +23,22 @@ where { /// Only pass printable data to the inner `Write` #[inline] - pub fn new(console: anstyle_wincon::Console) -> Self { + pub fn new(raw: S) -> Self { Self { - console, + raw, state: Box::default(), } } /// Get the wrapped [`RawStream`] #[inline] - pub fn into_inner(self) -> anstyle_wincon::Console { - self.console + pub fn into_inner(self) -> S { + self.raw } #[inline] pub fn is_terminal(&self) -> bool { - // HACK: We can't get the console's stream to check but if there is a console, it likely is - // a terminal - true + self.raw.is_terminal() } } @@ -62,7 +60,7 @@ where for (style, printable) in self.state.extract_next(buf) { let fg = style.get_fg_color().and_then(cap_wincon_color); let bg = style.get_bg_color().and_then(cap_wincon_color); - let written = self.console.write(fg, bg, printable.as_bytes())?; + let written = self.raw.write_colored(fg, bg, printable.as_bytes())?; let possible = printable.len(); if possible != written { // HACK: Unsupported atm @@ -73,7 +71,7 @@ where } #[inline] fn flush(&mut self) -> std::io::Result<()> { - self.console.flush() + self.raw.flush() } } @@ -83,7 +81,7 @@ impl Lockable for WinconStream { #[inline] fn lock(self) -> Self::Locked { Self::Locked { - console: self.console.lock(), + raw: self.raw.lock(), state: self.state, } } @@ -95,7 +93,7 @@ impl Lockable for WinconStream { #[inline] fn lock(self) -> Self::Locked { Self::Locked { - console: self.console.lock(), + raw: self.raw.lock(), state: self.state, } } @@ -120,9 +118,9 @@ mod test { #[cfg_attr(miri, ignore)] // See https://github.com/AltSysrq/proptest/issues/253 fn write_all_no_escapes(s in "\\PC*") { let buffer = crate::Buffer::new(); - let mut stream = WinconStream::new(anstyle_wincon::Console::new(buffer).unwrap()); + let mut stream = WinconStream::new(buffer); stream.write_all(s.as_bytes()).unwrap(); - let buffer = stream.into_inner().into_inner(); + let buffer = stream.into_inner(); let actual = std::str::from_utf8(buffer.as_ref()).unwrap(); assert_eq!(s, actual); } @@ -131,11 +129,11 @@ mod test { #[cfg_attr(miri, ignore)] // See https://github.com/AltSysrq/proptest/issues/253 fn write_byte_no_escapes(s in "\\PC*") { let buffer = crate::Buffer::new(); - let mut stream = WinconStream::new(anstyle_wincon::Console::new(buffer).unwrap()); + let mut stream = WinconStream::new(buffer); for byte in s.as_bytes() { stream.write_all(&[*byte]).unwrap(); } - let buffer = stream.into_inner().into_inner(); + let buffer = stream.into_inner(); let actual = std::str::from_utf8(buffer.as_ref()).unwrap(); assert_eq!(s, actual); } @@ -144,7 +142,7 @@ mod test { #[cfg_attr(miri, ignore)] // See https://github.com/AltSysrq/proptest/issues/253 fn write_all_random(s in any::>()) { let buffer = crate::Buffer::new(); - let mut stream = WinconStream::new(anstyle_wincon::Console::new(buffer).unwrap()); + let mut stream = WinconStream::new(buffer); stream.write_all(s.as_slice()).unwrap(); } @@ -152,7 +150,7 @@ mod test { #[cfg_attr(miri, ignore)] // See https://github.com/AltSysrq/proptest/issues/253 fn write_byte_random(s in any::>()) { let buffer = crate::Buffer::new(); - let mut stream = WinconStream::new(anstyle_wincon::Console::new(buffer).unwrap()); + let mut stream = WinconStream::new(buffer); for byte in s.as_slice() { stream.write_all(&[*byte]).unwrap(); } diff --git a/crates/anstyle-wincon/examples/dump.rs b/crates/anstyle-wincon/examples/dump.rs index 0709a679..bb80bf42 100644 --- a/crates/anstyle-wincon/examples/dump.rs +++ b/crates/anstyle-wincon/examples/dump.rs @@ -1,32 +1,33 @@ +use anstyle_wincon::WinconStream as _; + fn main() -> Result<(), lexopt::Error> { let args = Args::parse()?; let stdout = std::io::stdout(); - let mut stdout = anstyle_wincon::Console::new(stdout.lock()) - .map_err(|_err| lexopt::Error::from("could not open `stdout` for color control"))?; + let mut stdout = stdout.lock(); for fixed in 0..16 { let style = style(fixed, args.layer, args.effects); let _ = print_number(&mut stdout, fixed, style); if fixed == 7 || fixed == 15 { - let _ = stdout.write(None, None, &b"\n"[..]); + let _ = stdout.write_colored(None, None, &b"\n"[..]); } } for r in 0..6 { - let _ = stdout.write(None, None, &b"\n"[..]); + let _ = stdout.write_colored(None, None, &b"\n"[..]); for g in 0..6 { for b in 0..6 { let fixed = r * 36 + g * 6 + b + 16; let style = style(fixed, args.layer, args.effects); let _ = print_number(&mut stdout, fixed, style); } - let _ = stdout.write(None, None, &b"\n"[..]); + let _ = stdout.write_colored(None, None, &b"\n"[..]); } } for c in 0..24 { if 0 == c % 8 { - let _ = stdout.write(None, None, &b"\n"[..]); + let _ = stdout.write_colored(None, None, &b"\n"[..]); } let fixed = 232 + c; let style = style(fixed, args.layer, args.effects); @@ -46,7 +47,7 @@ fn style(fixed: u8, layer: Layer, effects: anstyle::Effects) -> anstyle::Style { } fn print_number( - stdout: &mut anstyle_wincon::Console>, + stdout: &mut std::io::StdoutLock<'static>, fixed: u8, style: anstyle::Style, ) -> std::io::Result<()> { @@ -62,7 +63,7 @@ fn print_number( }); stdout - .write(fg, bg, format!("{:>4}", fixed).as_bytes()) + .write_colored(fg, bg, format!("{:>4}", fixed).as_bytes()) .map(|_| ()) } diff --git a/crates/anstyle-wincon/examples/set.rs b/crates/anstyle-wincon/examples/set.rs index 3b9f9093..6febb0ae 100644 --- a/crates/anstyle-wincon/examples/set.rs +++ b/crates/anstyle-wincon/examples/set.rs @@ -7,15 +7,16 @@ fn main() { #[cfg(windows)] fn main() -> Result<(), lexopt::Error> { + use anstyle_wincon::WinconStream as _; + let args = Args::parse()?; let stdout = std::io::stdout(); - let mut stdout = anstyle_wincon::Console::new(stdout.lock()) - .map_err(|_err| lexopt::Error::from("could not open `stdout` for color control"))?; + let mut stdout = stdout.lock(); let fg = args.fg.and_then(|c| c.into_ansi()); let bg = args.bg.and_then(|c| c.into_ansi()); - let _ = stdout.write(fg, bg, "".as_bytes()); + let _ = stdout.write_colored(fg, bg, "".as_bytes()); std::mem::forget(stdout); diff --git a/crates/anstyle-wincon/src/ansi.rs b/crates/anstyle-wincon/src/ansi.rs new file mode 100644 index 00000000..6ac71930 --- /dev/null +++ b/crates/anstyle-wincon/src/ansi.rs @@ -0,0 +1,25 @@ +//! Low-level ANSI-styling + +/// Write ANSI colored text to the stream +pub fn write_colored( + stream: &mut S, + fg: Option, + bg: Option, + data: &[u8], +) -> std::io::Result { + let non_default = fg.is_some() || bg.is_some(); + + if non_default { + if let Some(fg) = fg { + write!(stream, "{}", fg.render_fg())?; + } + if let Some(bg) = bg { + write!(stream, "{}", bg.render_bg())?; + } + } + let written = stream.write(data)?; + if non_default { + write!(stream, "{}", anstyle::Reset.render())?; + } + Ok(written) +} diff --git a/crates/anstyle-wincon/src/console.rs b/crates/anstyle-wincon/src/console.rs deleted file mode 100644 index a6d1af52..00000000 --- a/crates/anstyle-wincon/src/console.rs +++ /dev/null @@ -1,99 +0,0 @@ -/// Write colored text to the screen -#[derive(Debug)] -pub struct Console -where - S: crate::WinconStream + std::io::Write, -{ - stream: Option, - state: crate::stream::ConsoleState, -} - -impl Console -where - S: crate::WinconStream + std::io::Write, -{ - pub fn new(stream: S) -> Result { - match crate::stream::ConsoleState::new(&stream) { - Ok(state) => Ok(Self { - stream: Some(stream), - state, - }), - Err(_err) => Err(stream), - } - } - - /// Write colored text to the screen - pub fn write( - &mut self, - fg: Option, - bg: Option, - data: &[u8], - ) -> std::io::Result { - self.state - .write(self.stream.as_mut().unwrap(), fg, bg, data) - } - - pub fn flush(&mut self) -> std::io::Result<()> { - self.stream.as_mut().unwrap().flush() - } - - /// Change the terminal back to the initial colors - pub fn reset(&mut self) -> std::io::Result<()> { - self.state.reset(self.stream.as_mut().unwrap()) - } - - /// Close the stream, reporting any errors - #[allow(unused)] - pub fn close(mut self) -> std::io::Result<()> { - Ok(()) - } - - /// Get the inner writer - #[inline] - pub fn into_inner(mut self) -> S { - self.stream.take().unwrap() - } -} - -impl Console -where - S: crate::WinconStream + std::io::Write + std::io::IsTerminal, -{ - pub fn is_terminal(&self) -> bool { - std::io::IsTerminal::is_terminal(self.stream.as_ref().unwrap()) - } -} - -impl Console -where - S: crate::WinconStream + std::io::Write, - S: crate::Lockable, - ::Locked: crate::WinconStream + std::io::Write, -{ - /// Get exclusive access to the `Console` - /// - /// Why? - /// - Faster performance when writing in a loop - /// - Avoid other threads interleaving output with the current thread - #[inline] - pub fn lock(mut self) -> ::Locked { - Console { - stream: Some(self.stream.take().unwrap().lock()), - state: self.state.clone(), - } - } -} - -impl crate::Lockable for Console -where - S: crate::WinconStream + std::io::Write, - S: crate::Lockable, - ::Locked: crate::WinconStream + std::io::Write, -{ - type Locked = Console<::Locked>; - - #[inline] - fn lock(self) -> Self::Locked { - self.lock() - } -} diff --git a/crates/anstyle-wincon/src/lib.rs b/crates/anstyle-wincon/src/lib.rs index 670ae7f7..e04fab6e 100644 --- a/crates/anstyle-wincon/src/lib.rs +++ b/crates/anstyle-wincon/src/lib.rs @@ -1,6 +1,6 @@ //! Styling legacy Windows terminals //! -//! See [`Console`] +//! See [`WinconStream`] //! //! This fills a similar role as [`winapi-util`](https://crates.io/crates/winapi-util) does for //! [`termcolor`](https://crates.io/crates/termcolor) with the differences @@ -10,12 +10,9 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] -mod console; -mod lockable; +pub mod ansi; mod stream; #[cfg(windows)] pub mod windows; -pub use console::Console; -pub use lockable::Lockable; pub use stream::WinconStream; diff --git a/crates/anstyle-wincon/src/lockable.rs b/crates/anstyle-wincon/src/lockable.rs deleted file mode 100644 index 47f305e3..00000000 --- a/crates/anstyle-wincon/src/lockable.rs +++ /dev/null @@ -1,31 +0,0 @@ -/// 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 { - #[allow(clippy::needless_borrow)] // Its needed to avoid recursion - (&self).lock() - } -} - -impl Lockable for std::io::Stderr { - type Locked = std::io::StderrLock<'static>; - - #[inline] - fn lock(self) -> Self::Locked { - #[allow(clippy::needless_borrow)] // Its needed to avoid recursion - (&self).lock() - } -} diff --git a/crates/anstyle-wincon/src/stream.rs b/crates/anstyle-wincon/src/stream.rs index 73b15bad..9d446769 100644 --- a/crates/anstyle-wincon/src/stream.rs +++ b/crates/anstyle-wincon/src/stream.rs @@ -1,268 +1,112 @@ -#[cfg(not(windows))] -use ansi as inner; -#[cfg(windows)] -use wincon as inner; - /// Extend `std::io::Write` with wincon styling -/// -/// Generally, you will want to use [`Console`][crate::Console] instead pub trait WinconStream { - /// Change the foreground/background - /// - /// A common pitfall is to forget to flush writes to - /// stdout before setting new text attributes. - fn set_colors( - &mut self, - fg: Option, - bg: Option, - ) -> std::io::Result<()>; - - /// Get the current foreground/background colors - fn get_colors( - &self, - ) -> std::io::Result<(Option, Option)>; -} - -impl WinconStream for std::io::Stdout { - fn set_colors( - &mut self, - fg: Option, - bg: Option, - ) -> std::io::Result<()> { - inner::set_colors(self, fg, bg) - } - - fn get_colors( - &self, - ) -> std::io::Result<(Option, Option)> { - inner::get_colors(self) - } -} - -impl WinconStream for std::io::StdoutLock<'static> { - fn set_colors( + /// Write colored text to the stream + fn write_colored( &mut self, fg: Option, bg: Option, - ) -> std::io::Result<()> { - inner::set_colors(self, fg, bg) - } - - fn get_colors( - &self, - ) -> std::io::Result<(Option, Option)> { - inner::get_colors(self) - } -} - -impl WinconStream for std::io::Stderr { - fn set_colors( - &mut self, - fg: Option, - bg: Option, - ) -> std::io::Result<()> { - inner::set_colors(self, fg, bg) - } - - fn get_colors( - &self, - ) -> std::io::Result<(Option, Option)> { - inner::get_colors(self) - } -} - -impl WinconStream for std::io::StderrLock<'static> { - fn set_colors( - &mut self, - fg: Option, - bg: Option, - ) -> std::io::Result<()> { - inner::set_colors(self, fg, bg) - } - - fn get_colors( - &self, - ) -> std::io::Result<(Option, Option)> { - inner::get_colors(self) - } + data: &[u8], + ) -> std::io::Result; } impl WinconStream for std::fs::File { - fn set_colors( + fn write_colored( &mut self, fg: Option, bg: Option, - ) -> std::io::Result<()> { - ansi::set_colors(self, fg, bg) - } - - fn get_colors( - &self, - ) -> std::io::Result<(Option, Option)> { - ansi::get_colors(self) + data: &[u8], + ) -> std::io::Result { + crate::ansi::write_colored(self, fg, bg, data) } } -/// Write colored text to the screen -#[derive(Clone, Debug)] -pub(crate) enum ConsoleState { - Wincon(WinconAdapter), - Pass(PassThroughAdapter), -} - -impl ConsoleState { - pub(crate) fn new( - stream: &S, - ) -> std::io::Result { - let adapter = match stream.get_colors() { - Ok((Some(initial_fg), Some(initial_bg))) => { - Self::Wincon(WinconAdapter::new(initial_fg, initial_bg)) - } - // Can only happen on non-wincon systems - Ok(_) => Self::Pass(PassThroughAdapter::new()), - Err(err) => { - return Err(err); - } - }; - Ok(adapter) - } - - pub(crate) fn write( +impl WinconStream for Vec { + fn write_colored( &mut self, - stream: &mut S, fg: Option, bg: Option, data: &[u8], ) -> std::io::Result { - let non_default = fg.is_some() || bg.is_some(); - - if non_default { - self.apply(stream, fg, bg)?; - } - let written = stream.write(data)?; - if non_default { - self.apply(stream, None, None)?; - } - Ok(written) - } - - pub(crate) fn reset( - &mut self, - stream: &mut S, - ) -> std::io::Result<()> { - self.apply(stream, None, None) + crate::ansi::write_colored(self, fg, bg, data) } +} - fn apply( +impl WinconStream for std::io::Stdout { + fn write_colored( &mut self, - stream: &mut S, fg: Option, bg: Option, - ) -> std::io::Result<()> { - match self { - Self::Wincon(adapter) => adapter.apply(stream, fg, bg), - Self::Pass(adapter) => adapter.apply(stream, fg, bg), - } + data: &[u8], + ) -> std::io::Result { + // Ensure exclusive access + self.lock().write_colored(fg, bg, data) } } -#[derive(Default, Clone, Debug)] -#[non_exhaustive] -pub(crate) struct PassThroughAdapter {} - -impl PassThroughAdapter { - fn new() -> Self { - Self {} - } - - fn apply( +impl WinconStream for std::io::Stderr { + fn write_colored( &mut self, - stream: &mut S, fg: Option, bg: Option, - ) -> std::io::Result<()> { - stream.set_colors(fg, bg)?; - - Ok(()) + data: &[u8], + ) -> std::io::Result { + // Ensure exclusive access + self.lock().write_colored(fg, bg, data) } } -#[derive(Clone, Debug)] -pub(crate) struct WinconAdapter { - initial_fg: anstyle::AnsiColor, - initial_bg: anstyle::AnsiColor, -} - -impl WinconAdapter { - fn new(initial_fg: anstyle::AnsiColor, initial_bg: anstyle::AnsiColor) -> Self { - Self { - initial_fg, - initial_bg, +#[cfg(not(windows))] +mod platform { + use super::*; + + impl WinconStream for std::io::StdoutLock<'static> { + fn write_colored( + &mut self, + fg: Option, + bg: Option, + data: &[u8], + ) -> std::io::Result { + crate::ansi::write_colored(self, fg, bg, data) } } - fn apply( - &mut self, - stream: &mut S, - fg: Option, - bg: Option, - ) -> std::io::Result<()> { - let fg = fg.unwrap_or(self.initial_fg); - let bg = bg.unwrap_or(self.initial_bg); - - // Ensure everything is written with the last set of colors before applying the next set - stream.flush()?; - stream.set_colors(Some(fg), Some(bg))?; - - Ok(()) + impl WinconStream for std::io::StderrLock<'static> { + fn write_colored( + &mut self, + fg: Option, + bg: Option, + data: &[u8], + ) -> std::io::Result { + crate::ansi::write_colored(self, fg, bg, data) + } } } #[cfg(windows)] -mod wincon { - use std::os::windows::io::AsHandle; - - pub(super) fn set_colors( - stream: &mut S, - fg: Option, - bg: Option, - ) -> std::io::Result<()> { - if let (Some(fg), Some(bg)) = (fg, bg) { - crate::windows::set_colors(stream, fg, bg) - } else { - Ok(()) +mod platform { + use super::*; + + impl WinconStream for std::io::StdoutLock<'static> { + fn write_colored( + &mut self, + fg: Option, + bg: Option, + data: &[u8], + ) -> std::io::Result { + let initial = crate::windows::stdout_initial_colors(); + crate::windows::write_colored(self, fg, bg, data, initial) } } - pub(super) fn get_colors( - stream: &S, - ) -> std::io::Result<(Option, Option)> { - crate::windows::get_colors(stream).map(|(fg, bg)| (Some(fg), Some(bg))) - } -} - -mod ansi { - pub(super) fn set_colors( - stream: &mut S, - fg: Option, - bg: Option, - ) -> std::io::Result<()> { - if let Some(fg) = fg { - write!(stream, "{}", fg.render_fg())?; - } - if let Some(bg) = bg { - write!(stream, "{}", bg.render_bg())?; - } - if fg.is_none() && bg.is_none() { - write!(stream, "{}", anstyle::Reset.render())?; + impl WinconStream for std::io::StderrLock<'static> { + fn write_colored( + &mut self, + fg: Option, + bg: Option, + data: &[u8], + ) -> std::io::Result { + let initial = crate::windows::stderr_initial_colors(); + crate::windows::write_colored(self, fg, bg, data, initial) } - Ok(()) - } - - pub(super) fn get_colors( - _stream: &S, - ) -> std::io::Result<(Option, Option)> { - // No idea what state the stream was left in, so just assume default - Ok((None, None)) } } diff --git a/crates/anstyle-wincon/src/windows.rs b/crates/anstyle-wincon/src/windows.rs index 188a8e54..bf1cf565 100644 --- a/crates/anstyle-wincon/src/windows.rs +++ b/crates/anstyle-wincon/src/windows.rs @@ -1,20 +1,57 @@ +//! Low-level wincon-styling + use std::os::windows::io::AsHandle; use std::os::windows::io::AsRawHandle; +type StdioColorResult = std::io::Result<(anstyle::AnsiColor, anstyle::AnsiColor)>; +type StdioColorInnerResult = Result<(anstyle::AnsiColor, anstyle::AnsiColor), inner::IoError>; + +/// Cached [`get_colors`] call for [`std::io::stdout`] +pub fn stdout_initial_colors() -> StdioColorResult { + static INITIAL: std::sync::OnceLock = std::sync::OnceLock::new(); + INITIAL + .get_or_init(|| get_colors_(&std::io::stdout())) + .clone() + .map_err(Into::into) +} + +/// Cached [`get_colors`] call for [`std::io::stderr`] +pub fn stderr_initial_colors() -> StdioColorResult { + static INITIAL: std::sync::OnceLock = std::sync::OnceLock::new(); + INITIAL + .get_or_init(|| get_colors_(&std::io::stderr())) + .clone() + .map_err(Into::into) +} + +/// Apply colors to future writes +/// +/// **Note:** Make sure any buffers are first flushed or else these colors will apply pub fn set_colors( stream: &mut S, fg: anstyle::AnsiColor, bg: anstyle::AnsiColor, ) -> std::io::Result<()> { + set_colors_(stream, fg, bg).map_err(Into::into) +} + +fn set_colors_( + stream: &mut S, + fg: anstyle::AnsiColor, + bg: anstyle::AnsiColor, +) -> Result<(), inner::IoError> { let handle = stream.as_handle(); let handle = handle.as_raw_handle(); let attributes = inner::set_colors(fg, bg); inner::set_console_text_attributes(handle, attributes) } -pub fn get_colors( - stream: &S, -) -> std::io::Result<(anstyle::AnsiColor, anstyle::AnsiColor)> { +/// Get the colors currently active on the console +pub fn get_colors(stream: &S) -> StdioColorResult { + get_colors_(stream).map_err(Into::into) +} + +fn get_colors_(stream: &S) -> StdioColorInnerResult { let handle = stream.as_handle(); let handle = handle.as_raw_handle(); let info = inner::get_screen_buffer_info(handle)?; @@ -22,6 +59,32 @@ pub fn get_colors( Ok((fg, bg)) } +pub(crate) fn write_colored( + stream: &mut S, + fg: Option, + bg: Option, + data: &[u8], + initial: StdioColorResult, +) -> std::io::Result { + let (initial_fg, initial_bg) = initial?; + let non_default = fg.is_some() || bg.is_some(); + + if non_default { + let fg = fg.unwrap_or(initial_fg); + let bg = bg.unwrap_or(initial_bg); + // Ensure everything is written with the last set of colors before applying the next set + stream.flush()?; + set_colors(stream, fg, bg)?; + } + let written = stream.write(data)?; + if non_default { + // Ensure everything is written with the last set of colors before applying the next set + stream.flush()?; + set_colors(stream, initial_fg, initial_bg)?; + } + Ok(written) +} + mod inner { use windows_sys::Win32::System::Console::CONSOLE_CHARACTER_ATTRIBUTES; use windows_sys::Win32::System::Console::CONSOLE_SCREEN_BUFFER_INFO; @@ -38,16 +101,36 @@ mod inner { const FOREGROUND_WHITE: CONSOLE_CHARACTER_ATTRIBUTES = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED; - pub fn get_screen_buffer_info( + #[derive(Copy, Clone, Debug)] + pub(crate) enum IoError { + BrokenPipe, + RawOs(i32), + } + + impl From for std::io::Error { + fn from(io: IoError) -> Self { + match io { + IoError::BrokenPipe => { + std::io::Error::new(std::io::ErrorKind::BrokenPipe, "console is detached") + } + IoError::RawOs(code) => std::io::Error::from_raw_os_error(code), + } + } + } + + impl IoError { + fn last_os_error() -> Self { + Self::RawOs(std::io::Error::last_os_error().raw_os_error().unwrap()) + } + } + + pub(crate) fn get_screen_buffer_info( handle: RawHandle, - ) -> std::io::Result { + ) -> Result { unsafe { let handle = std::mem::transmute(handle); if handle == 0 { - return Err(std::io::Error::new( - std::io::ErrorKind::BrokenPipe, - "console is detached", - )); + return Err(IoError::BrokenPipe); } let mut info: CONSOLE_SCREEN_BUFFER_INFO = std::mem::zeroed(); @@ -56,34 +139,31 @@ mod inner { { Ok(info) } else { - Err(std::io::Error::last_os_error()) + Err(IoError::last_os_error()) } } } - pub fn set_console_text_attributes( + pub(crate) fn set_console_text_attributes( handle: RawHandle, attributes: CONSOLE_CHARACTER_ATTRIBUTES, - ) -> std::io::Result<()> { + ) -> Result<(), IoError> { unsafe { let handle = std::mem::transmute(handle); if handle == 0 { - return Err(std::io::Error::new( - std::io::ErrorKind::BrokenPipe, - "console is detached", - )); + return Err(IoError::BrokenPipe); } if windows_sys::Win32::System::Console::SetConsoleTextAttribute(handle, attributes) != 0 { Ok(()) } else { - Err(std::io::Error::last_os_error()) + Err(IoError::last_os_error()) } } } - pub fn get_colors( + pub(crate) fn get_colors( info: &CONSOLE_SCREEN_BUFFER_INFO, ) -> (anstyle::AnsiColor, anstyle::AnsiColor) { let attributes = info.wAttributes; @@ -92,7 +172,7 @@ mod inner { (fg, bg) } - pub fn set_colors( + pub(crate) fn set_colors( fg: anstyle::AnsiColor, bg: anstyle::AnsiColor, ) -> CONSOLE_CHARACTER_ATTRIBUTES {