diff --git a/codex-rs/tui/src/style.rs b/codex-rs/tui/src/style.rs index ee1c7527e2..e88bc04f1c 100644 --- a/codex-rs/tui/src/style.rs +++ b/codex-rs/tui/src/style.rs @@ -1,6 +1,7 @@ use crate::color::blend; use crate::color::is_light; use crate::color::perceptual_distance; +use crate::terminal_palette::basic_palette; use crate::terminal_palette::terminal_palette; use ratatui::style::Color; use ratatui::style::Style; @@ -52,8 +53,15 @@ pub fn user_message_bg(terminal_bg: (u8, u8, u8)) -> Color { // perceptually closest to the blended target. Color::Indexed(i as u8) } else if color_level.has_basic { - // Finally, degrade to the basic 16 ANSI colors using a perceptual distance match. - closest_basic_color(target) + if let Some(palette) = basic_palette() { + // On Windows terminals the palette is configurable, so evaluate the actual + // runtime color table to keep the blended shading aligned with custom themes. + closest_runtime_basic_color(target, &palette) + } else { + // Finally, degrade to the well-known ANSI 16-color defaults using a perceptual + // distance match. + closest_basic_color(target) + } } else { // If the runtime reports no color support at all, keep the default background to // avoid rendering garbage escape sequences. @@ -61,6 +69,19 @@ pub fn user_message_bg(terminal_bg: (u8, u8, u8)) -> Color { } } +fn closest_runtime_basic_color(target: (u8, u8, u8), palette: &[(u8, u8, u8); 16]) -> Color { + palette + .iter() + .enumerate() + .min_by(|(_, a), (_, b)| { + perceptual_distance(**a, target) + .partial_cmp(&perceptual_distance(**b, target)) + .unwrap_or(std::cmp::Ordering::Equal) + }) + .and_then(|(idx, _)| BASIC_TERMINAL_COLORS.get(idx).map(|(color, _)| *color)) + .unwrap_or(Color::default()) +} + fn closest_basic_color(target: (u8, u8, u8)) -> Color { // Iterate through the baked-in ANSI colors and return whichever one is closest to the // desired RGB shade. This mirrors the logic used for the 256-color lookup but avoids diff --git a/codex-rs/tui/src/terminal_palette.rs b/codex-rs/tui/src/terminal_palette.rs index b320232e06..7550ee0f99 100644 --- a/codex-rs/tui/src/terminal_palette.rs +++ b/codex-rs/tui/src/terminal_palette.rs @@ -6,6 +6,15 @@ pub fn terminal_palette() -> Option<[(u8, u8, u8); 256]> { imp::terminal_palette() } +/// Returns the runtime palette for basic (0-15) color slots when the terminal reports one. +/// +/// Windows exposes a mutable color table via the console API so callers can render subtle +/// shading that matches the configured theme. Unix terminals rarely offer similar hooks, so +/// this returns `None` there and consumers should fall back to well-known ANSI defaults. +pub fn basic_palette() -> Option<[(u8, u8, u8); 16]> { + imp::basic_palette() +} + /// Forces the palette cache to re-run the default color probe for the current terminal. pub fn requery_default_colors() { imp::requery_default_colors(); @@ -91,6 +100,10 @@ mod imp { }) } + pub(super) fn basic_palette() -> Option<[(u8, u8, u8); 16]> { + None + } + pub(super) fn default_colors() -> Option { let cache = default_colors_cache(); let mut cache = cache.lock().ok()?; @@ -473,9 +486,13 @@ mod imp { mod imp { use super::DefaultColors; use std::mem::MaybeUninit; + use std::sync::Mutex; + use std::sync::OnceLock; use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE; use windows_sys::Win32::System::Console::CONSOLE_SCREEN_BUFFER_INFO; + use windows_sys::Win32::System::Console::CONSOLE_SCREEN_BUFFER_INFOEX; use windows_sys::Win32::System::Console::GetConsoleScreenBufferInfo; + use windows_sys::Win32::System::Console::GetConsoleScreenBufferInfoEx; use windows_sys::Win32::System::Console::GetStdHandle; use windows_sys::Win32::System::Console::STD_OUTPUT_HANDLE; @@ -499,43 +516,152 @@ mod imp { (255, 255, 255), ]; + /// Captures the console defaults along with the resolved 16-color palette so lookups can + /// serve both `default_colors()` and `basic_palette()` without re-querying Win32 APIs. + #[derive(Clone, Copy)] + struct ConsoleSnapshot { + defaults: DefaultColors, + palette: [(u8, u8, u8); 16], + } + + /// Lightweight memoization helper mirroring the Unix implementation so the Windows probe + /// only runs when necessary. + 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 console_snapshot_cache() -> &'static Mutex> { + static CACHE: OnceLock>> = OnceLock::new(); + CACHE.get_or_init(|| Mutex::new(Cache::default())) + } + pub(super) fn terminal_palette() -> Option<[(u8, u8, u8); 256]> { // Legacy Windows terminals only support the 16 basic colors so there is no // meaningful OSC palette to retrieve. None } + pub(super) fn basic_palette() -> Option<[(u8, u8, u8); 16]> { + let cache = console_snapshot_cache(); + let mut cache = cache.lock().ok()?; + cache + .get_or_init_with(query_console_snapshot) + .map(|snapshot| snapshot.palette) + } + /// Uses the Win32 console APIs to look up the active color attribute indices and map /// them into RGB triplets using the standard Windows palette table above. pub(super) fn default_colors() -> Option { + let cache = console_snapshot_cache(); + let mut cache = cache.lock().ok()?; + cache + .get_or_init_with(query_console_snapshot) + .map(|snapshot| snapshot.defaults) + } + + pub(super) fn requery_default_colors() { + if let Ok(mut cache) = console_snapshot_cache().lock() { + cache.refresh_with(query_console_snapshot); + } + } + + /// Queries the Win32 console for the color attribute indices and, when possible, the + /// active 16-color palette that modern Windows Terminal exposes for custom themes. + fn query_console_snapshot() -> Option { unsafe { let handle = GetStdHandle(STD_OUTPUT_HANDLE); - // Windows exposes null (0) and INVALID_HANDLE_VALUE (-1) as the two - // sentinel return values from GetStdHandle. HANDLE is a raw pointer - // so rely on is_null() for the zero check to keep pointer typing - // consistent across the windows-sys bindings. if handle.is_null() || handle == INVALID_HANDLE_VALUE { return None; } - let mut info = MaybeUninit::::uninit(); - if GetConsoleScreenBufferInfo(handle, info.as_mut_ptr()) == 0 { - return None; + + // Prefer the extended console info call so that we can honor custom palettes. + if let Some(snapshot) = query_with_ex(handle) { + return Some(snapshot); } - let info = info.assume_init(); - let attrs = info.wAttributes as usize; - let fg_idx = attrs & 0x0f; - let bg_idx = (attrs >> 4) & 0x0f; - Some(DefaultColors { + + // Fall back to the legacy structure when the extended query is unavailable. + query_with_basic_info(handle) + } + } + + unsafe fn query_with_ex(handle: isize) -> Option { + let mut info = MaybeUninit::::zeroed(); + let info_ptr = info.as_mut_ptr(); + (*info_ptr).cbSize = std::mem::size_of::() as u32; + if GetConsoleScreenBufferInfoEx(handle, info_ptr) == 0 { + return None; + } + let info = info.assume_init(); + let attrs = info.wAttributes as usize; + let fg_idx = attrs & 0x0f; + let bg_idx = (attrs >> 4) & 0x0f; + let mut palette = [(0u8, 0u8, 0u8); 16]; + for (slot, colorref) in palette.iter_mut().zip(info.ColorTable.iter().copied()) { + *slot = unpack_colorref(colorref); + } + Some(ConsoleSnapshot { + defaults: DefaultColors { + fg: palette.get(fg_idx).copied().unwrap_or((255, 255, 255)), + bg: palette.get(bg_idx).copied().unwrap_or((0, 0, 0)), + }, + palette, + }) + } + + unsafe fn query_with_basic_info(handle: isize) -> Option { + let mut info = MaybeUninit::::uninit(); + if GetConsoleScreenBufferInfo(handle, info.as_mut_ptr()) == 0 { + return None; + } + let info = info.assume_init(); + let attrs = info.wAttributes as usize; + let fg_idx = attrs & 0x0f; + let bg_idx = (attrs >> 4) & 0x0f; + Some(ConsoleSnapshot { + defaults: DefaultColors { fg: WINDOWS_COLORS .get(fg_idx) .copied() .unwrap_or((255, 255, 255)), bg: WINDOWS_COLORS.get(bg_idx).copied().unwrap_or((0, 0, 0)), - }) - } + }, + palette: WINDOWS_COLORS, + }) } - pub(super) fn requery_default_colors() {} + /// Unpacks a Windows COLORREF (0x00BBGGRR) triple into conventional RGB ordering. + fn unpack_colorref(colorref: u32) -> (u8, u8, u8) { + let r = (colorref & 0xff) as u8; + let g = ((colorref >> 8) & 0xff) as u8; + let b = ((colorref >> 16) & 0xff) as u8; + (r, g, b) + } } #[cfg(not(any(all(unix, not(test)), all(windows, not(test)))))] @@ -546,6 +672,10 @@ mod imp { None } + pub(super) fn basic_palette() -> Option<[(u8, u8, u8); 16]> { + None + } + pub(super) fn default_colors() -> Option { None }