Skip to main content

tear_types/
theme.rs

1//! Theme — colors + typography for tear's status bar / pane borders /
2//! message areas. Operators pick from a named theme or roll their own;
3//! the canonical fleet theme is DERIVED from `ishou_tokens::FleetDefaults`
4//! — the SAME source mado reads — so a palette change in ishou propagates
5//! to tear and mado together by construction (they can never drift).
6
7use serde::{Deserialize, Serialize};
8
9/// Per-tear theme. Stores hex strings rather than typed Color values
10/// so the serde wire format stays stable across renderer changes.
11/// At runtime the in-process backend resolves these via
12/// `ishou-tokens` semantics; the tmux backend writes them into the
13/// rendered tmux.conf as `colour#XXXXXX`.
14#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
15pub struct TearTheme {
16    /// Theme name — typically one of `"vellum"` (the fleet prescribed
17    /// default), `"nord"`, `"solarized-dark"`, `"gruvbox-dark"`, or
18    /// `"custom"` for inline-override themes.
19    pub name: String,
20    /// Foreground color (status bar text default).
21    pub fg: HexColor,
22    /// Background color (status bar background default).
23    pub bg: HexColor,
24    /// Accent for the active window's segment.
25    pub active_fg: HexColor,
26    pub active_bg: HexColor,
27    /// Accent for inactive windows.
28    pub inactive_fg: HexColor,
29    pub inactive_bg: HexColor,
30    /// Pane border color when the pane is focused.
31    pub border_active: HexColor,
32    /// Pane border color when the pane is not focused.
33    pub border_inactive: HexColor,
34    /// Message area (e.g. tmux `command-prompt` line) colors.
35    pub message_fg: HexColor,
36    pub message_bg: HexColor,
37}
38
39impl Default for TearTheme {
40    /// The fleet prescribed theme — DERIVED from
41    /// `ishou_tokens::FleetDefaults::prescribed()` (today: Vellum, the
42    /// warm aged-paper Nord-matte). This is the SAME source mado's
43    /// `FleetThemedConfig::from_fleet` reads, so tear and mado converge
44    /// on identical colors by construction. A fleet rebrand touches
45    /// `FleetDefaults::prescribed()` / `FleetTheme::prescribed_default()`
46    /// and moves both at the next compile.
47    fn default() -> Self {
48        Self::from_fleet(&ishou_tokens::FleetDefaults::prescribed())
49    }
50}
51
52impl TearTheme {
53    /// Derive a `TearTheme` from `ishou_tokens::FleetDefaults` — the
54    /// canonical fleet-themed constructor. Mirrors the pattern mado uses
55    /// in `impl FleetThemedConfig for MadoConfig` (mado/src/config.rs):
56    /// resolve the fleet theme to its BORN ishou tokens, then map each
57    /// surface onto tear's status-bar / pane-border / message fields.
58    ///
59    /// What derives from where (all from `fd.theme.resolve()`):
60    ///
61    /// | tear field                  | ishou `ResolvedTheme` source     |
62    /// |-----------------------------|----------------------------------|
63    /// | `name`                      | `resolved.name`                  |
64    /// | `fg` / `inactive_fg`        | `resolved.foreground`            |
65    /// | `bg`                        | `resolved.background`            |
66    /// | `active_bg` / `border_active` | `ansi_16[14]` (bright cyan / frost) |
67    /// | `active_fg` / `message_fg`  | `resolved.background` (inverse)  |
68    /// | `inactive_bg`               | `ansi_16[0]` (surface / night)   |
69    /// | `border_inactive`           | `ansi_16[8]` (bright black / divider) |
70    /// | `message_bg`                | `ansi_16[3]` (yellow / aurora)   |
71    ///
72    /// The active/message accents pick the SAME semantic slots the old
73    /// hard-coded `nord()` used (frost-2 → ANSI bright-cyan, aurora-yellow
74    /// → ANSI yellow), so the *roles* are preserved while the actual hex
75    /// now flows from the fleet truth.
76    #[must_use]
77    pub fn from_fleet(fd: &ishou_tokens::FleetDefaults) -> Self {
78        let resolved = fd.theme.resolve();
79        let ansi = &resolved.ansi_16;
80        Self {
81            name: resolved.name.clone(),
82            fg: HexColor(resolved.foreground.clone()),
83            bg: HexColor(resolved.background.clone()),
84            // Active window: inverse text (bg) on the frost/cyan accent.
85            active_fg: HexColor(resolved.background.clone()),
86            active_bg: HexColor(ansi[14].clone()), // bright cyan (frost)
87            // Inactive window: foreground text on the night surface.
88            inactive_fg: HexColor(resolved.foreground.clone()),
89            inactive_bg: HexColor(ansi[0].clone()), // ANSI-0 surface (night)
90            border_active: HexColor(ansi[14].clone()), // bright cyan (frost)
91            border_inactive: HexColor(ansi[8].clone()), // bright black (divider)
92            // Message line: inverse text (bg) on the aurora-yellow accent.
93            message_fg: HexColor(resolved.background.clone()),
94            message_bg: HexColor(ansi[3].clone()), // yellow (aurora)
95        }
96    }
97
98    /// Classic-Nord named theme (Polar Night / Snow Storm / Frost). This
99    /// is the EXPLICIT `"nord"` palette an operator selects by name — NOT
100    /// the fleet default (which is `from_fleet`, today Vellum). Kept as a
101    /// first-class option so `name = "nord"` resolves to true classic Nord
102    /// rather than the warm Vellum matte. Values mirror
103    /// `ishou_tokens` Nord (`PlemeDark`/`ResolvedTheme::pleme_dark`) intent.
104    #[must_use]
105    pub fn nord() -> Self {
106        let resolved = ishou_tokens::FleetTheme::PlemeDark.resolve();
107        let ansi = &resolved.ansi_16;
108        Self {
109            name: "nord".into(),
110            fg: HexColor(resolved.foreground.clone()),
111            bg: HexColor(resolved.background.clone()),
112            active_fg: HexColor(resolved.background.clone()),
113            active_bg: HexColor(ansi[14].clone()), // bright cyan (frost-0)
114            inactive_fg: HexColor(resolved.foreground.clone()),
115            inactive_bg: HexColor(ansi[0].clone()),
116            border_active: HexColor(ansi[14].clone()),
117            border_inactive: HexColor(ansi[8].clone()), // polar-night-3
118            message_fg: HexColor(resolved.background.clone()),
119            message_bg: HexColor(ansi[3].clone()), // aurora-yellow
120        }
121    }
122}
123
124/// Hex color — `"#rgb"` or `"#rrggbb"`. The transparent newtype keeps
125/// serde output as plain strings.
126#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
127#[serde(transparent)]
128pub struct HexColor(pub String);
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    /// The default theme is DERIVED from the fleet prescribed palette —
135    /// not a hand-pinned constant. This is the convergence guarantee:
136    /// touching `FleetDefaults::prescribed()` moves tear and mado together.
137    #[test]
138    fn default_equals_from_fleet_prescribed() {
139        let prescribed =
140            TearTheme::from_fleet(&ishou_tokens::FleetDefaults::prescribed());
141        assert_eq!(TearTheme::default(), prescribed);
142    }
143
144    /// Tear's default colors come from the SAME ishou `ResolvedTheme`
145    /// surfaces mado reads — proving the two converge by construction.
146    #[test]
147    fn default_colors_match_ishou_resolved_theme() {
148        let fd = ishou_tokens::FleetDefaults::prescribed();
149        let resolved = fd.theme.resolve();
150        let theme = TearTheme::default();
151
152        // Name + the load-bearing surfaces flow straight from ishou.
153        assert_eq!(theme.name, resolved.name);
154        assert_eq!(theme.fg.0, resolved.foreground);
155        assert_eq!(theme.bg.0, resolved.background);
156        // Accents pull the documented ANSI slots (frost/cyan, aurora-yellow).
157        assert_eq!(theme.active_bg.0, resolved.ansi_16[14]);
158        assert_eq!(theme.border_active.0, resolved.ansi_16[14]);
159        assert_eq!(theme.message_bg.0, resolved.ansi_16[3]);
160        // Today the fleet prescribed theme is Vellum (warm Nord-matte) —
161        // NOT classic Nord. Tear converges onto the fleet truth.
162        assert_eq!(resolved.name, "vellum");
163    }
164
165    /// The explicit `"nord"` named theme stays classic Nord (distinct
166    /// from the Vellum default) and still sources its hex from ishou.
167    #[test]
168    fn nord_is_classic_nord_distinct_from_default() {
169        let nord = TearTheme::nord();
170        assert_eq!(nord.name, "nord");
171        // Classic Nord background differs from the Vellum default.
172        assert_ne!(nord.bg, TearTheme::default().bg);
173    }
174}