Compare commits

...

11 Commits

Author SHA1 Message Date
David Wiesen
c81c133070 remove now unused dep 2025-11-07 10:07:09 -08:00
David Wiesen
fd31fbca87 use crossterm for ANSI support check 2025-11-07 09:51:43 -08:00
David Wiesen
e33c049c36 fix lint/fmt issues 2025-11-06 13:35:45 -08:00
iceweasel-oai
c5bd2f4cea cargo fmt 2025-11-06 13:04:18 -08:00
iceweasel-oai
aa9d1cac70 gate OnceLock import to Windows 2025-11-06 13:03:01 -08:00
iceweasel-oai
d6e722e11e revert crossterm call, since our version doesn't have the method 2025-11-06 12:56:00 -08:00
David Wiesen
d7eb134692 more review comment fixes. 2025-11-06 12:40:12 -08:00
iceweasel-oai
7469e4da79 use crossterm for ansi support detection. 2025-11-06 11:24:09 -08:00
iceweasel-oai
09b57f590d address review feedback 2025-11-06 11:24:09 -08:00
David Wiesen
eb5d0fd48b cargo fmt fixes 2025-11-06 11:24:09 -08:00
iceweasel-oai
e52f289bf0 support user_message bg color for modern Windows terminals 2025-11-06 11:24:09 -08:00
5 changed files with 186 additions and 62 deletions

43
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",
@@ -3748,14 +3749,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 +5751,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 +6136,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 +7684,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,9 @@ url = { workspace = true }
codex-windows-sandbox = { workspace = true }
[target.'cfg(windows)'.dependencies]
terminal-colorsaurus = "1"
[target.'cfg(unix)'.dependencies]
libc = { workspace = true }

View File

