diff options
| author | Yifeng He <yfhe.prsn@gmail.com> | 2026-04-21 14:49:24 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-04-22 00:49:24 +0300 |
| commit | d2d1cc79ea0390644c52609ebbf54f0e3a22e5a8 (patch) | |
| tree | 5763981e4f9af145e9eeb7dbfc6b4450bb88c5ba /src/pane | |
| parent | 16ac5bffdabd53fb1c3f39c95840a06c615823b4 (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/pane')
| -rw-r--r-- | src/pane/terminal.rs | 54 |
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); |
