mirror of
https://github.com/openai/codex.git
synced 2026-02-01 22:47:52 +00:00
Compare commits
10 Commits
dev/zhao/r
...
dev/icewea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d3ca6475d | ||
|
|
5d1e66f7b5 | ||
|
|
c8ecce9e54 | ||
|
|
494b97c87f | ||
|
|
b8e1580002 | ||
|
|
bff44cd091 | ||
|
|
d41ab7290e | ||
|
|
b28888f16a | ||
|
|
8029ad9919 | ||
|
|
5b6656101e |
1
codex-rs/Cargo.lock
generated
1
codex-rs/Cargo.lock
generated
@@ -1428,6 +1428,7 @@ dependencies = [
|
||||
"unicode-width 0.2.1",
|
||||
"url",
|
||||
"vt100",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -188,6 +188,7 @@ walkdir = "2.5.0"
|
||||
webbrowser = "1.0"
|
||||
which = "6"
|
||||
wildmatch = "2.5.0"
|
||||
windows-sys = "0.59.0"
|
||||
wiremock = "0.6"
|
||||
zeroize = "1.8.1"
|
||||
|
||||
|
||||
@@ -92,6 +92,12 @@ libc = { workspace = true }
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||
arboard = { workspace = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { workspace = true, features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_System_Console",
|
||||
] }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = { workspace = true }
|
||||
|
||||
@@ -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;
|
||||
@@ -15,18 +16,29 @@ pub fn user_message_style(terminal_bg: Option<(u8, u8, u8)>) -> Style {
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
pub fn user_message_bg(terminal_bg: (u8, u8, u8)) -> Color {
|
||||
// Determine a reference "top" color to blend toward. For dark backgrounds we lighten
|
||||
// the mix with white, and for light backgrounds we darken it with black.
|
||||
let top = if is_light(terminal_bg) {
|
||||
(0, 0, 0)
|
||||
} else {
|
||||
(255, 255, 255)
|
||||
};
|
||||
let bottom = terminal_bg;
|
||||
let Some(color_level) = supports_color::on_cached(supports_color::Stream::Stdout) else {
|
||||
let Some(mut color_level) = supports_color::on_cached(supports_color::Stream::Stdout) else {
|
||||
return Color::default();
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
// Windows Terminal has been the default shell application for Windows since October 2022
|
||||
// and has supported truecolor even longer. However it usually does not set COLORTERM to indicate that.
|
||||
// so this is a pretty safe heuristic.
|
||||
if std::env::var_os("WT_SESSION").is_some() {
|
||||
color_level.has_16m = true;
|
||||
}
|
||||
|
||||
let target = blend(top, bottom, 0.1);
|
||||
if color_level.has_16m {
|
||||
// In truecolor terminals we can use the exact RGB value.
|
||||
let (r, g, b) = target;
|
||||
Color::Rgb(r, g, b)
|
||||
} else if color_level.has_256
|
||||
@@ -37,8 +49,94 @@ pub fn user_message_bg(terminal_bg: (u8, u8, u8)) -> Color {
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
})
|
||||
{
|
||||
// If we have a captured 256-color palette, pick whichever indexed color is
|
||||
// perceptually closest to the blended target.
|
||||
Color::Indexed(i as u8)
|
||||
} else if color_level.has_basic {
|
||||
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, terminal_bg, &palette)
|
||||
} else {
|
||||
// Finally, degrade to the well-known ANSI 16-color defaults using a perceptual
|
||||
// distance match.
|
||||
closest_basic_color(target, terminal_bg)
|
||||
}
|
||||
} else {
|
||||
Color::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn closest_runtime_basic_color(
|
||||
target: (u8, u8, u8),
|
||||
terminal_bg: (u8, u8, u8),
|
||||
palette: &[(u8, u8, u8); 16],
|
||||
) -> Color {
|
||||
select_basic_palette_color(
|
||||
target,
|
||||
terminal_bg,
|
||||
palette.iter().enumerate().filter_map(|(idx, rgb)| {
|
||||
BASIC_TERMINAL_COLORS
|
||||
.get(idx)
|
||||
.map(|(color, _)| (*rgb, *color))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn closest_basic_color(target: (u8, u8, u8), terminal_bg: (u8, u8, u8)) -> Color {
|
||||
// Iterate through the baked-in ANSI colors and return whichever one is closest to the
|
||||
// desired RGB shade while maintaining contrast with the background.
|
||||
select_basic_palette_color(
|
||||
target,
|
||||
terminal_bg,
|
||||
BASIC_TERMINAL_COLORS
|
||||
.iter()
|
||||
.map(|(color, rgb)| (*rgb, *color)),
|
||||
)
|
||||
}
|
||||
|
||||
const MIN_PERCEPTUAL_DISTANCE: f32 = 6.0;
|
||||
|
||||
fn select_basic_palette_color(
|
||||
target: (u8, u8, u8),
|
||||
terminal_bg: (u8, u8, u8),
|
||||
entries: impl Iterator<Item = ((u8, u8, u8), Color)>,
|
||||
) -> Color {
|
||||
let mut best = None;
|
||||
let mut fallback = None;
|
||||
for (rgb, color) in entries {
|
||||
let dist = perceptual_distance(rgb, target);
|
||||
if fallback.is_none_or(|(_, best_dist)| dist < best_dist) {
|
||||
fallback = Some((color, dist));
|
||||
}
|
||||
if perceptual_distance(rgb, terminal_bg) > MIN_PERCEPTUAL_DISTANCE
|
||||
&& best.is_none_or(|(_, best_dist)| dist < best_dist)
|
||||
{
|
||||
best = Some((color, dist));
|
||||
}
|
||||
}
|
||||
best.or(fallback)
|
||||
.map(|(color, _)| color)
|
||||
.unwrap_or(Color::default())
|
||||
}
|
||||
|
||||
// Mapping of ANSI color indices to approximate RGB tuples. These values mirror Windows'
|
||||
// default console palette so the fallback path stays visually consistent across platforms.
|
||||
const BASIC_TERMINAL_COLORS: [(Color, (u8, u8, u8)); 16] = [
|
||||
(Color::Black, (0, 0, 0)),
|
||||
(Color::Blue, (0, 0, 128)),
|
||||
(Color::Green, (0, 128, 0)),
|
||||
(Color::Cyan, (0, 128, 128)),
|
||||
(Color::Red, (128, 0, 0)),
|
||||
(Color::Magenta, (128, 0, 128)),
|
||||
(Color::Yellow, (128, 128, 0)),
|
||||
(Color::Gray, (192, 192, 192)),
|
||||
(Color::DarkGray, (128, 128, 128)),
|
||||
(Color::LightBlue, (0, 0, 255)),
|
||||
(Color::LightGreen, (0, 255, 0)),
|
||||
(Color::LightCyan, (0, 255, 255)),
|
||||
(Color::LightRed, (255, 0, 0)),
|
||||
(Color::LightMagenta, (255, 0, 255)),
|
||||
(Color::LightYellow, (255, 255, 0)),
|
||||
(Color::White, (255, 255, 255)),
|
||||
];
|
||||
|
||||
@@ -2,11 +2,21 @@ 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()
|
||||
}
|
||||
|
||||
pub fn requery_default_colors() {
|
||||
imp::requery_default_colors();
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
/// Snapshot of the terminal's default foreground and background RGB values.
|
||||
pub struct DefaultColors {
|
||||
#[allow(dead_code)]
|
||||
fg: (u8, u8, u8),
|
||||
@@ -77,6 +87,10 @@ mod imp {
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn basic_palette() -> Option<[(u8, u8, u8); 16]> {
|
||||
None
|
||||
}
|
||||
|
||||
pub(super) fn default_colors() -> Option<DefaultColors> {
|
||||
let cache = default_colors_cache();
|
||||
let mut cache = cache.lock().ok()?;
|
||||
@@ -109,6 +123,7 @@ mod imp {
|
||||
Err(_) => return Ok(None),
|
||||
};
|
||||
|
||||
// Request the RGB value for each extended color slot (OSC 4 ; index ; ? BEL).
|
||||
for index in 0..256 {
|
||||
write!(tty, "\x1b]4;{index};?\x07")?;
|
||||
}
|
||||
@@ -262,6 +277,8 @@ mod imp {
|
||||
if Instant::now() >= idle_deadline {
|
||||
break;
|
||||
}
|
||||
// Sleep briefly to avoid spinning while still allowing slow terminals
|
||||
// to provide more data before the idle timeout expires.
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
Err(err) if err.kind() == ErrorKind::Interrupted => continue,
|
||||
@@ -429,7 +446,190 @@ mod imp {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(all(unix, not(test))))]
|
||||
#[cfg(all(windows, not(test)))]
|
||||
mod imp {
|
||||
use super::DefaultColors;
|
||||
use std::mem::MaybeUninit;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::OnceLock;
|
||||
use windows_sys::Win32::Foundation::HANDLE;
|
||||
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;
|
||||
|
||||
/// Matches the historical Windows console palette where indices 0-15 are fixed.
|
||||
const WINDOWS_COLORS: [(u8, u8, u8); 16] = [
|
||||
(0, 0, 0),
|
||||
(0, 0, 128),
|
||||
(0, 128, 0),
|
||||
(0, 128, 128),
|
||||
(128, 0, 0),
|
||||
(128, 0, 128),
|
||||
(128, 128, 0),
|
||||
(192, 192, 192),
|
||||
(128, 128, 128),
|
||||
(0, 0, 255),
|
||||
(0, 255, 0),
|
||||
(0, 255, 255),
|
||||
(255, 0, 0),
|
||||
(255, 0, 255),
|
||||
(255, 255, 0),
|
||||
(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],
|
||||
}
|
||||
|
||||
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 console_snapshot_cache() -> &'static Mutex<Cache<ConsoleSnapshot>> {
|
||||
static CACHE: OnceLock<Mutex<Cache<ConsoleSnapshot>>> = 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<DefaultColors> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
fn query_console_snapshot() -> Option<ConsoleSnapshot> {
|
||||
unsafe {
|
||||
let handle = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
if handle.is_null() || std::ptr::eq(handle, INVALID_HANDLE_VALUE) {
|
||||
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);
|
||||
}
|
||||
|
||||
// Fall back to the legacy structure when the extended query is unavailable.
|
||||
query_with_basic_info(handle)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn query_with_ex(handle: HANDLE) -> Option<ConsoleSnapshot> {
|
||||
let mut info = MaybeUninit::<CONSOLE_SCREEN_BUFFER_INFOEX>::zeroed();
|
||||
let info_ptr = info.as_mut_ptr();
|
||||
unsafe {
|
||||
(*info_ptr).cbSize = std::mem::size_of::<CONSOLE_SCREEN_BUFFER_INFOEX>() as u32;
|
||||
if GetConsoleScreenBufferInfoEx(handle, info_ptr) == 0 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let info = unsafe { 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: HANDLE) -> Option<ConsoleSnapshot> {
|
||||
let mut info = MaybeUninit::<CONSOLE_SCREEN_BUFFER_INFO>::uninit();
|
||||
unsafe {
|
||||
if GetConsoleScreenBufferInfo(handle, info.as_mut_ptr()) == 0 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let info = unsafe { 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,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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)))))]
|
||||
mod imp {
|
||||
use super::DefaultColors;
|
||||
|
||||
@@ -437,6 +637,10 @@ mod imp {
|
||||
None
|
||||
}
|
||||
|
||||
pub(super) fn basic_palette() -> Option<[(u8, u8, u8); 16]> {
|
||||
None
|
||||
}
|
||||
|
||||
pub(super) fn default_colors() -> Option<DefaultColors> {
|
||||
None
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user