Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions codex-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ uuid = "1"
vt100 = "0.16.2"
walkdir = "2.5.0"
webbrowser = "1.0"
windows-sys = "0.59.0"
which = "6"
wildmatch = "2.5.0"
wiremock = "0.6"
Expand Down
8 changes: 8 additions & 0 deletions codex-rs/tui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ url = { workspace = true }
[target.'cfg(unix)'.dependencies]
libc = { workspace = true }

[target.'cfg(windows)'.dependencies]
windows-sys = { workspace = true, features = [
"Win32_Foundation",
"Win32_Storage_FileSystem",
"Win32_System_Console",
"Win32_System_Threading",
] }

# Clipboard support via `arboard` is not available on Android/Termux.
# Only include it for non-Android targets so the crate builds on Android.
[target.'cfg(not(target_os = "android"))'.dependencies]
Expand Down
169 changes: 13 additions & 156 deletions codex-rs/tui/src/terminal_palette.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#[cfg(any(all(unix, not(test)), all(windows, not(test))))]
#[path = "terminal_palette/common.rs"]
mod terminal_palette_common;

pub fn terminal_palette() -> Option<[(u8, u8, u8); 256]> {
imp::terminal_palette()
}
Expand All @@ -22,48 +26,22 @@ pub fn default_fg() -> Option<(u8, u8, u8)> {
default_colors().map(|c| c.fg)
}

#[allow(dead_code)]
pub fn default_bg() -> Option<(u8, u8, u8)> {
default_colors().map(|c| c.bg)
}

#[cfg(all(unix, not(test)))]
mod imp {
use super::DefaultColors;
use super::terminal_palette_common::Cache;
use super::terminal_palette_common::apply_palette_responses;
use super::terminal_palette_common::parse_osc_color;
use std::mem::MaybeUninit;
use std::os::fd::RawFd;
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()))
Expand Down Expand Up @@ -302,134 +280,13 @@ mod imp {
original: termios,
})
}

fn apply_palette_responses(
buffer: &mut Vec<u8>,
palette: &mut [Option<(u8, u8, u8)>; 256],
) -> usize {
let mut newly_filled = 0;

while let Some(start) = buffer.windows(2).position(|window| window == [0x1b, b']']) {
if start > 0 {
buffer.drain(..start);
continue;
}

let mut index = 2; // skip ESC ]
let mut terminator_len = None;
while index < buffer.len() {
match buffer[index] {
0x07 => {
terminator_len = Some(1);
break;
}
0x1b if index + 1 < buffer.len() && buffer[index + 1] == b'\\' => {
terminator_len = Some(2);
break;
}
_ => index += 1,
}
}

let Some(terminator_len) = terminator_len else {
break;
};

let end = index;
let parsed = std::str::from_utf8(&buffer[2..end])
.ok()
.and_then(parse_palette_message);
let processed = end + terminator_len;
buffer.drain(..processed);

if let Some((slot, color)) = parsed
&& palette[slot].is_none()
{
palette[slot] = Some(color);
newly_filled += 1;
}
}

newly_filled
}

fn parse_palette_message(message: &str) -> Option<(usize, (u8, u8, u8))> {
let mut parts = message.splitn(3, ';');
if parts.next()? != "4" {
return None;
}
let index: usize = parts.next()?.trim().parse().ok()?;
if index >= 256 {
return None;
}
let payload = parts.next()?;
let (model, values) = payload.split_once(':')?;
if model != "rgb" && model != "rgba" {
return None;
}
let mut components = values.split('/');
let r = parse_component(components.next()?)?;
let g = parse_component(components.next()?)?;
let b = parse_component(components.next()?)?;
Some((index, (r, g, b)))
}

fn parse_component(component: &str) -> Option<u8> {
let trimmed = component.trim();
if trimmed.is_empty() {
return None;
}
let bits = trimmed.len().checked_mul(4)?;
if bits == 0 || bits > 64 {
return None;
}
let max = if bits == 64 {
u64::MAX
} else {
(1u64 << bits) - 1
};
let value = u64::from_str_radix(trimmed, 16).ok()?;
Some(((value * 255 + max / 2) / max) as u8)
}

fn parse_osc_color(buffer: &[u8], code: u8) -> Option<(u8, u8, u8)> {
let text = std::str::from_utf8(buffer).ok()?;
let prefix = match code {
10 => "\u{1b}]10;",
11 => "\u{1b}]11;",
_ => return None,
};
let start = text.rfind(prefix)?;
let after_prefix = &text[start + prefix.len()..];
let end_bel = after_prefix.find('\u{7}');
let end_st = after_prefix.find("\u{1b}\\");
let end_idx = match (end_bel, end_st) {
(Some(bel), Some(st)) => bel.min(st),
(Some(bel), None) => bel,
(None, Some(st)) => st,
(None, None) => return None,
};
let payload = after_prefix[..end_idx].trim();
parse_color_payload(payload)
}