@@ -140,10 +140,7 @@ pub(crate) fn output_lines(
pub(crate) fn spinner(start_time: Option<Instant>) -> Span<'static> {
let elapsed = start_time.map(|st| st.elapsed()).unwrap_or_default();
if supports_color::on_cached(supports_color::Stream::Stdout)
.map(|level| level.has_16m)
.unwrap_or(false)
{
if crate::terminal_palette::stdout_supports_truecolor() {
shimmer_spans("")[0].clone()
} else {
let blink_on = (elapsed.as_millis() / 600).is_multiple_of(2);

View File

@@ -30,9 +30,7 @@ pub(crate) fn shimmer_spans(text: &str) -> Vec<Span<'static>> {
let pos_f =
(elapsed_since_start().as_secs_f32() % sweep_seconds) / sweep_seconds * (period as f32);
let pos = pos_f as usize;
let has_true_color = supports_color::on_cached(supports_color::Stream::Stdout)
.map(|level| level.has_16m)
.unwrap_or(false);
let has_true_color = crate::terminal_palette::stdout_supports_truecolor();
let band_half_width = 5.0;
let mut spans: Vec<Span<'static>> = Vec::with_capacity(chars.len());

View File

@@ -1,12 +1,18 @@
use crate::color::perceptual_distance;
use ratatui::style::Color;
#[cfg(not(test))]
use std::sync::LazyLock;
#[cfg(not(test))]
use std::sync::Mutex;
#[cfg(windows)]
use std::sync::OnceLock;
/// Returns the closest color to the target color that the terminal can display.
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_supports_truecolor() {
let (r, g, b) = target;
#[allow(clippy::disallowed_methods)]
Color::Rgb(r, g, b)
@@ -25,8 +31,37 @@ pub fn best_color(target: (u8, u8, u8)) -> Color {
}
}
/// Returns true if stdout supports truecolor (24-bit), using a single shared decision.
///
/// - On non-Windows, this is simply `supports_color`'s 16m capability.
/// - On Windows, we upgrade to truecolor on Windows Terminal if Virtual Terminal
/// Processing can be enabled successfully via crossterm. This probing happens once and is cached.
pub fn stdout_supports_truecolor() -> bool {
#[cfg(windows)]
{
static TRUECOLOR: OnceLock<bool> = OnceLock::new();
*TRUECOLOR.get_or_init(|| {
let has = supports_color::on_cached(supports_color::Stream::Stdout)
.map(|level| level.has_16m)
.unwrap_or(false);
if has {
return true;
}
// Upgrade to truecolor on Windows Terminal when VT processing is available.
// We only attempt to enable VT once per process to avoid per-frame overhead.
std::env::var_os("WT_SESSION").is_some() && crossterm::ansi_support::supports_ansi()
})
}
#[cfg(not(windows))]
{
supports_color::on_cached(supports_color::Stream::Stdout)
.map(|level| level.has_16m)
.unwrap_or(false)
}
}
pub fn requery_default_colors() {
imp::requery_default_colors();
imp::refresh_cached_default_colors();
}
#[derive(Clone, Copy)]
@@ -36,7 +71,7 @@ pub struct DefaultColors {
}
pub fn default_colors() -> Option<DefaultColors> {
imp::default_colors()
imp::cached_default_colors()
}
pub fn default_fg() -> Option<(u8, u8, u8)> {
@@ -47,67 +82,87 @@ pub fn default_bg() -> Option<(u8, u8, u8)> {
default_colors().map(|c| c.bg)
}
// -------------------------------------------------------------------------------------------------
// Shared cache utility for default terminal colors
// -------------------------------------------------------------------------------------------------
/// Small helper cache that remembers whether initialization was attempted and stores a value.
///
/// Used to memoize the terminal's default foreground/background colors. We expose both
/// `get_or_init_with` for the first successful query and `refresh_with` for a manual re-query.
/// If a first query fails, we remember the failure to avoid repeated failed probes, unless
/// a subsequent explicit refresh is performed.
#[cfg(not(test))]
struct Cache<T> {
attempted: bool,
value: Option<T>,
}
#[cfg(not(test))]
impl<T> Default for Cache<T> {
fn default() -> Self {
Self {
attempted: false,
value: None,
}
}
}
#[cfg(not(test))]
impl<T: Copy> Cache<T> {
/// Returns the cached value if available; otherwise runs `init` once and caches its result.
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
}
/// Re-runs `init` and replaces the cached value. Marks the cache as attempted.
fn refresh_with(&mut self, mut init: impl FnMut() -> Option<T>) -> Option<T> {
self.value = init();
self.attempted = true;
self.value
}
}
#[cfg(not(test))]
fn default_terminal_colors_cache() -> &'static Mutex<Cache<DefaultColors>> {
// LazyLock creates the single cache instance; the Mutex guards concurrent refreshes/reads.
static CACHE: LazyLock<Mutex<Cache<DefaultColors>>> =
LazyLock::new(|| Mutex::new(Cache::default()));
&CACHE
}
#[cfg(all(unix, not(test)))]
mod imp {
use super::DefaultColors;
use super::default_terminal_colors_cache;
use crossterm::style::Color as CrosstermColor;
use crossterm::style::query_background_color;
use crossterm::style::query_foreground_color;
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> {
let cache = default_colors_cache();
// Returns cached terminal defaults, probing once on first use.
pub(super) fn cached_default_colors() -> Option<DefaultColors> {
let cache = default_terminal_colors_cache();
let mut cache = cache.lock().ok()?;
cache.get_or_init_with(|| query_default_colors().unwrap_or_default())
cache.get_or_init_with(|| probe_default_terminal_colors().unwrap_or_default())
}
pub(super) fn requery_default_colors() {
if let Ok(mut cache) = default_colors_cache().lock() {
// Refreshes cached defaults unless we already know probing fails.
pub(super) fn refresh_cached_default_colors() {
if let Ok(mut cache) = default_terminal_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());
cache.refresh_with(|| probe_default_terminal_colors().unwrap_or_default());
}
}
fn query_default_colors() -> std::io::Result<Option<DefaultColors>> {
// Probes the terminal for default colors; returns None when unsupported/unavailable.
fn probe_default_terminal_colors() -> std::io::Result<Option<DefaultColors>> {
let fg = query_foreground_color()?.and_then(color_to_tuple);
let bg = query_background_color()?.and_then(color_to_tuple);
Ok(fg.zip(bg).map(|(fg, bg)| DefaultColors { fg, bg }))
@@ -121,15 +176,53 @@ mod imp {
}
}
#[cfg(not(all(unix, not(test))))]
#[cfg(all(windows, not(test)))]
mod imp {
use super::DefaultColors;
use super::default_terminal_colors_cache;
pub(super) fn default_colors() -> Option<DefaultColors> {
// Returns cached terminal defaults, probing once on first use.
pub(super) fn cached_default_colors() -> Option<DefaultColors> {
let cache = default_terminal_colors_cache();
let mut cache = cache.lock().ok()?;
cache.get_or_init_with(|| probe_default_terminal_colors().unwrap_or_default())
}
// Refreshes cached defaults unless we already know probing fails.
pub(super) fn refresh_cached_default_colors() {
if let Ok(mut cache) = default_terminal_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(|| probe_default_terminal_colors().unwrap_or_default());
}
}
// Probes the terminal for default colors; returns None when unsupported/unavailable.
fn probe_default_terminal_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 cached_default_colors() -> Option<DefaultColors> {
None
}
pub(super) fn requery_default_colors() {}
pub(super) fn refresh_cached_default_colors() {}
}
/// The subset of Xterm colors that are usually consistent across terminals.