chore(tui): instrument Windows palette probing

This commit is contained in:
Felipe Coury
2026-06-18 11:15:46 -04:00
parent 5670360009
commit cbb36acfe3
3 changed files with 144 additions and 17 deletions
+22 -5
View File
@@ -40,7 +40,7 @@ pub fn best_color_for_level(target: (u8, u8, u8), color_level: StdoutColorLevel)
best_color_for_color_level(target, color_level)
}
fn effective_stdout_color_level() -> StdoutColorLevel {
pub(crate) fn effective_stdout_color_level() -> StdoutColorLevel {
stdout_color_level_for_terminal(
stdout_color_level(),
terminal_info().name,
@@ -87,7 +87,7 @@ pub fn requery_default_colors() {
imp::requery_default_colors();
}
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Debug)]
pub struct DefaultColors {
fg: (u8, u8, u8),
bg: (u8, u8, u8),
@@ -264,19 +264,36 @@ mod imp {
bg: colors.bg,
});
cache.attempted = true;
tracing::info!(
?colors,
stdout_color_level = ?super::stdout_color_level(),
effective_stdout_color_level = ?super::effective_stdout_color_level(),
"cached Windows terminal default colors"
);
} else {
tracing::warn!("failed to lock Windows terminal default color cache");
}
}
pub(super) fn requery_default_colors() {}
pub(super) fn requery_default_colors() {
tracing::info!(
"Windows terminal default color requery skipped; retaining startup probe cache"
);
}
fn query_default_colors() -> Option<DefaultColors> {
crate::terminal_probe::default_colors(crate::terminal_probe::DEFAULT_TIMEOUT)
let colors = crate::terminal_probe::default_colors(crate::terminal_probe::DEFAULT_TIMEOUT)
.ok()
.flatten()
.map(|colors| DefaultColors {
fg: colors.fg,
bg: colors.bg,
})
});
tracing::info!(
?colors,
"initialized Windows terminal default color cache outside startup probe"
);
colors
}
}
+105 -12
View File
@@ -593,17 +593,73 @@ mod imp {
/// currently Unix-only. Failures and missing responses are reported as `Ok(None)` by callers so
/// terminals without OSC 10/11 support keep the existing conservative palette fallback.
pub(crate) fn default_colors(timeout: Duration) -> io::Result<Option<DefaultColors>> {
let Ok(output) = std_handle(STD_OUTPUT_HANDLE) else {
return Ok(None);
let output = match std_handle(STD_OUTPUT_HANDLE) {
Ok(output) => output,
Err(err) => {
tracing::warn!(error = %err, "terminal default color probe has no stdout handle");
return Ok(None);
}
};
if let Ok(input) = std_handle(STD_INPUT_HANDLE)
&& let Ok(Some(colors)) = query_osc_default_colors(input, output, timeout)
{
return Ok(Some(colors));
let input = std_handle(STD_INPUT_HANDLE);
tracing::info!(
stdin_handle = ?input.as_ref().map(|handle| *handle as usize),
stdin_is_console = input
.as_ref()
.ok()
.is_some_and(|handle| console_mode(*handle).is_some()),
stdout_handle = output as usize,
stdout_is_console = console_mode(output).is_some(),
term = ?std::env::var_os("TERM"),
colorterm = ?std::env::var_os("COLORTERM"),
wt_session = std::env::var_os("WT_SESSION").is_some(),
ssh_connection = std::env::var_os("SSH_CONNECTION").is_some(),
"starting Windows terminal default color probe"
);
match input {
Ok(input) => match query_osc_default_colors(input, output, timeout) {
Ok(Some(colors)) => {
tracing::info!(
?colors,
source = "osc_10_11",
"terminal default colors resolved"
);
return Ok(Some(colors));
}
Ok(None) => {
tracing::info!(
source = "osc_10_11",
"terminal default color probe returned no colors"
);
}
Err(err) => {
tracing::warn!(error = %err, source = "osc_10_11", "terminal default color probe failed");
}
},
Err(err) => {
tracing::warn!(error = %err, "terminal default color probe has no stdin handle");
}
}
Ok(query_console_default_colors(output).ok().flatten())
match query_console_default_colors(output) {
Ok(Some(colors)) => {
tracing::info!(
?colors,
source = "console_attributes",
"terminal default colors resolved"
);
Ok(Some(colors))
}
Ok(None) => {
tracing::info!(source = "none", "terminal default colors unavailable");
Ok(None)
}
Err(err) => {
tracing::warn!(error = %err, source = "console_attributes", "terminal default color fallback failed");
Ok(None)
}
}
}
fn query_osc_default_colors(
@@ -622,10 +678,13 @@ mod imp {
if unsafe { GetConsoleScreenBufferInfoEx(output, &mut info) } == 0 {
return Err(io::Error::last_os_error());
}
Ok(Some(decode_console_default_colors(
info.wAttributes,
&info.ColorTable,
)))
let colors = decode_console_default_colors(info.wAttributes, &info.ColorTable);
tracing::info!(
attributes = %format_args!("{:#06x}", info.wAttributes),
?colors,
"decoded Windows console default colors"
);
Ok(Some(colors))
}
fn decode_console_default_colors(attributes: u16, color_table: &[u32; 16]) -> DefaultColors {
@@ -655,6 +714,11 @@ mod imp {
Ok(handle)
}
fn console_mode(handle: HANDLE) -> Option<u32> {
let mut mode = 0;
(unsafe { GetConsoleMode(handle, &mut mode) } != 0).then_some(mode)
}
struct VirtualTerminalInputMode {
handle: HANDLE,
original_mode: u32,
@@ -672,6 +736,12 @@ mod imp {
return Err(io::Error::last_os_error());
}
tracing::info!(
original_mode = %format_args!("{original_mode:#010x}"),
requested_mode = %format_args!("{requested_mode:#010x}"),
"enabled Windows virtual terminal input for default color probe"
);
Ok(Self {
handle,
original_mode,
@@ -719,11 +789,13 @@ mod imp {
let mut buffer = Vec::new();
loop {
if let Some(value) = parse(&buffer) {
log_probe_bytes("parsed", &buffer);
return Ok(Some(value));
}
let now = Instant::now();
if now >= deadline {
log_probe_bytes("deadline", &buffer);
return Ok(None);
}
let timeout_ms = deadline
@@ -732,12 +804,33 @@ mod imp {
.min(u32::MAX as u128) as u32;
match unsafe { WaitForSingleObject(handle, timeout_ms) } {
WAIT_OBJECT_0 => read_once(handle, &mut buffer)?,
WAIT_TIMEOUT => return Ok(None),
WAIT_TIMEOUT => {
log_probe_bytes("wait_timeout", &buffer);
return Ok(None);
}
_ => return Err(io::Error::last_os_error()),
}
}
}
fn log_probe_bytes(outcome: &str, buffer: &[u8]) {
const MAX_LOGGED_BYTES: usize = 512;
let escaped = buffer
.iter()
.take(MAX_LOGGED_BYTES)
.flat_map(|byte| std::ascii::escape_default(*byte))
.map(char::from)
.collect::<String>();
tracing::info!(
outcome,
byte_count = buffer.len(),
truncated = buffer.len() > MAX_LOGGED_BYTES,
escaped_bytes = %escaped,
"Windows OSC default color probe input"
);
}
fn read_once(handle: HANDLE, buffer: &mut Vec<u8>) -> io::Result<()> {
let mut chunk = [0_u8; 256];
let mut read = 0;
+17
View File
@@ -488,6 +488,7 @@ fn probe_windows_default_colors() {
"terminal default color probe completed"
);
crate::terminal_palette::set_default_colors_from_startup_probe(colors);
log_windows_terminal_palette_resolution();
}
Err(err) => {
tracing::warn!(
@@ -495,10 +496,26 @@ fn probe_windows_default_colors() {
"terminal default color probe failed: {err}"
);
crate::terminal_palette::set_default_colors_from_startup_probe(/*colors*/ None);
log_windows_terminal_palette_resolution();
}
}
}
#[cfg(windows)]
fn log_windows_terminal_palette_resolution() {
let foreground = crate::terminal_palette::default_fg();
let background = crate::terminal_palette::default_bg();
let composer_background = crate::style::user_message_style_for(background).bg;
tracing::info!(
?foreground,
?background,
stdout_color_level = ?crate::terminal_palette::stdout_color_level(),
effective_stdout_color_level = ?crate::terminal_palette::effective_stdout_color_level(),
?composer_background,
"resolved Windows terminal palette and composer style"
);
}
fn set_panic_hook() {
let hook = panic::take_hook();
panic::set_hook(Box::new(move |panic_info| {