Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/ghostty/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
106 changes: 101 additions & 5 deletions src/pane/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -1976,6 +1979,7 @@ impl GhosttyPaneTerminal {
default_bg,
resolved_fg,
resolved_bg,
palette_overrides.as_ref(),
);
let symbol = match ghostty_buffer_symbol_into(
&cells,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -2773,19 +2781,20 @@ 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
.content_bg_color()
.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 {
Expand All @@ -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();
Expand Down Expand Up @@ -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),
}
}
Expand Down Expand Up @@ -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 {
Expand Down
Loading