support Windows themes, which override the color table.

This commit is contained in:
David Wiesen
2025-10-06 11:29:16 -07:00
parent 8029ad9919
commit b28888f16a
2 changed files with 168 additions and 17 deletions

View File

@@ -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

View File

@@ -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<DefaultColors> {
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<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);
}
}
/// 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<ConsoleSnapshot> {
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::<CONSOLE_SCREEN_BUFFER_INFO>::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<ConsoleSnapshot> {
let mut info = MaybeUninit::<CONSOLE_SCREEN_BUFFER_INFOEX>::zeroed();
let info_ptr = info.as_mut_ptr();
(*info_ptr).cbSize = std::mem::size_of::<CONSOLE_SCREEN_BUFFER_INFOEX>() 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<ConsoleSnapshot> {
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(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<DefaultColors> {
None
}