fn parse_color_payload(payload: &str) -> Option<(u8, u8, u8)> {
if payload.is_empty() || payload == "?" {
return None;
}
let (model, values) = payload.split_once(':')?;
if model != "rgb" && model != "rgba" {
return None;
}
let mut parts = values.split('/');
let r = parse_component(parts.next()?)?;
let g = parse_component(parts.next()?)?;
let b = parse_component(parts.next()?)?;
Some((r, g, b))
}
}

#[cfg(not(all(unix, not(test))))]
#[cfg(all(windows, not(test)))]
#[path = "terminal_palette_windows.rs"]
mod imp;

#[cfg(not(any(all(unix, not(test)), all(windows, not(test)))))]
mod imp {
use super::DefaultColors;

Expand Down
156 changes: 156 additions & 0 deletions codex-rs/tui/src/terminal_palette/common.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
use std::str;

pub(crate) 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> {
pub(crate) 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
}

pub(crate) fn refresh_with(&mut self, mut init: impl FnMut() -> Option<T>) -> Option<T> {
self.value = init();
self.attempted = true;
self.value
}
}

pub(crate) fn apply_palette_responses(
buffer: &mut Vec<u8>,
palette: &mut [Option<(u8, u8, u8)>; 256],
) -> usize {
let mut newly_filled = 0;

while let Some(start) = buffer.windows(2).position(|window| window == [0x1b, b']']) {
if start > 0 {
buffer.drain(..start);
continue;
}

let mut index = 2; // skip ESC ]
let mut terminator_len = None;
while index < buffer.len() {
match buffer[index] {
0x07 => {
terminator_len = Some(1);
break;
}
0x1b if index + 1 < buffer.len() && buffer[index + 1] == b'\\' => {
terminator_len = Some(2);
break;
}
_ => index += 1,
}
}

let Some(terminator_len) = terminator_len else {
break;
};

let end = index;
let parsed = str::from_utf8(&buffer[2..end])
.ok()
.and_then(parse_palette_message);
let processed = end + terminator_len;
buffer.drain(..processed);

if let Some((slot, color)) = parsed
&& palette[slot].is_none()
{
palette[slot] = Some(color);
newly_filled += 1;
}
}

newly_filled
}

pub(crate) fn parse_osc_color(buffer: &[u8], code: u8) -> Option<(u8, u8, u8)> {
let text = str::from_utf8(buffer).ok()?;
let prefix = match code {
10 => "\u{1b}]10;",
11 => "\u{1b}]11;",
_ => return None,
};
let start = text.rfind(prefix)?;
let after_prefix = &text[start + prefix.len()..];
let end_bel = after_prefix.find('\u{7}');
let end_st = after_prefix.find("\u{1b}\\");
let end_idx = match (end_bel, end_st) {
(Some(bel), Some(st)) => bel.min(st),
(Some(bel), None) => bel,
(None, Some(st)) => st,
(None, None) => return None,
};
let payload = after_prefix[..end_idx].trim();
parse_color_payload(payload)
}

fn parse_palette_message(message: &str) -> Option<(usize, (u8, u8, u8))> {
let mut parts = message.splitn(3, ';');
if parts.next()? != "4" {
return None;
}
let index: usize = parts.next()?.trim().parse().ok()?;
if index >= 256 {
return None;
}
let payload = parts.next()?;
let (model, values) = payload.split_once(':')?;
if model != "rgb" && model != "rgba" {
return None;
}
let mut components = values.split('/');
let r = parse_component(components.next()?)?;
let g = parse_component(components.next()?)?;
let b = parse_component(components.next()?)?;
Some((index, (r, g, b)))
}

fn parse_color_payload(payload: &str) -> Option<(u8, u8, u8)> {
if payload.is_empty() || payload == "?" {
return None;
}
let (model, values) = payload.split_once(':')?;
if model != "rgb" && model != "rgba" {
return None;
}
let mut parts = values.split('/');
let r = parse_component(parts.next()?)?;
let g = parse_component(parts.next()?)?;
let b = parse_component(parts.next()?)?;
Some((r, g, b))
}

fn parse_component(component: &str) -> Option<u8> {
let trimmed = component.trim();
if trimmed.is_empty() {
return None;
}
let bits = trimmed.len().checked_mul(4)?;
if bits == 0 || bits > 64 {
return None;
}
let max = if bits == 64 {
u64::MAX
} else {
(1u64 << bits) - 1
};
let value = u64::from_str_radix(trimmed, 16).ok()?;
Some(((value * 255 + max / 2) / max) as u8)
}
Loading
Loading