fix: render OSC 4 palette overrides instead of forwarding the index (#2162)

Co-authored-by: Can Celik <ogulcancelik@gmail.com>
This commit is contained in:
Mo 2026-08-06 00:54:01 +02:00 committed by GitHub
parent ea047db8e4
commit f83980db5c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 116 additions and 5 deletions

View File

@ -865,6 +865,21 @@ impl Terminal {
}
}
pub fn default_palette(&self) -> Result<[RgbColor; 256], Error> {
let mut out = [ffi::GhosttyColorRgb::default(); 256];
// SAFETY: self.raw is a live terminal handle, and out is exactly the
// 256-entry array this data kind writes.
unsafe {
ffi::ghostty_terminal_get(
self.raw,
ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_COLOR_PALETTE_DEFAULT,
out.as_mut_ptr().cast(),
)
.into_result()?;
}
Ok(out.map(Into::into))
}
pub fn resize(
&mut self,
cols: u16,

View File

@ -1942,6 +1942,9 @@ impl GhosttyPaneTerminal {
.and_then(|c| ghostty_default_fg(c.foreground, host_theme, initial_default_foreground));
let resolved_fg = colors.map(|c| ghostty_color(c.foreground));
let resolved_bg = colors.map(|c| ghostty_color(c.background));
let palette_overrides = colors
.zip(terminal.default_palette().ok())
.and_then(|(colors, default)| PaletteOverrides::new(&colors.palette, &default));
let hide_kitty_placeholders = crate::kitty_graphics::is_enabled();
let mut row_iterator = match crate::ghostty::RowIterator::new() {
@ -1976,6 +1979,7 @@ impl GhosttyPaneTerminal {
default_bg,
resolved_fg,
resolved_bg,
palette_overrides.as_ref(),
);
let symbol = match ghostty_buffer_symbol_into(
&cells,
@ -2201,6 +2205,9 @@ fn ghostty_collect_dirty_patch(
.and_then(|c| ghostty_default_fg(c.foreground, host_theme, initial_default_foreground));
let resolved_fg = colors.map(|c| ghostty_color(c.foreground));
let resolved_bg = colors.map(|c| ghostty_color(c.background));
let palette_overrides = colors
.zip(terminal.default_palette().ok())
.and_then(|(colors, default)| PaletteOverrides::new(&colors.palette, &default));
let hide_kitty_placeholders = crate::kitty_graphics::is_enabled();
let Ok(mut row_iterator) = crate::ghostty::RowIterator::new() else {
@ -2245,6 +2252,7 @@ fn ghostty_collect_dirty_patch(
default_bg,
resolved_fg,
resolved_bg,
palette_overrides.as_ref(),
);
let symbol = match ghostty_buffer_symbol_into(
&cells,
@ -2773,11 +2781,12 @@ fn ghostty_cell_style(
default_bg: Option<Color>,
resolved_fg: Option<Color>,
resolved_bg: Option<Color>,
palette_overrides: Option<&PaletteOverrides>,
) -> Style {
let mut fg = basic
.style
.fg_color
.map(ghostty_cell_color)
.map(|color| ghostty_cell_color(color, palette_overrides))
.or_else(|| cells.fg_color().ok().flatten().map(ghostty_color))
.or(default_fg);
let mut bg = cells
@ -2785,7 +2794,7 @@ fn ghostty_cell_style(
.ok()
.flatten()
.or(basic.style.bg_color)
.map(ghostty_cell_color)
.map(|color| ghostty_cell_color(color, palette_overrides))
.or_else(|| cells.bg_color().ok().flatten().map(ghostty_color))
.or(default_bg);
if basic.style.invisible {
@ -2807,7 +2816,11 @@ fn ghostty_cell_style(
}
let mut style = ghostty_default_style(fg, bg);
if let Some(underline_color) = basic.style.underline_color.map(ghostty_cell_color) {
if let Some(underline_color) = basic
.style
.underline_color
.map(|color| ghostty_cell_color(color, palette_overrides))
{
style = style.underline_color(underline_color);
}
let mut modifiers = Modifier::empty();
@ -3040,9 +3053,44 @@ fn terminal_theme_color(color: crate::ghostty::RgbColor) -> crate::terminal_them
}
}
fn ghostty_cell_color(color: crate::ghostty::CellColor) -> Color {
// Palette entries the program redefined with OSC 4. Forwarding a palette index to the
// host makes it resolve against the host's own palette, discarding the redefinition.
// Only overridden entries become RGB; the rest stay indexed and keep following the
// host theme. None when nothing was redefined, which is the common case.
struct PaletteOverrides([Option<crate::ghostty::RgbColor>; 256]);
impl PaletteOverrides {
fn new(
active: &[crate::ghostty::RgbColor; 256],
default: &[crate::ghostty::RgbColor; 256],
) -> Option<Self> {
let mut overrides = [None; 256];
let mut any = false;
for (index, (active, default)) in active.iter().zip(default.iter()).enumerate() {
if active != default {
overrides[index] = Some(*active);
any = true;
}
}
any.then_some(Self(overrides))
}
fn get(&self, index: u8) -> Option<crate::ghostty::RgbColor> {
self.0[usize::from(index)]
}
}
fn ghostty_cell_color(
color: crate::ghostty::CellColor,
palette_overrides: Option<&PaletteOverrides>,
) -> Color {
match color {
crate::ghostty::CellColor::Palette(index) => Color::Indexed(index),
crate::ghostty::CellColor::Palette(index) => {
match palette_overrides.and_then(|overrides| overrides.get(index)) {
Some(color) => ghostty_color(color),
None => Color::Indexed(index),
}
}
crate::ghostty::CellColor::Rgb(color) => ghostty_color(color),
}
}
@ -3121,6 +3169,54 @@ mod tests {
}
}
fn rgb(r: u8, g: u8, b: u8) -> crate::ghostty::RgbColor {
crate::ghostty::RgbColor { r, g, b }
}
#[test]
fn palette_overrides_are_none_without_an_osc4_write() {
let default = [rgb(1, 2, 3); 256];
assert!(PaletteOverrides::new(&default, &default).is_none());
}
#[test]
fn redefined_palette_entries_render_as_rgb_and_others_stay_indexed() {
let default = [rgb(1, 2, 3); 256];
let mut active = default;
active[18] = rgb(169, 177, 214);
let overrides = PaletteOverrides::new(&active, &default).expect("index 18 differs");
assert_eq!(
ghostty_cell_color(crate::ghostty::CellColor::Palette(18), Some(&overrides)),
Color::Rgb(169, 177, 214)
);
// Untouched entries keep being forwarded, so they still follow the host theme.
assert_eq!(
ghostty_cell_color(crate::ghostty::CellColor::Palette(19), Some(&overrides)),
Color::Indexed(19)
);
// ...and so does everything when the program never wrote a palette at all.
assert_eq!(
ghostty_cell_color(crate::ghostty::CellColor::Palette(18), None),
Color::Indexed(18)
);
}
#[test]
fn direct_rgb_cells_are_unaffected_by_palette_overrides() {
let default = [rgb(1, 2, 3); 256];
let mut active = default;
active[18] = rgb(169, 177, 214);
let overrides = PaletteOverrides::new(&active, &default).expect("index 18 differs");
assert_eq!(
ghostty_cell_color(
crate::ghostty::CellColor::Rgb(rgb(122, 162, 247)),
Some(&overrides)
),
Color::Rgb(122, 162, 247)
);
}
fn wide_text_cells(text: &str) -> [crate::ghostty::ScreenTextCell; 2] {
[
crate::ghostty::ScreenTextCell {