mirror of
https://github.com/openai/codex.git
synced 2026-11-02 19:43:24 +00:00
initial implementation of user message background color for Windows.
This commit is contained in:
Generated
+1
@@ -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 }
|
||||
|
||||
@@ -14,7 +14,15 @@ pub fn user_message_style(terminal_bg: Option<(u8, u8, u8)>) -> Style {
|
||||
}
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
/// Derives a background color for the user input field that contrasts with the terminal.
|
||||
///
|
||||
/// The goal is to blend the user's default terminal background with either black or
|
||||
/// white (depending on whether the background is light/dark) so the composer area feels
|
||||
/// consistent with the host theme. The function progressively falls back to palettes of
|
||||
/// decreasing fidelity depending on what the runtime reports about color support.
|
||||
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 {
|
||||
@@ -25,8 +33,11 @@ pub fn user_message_bg(terminal_bg: (u8, u8, u8)) -> Color {
|
||||
return Color::default();
|
||||
};
|
||||
|
||||
// Blend 10% toward the contrasting top color to create a subtle shading effect that
|
||||
// keeps the composer distinct without overwhelming the terminal theme.
|
||||
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 +48,51 @@ 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 {
|
||||
// Finally, degrade to the basic 16 ANSI colors 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.
|
||||
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
|
||||
// allocating an array for each call.
|
||||
BASIC_TERMINAL_COLORS
|
||||
.iter()
|
||||
.min_by(|(_, a), (_, b)| {
|
||||
perceptual_distance(*a, target)
|
||||
.partial_cmp(&perceptual_distance(*b, target))
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
})
|
||||
.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)),
|
||||
];
|
||||
|
||||
@@ -1,27 +1,37 @@
|
||||
/// Returns the cached 256-color palette reported by the active terminal, when available.
|
||||
///
|
||||
/// Unix terminals often respond to OSC 4 palette dumps while Windows does not, so callers
|
||||
/// should always be prepared for this to return `None`.
|
||||
pub fn terminal_palette() -> Option<[(u8, u8, u8); 256]> {
|
||||
imp::terminal_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();
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
/// Snapshot of the terminal's default foreground and background RGB values.
|
||||
pub struct DefaultColors {
|
||||
#[allow(dead_code)]
|
||||
fg: (u8, u8, u8),
|
||||
bg: (u8, u8, u8),
|
||||
}
|
||||
|
||||
/// Returns the default colors (if known), priming the cache by probing the terminal when
|
||||
/// necessary.
|
||||
pub fn default_colors() -> Option<DefaultColors> {
|
||||
imp::default_colors()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Convenience accessor for only the cached default foreground.
|
||||
pub fn default_fg() -> Option<(u8, u8, u8)> {
|
||||
default_colors().map(|c| c.fg)
|
||||
}
|
||||
|
||||
/// Convenience accessor for only the cached default background.
|
||||
pub fn default_bg() -> Option<(u8, u8, u8)> {
|
||||
default_colors().map(|c| c.bg)
|
||||
}
|
||||
@@ -34,6 +44,8 @@ mod imp {
|
||||
use std::sync::Mutex;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// Lightweight memoization helper storing the last probe result and whether a probe
|
||||
/// has already been attempted.
|
||||
struct Cache<T> {
|
||||
attempted: bool,
|
||||
value: Option<T>,
|
||||
@@ -66,6 +78,8 @@ mod imp {
|
||||
|
||||
fn default_colors_cache() -> &'static Mutex<Cache<DefaultColors>> {
|
||||
static CACHE: OnceLock<Mutex<Cache<DefaultColors>>> = OnceLock::new();
|
||||
// Lazily construct a mutex-protected cache so multiple threads can reuse the
|
||||
// expensive OSC probe without data races.
|
||||
CACHE.get_or_init(|| Mutex::new(Cache::default()))
|
||||
}
|
||||
|
||||
@@ -85,6 +99,8 @@ mod imp {
|
||||
|
||||
pub(super) fn requery_default_colors() {
|
||||
if let Ok(mut cache) = default_colors_cache().lock() {
|
||||
// Force the expensive OSC query to run again so palette adjustments in the
|
||||
// user's terminal are picked up immediately.
|
||||
cache.refresh_with(|| query_default_colors().unwrap_or_default());
|
||||
}
|
||||
}
|
||||
@@ -109,6 +125,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")?;
|
||||
}
|
||||
@@ -147,6 +164,8 @@ mod imp {
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to parse any trailing bytes that are already buffered, then pull in any
|
||||
// remaining responses until the terminal has been idle briefly.
|
||||
remaining = remaining.saturating_sub(apply_palette_responses(&mut buffer, &mut palette));
|
||||
remaining = remaining.saturating_sub(drain_remaining(&mut tty, &mut buffer, &mut palette));
|
||||
|
||||
@@ -181,6 +200,8 @@ mod imp {
|
||||
if !stdout_handle.is_terminal() {
|
||||
return Ok(None);
|
||||
}
|
||||
// Ask for both the default foreground (OSC 10) and background (OSC 11) in a single
|
||||
// write so that we minimize flush latency.
|
||||
stdout_handle.write_all(b"\x1b]10;?\x07\x1b]11;?\x07")?;
|
||||
stdout_handle.flush()?;
|
||||
|
||||
@@ -209,6 +230,8 @@ mod imp {
|
||||
Ok(n) => {
|
||||
buffer.extend_from_slice(&chunk[..n]);
|
||||
if fg.is_none() {
|
||||
// OSC replies can be interleaved; keep checking the buffered text
|
||||
// until we observe the requested color.
|
||||
fg = parse_osc_color(&buffer, 10);
|
||||
}
|
||||
if bg.is_none() {
|
||||
@@ -247,6 +270,8 @@ mod imp {
|
||||
use std::time::Instant;
|
||||
|
||||
let mut chunk = [0u8; 512];
|
||||
// Continue draining until we observe no new data for a short period; this guards
|
||||
// against leaving responses unread when terminals stream slowly.
|
||||
let mut idle_deadline = Instant::now() + Duration::from_millis(50);
|
||||
let mut newly_filled = 0usize;
|
||||
|
||||
@@ -262,6 +287,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,
|
||||
@@ -309,6 +336,8 @@ mod imp {
|
||||
) -> usize {
|
||||
let mut newly_filled = 0;
|
||||
|
||||
// Search for OSC introducers (ESC ]) inside the rolling buffer and peel off
|
||||
// responses as soon as we have a full payload.
|
||||
while let Some(start) = buffer.windows(2).position(|window| window == [0x1b, b']']) {
|
||||
if start > 0 {
|
||||
buffer.drain(..start);
|
||||
@@ -320,10 +349,12 @@ mod imp {
|
||||
while index < buffer.len() {
|
||||
match buffer[index] {
|
||||
0x07 => {
|
||||
// BEL terminated response.
|
||||
terminator_len = Some(1);
|
||||
break;
|
||||
}
|
||||
0x1b if index + 1 < buffer.len() && buffer[index + 1] == b'\\' => {
|
||||
// ESC \ (String Terminator) variant.
|
||||
terminator_len = Some(2);
|
||||
break;
|
||||
}
|
||||
@@ -345,6 +376,8 @@ mod imp {
|
||||
if let Some((slot, color)) = parsed
|
||||
&& palette[slot].is_none()
|
||||
{
|
||||
// Record the color only if the slot hasn't been filled yet to avoid
|
||||
// clobbering data when terminals echo duplicate responses.
|
||||
palette[slot] = Some(color);
|
||||
newly_filled += 1;
|
||||
}
|
||||
@@ -367,6 +400,8 @@ mod imp {
|
||||
if model != "rgb" && model != "rgba" {
|
||||
return None;
|
||||
}
|
||||
// The OSC payload encodes each color component as hexadecimal strings separated by
|
||||
// '/', so decode them in order.
|
||||
let mut components = values.split('/');
|
||||
let r = parse_component(components.next()?)?;
|
||||
let g = parse_component(components.next()?)?;
|
||||
@@ -374,6 +409,7 @@ mod imp {
|
||||
Some((index, (r, g, b)))
|
||||
}
|
||||
|
||||
/// Normalizes a hexadecimal OSC color component (which can be 4-64 bits) to 0-255.
|
||||
fn parse_component(component: &str) -> Option<u8> {
|
||||
let trimmed = component.trim();
|
||||
if trimmed.is_empty() {
|
||||
@@ -399,6 +435,8 @@ mod imp {
|
||||
11 => "\u{1b}]11;",
|
||||
_ => return None,
|
||||
};
|
||||
// Locate the most recent reply for the requested OSC code in case multiple
|
||||
// sequences are buffered together.
|
||||
let start = text.rfind(prefix)?;
|
||||
let after_prefix = &text[start + prefix.len()..];
|
||||
let end_bel = after_prefix.find('\u{7}');
|
||||
@@ -425,11 +463,78 @@ mod imp {
|
||||
let r = parse_component(parts.next()?)?;
|
||||
let g = parse_component(parts.next()?)?;
|
||||
let b = parse_component(parts.next()?)?;
|
||||
// Some terminals append an alpha component for rgba; ignore it if present since the
|
||||
// OSC format represents the alpha channel as the next slash-separated part.
|
||||
Some((r, g, b))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(all(unix, not(test))))]
|
||||
#[cfg(all(windows, not(test)))]
|
||||
mod imp {
|
||||
use super::DefaultColors;
|
||||
use std::mem::MaybeUninit;
|
||||
use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
|
||||
use windows_sys::Win32::System::Console::CONSOLE_SCREEN_BUFFER_INFO;
|
||||
use windows_sys::Win32::System::Console::GetConsoleScreenBufferInfo;
|
||||
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),
|
||||
];
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
unsafe {
|
||||
let handle = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
if handle == 0 || handle == INVALID_HANDLE_VALUE {
|
||||
return None;
|
||||
}
|
||||
let mut info = MaybeUninit::<CONSOLE_SCREEN_BUFFER_INFO>::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(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)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn requery_default_colors() {}
|
||||
}
|
||||
|
||||
#[cfg(not(any(all(unix, not(test)), all(windows, not(test)))))]
|
||||
mod imp {
|
||||
use super::DefaultColors;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user