From 77cfc5dd381fc16b2bbda7d70a2602f72c633f37 Mon Sep 17 00:00:00 2001 From: Finn Eitreim <48069764+feitreim@users.noreply.github.com> Date: Thu, 30 Apr 2026 10:01:32 -0400 Subject: [PATCH] terminal: Fix 8 bit colors (#54565) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Right now the terminal rendering code applies a minimum contrast value (which is good, and for accessibility) to colors that appear, there was already an exemption from this for 24bit color specification, because the application explicitly asked for that color, so it should be rendered that color. My change changes that exemption to also include ansi colors 16-255, because these are also set explicitly, the normal, non-exempt case is now the default ansi colors, 0-15, these are set by the theme and are usually specified as just 'red' or 'green', hence the importance of the min contrast. Heres what the gradient ramp from the original issue looks like now: Screenshot 2026-04-22 at 6 45 43 PM This matches ghostty and VSCodium from the original issue. test prevents regressions but idk, maybe not nessecary. Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Closes #54396 Release Notes: - terminal: Improved 256 color ansi rendering --- crates/terminal_view/src/terminal_element.rs | 69 ++++++++++++++++++-- 1 file changed, 65 insertions(+), 4 deletions(-) diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 8f46e8f9b2..99918d4784 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -538,6 +538,20 @@ impl TerminalElement { ) } + /// Whether the application explicitly picked this foreground color and does not + /// want it adjusted for contrast: 24-bit true color (`\e[38;2;R;G;Bm`) or a + /// specific entry in the 256-color palette (`\e[38;5;Nm`) where N >= 16 (the + /// 6x6x6 cube at 16..=231 and the 24-step grayscale ramp at 232..=255). + /// Indices 0..=15 still go through contrast adjustment since those map to + /// theme-defined ANSI colors that can clash with the theme background. + fn is_app_chosen_exact_color(fg: &terminal::alacritty_terminal::vte::ansi::Color) -> bool { + matches!( + fg, + terminal::alacritty_terminal::vte::ansi::Color::Spec(_) + | terminal::alacritty_terminal::vte::ansi::Color::Indexed(16..=255) + ) + } + /// Converts the Alacritty cell styles to GPUI text styles and background color. fn cell_style( indexed: &IndexedCell, @@ -549,13 +563,11 @@ impl TerminalElement { minimum_contrast: f32, ) -> TextRun { let flags = indexed.cell.flags; - let is_true_color = matches!(fg, terminal::alacritty_terminal::vte::ansi::Color::Spec(_)); + let skip_contrast = Self::is_app_chosen_exact_color(&fg); let mut fg = convert_color(&fg, colors); let bg = convert_color(&bg, colors); - // Skip contrast adjustment for true-color (24-bit RGB) foregrounds — the - // application chose that exact color. Also skip for decorative characters. - if !is_true_color && !Self::is_decorative_character(indexed.c) { + if !skip_contrast && !Self::is_decorative_character(indexed.c) { fg = ensure_minimum_contrast(fg, bg, minimum_contrast); } @@ -1819,6 +1831,55 @@ mod tests { assert!(!TerminalElement::is_decorative_character(' ')); } + #[test] + fn test_is_app_chosen_exact_color() { + use terminal::alacritty_terminal::vte::ansi::{Color, NamedColor, Rgb}; + + // Indices 0..=15 are theme-overridable ANSI colors; contrast adjustment must still apply. + assert!(!TerminalElement::is_app_chosen_exact_color( + &Color::Indexed(0) + )); + assert!(!TerminalElement::is_app_chosen_exact_color( + &Color::Indexed(15) + )); + + // Boundary: index 16 is the first entry of the 6x6x6 cube — application-chosen. + assert!(TerminalElement::is_app_chosen_exact_color(&Color::Indexed( + 16 + ))); + // Interior of the cube. + assert!(TerminalElement::is_app_chosen_exact_color(&Color::Indexed( + 17 + ))); + assert!(TerminalElement::is_app_chosen_exact_color(&Color::Indexed( + 231 + ))); + // Grayscale ramp boundaries. + assert!(TerminalElement::is_app_chosen_exact_color(&Color::Indexed( + 232 + ))); + assert!(TerminalElement::is_app_chosen_exact_color(&Color::Indexed( + 255 + ))); + + // 24-bit true color is always application-chosen. + assert!(TerminalElement::is_app_chosen_exact_color(&Color::Spec( + Rgb { + r: 10, + g: 20, + b: 30 + } + ))); + + // Named colors are theme-defined and must go through contrast adjustment. + assert!(!TerminalElement::is_app_chosen_exact_color(&Color::Named( + NamedColor::Red + ))); + assert!(!TerminalElement::is_app_chosen_exact_color(&Color::Named( + NamedColor::Foreground + ))); + } + #[test] fn test_contrast_adjustment_logic() { // Test the core contrast adjustment logic without needing full app context