feat(tui): add "Terminal" theme that fully inherits the host terminal's colors

Adds a new selectable theme `terminal` (alongside System / Whale / Whale
Light / Grayscale / Catppuccin / Tokyo Night / Dracula / Gruvbox) that
paints every surface with `Color::Reset` instead of any RGB so the
host terminal's own background, foreground, and palette show through.

The existing `system` theme only chose between two RGB themes (Whale dark
or Whale Light) based on COLORFGBG / macOS appearance — useful, but it
still painted brand-colored RGB surfaces. Users with custom terminal
themes (Solarized, Nord, transparent backgrounds, custom Ghostty/iTerm
schemes) had no way to make the TUI respect their terminal palette.

Implementation:
- New `TERMINAL_UI_THEME` const where every `*_bg` and most text slots
  are `Color::Reset`, and accents (mode_agent/yolo/plan, status_working,
  status_warning) use ANSI named colors so they also inherit the user's
  terminal palette rather than DeepSeek brand RGB.
- `ThemeId::Terminal` plumbed through `from_name` / `name` /
  `display_name` / `tagline` / `ui_theme` / `SELECTABLE_THEMES`, and
  registered in `normalize_theme_name` with aliases `term`,
  `transparent`, `follow-terminal`, `inherit` so existing
  user-friendly config strings just work.
- `theme_remap_active(Terminal) → true` so the existing per-cell remap
  in `ColorCompatBackend` rewrites every hard-coded palette constant
  (`DEEPSEEK_INK`, `DEEPSEEK_SLATE`, `BORDER_COLOR`, `TEXT_BODY`, …) to
  `Color::Reset`. Without this, the many render sites that reach for
  the named palette constants directly would still paint brand RGB.
- `theme_green` / `theme_red` return `Color::Green` / `Color::Red`
  for Terminal so diff "+"/"−" stay green/red but follow the user's
  terminal palette.
- `theme_diff_added_bg` / `theme_diff_deleted_bg` return `Color::Reset`
  for Terminal — diff highlight is conveyed by foreground color only.
- The new theme is the second entry in `SELECTABLE_THEMES` (right after
  System) so it surfaces prominently in the `/theme` picker.

theme_picker tests: the new theme is inserted in row 2 of
`SELECTABLE_THEMES`, which shifts the indices three existing tests
relied on — `arrow_down_previews_next_theme`,
`enter_commits_with_persist_true`, and `digit_jumps_to_row` — so those
expectations are updated to match the new ordering. No production
behavior change in those tests, just index arithmetic.

