aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorYifeng He <yfhe.prsn@gmail.com>2026-04-21 14:49:24 -0700
committerGitHub <noreply@github.com>2026-04-22 00:49:24 +0300
commitd2d1cc79ea0390644c52609ebbf54f0e3a22e5a8 (patch)
tree5763981e4f9af145e9eeb7dbfc6b4450bb88c5ba /src
parent16ac5bffdabd53fb1c3f39c95840a06c615823b4 (diff)
fix: resolve transparent bg before inverse swap to prevent invisible text (#45)
Shells like zsh highlight pasted text using reverse video (SGR 7). The render pipeline makes the default background transparent (None) when it matches the host terminal background. When inverse swaps fg and bg, the transparent bg becomes fg=None (Color::Reset), which the host terminal renders as its default foreground — the same hue as the new bg, making the text invisible. Resolve None to the actual terminal background color before swapping so inverse produces correct contrasting colors. Fixes ogulcancelik/herdr#44 Co-authored-by: Can Celik <ogulcancelik@gmail.com>
Diffstat (limited to 'src')
-rw-r--r--src/pane/terminal.rs54
1 files changed, 53 insertions, 1 deletions
diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs
index de4781b4..c688dcc9 100644
--- a/src/pane/terminal.rs
+++ b/src/pane/terminal.rs
@@ -554,6 +554,7 @@ impl GhosttyPaneTerminal {
let colors = render_state.colors().ok();
let default_bg = colors.and_then(|c| ghostty_default_bg(c.background, host_theme));
let default_fg = colors.map(|c| ghostty_color(c.foreground));
+ let resolved_bg = colors.map(|c| ghostty_color(c.background));
let mut row_iterator = match crate::ghostty::RowIterator::new() {
Ok(iterator) => iterator,
@@ -580,7 +581,7 @@ impl GhosttyPaneTerminal {
let mut x = 0u16;
while x < area.width && cells.next() {
let wide = cells.wide().unwrap_or(crate::ghostty::CellWide::Narrow);
- let style = ghostty_cell_style(&cells, default_fg, default_bg);
+ let style = ghostty_cell_style(&cells, default_fg, default_bg, resolved_bg);
let symbol = match ghostty_buffer_symbol_into(
&cells,
wide,
@@ -836,6 +837,7 @@ fn ghostty_cell_style(
cells: &crate::ghostty::RowCellIter<'_>,
default_fg: Option<Color>,
default_bg: Option<Color>,
+ resolved_bg: Option<Color>,
) -> Style {
let style_data = cells.style().unwrap_or_default();
let mut fg = cells
@@ -854,6 +856,17 @@ fn ghostty_cell_style(
fg = bg.or(default_bg);
}
if style_data.inverse {
+ // When the background is transparent (None), resolve it to the
+ // actual terminal background color before swapping. Otherwise
+ // the swapped fg becomes None (Color::Reset) which the host
+ // terminal renders as its default foreground — the same hue as
+ // the new bg, making inverse text invisible.
+ if bg.is_none() {
+ bg = resolved_bg;
+ }
+ if fg.is_none() {
+ fg = default_fg;
+ }
std::mem::swap(&mut fg, &mut bg);
}
@@ -1440,6 +1453,45 @@ mod tests {
}
#[test]
+ fn render_inverse_text_swaps_fg_and_resolved_bg_when_bg_is_transparent() {
+ let (tx, _rx) = mpsc::channel(4);
+ let terminal = crate::ghostty::Terminal::new(20, 5, 0).unwrap();
+ let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap();
+ let host_theme = crate::terminal_theme::TerminalTheme {
+ foreground: Some(crate::terminal_theme::RgbColor {
+ r: 0xaa,
+ g: 0xbb,
+ b: 0xcc,
+ }),
+ background: Some(crate::terminal_theme::RgbColor {
+ r: 0x11,
+ g: 0x22,
+ b: 0x33,
+ }),
+ };
+ pane.apply_host_terminal_theme(host_theme);
+ {
+ let mut core = pane.core.lock().unwrap();
+ // SGR 7 enables inverse/reverse video
+ core.terminal.write(b"\x1b[7mhi\x1b[27m");
+ }
+
+ 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();
+ let cell = &buffer[(0, 0)];
+ assert_eq!(cell.symbol(), "h");
+ // After inverse: fg should be the resolved bg, bg should be the original fg.
+ // fg must NOT be Color::Reset (which would be the same hue as bg).
+ assert_eq!(cell.style().fg, Some(Color::Rgb(0x11, 0x22, 0x33)));
+ assert_eq!(cell.style().bg, Some(Color::Rgb(0xaa, 0xbb, 0xcc)));
+ }
+
+ #[test]
fn trim_trailing_blank_rows_drops_empty_viewport_tail() {
let mut rows = vec!["hello".to_string(), "".to_string(), " ".to_string()];
trim_trailing_blank_rows(&mut rows);