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}