From e201a647635905a5141bfce335619b0e232b396d Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Mon, 11 May 2026 02:09:48 +0300 Subject: [PATCH] fix: preserve indexed terminal colors fixes #74 --- src/ghostty/mod.rs | 65 +++++++++++++++++++++++++++++ src/pane/terminal.rs | 97 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 155 insertions(+), 7 deletions(-) diff --git a/src/ghostty/mod.rs b/src/ghostty/mod.rs index 02bae73f..bc21ab99 100644 --- a/src/ghostty/mod.rs +++ b/src/ghostty/mod.rs @@ -179,8 +179,16 @@ impl From for RgbColor { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CellColor { + Palette(u8), + Rgb(RgbColor), +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct CellStyle { + pub fg_color: Option, + pub bg_color: Option, pub bold: bool, pub italic: bool, pub faint: bool, @@ -195,6 +203,8 @@ pub struct CellStyle { impl From for CellStyle { fn from(value: ffi::GhosttyStyle) -> Self { Self { + fg_color: cell_color_from_style_color(value.fg_color), + bg_color: cell_color_from_style_color(value.bg_color), bold: value.bold, italic: value.italic, faint: value.faint, @@ -208,6 +218,20 @@ impl From for CellStyle { } } +fn cell_color_from_style_color(color: ffi::GhosttyStyleColor) -> Option { + match color.tag { + ffi::GhosttyStyleColorTag_GHOSTTY_STYLE_COLOR_PALETTE => { + // SAFETY: Ghostty's tagged union stores `palette` when the tag is PALETTE. + Some(CellColor::Palette(unsafe { color.value.palette })) + } + ffi::GhosttyStyleColorTag_GHOSTTY_STYLE_COLOR_RGB => { + // SAFETY: Ghostty's tagged union stores `rgb` when the tag is RGB. + Some(CellColor::Rgb(unsafe { color.value.rgb }.into())) + } + _ => None, + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct RenderColors { pub background: RgbColor, @@ -1198,6 +1222,47 @@ impl<'a> RowCellIter<'a> { Ok(style.into()) } + pub fn content_bg_color(&self) -> Result, Error> { + let raw = self.raw_cell()?; + let mut tag = ffi::GhosttyCellContentTag_GHOSTTY_CELL_CONTENT_CODEPOINT; + unsafe { + ffi::ghostty_cell_get( + raw, + ffi::GhosttyCellData_GHOSTTY_CELL_DATA_CONTENT_TAG, + (&mut tag as *mut ffi::GhosttyCellContentTag).cast(), + ) + .into_result()?; + } + + match tag { + ffi::GhosttyCellContentTag_GHOSTTY_CELL_CONTENT_BG_COLOR_PALETTE => { + let mut index = 0u8; + unsafe { + ffi::ghostty_cell_get( + raw, + ffi::GhosttyCellData_GHOSTTY_CELL_DATA_COLOR_PALETTE, + (&mut index as *mut u8).cast(), + ) + .into_result()?; + } + Ok(Some(CellColor::Palette(index))) + } + ffi::GhosttyCellContentTag_GHOSTTY_CELL_CONTENT_BG_COLOR_RGB => { + let mut color = ffi::GhosttyColorRgb::default(); + unsafe { + ffi::ghostty_cell_get( + raw, + ffi::GhosttyCellData_GHOSTTY_CELL_DATA_COLOR_RGB, + (&mut color as *mut ffi::GhosttyColorRgb).cast(), + ) + .into_result()?; + } + Ok(Some(CellColor::Rgb(color.into()))) + } + _ => Ok(None), + } + } + pub fn fg_color(&self) -> Result, Error> { let mut color = ffi::GhosttyColorRgb::default(); let result = unsafe { diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs index ad040ba2..5c5ad211 100644 --- a/src/pane/terminal.rs +++ b/src/pane/terminal.rs @@ -987,17 +987,18 @@ fn ghostty_cell_style( resolved_bg: Option, ) -> Style { let style_data = cells.style().unwrap_or_default(); - let mut fg = cells - .fg_color() - .ok() - .flatten() - .map(ghostty_color) + let mut fg = style_data + .fg_color + .map(ghostty_cell_color) + .or_else(|| cells.fg_color().ok().flatten().map(ghostty_color)) .or(default_fg); let mut bg = cells - .bg_color() + .content_bg_color() .ok() .flatten() - .map(ghostty_color) + .or(style_data.bg_color) + .map(ghostty_cell_color) + .or_else(|| cells.bg_color().ok().flatten().map(ghostty_color)) .or(default_bg); if style_data.invisible { fg = bg.or(default_bg); @@ -1073,6 +1074,13 @@ fn terminal_theme_color(color: crate::ghostty::RgbColor) -> crate::terminal_them } } +fn ghostty_cell_color(color: crate::ghostty::CellColor) -> Color { + match color { + crate::ghostty::CellColor::Palette(index) => Color::Indexed(index), + crate::ghostty::CellColor::Rgb(color) => ghostty_color(color), + } +} + fn ghostty_color(color: crate::ghostty::RgbColor) -> Color { Color::Rgb(color.r, color.g, color.b) } @@ -1643,6 +1651,81 @@ mod tests { assert_eq!(buffer[(2, 0)].style().bg, Some(Color::Reset)); } + #[test] + fn render_preserves_palette_colors_instead_of_flattening_to_rgb() { + let (tx, _rx) = mpsc::channel(4); + let terminal = crate::ghostty::Terminal::new(20, 5, 0).unwrap(); + let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap(); + { + let mut core = pane.core.lock().unwrap(); + core.terminal.write( + b"\x1b[31mR\x1b[0m \x1b[38;5;171mI\x1b[0m \x1b[48;5;4mB\x1b[0m \x1b[38;2;1;2;3mT", + ); + } + + let backend = ratatui::backend::TestBackend::new(20, 5); + let mut terminal = ratatui::Terminal::new(backend).unwrap(); + terminal + .draw(|frame| pane.render(frame, Rect::new(0, 0, 20, 5), false)) + .unwrap(); + + let buffer = terminal.backend().buffer(); + assert_eq!(buffer[(0, 0)].symbol(), "R"); + assert_eq!(buffer[(0, 0)].style().fg, Some(Color::Indexed(1))); + assert_eq!(buffer[(2, 0)].symbol(), "I"); + assert_eq!(buffer[(2, 0)].style().fg, Some(Color::Indexed(171))); + assert_eq!(buffer[(4, 0)].symbol(), "B"); + assert_eq!(buffer[(4, 0)].style().bg, Some(Color::Indexed(4))); + assert_eq!(buffer[(6, 0)].symbol(), "T"); + assert_eq!(buffer[(6, 0)].style().fg, Some(Color::Rgb(1, 2, 3))); + } + + #[test] + fn render_preserves_palette_background_fill_cells() { + let (tx, _rx) = mpsc::channel(4); + let terminal = crate::ghostty::Terminal::new(20, 5, 0).unwrap(); + let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap(); + { + let mut core = pane.core.lock().unwrap(); + core.terminal.write(b"\x1b[48;5;4m\x1b[K"); + } + + let backend = ratatui::backend::TestBackend::new(20, 5); + let mut terminal = ratatui::Terminal::new(backend).unwrap(); + terminal + .draw(|frame| pane.render(frame, Rect::new(0, 0, 20, 5), false)) + .unwrap(); + + let buffer = terminal.backend().buffer(); + for x in 0..20 { + assert_eq!(buffer[(x, 0)].symbol(), " "); + assert_eq!(buffer[(x, 0)].style().bg, Some(Color::Indexed(4))); + } + } + + #[test] + fn render_preserves_rgb_background_fill_cells() { + let (tx, _rx) = mpsc::channel(4); + let terminal = crate::ghostty::Terminal::new(20, 5, 0).unwrap(); + let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap(); + { + let mut core = pane.core.lock().unwrap(); + core.terminal.write(b"\x1b[48;2;17;34;51m\x1b[K"); + } + + let backend = ratatui::backend::TestBackend::new(20, 5); + let mut terminal = ratatui::Terminal::new(backend).unwrap(); + terminal + .draw(|frame| pane.render(frame, Rect::new(0, 0, 20, 5), false)) + .unwrap(); + + let buffer = terminal.backend().buffer(); + for x in 0..20 { + assert_eq!(buffer[(x, 0)].symbol(), " "); + assert_eq!(buffer[(x, 0)].style().bg, Some(Color::Rgb(17, 34, 51))); + } + } + #[test] fn render_leaves_host_default_background_transparent() { let (tx, _rx) = mpsc::channel(4);