From e52f289bf08c0d350407404dfecff552efb6c7a5 Mon Sep 17 00:00:00 2001 From: iceweasel-oai Date: Mon, 3 Nov 2025 16:28:11 -0800 Subject: [PATCH] support user_message bg color for modern Windows terminals --- codex-rs/Cargo.lock | 44 ++++++++-- codex-rs/tui/Cargo.toml | 7 ++ codex-rs/tui/src/terminal_palette.rs | 124 ++++++++++++++++++++++++++- 3 files changed, 167 insertions(+), 8 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index e8ce0bfe6a..f74d9ee9ff 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1480,6 +1480,7 @@ dependencies = [ "strum_macros 0.27.2", "supports-color", "tempfile", + "terminal-colorsaurus", "textwrap 0.16.2", "tokio", "tokio-stream", @@ -1493,6 +1494,7 @@ dependencies = [ "unicode-width 0.2.1", "url", "vt100", + "windows-sys 0.52.0", ] [[package]] @@ -3748,14 +3750,14 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", "log", "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "windows-sys 0.61.1", ] [[package]] @@ -5750,9 +5752,9 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", "mio", @@ -6135,6 +6137,32 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal-colorsaurus" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8909f33134da34b43f69145e748790de650a6abd84faf1f82e773444dd293ec8" +dependencies = [ + "cfg-if", + "libc", + "memchr", + "mio", + "terminal-trx", + "windows-sys 0.61.1", + "xterm-color", +] + +[[package]] +name = "terminal-trx" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "662a3cd5ca570df622e848ef18b50c151e65c9835257465417242243b0bce783" +dependencies = [ + "cfg-if", + "libc", + "windows-sys 0.61.1", +] + [[package]] name = "terminal_size" version = "0.4.2" @@ -7657,6 +7685,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "xterm-color" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4de5f056fb9dc8b7908754867544e26145767187aaac5a98495e88ad7cb8a80f" + [[package]] name = "yansi" version = "1.0.1" diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index ac00e9054b..99baf35f31 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -89,6 +89,13 @@ url = { workspace = true } codex-windows-sandbox = { workspace = true } +[target.'cfg(windows)'.dependencies] +windows-sys = { version = "0.52", features = [ + "Win32_Foundation", + "Win32_System_Console", +] } +terminal-colorsaurus = "1" + [target.'cfg(unix)'.dependencies] libc = { workspace = true } diff --git a/codex-rs/tui/src/terminal_palette.rs b/codex-rs/tui/src/terminal_palette.rs index 5c6f32cd9e..b156e1dafb 100644 --- a/codex-rs/tui/src/terminal_palette.rs +++ b/codex-rs/tui/src/terminal_palette.rs @@ -6,7 +6,7 @@ pub fn best_color(target: (u8, u8, u8)) -> Color { let Some(color_level) = supports_color::on_cached(supports_color::Stream::Stdout) else { return Color::default(); }; - if color_level.has_16m { + if stdout_has_truecolor(&color_level) { let (r, g, b) = target; #[allow(clippy::disallowed_methods)] Color::Rgb(r, g, b) @@ -25,6 +25,58 @@ pub fn best_color(target: (u8, u8, u8)) -> Color { } } + +#[cfg(not(windows))] +fn stdout_has_truecolor(level: &supports_color::ColorLevel) -> bool { + level.has_16m +} + +#[cfg(windows)] +fn stdout_has_truecolor(level: &supports_color::ColorLevel) -> bool { + if level.has_16m { + return true; + } + // Upgrade to truecolor on Windows Terminal when VT processing is available. + std::env::var_os("WT_SESSION").is_some() && enable_vt_stdout().is_ok() +} + +/// Enables Virtual Terminal Processing (ANSI escape sequence handling) on Windows for stdout. +/// +/// This ensures that ANSI SGR sequences (including 24‑bit truecolor) are interpreted by the +/// Windows console pipeline. We use this as a lightweight capability gate before upgrading to +/// truecolor on Windows Terminal: +/// +/// - If enabling VT processing succeeds, we can safely emit 24‑bit color. +/// - If it fails (e.g., legacy consoles or non‑TTY), we do not upgrade and fall back to the +/// base `supports_color` decision to avoid printing raw escape sequences. +/// +/// Calling this repeatedly is idempotent (it simply sets the corresponding console mode flag). +#[cfg(windows)] +fn enable_vt_stdout() -> std::io::Result<()> { + use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE; + use windows_sys::Win32::System::Console::GetConsoleMode; + use windows_sys::Win32::System::Console::GetStdHandle; + use windows_sys::Win32::System::Console::SetConsoleMode; + use windows_sys::Win32::System::Console::ENABLE_VIRTUAL_TERMINAL_PROCESSING; + use windows_sys::Win32::System::Console::STD_OUTPUT_HANDLE; + + unsafe { + let handle = GetStdHandle(STD_OUTPUT_HANDLE); + if handle == INVALID_HANDLE_VALUE || handle == 0 { + return Err(std::io::Error::last_os_error()); + } + let mut mode: u32 = 0; + if GetConsoleMode(handle, &mut mode) == 0 { + return Err(std::io::Error::last_os_error()); + } + // Idempotent if already enabled. + if SetConsoleMode(handle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0 { + return Err(std::io::Error::last_os_error()); + } + } + Ok(()) +} + pub fn requery_default_colors() { imp::requery_default_colors(); } @@ -121,14 +173,80 @@ mod imp { } } -#[cfg(not(all(unix, not(test))))] +#[cfg(all(windows, not(test)))] mod imp { use super::DefaultColors; + use std::sync::Mutex; + use std::sync::OnceLock; + + struct Cache { + attempted: bool, + value: Option, + } + + impl Default for Cache { + fn default() -> Self { + Self { + attempted: false, + value: None, + } + } + } + + impl Cache { + fn get_or_init_with(&mut self, mut init: impl FnMut() -> Option) -> Option { + if !self.attempted { + self.value = init(); + self.attempted = true; + } + self.value + } + + fn refresh_with(&mut self, mut init: impl FnMut() -> Option) -> Option { + self.value = init(); + self.attempted = true; + self.value + } + } + + fn default_colors_cache() -> &'static Mutex> { + static CACHE: OnceLock>> = OnceLock::new(); + CACHE.get_or_init(|| Mutex::new(Cache::default())) + } pub(super) fn default_colors() -> Option { - None + let cache = default_colors_cache(); + let mut cache = cache.lock().ok()?; + cache.get_or_init_with(|| query_default_colors().unwrap_or_default()) } + pub(super) fn requery_default_colors() { + if let Ok(mut cache) = default_colors_cache().lock() { + // Don't try to refresh if the cache is already attempted and failed. + if cache.attempted && cache.value.is_none() { + return; + } + cache.refresh_with(|| query_default_colors().unwrap_or_default()); + } + } + + fn query_default_colors() -> std::io::Result> { + match terminal_colorsaurus::color_palette(terminal_colorsaurus::QueryOptions::default()) { + Ok(p) => { + let (fr, fg, fb) = p.foreground.scale_to_8bit(); + let (br, bg, bb) = p.background.scale_to_8bit(); + Ok(Some(DefaultColors { fg: (fr, fg, fb), bg: (br, bg, bb) })) + } + Err(_) => Ok(None), + } + } +} + +#[cfg(not(any(all(unix, not(test)), all(windows, not(test)))))] +mod imp { + use super::DefaultColors; + pub(super) fn default_colors() -> Option { None } + pub(super) fn requery_default_colors() {} }