Default (`theme = "system"`) is unchanged; existing users see no
difference. Users who want full terminal pass-through opt in via
`/theme` or `theme = "terminal"` in settings.toml.
This commit is contained in:
liushiao
2026-05-20 16:39:25 +08:00
committed by Hunter Bown
parent 95e13155a5
commit 0ea84dce7d
2 changed files with 52 additions and 4 deletions
+48 -1
View File
@@ -825,6 +825,39 @@ pub const DRACULA_UI_THEME: UiTheme = UiTheme {
tool_failed: Color::Rgb(0xff, 0x55, 0x55), // red
};
/// "Terminal" theme: lets the host terminal's color scheme show through
/// instead of painting any RGB surface. Backgrounds use `Color::Reset`
/// (the terminal's own default bg) and most text uses `Color::Reset`
/// (terminal's own default fg). Accents are ANSI named colors so they
/// also inherit the user's terminal palette (Solarized, Nord, custom
/// schemes, etc.) rather than DeepSeek brand RGB.
pub const TERMINAL_UI_THEME: UiTheme = UiTheme {
name: "terminal",
// Mode is reported as Dark to avoid the dark→light cell remap kicking
// in; the terminal-theme cell remap already normalizes everything to
// `Color::Reset`, and we never want a second pass overwriting that.
mode: PaletteMode::Dark,
surface_bg: Color::Reset,
panel_bg: Color::Reset,
elevated_bg: Color::Reset,
composer_bg: Color::Reset,
selection_bg: Color::Reset,
header_bg: Color::Reset,
footer_bg: Color::Reset,
mode_agent: Color::Blue,
mode_yolo: Color::Red,
mode_plan: Color::Yellow,
status_ready: Color::Reset,
status_working: Color::Cyan,
status_warning: Color::Yellow,
text_dim: Color::Reset,
text_hint: Color::Reset,
text_muted: Color::Reset,
text_body: Color::Reset,
text_soft: Color::Reset,
border: Color::Reset,
};
pub const GRUVBOX_DARK_UI_THEME: UiTheme = UiTheme {
name: "gruvbox-dark",
mode: PaletteMode::Dark,
@@ -874,6 +907,7 @@ pub const GRUVBOX_DARK_UI_THEME: UiTheme = UiTheme {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ThemeId {
System,
Terminal,
Whale,
WhaleLight,
Grayscale,
@@ -891,6 +925,7 @@ impl ThemeId {
pub fn from_name(value: &str) -> Option<Self> {
match normalize_theme_name(value)? {
"system" => Some(Self::System),
"terminal" => Some(Self::Terminal),
"dark" => Some(Self::Whale),
"light" => Some(Self::WhaleLight),
"grayscale" => Some(Self::Grayscale),
@@ -908,6 +943,7 @@ impl ThemeId {
pub const fn name(self) -> &'static str {
match self {
Self::System => "system",
Self::Terminal => "terminal",
Self::Whale => "dark",
Self::WhaleLight => "light",
Self::Grayscale => "grayscale",
@@ -923,6 +959,7 @@ impl ThemeId {
pub const fn display_name(self) -> &'static str {
match self {
Self::System => "System",
Self::Terminal => "Terminal",
Self::Whale => "Whale (Dark)",
Self::WhaleLight => "Whale Light",
Self::Grayscale => "Grayscale",
@@ -938,6 +975,9 @@ impl ThemeId {
pub const fn tagline(self) -> &'static str {
match self {
Self::System => "Follow terminal background (COLORFGBG / macOS appearance)",
Self::Terminal => {
"Inherit terminal colors fully (transparent surfaces, ANSI accents)"
}
Self::Whale => "Whale dark — deep navy & gold",
Self::WhaleLight => "DeepSeek light, paper-ish",
Self::Grayscale => "Color-minimal high contrast",
@@ -956,6 +996,7 @@ impl ThemeId {
pub fn ui_theme(self) -> UiTheme {
match self {
Self::System => UiTheme::detect(),
Self::Terminal => TERMINAL_UI_THEME,
Self::Whale => UI_THEME,
Self::WhaleLight => LIGHT_UI_THEME,
Self::Grayscale => GRAYSCALE_UI_THEME,
@@ -970,6 +1011,7 @@ impl ThemeId {
/// Themes shown in the `/theme` picker, in display order.
pub const SELECTABLE_THEMES: &[ThemeId] = &[
ThemeId::System,
ThemeId::Terminal,
ThemeId::Whale,
ThemeId::WhaleLight,
ThemeId::Grayscale,
@@ -1012,6 +1054,7 @@ impl UiTheme {
pub fn normalize_theme_name(value: &str) -> Option<&'static str> {
match value.trim().to_ascii_lowercase().as_str() {
"" | "auto" | "system" | "default" => Some("system"),
"terminal" | "term" | "transparent" | "follow-terminal" | "inherit" => Some("terminal"),
"dark" | "whale" | "whale-dark" => Some("dark"),
"light" | "whale-light" => Some("light"),
"grayscale" | "greyscale" | "gray" | "grey" | "mono" | "monochrome" | "black-white"
@@ -1189,7 +1232,11 @@ const fn theme_diff_deleted_bg(ui: &UiTheme) -> Color {
pub const fn theme_remap_active(theme: ThemeId) -> bool {
matches!(
theme,
ThemeId::CatppuccinMocha | ThemeId::TokyoNight | ThemeId::Dracula | ThemeId::GruvboxDark
ThemeId::Terminal
| ThemeId::CatppuccinMocha
| ThemeId::TokyoNight
| ThemeId::Dracula
| ThemeId::GruvboxDark
)
}
+4 -3
View File
@@ -317,7 +317,7 @@ mod tests {
let mut v = ThemePickerView::new("system".to_string());
let action = v.handle_key(key(KeyCode::Down));
assert!(matches!(action, ViewAction::Emit(_)));
assert_eq!(selected_name(&action), Some(ThemeId::Whale.name()));
assert_eq!(selected_name(&action), Some(ThemeId::Terminal.name()));
}
#[test]
@@ -337,6 +337,7 @@ mod tests {
v.handle_key(key(KeyCode::Down));
v.handle_key(key(KeyCode::Down));
v.handle_key(key(KeyCode::Down));
v.handle_key(key(KeyCode::Down));
v.handle_key(key(KeyCode::Down)); // -> CatppuccinMocha
let action = v.handle_key(key(KeyCode::Enter));
match action {
@@ -376,8 +377,8 @@ mod tests {
#[test]
fn digit_jumps_to_row() {
let mut v = ThemePickerView::new("system".to_string());
let action = v.handle_key(key(KeyCode::Char('5')));
// Row 5 (1-indexed) -> index 4 -> CatppuccinMocha
let action = v.handle_key(key(KeyCode::Char('6')));
// Row 6 (1-indexed) -> index 5 -> CatppuccinMocha
assert_eq!(
selected_name(&action),
Some(ThemeId::CatppuccinMocha.name())