From f1b41f33bffd6fa5465d5d2e7880e278408ef520 Mon Sep 17 00:00:00 2001 From: Val Alexander <68980965+BunsDev@users.noreply.github.com> Date: Wed, 24 Jun 2026 15:49:09 -0500 Subject: [PATCH] fix(tui): make the model-line badge text readable on the accent color MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mode badge (e.g. `KITTY`/`BUILD`) and the `/connect` pill styled their text with `.fg(Color::Black).add_modifier(Modifier::BOLD)`. `Color::Black` is ANSI index 0, and many terminals render *bold + indexed black* as "bright black" (gray), which washes out against the violet/blue/amber accent background — the reported low-contrast badge. Add `readable_fg_on(bg)`, which returns an explicit `Color::Rgb` black or white chosen by WCAG relative-luminance contrast. RGB colors aren't subject to bold-brightening, so the badge text stays solid. For all three current accents it resolves to true black (contrast 4.96 / 6.02 / 12.24 vs white's 4.23 / 3.49 / 1.72), and it auto-flips to white if a darker accent is added. Co-Authored-By: Claude Opus 4.8 (1M context) --- src-rust/crates/tui/src/app.rs | 34 +++++++++++++++++++++++++++++++ src-rust/crates/tui/src/render.rs | 8 +++++--- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src-rust/crates/tui/src/app.rs b/src-rust/crates/tui/src/app.rs index 12a38fe..d0fcb90 100644 --- a/src-rust/crates/tui/src/app.rs +++ b/src-rust/crates/tui/src/app.rs @@ -1619,6 +1619,40 @@ pub fn accent_for_mode(mode: Option<&str>) -> Color { } } +/// Pick a foreground color that reads clearly on top of `bg`. +/// +/// Returns explicit `Color::Rgb` black or white (never the indexed +/// `Color::Black`/`Color::White`) so terminals don't brighten a *bold* indexed +/// black into low-contrast gray — the cause of the washed-out badge text. The +/// choice is made by comparing WCAG relative-luminance contrast ratios, so it +/// stays correct if the accent palette changes. +pub fn readable_fg_on(bg: Color) -> Color { + let (r, g, b) = match bg { + Color::Rgb(r, g, b) => (r, g, b), + // Non-RGB backgrounds (themes/indexed): default to black, which reads + // on the light/mid accent tones used here. + _ => return Color::Rgb(0, 0, 0), + }; + + fn channel_lum(c: u8) -> f32 { + let c = c as f32 / 255.0; + if c <= 0.03928 { + c / 12.92 + } else { + ((c + 0.055) / 1.055).powf(2.4) + } + } + + let lum = 0.2126 * channel_lum(r) + 0.7152 * channel_lum(g) + 0.0722 * channel_lum(b); + let black_contrast = (lum + 0.05) / 0.05; + let white_contrast = 1.05 / (lum + 0.05); + if black_contrast >= white_contrast { + Color::Rgb(0, 0, 0) + } else { + Color::Rgb(255, 255, 255) + } +} + fn format_elapsed_ms(ms: u128) -> String { let total_secs = ((ms + 500) / 1000) as u64; // round to nearest second if total_secs < 60 { diff --git a/src-rust/crates/tui/src/render.rs b/src-rust/crates/tui/src/render.rs index 206c965..b60f105 100644 --- a/src-rust/crates/tui/src/render.rs +++ b/src-rust/crates/tui/src/render.rs @@ -3,7 +3,9 @@ use std::cell::RefCell; use crate::agents_view::render_agents_menu; -use crate::app::{App, ContextMenuKind, SystemAnnotation, SystemMessageStyle, ToolStatus}; +use crate::app::{ + readable_fg_on, App, ContextMenuKind, SystemAnnotation, SystemMessageStyle, ToolStatus, +}; use crate::ask_user_dialog::render_ask_user_dialog; use crate::bypass_permissions_dialog::render_bypass_permissions_dialog; use crate::context_viz::render_context_viz; @@ -2153,7 +2155,7 @@ fn render_input(frame: &mut Frame, app: &App, area: Rect, focused: bool) { Span::styled( format!(" {} ", agent_mode.to_uppercase()), Style::default() - .fg(Color::Black) + .fg(readable_fg_on(pink)) .bg(pink) .add_modifier(Modifier::BOLD), ), @@ -2181,7 +2183,7 @@ fn render_input(frame: &mut Frame, app: &App, area: Rect, focused: bool) { Span::styled( " /connect ", Style::default() - .fg(Color::Black) + .fg(readable_fg_on(pink)) .bg(pink) .add_modifier(Modifier::BOLD), ),