commit f6829bf02ce67a9a7de647eaff6983bcd05d4a12
parent 85a303fa45a6b1c42c91a9314ee638c2497e916d
Author: Matthew Gantenbein <ganten1998@gmail.com>
Date: Mon, 1 Jun 2026 23:19:52 -0500
themes: "Abyssal" default (distinct from transparent) + persist transparency on start
Three fixes:
1. Transparency now persists across launches. Bug: picking a theme saved only its
name, so a stale `transparent` toggle in ui.toml clobbered a transparent
theme's own setting on load — it came back opaque. save_active now records the
theme's transparency too; load() applies it. Regression-tested.
2. Deleted the old `default` theme — it duplicated `transparent`'s slate palette.
3. New built-in default "Abyssal": Hydra's signature deep-sea palette (abyssal
blue-green, bioluminescent seafoam highlight, coral accent) — fits the
water-serpent. Shipped as both the built-in and an editable themes/abyssal.toml.
by_name prefers a user abyssal.toml if present; "default" stays a back-compat
alias. Still 10 themes.
30 tests green (added: transparent-loads-transparent, abyssal-distinct), 0 warns.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat:
5 files changed, 86 insertions(+), 50 deletions(-)
diff --git a/crates/hydra/src/app.rs b/crates/hydra/src/app.rs
@@ -125,7 +125,9 @@ impl App {
let Some((names, sel)) = self.theme_picker.as_ref() else { return };
let Some(name) = names.get(*sel).cloned() else { return };
self.theme = Theme::by_name(&name);
- crate::theme::save_active(&name);
+ // Persist name + the theme's own transparency, so a transparent theme reloads
+ // transparent on next launch (not clobbered by a stale toggle).
+ crate::theme::save_active(&name, self.theme.transparent);
self.theme_picker = None;
self.status = format!("theme → {name}");
}
diff --git a/crates/hydra/src/theme.rs b/crates/hydra/src/theme.rs
@@ -46,21 +46,22 @@ impl Theme {
}
impl Default for Theme {
- /// Neutral built-in palette: a calm dark slate with a teal highlight and amber accent.
- /// Intentionally generic and personal-palette-free so the repo ships nothing branded.
+ /// "Abyssal" — Hydra's signature default: the deep, where the water-serpent lives.
+ /// Abyssal blue-green depths, a bioluminescent seafoam highlight, a coral accent.
+ /// Distinct from `transparent`'s neutral slate (which it used to duplicate).
fn default() -> Self {
Theme {
- name: "default".to_string(),
- bg: rgb(0x16, 0x18, 0x1d), // slate-900
- bg_elevated: rgb(0x22, 0x26, 0x2e), // slate-800
- fg: rgb(0xe6, 0xe8, 0xea), // near-white
- fg_dim: rgb(0x8a, 0x90, 0x99), // muted grey
- accent: rgb(0xe0, 0x9b, 0x3e), // amber
- ghost: rgb(0x4c, 0xc2, 0xb0), // teal highlight
- border: rgb(0x3a, 0x40, 0x4a), // slate border
- success: rgb(0x6c, 0xc6, 0x6c), // green
- warning: rgb(0xe0, 0x9b, 0x3e), // amber
- danger: rgb(0xe0, 0x6c, 0x6c), // red
+ name: "abyssal".to_string(),
+ bg: rgb(0x07, 0x14, 0x16), // abyssal depth (near-black teal)
+ bg_elevated: rgb(0x10, 0x24, 0x28), // submerged surface
+ fg: rgb(0xd6, 0xe8, 0xe4), // sea-foam white
+ fg_dim: rgb(0x6a, 0x8c, 0x88), // drowned grey-green
+ accent: rgb(0xe8, 0x84, 0x5c), // coral (the lone warm spark in the deep)
+ ghost: rgb(0x3f, 0xd9, 0xb8), // bioluminescent seafoam (highlight)
+ border: rgb(0x1c, 0x38, 0x3c), // deep current
+ success: rgb(0x4f, 0xd4, 0x9a), // phosphor green
+ warning: rgb(0xe0, 0xb0, 0x50), // amber glow
+ danger: rgb(0xf0, 0x5c, 0x78), // coral red
transparent: false,
}
}
@@ -118,10 +119,13 @@ impl Theme {
dirs::home_dir().map(|h| h.join(".config").join("hydra").join("themes"))
}
+ /// The built-in default theme's name. Always present in the picker; has no file.
+ pub const BUILTIN_NAME: &'static str = "abyssal";
+
/// List available theme names (file stems) from the themes folder, sorted, with the
- /// built-in "default" always first.
+ /// built-in "abyssal" always first.
pub fn available() -> Vec<String> {
- let mut names = vec!["default".to_string()];
+ let mut names = vec![Self::BUILTIN_NAME.to_string()];
if let Some(dir) = Self::themes_dir() {
if let Ok(rd) = std::fs::read_dir(&dir) {
let mut found: Vec<String> = rd
@@ -132,7 +136,7 @@ impl Theme {
.then(|| p.file_stem().and_then(|s| s.to_str()).map(String::from))
.flatten()
})
- .filter(|n| n != "default")
+ .filter(|n| n != Self::BUILTIN_NAME)
.collect();
found.sort();
names.extend(found);
@@ -141,16 +145,19 @@ impl Theme {
names
}
- /// Load a theme by name: "default" → built-in; otherwise `themes/<name>.toml`.
+ /// Load a theme by name. A matching `themes/<name>.toml` wins (so abyssal.toml is
+ /// editable); otherwise the built-in (for the built-in name, "default" back-compat
+ /// alias, or any unknown name) is used.
pub fn by_name(name: &str) -> Self {
- if name == "default" {
- return Theme::default();
- }
- Self::themes_dir()
+ if let Some(t) = Self::themes_dir()
.map(|d| d.join(format!("{name}.toml")))
.filter(|p| p.exists())
.and_then(|p| Self::from_file(&p).ok())
- .unwrap_or_default()
+ {
+ return t;
+ }
+ // No file: built-in name / "default" alias / unknown all fall back to the built-in.
+ Theme::default()
}
/// Parse a theme file, keeping the built-in default for any unspecified key. The theme's
@@ -261,10 +268,13 @@ impl UiSettings {
}
}
-/// Persist the chosen theme name (keeps the current transparency setting).
-pub fn save_active(name: &str) {
+/// Persist the chosen theme name AND its transparency, so the next launch loads exactly
+/// what the user is looking at. (Picking a `transparent = true` theme must come back
+/// transparent — recording the name alone let a stale toggle override it.)
+pub fn save_active(name: &str, transparent: bool) {
let mut s = UiSettings::load();
s.theme = Some(name.to_string());
+ s.transparent = Some(transparent);
s.save();
}
@@ -308,4 +318,30 @@ mod tests {
assert_eq!(t.bg, Theme::default().bg); // untouched ⇒ default
let _ = std::fs::remove_dir_all(&dir);
}
+
+ #[test]
+ fn transparent_theme_file_loads_transparent() {
+ // Regression: a `transparent = true` theme must come back transparent (the bug was a
+ // stale persisted toggle clobbering it; we now persist the theme's own value).
+ let dir = std::env::temp_dir().join(format!("hydra-theme-tr-{}", std::process::id()));
+ std::fs::create_dir_all(&dir).unwrap();
+ let path = dir.join("glass.toml");
+ std::fs::write(&path, "transparent = true\n[palette]\nfg = \"#ffffff\"\n").unwrap();
+ let t = Theme::from_file(&path).unwrap();
+ assert!(t.transparent, "transparent=true in file must load as transparent");
+ assert_eq!(t.surface(), Color::Reset, "transparent theme paints Color::Reset");
+ let opaque = Theme::default();
+ assert_eq!(opaque.surface(), opaque.bg, "opaque theme paints its bg");
+ let _ = std::fs::remove_dir_all(&dir);
+ }
+
+ #[test]
+ fn builtin_default_is_abyssal_and_distinct_from_transparent() {
+ // The default is now "abyssal" — and must NOT duplicate the transparent theme's
+ // palette (that duplication was why the old `default` got deleted).
+ let d = Theme::default();
+ assert_eq!(d.name, "abyssal");
+ // abyssal's bg is the deep teal, not the old shared slate #16181d.
+ assert_eq!(d.bg, Color::Rgb(0x07, 0x14, 0x16));
+ }
}
diff --git a/themes/README.md b/themes/README.md
@@ -8,11 +8,12 @@ no config syntax to learn beyond hex colors.
```sh
mkdir -p ~/.config/hydra/themes
-cp themes/default.toml ~/.config/hydra/themes/mytheme.toml
+cp themes/abyssal.toml ~/.config/hydra/themes/mytheme.toml
# edit the hex values, then in Hydra press `t` and pick "mytheme"
```
-The files in *this* folder (`default.toml`, `transparent.toml`) are examples — copy them
+The files in *this* folder (`abyssal.toml` = the built-in default, `transparent.toml`, …)
+are examples — copy them
into your config folder to use/edit them.
## Format
diff --git a/themes/abyssal.toml b/themes/abyssal.toml
@@ -0,0 +1,20 @@
+# Hydra theme — "Abyssal", the built-in default, written out as an editable example.
+#
+# The deep where the water-serpent lives: abyssal blue-green depths, a bioluminescent
+# seafoam highlight, a coral accent. This file mirrors Hydra's built-in default — edit a
+# copy in ~/.config/hydra/themes/ to make it yours. Every key is optional.
+
+name = "abyssal"
+transparent = false
+
+[palette]
+bg = "#071416" # abyssal depth (near-black teal)
+bg_elevated = "#102428" # submerged surface — overlays, selected rows
+fg = "#d6e8e4" # sea-foam white
+fg_dim = "#6a8c88" # drowned grey-green — hints, secondary text
+accent = "#e8845c" # coral — the lone warm spark (selected output, marks)
+ghost = "#3fd9b8" # bioluminescent seafoam — titles, cursor, active selection
+border = "#1c383c" # deep current — pane borders
+success = "#4fd49a" # phosphor green
+warning = "#e0b050" # amber glow
+danger = "#f05c78" # coral red — clip, recording, errors
diff --git a/themes/default.toml b/themes/default.toml
@@ -1,23 +0,0 @@
-# Hydra theme — the built-in neutral default, written out as an editable example.
-#
-# To add your own: copy any .toml into ~/.config/hydra/themes/ and it appears in the TUI
-# theme picker (press `t`). Every key is optional — unspecified keys keep the default below.
-# Colors are #rrggbb (the leading # is optional). btop-style: drop a file, it just works.
-
-name = "default"
-
-# Let the terminal's own background show through (e.g. Ghostty transparency/vibrancy).
-# Also toggleable live in the TUI with `T`. UI chrome stays opaque for legibility.
-transparent = false
-
-[palette]
-bg = "#16181d" # main background (ignored when transparent = true)
-bg_elevated = "#22262e" # overlays / selected rows
-fg = "#e6e8ea" # primary text
-fg_dim = "#8a9099" # secondary text, hints
-accent = "#e09b3e" # sparingly: selected output, marks
-ghost = "#4cc2b0" # highlight: titles, cursor, active selection
-border = "#3a404a" # pane borders
-success = "#6cc66c" # connected, active route
-warning = "#e09b3e" # approaching clip
-danger = "#e06c6c" # disconnected, clip, recording