diff --git a/src/ghostty/mod.rs b/src/ghostty/mod.rs index 685c0735da..ef67b86fd9 100644 --- a/src/ghostty/mod.rs +++ b/src/ghostty/mod.rs @@ -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, diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs index 58de71ce1f..3e43cf0633 100644 --- a/src/pane/terminal.rs +++ b/src/pane/terminal.rs @@ -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, resolved_fg: Option, resolved_bg: Option, + 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; 256]); + +impl PaletteOverrides { + fn new( + active: &[crate::ghostty::RgbColor; 256], + default: &[crate::ghostty::RgbColor; 256], + ) -> Option { + 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 { + 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 {