support user_message bg color for modern Windows terminals

This commit is contained in:
iceweasel-oai
2025-11-03 16:28:11 -08:00
committed by David Wiesen
parent fdb9fa301e
commit e52f289bf0
3 changed files with 167 additions and 8 deletions

44
codex-rs/Cargo.lock generated
View File

@@ -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"

View File

@@ -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 }

View File

@@ -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 24bit 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 24bit color.
/// - If it fails (e.g., legacy consoles or nonTTY), 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<T> {
attempted: bool,
value: Option<T>,
}
impl<T> Default for Cache<T> {
fn default() -> Self {
Self {
attempted: false,
value: None,
}
}
}
impl<T: Copy> Cache<T> {
fn get_or_init_with(&mut self, mut init: impl FnMut() -> Option<T>) -> Option<T> {
if !self.attempted {
self.value = init();
self.attempted = true;
}
self.value
}
fn refresh_with(&mut self, mut init: impl FnMut() -> Option<T>) -> Option<T> {
self.value = init();
self.attempted = true;
self.value
}
}
fn default_colors_cache() -> &'static Mutex<Cache<DefaultColors>> {
static CACHE: OnceLock<Mutex<Cache<DefaultColors>>> = OnceLock::new();
CACHE.get_or_init(|| Mutex::new(Cache::default()))
}
pub(super) fn default_colors() -> Option<DefaultColors> {
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<Option<DefaultColors>> {
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<DefaultColors> { None }
pub(super) fn requery_default_colors() {}
}