aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-04-11 23:53:21 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-04-11 23:53:21 +0300
commit90e51f704bd1799fd9f26673991fd89cf52b13e1 (patch)
treea16d1619b633bd2baf69712d077a9461ed7e4310 /src
parent6598c88d35268eec8f0902799152b34680f44fdd (diff)
feat: support transparent panel chrome
Diffstat (limited to 'src')
-rw-r--r--src/config.rs16
-rw-r--r--src/main.rs3
-rw-r--r--src/pane.rs95
-rw-r--r--src/ui.rs67
4 files changed, 162 insertions, 19 deletions
diff --git a/src/config.rs b/src/config.rs
index b57d67f4..ce317d33 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -750,11 +750,16 @@ pub struct Keybinds {
}
/// Parse a color string into a ratatui Color.
-/// Supports: hex (#rrggbb, #rgb), named colors, rgb(r,g,b).
+/// Supports: hex (#rrggbb, #rgb), named colors, rgb(r,g,b), and reset aliases.
pub fn parse_color(s: &str) -> ratatui::style::Color {
use ratatui::style::Color;
let s = s.trim().to_lowercase();
+ match s.as_str() {
+ "reset" | "default" | "none" | "transparent" => return Color::Reset,
+ _ => {}
+ }
+
// Hex: #rrggbb or #rgb
if let Some(hex) = s.strip_prefix('#') {
if hex.len() == 6 {
@@ -1481,6 +1486,15 @@ name = "dracula"
}
#[test]
+ fn parse_color_accepts_reset_aliases() {
+ use ratatui::style::Color;
+
+ for value in ["reset", "default", "none", "transparent"] {
+ assert_eq!(parse_color(value), Color::Reset, "value: {value}");
+ }
+ }
+
+ #[test]
fn theme_custom_overrides_parse() {
let toml = r##"
[theme]
diff --git a/src/main.rs b/src/main.rs
index 7256df66..8779fef1 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -84,8 +84,9 @@ const DEFAULT_CONFIG: &str = r##"# herdr configuration
# name = "catppuccin"
# Override individual color tokens on top of the base theme.
-# Accepts: hex (#rrggbb), named colors, or rgb(r,g,b)
+# Accepts: hex (#rrggbb), named colors, rgb(r,g,b), or panel_bg = "reset"
# [theme.custom]
+# panel_bg = "reset"
# accent = "#f5c2e7"
# red = "#ff6188"
# green = "#a6e3a1"
diff --git a/src/pane.rs b/src/pane.rs
index 3ae7a560..c0146e39 100644
--- a/src/pane.rs
+++ b/src/pane.rs
@@ -1006,6 +1006,7 @@ impl GhosttyPaneTerminal {
let Ok(mut core) = self.core.lock() else {
return;
};
+ let host_theme = core.host_terminal_theme;
let GhosttyPaneCore {
terminal,
render_state,
@@ -1015,7 +1016,7 @@ impl GhosttyPaneTerminal {
return;
}
let colors = render_state.colors().ok();
- let default_bg = colors.map(|c| ghostty_color(c.background));
+ 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 mut row_iterator = match crate::ghostty::RowIterator::new() {
@@ -1593,6 +1594,25 @@ fn ghostty_cell_style(
style.add_modifier(modifiers)
}
+fn ghostty_default_bg(
+ color: crate::ghostty::RgbColor,
+ host_theme: crate::terminal_theme::TerminalTheme,
+) -> Option<Color> {
+ if host_theme.background == Some(terminal_theme_color(color)) {
+ None
+ } else {
+ Some(ghostty_color(color))
+ }
+}
+
+fn terminal_theme_color(color: crate::ghostty::RgbColor) -> crate::terminal_theme::RgbColor {
+ crate::terminal_theme::RgbColor {
+ r: color.r,
+ g: color.g,
+ b: color.b,
+ }
+}
+
fn ghostty_color(color: crate::ghostty::RgbColor) -> Color {
Color::Rgb(color.r, color.g, color.b)
}
@@ -2868,6 +2888,79 @@ mod tests {
assert_eq!(pane_default_theme(&pane).foreground, host_theme.foreground);
}
+ #[test]
+ fn render_leaves_host_default_background_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();
+ core.terminal.write(b"hi");
+ }
+
+ 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(), "h");
+ assert_eq!(buffer[(0, 0)].style().bg, Some(Color::Reset));
+ assert_eq!(buffer[(2, 0)].symbol(), " ");
+ assert_eq!(buffer[(2, 0)].style().bg, Some(Color::Reset));
+ }
+
+ #[test]
+ fn render_keeps_explicit_default_background_when_it_differs_from_host() {
+ 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();
+ core.terminal.write(b"\x1b]11;rgb:44/55/66\x1b\\hi");
+ }
+
+ 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 expected_bg = Some(Color::Rgb(0x44, 0x55, 0x66));
+ assert_eq!(buffer[(0, 0)].symbol(), "h");
+ assert_eq!(buffer[(0, 0)].style().bg, expected_bg);
+ assert_eq!(buffer[(2, 0)].symbol(), " ");
+ assert_eq!(buffer[(2, 0)].style().bg, expected_bg);
+ }
+
#[tokio::test]
async fn focus_events_are_forwarded_when_enabled() {
let (tx, mut rx) = mpsc::channel(4);
diff --git a/src/ui.rs b/src/ui.rs
index 4401d231..e08158e1 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -758,7 +758,7 @@ fn render_tab_bar(app: &AppState, frame: &mut Frame, area: Rect) {
}
let active = idx == ws.active_tab;
let style = if active {
- let base = Style::default().fg(p.panel_bg).bg(p.accent);
+ let base = Style::default().fg(panel_contrast_fg(p)).bg(p.accent);
if tab.is_auto_named() {
base.add_modifier(Modifier::DIM)
} else {
@@ -1705,7 +1705,7 @@ fn render_selection_highlight(
for x in 0..inner.width {
if sel.contains(y, x, scroll_metrics) {
let cell = &mut buf[(inner.x + x, inner.y + y)];
- cell.set_style(Style::default().fg(p.panel_bg).bg(p.blue));
+ cell.set_style(Style::default().fg(panel_contrast_fg(p)).bg(p.blue));
}
}
}
@@ -1784,6 +1784,13 @@ fn render_panel_shell(
Some(inner)
}
+fn panel_contrast_fg(p: &Palette) -> Color {
+ match p.panel_bg {
+ Color::Reset => p.surface_dim,
+ color => color,
+ }
+}
+
pub(crate) fn centered_popup_rect(area: Rect, popup_w: u16, popup_h: u16) -> Option<Rect> {
let popup_w = popup_w.min(area.width.saturating_sub(4));
let popup_h = popup_h.min(area.height.saturating_sub(2));
@@ -1926,7 +1933,7 @@ fn render_release_notes_overlay(app: &AppState, frame: &mut Frame, area: Rect) {
Some("esc"),
"close",
Style::default()
- .fg(app.palette.panel_bg)
+ .fg(panel_contrast_fg(&app.palette))
.bg(app.palette.accent)
.add_modifier(Modifier::BOLD),
);
@@ -2224,7 +2231,7 @@ fn render_onboarding_welcome(app: &AppState, frame: &mut Frame, area: Rect) {
Some("↵"),
"continue",
Style::default()
- .fg(app.palette.panel_bg)
+ .fg(panel_contrast_fg(&app.palette))
.bg(app.palette.accent)
.add_modifier(Modifier::BOLD),
);
@@ -2279,7 +2286,7 @@ fn render_onboarding_notifications(app: &AppState, frame: &mut Frame, area: Rect
let prefix = if selected { "›" } else { " " };
let style = if selected {
Style::default()
- .fg(app.palette.panel_bg)
+ .fg(panel_contrast_fg(&app.palette))
.bg(app.palette.accent)
} else {
Style::default().fg(app.palette.text)
@@ -2308,7 +2315,7 @@ fn render_onboarding_notifications(app: &AppState, frame: &mut Frame, area: Rect
Some("↵"),
"start",
Style::default()
- .fg(app.palette.panel_bg)
+ .fg(panel_contrast_fg(&app.palette))
.bg(app.palette.accent)
.add_modifier(Modifier::BOLD),
);
@@ -2322,7 +2329,7 @@ fn render_navigate_overlay(app: &AppState, frame: &mut Frame, area: Rect) {
let dim = Style::default().fg(app.palette.overlay0);
let mode_style = Style::default()
- .fg(app.palette.panel_bg)
+ .fg(panel_contrast_fg(&app.palette))
.bg(app.palette.accent)
.add_modifier(Modifier::BOLD);
@@ -2403,7 +2410,7 @@ fn render_global_launcher_menu(app: &AppState, frame: &mut Frame) {
let selected = idx == app.global_menu.highlighted;
let style = if selected {
Style::default()
- .fg(app.palette.panel_bg)
+ .fg(panel_contrast_fg(&app.palette))
.bg(app.palette.accent)
.add_modifier(Modifier::BOLD)
} else {
@@ -2563,7 +2570,7 @@ fn render_keybind_help_overlay(app: &AppState, frame: &mut Frame) {
Some("esc"),
"close",
Style::default()
- .fg(app.palette.panel_bg)
+ .fg(panel_contrast_fg(&app.palette))
.bg(app.palette.accent)
.add_modifier(Modifier::BOLD),
);
@@ -2636,7 +2643,7 @@ fn render_resize_overlay(app: &AppState, frame: &mut Frame, area: Rect) {
let dim = Style::default().fg(app.palette.overlay0);
let mode_style = Style::default()
- .fg(app.palette.panel_bg)
+ .fg(panel_contrast_fg(&app.palette))
.bg(app.palette.mauve)
.add_modifier(Modifier::BOLD);
@@ -2734,7 +2741,7 @@ fn render_rename_overlay(app: &AppState, frame: &mut Frame, area: Rect) {
Some("↵"),
"save",
Style::default()
- .fg(app.palette.panel_bg)
+ .fg(panel_contrast_fg(&app.palette))
.bg(app.palette.accent)
.add_modifier(Modifier::BOLD),
);
@@ -2832,7 +2839,7 @@ fn render_confirm_close_overlay(app: &AppState, frame: &mut Frame, area: Rect) {
Some("↵"),
"confirm",
Style::default()
- .fg(app.palette.panel_bg)
+ .fg(panel_contrast_fg(&app.palette))
.bg(app.palette.red)
.add_modifier(Modifier::BOLD),
);
@@ -2991,7 +2998,7 @@ fn render_settings_overlay(app: &AppState, frame: &mut Frame, area: Rect) {
.style(Style::default().fg(p.overlay1))
.highlight_style(
Style::default()
- .fg(p.panel_bg)
+ .fg(panel_contrast_fg(p))
.bg(p.accent)
.add_modifier(Modifier::BOLD),
)
@@ -3048,7 +3055,7 @@ fn render_settings_overlay(app: &AppState, frame: &mut Frame, area: Rect) {
Some("↵"),
"apply",
Style::default()
- .fg(p.panel_bg)
+ .fg(panel_contrast_fg(p))
.bg(p.accent)
.add_modifier(Modifier::BOLD),
);
@@ -3182,7 +3189,7 @@ fn render_context_menu(app: &AppState, frame: &mut Frame) {
.highlight_style(
Style::default()
.bg(p.accent)
- .fg(p.panel_bg)
+ .fg(panel_contrast_fg(p))
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(" ");
@@ -3271,7 +3278,7 @@ fn render_config_diagnostic(frame: &mut Frame, area: Rect, message: &str, p: &Pa
Paragraph::new(Span::styled(
text,
Style::default()
- .fg(p.panel_bg)
+ .fg(panel_contrast_fg(p))
.bg(p.yellow)
.add_modifier(Modifier::BOLD),
)),
@@ -3413,6 +3420,34 @@ mod tests {
}
#[test]
+ fn tab_bar_uses_surface_dim_when_panel_background_resets() {
+ let mut app = crate::app::state::AppState::test_new();
+ let mut ws = Workspace::test_new("test");
+ let custom_tab = ws.test_add_tab(Some("logs"));
+ ws.switch_tab(custom_tab);
+
+ app.palette.panel_bg = Color::Reset;
+ app.workspaces = vec![ws];
+ app.active = Some(0);
+ app.selected = 0;
+ app.mode = Mode::Terminal;
+
+ compute_view(&mut app, Rect::new(0, 0, 80, 20));
+
+ let backend = TestBackend::new(80, 20);
+ let mut terminal = Terminal::new(backend).unwrap();
+ terminal.draw(|frame| render(&app, frame)).unwrap();
+ let buffer = terminal.backend().buffer();
+
+ let custom_rect = app.view.tab_hit_areas[1];
+ let custom_style = buffer[(custom_rect.x + 1, custom_rect.y)].style();
+
+ assert_eq!(custom_style.bg, Some(app.palette.accent));
+ assert_eq!(custom_style.fg, Some(app.palette.surface_dim));
+ assert!(custom_style.add_modifier.contains(Modifier::BOLD));
+ }
+
+ #[test]
fn tab_bar_shows_scroll_controls_when_tabs_overflow() {
let mut app = crate::app::state::AppState::test_new();
let mut ws = Workspace::test_new("test");