Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ANSI color is not retained across multiple print(ln)!'s in Windows legacy console #230

Open
nooriro opened this issue Nov 22, 2024 · 1 comment
Labels
A-stream Area: anstream C-bug Category: Things not working as expected

Comments

@nooriro
Copy link

nooriro commented Nov 22, 2024

Cargo.toml

[package]
name = "anstream-test"
version = "0.1.0"
edition = "2021"

[dependencies]
anstream = "0.6.18"

src/main.rs

use anstream::{print, println};

fn main() {
    print!("\x1b[90m(print!) This text is displayed in gray.\n");
    print!("(print!) This text should also be displayed in gray.\n");
    println!("\x1b[90m(println!) This text is displayed in gray.");
    println!("(println!) This text should also be displayed in gray.\x1b[0m");
}

Steps to reproduce

Run cargo run in CMD/PowerShell with 'Legacy Console mode' enabled.

anstream-win-legacyconsole-01-241121

Expected result

All the lines should be displayed in gray, just like in the non-legacy Windows Console.

anstream-win-legacyconsole-02-241121 anstream-win-legacyconsole-03-241121

Actual result

Only the lines that output \x1b[90m are displayed in gray (1st and 3rd lines).

anstream-win-legacyconsole-04-241121

Environment

  • Windows 10 22H2 Home x64 10.0.19045.5011
  • Visual Studio Community 2022 17.11.5 / Windows 11 SDK 10.0.26100.0
  • Default host: x86_64-pc-windows-msvc
  • Active toolchain: stable-x86_64-pc-windows-msvc (default) / rustc 1.82.0 (f6e511eec 2024-10-15)
@epage epage added C-bug Category: Things not working as expected A-stream Area: anstream labels Nov 22, 2024
@epage
Copy link
Collaborator

epage commented Nov 22, 2024

Each println! call will call stdout

macro_rules! println {
() => {
$crate::print!("\n")
};
($($arg:tt)*) => {{
if cfg!(test) || $crate::_macros::FEATURE_TEST_ACTIVATED {
let target_stream = std::io::stdout();
let buffer = $crate::_macros::to_adapted_string(&format_args!($($arg)*), &target_stream);
::std::println!("{}", buffer)
} else {
use std::io::Write as _;
let mut stream = $crate::stdout();
match ::std::writeln!(&mut stream, $($arg)*) {
Err(e) if e.kind() != ::std::io::ErrorKind::BrokenPipe => {
::std::panic!("failed printing to stdout: {e}");
}
Err(_) | Ok(_) => {}
}
}
}};
}

Each stdout call creates a new AutoStream

pub fn stdout() -> Stdout {
let stdout = std::io::stdout();
AutoStream::auto(stdout)
}

AutoStream is stateless, forwarding on calls

fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
match &mut self.inner {
StreamInner::PassThrough(w) => w.as_locked_write().write(buf),
StreamInner::Strip(w) => w.write(buf),
#[cfg(all(windows, feature = "wincon"))]
StreamInner::Wincon(w) => w.write(buf),
}

WinconStream is stateful but only within an instance and not across instances

fn write(
raw: &mut dyn anstyle_wincon::WinconStream,
state: &mut WinconBytes,
buf: &[u8],
) -> std::io::Result<usize> {
for (style, printable) in 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 = raw.write_colored(fg, bg, printable.as_bytes())?;
let possible = printable.len();
if possible != written {
// HACK: Unsupported atm
break;
}
}
Ok(buf.len())
}

The next layer down of WinconStream only deals with global for tracking the original colors of the terminal

impl super::WinconStream for std::io::StdoutLock<'_> {
fn write_colored(
&mut self,
fg: Option<anstyle::AnsiColor>,
bg: Option<anstyle::AnsiColor>,
data: &[u8],
) -> std::io::Result<usize> {
let initial = crate::windows::stdout_initial_colors();
crate::windows::write_colored(self, fg, bg, data, initial)
}
}

During one of the redesigns, I had explored keeping global state to "remember" where we left off between instances of WinconStream I think the big problem I ran into was that the stream would be unlocked and I couldn't guarantee what happened to the output between instances. This was assuming someone was explicitly acquiring and releasing the AutoStream for complete messages. The interaction of this with println, especially with a user intentionally bleeding output across calls, was overlooked in this case.

The easy workaround is to call stdout() and write to it, especially locking it. Writing to locked output is already a best practice because of how slow individual print calls can be from acquiring and releasing the lock (Rust-wide and not just our wrappers).

As for fixing this, we'll need to think on this more of what we want to optimize for and what is the right answer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-stream Area: anstream C-bug Category: Things not working as expected
Projects
None yet
Development

No branches or pull requests

2 participants