diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-11 23:53:21 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-11 23:53:21 +0300 |
| commit | 90e51f704bd1799fd9f26673991fd89cf52b13e1 (patch) | |
| tree | a16d1619b633bd2baf69712d077a9461ed7e4310 /src | |
| parent | 6598c88d35268eec8f0902799152b34680f44fdd (diff) | |
feat: support transparent panel chrome
Diffstat (limited to 'src')
| -rw-r--r-- | src/config.rs | 16 | ||||
| -rw-r--r-- | src/main.rs | 3 | ||||
| -rw-r--r-- | src/pane.rs | 95 | ||||
| -rw-r--r-- | src/ui.rs | 67 |
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); @@ -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"); |
