termcinema_engine/core/
theme.rs

1use crate::core::{ControlSpec, CursorSpec, StyleSpec};
2use once_cell::sync::Lazy;
3use std::convert::Into;
4use std::fmt;
5
6/// Represents a full visual theme preset.
7///
8/// A `ThemePreset` bundles together style, cursor, and control settings,
9/// each of which affects the appearance and animation behavior of the output.
10///
11/// Themes can be predefined or user-resolved via name matching.
12pub struct ThemePreset {
13    /// Display name of the theme (used for resolution and UI).
14    pub name: &'static str,
15
16    /// Font and color settings.
17    pub style: StyleSpec,
18
19    /// Cursor shape and animation behavior.
20    pub cursor: CursorSpec,
21
22    /// Typing speed and visual effects.
23    pub control: ControlSpec,
24}
25
26impl ThemePreset {
27    /// Clone the style component.
28    pub fn to_style(&self) -> StyleSpec {
29        self.style.clone()
30    }
31
32    /// Clone the cursor component.
33    pub fn to_cursor(&self) -> CursorSpec {
34        self.cursor.clone()
35    }
36
37    /// Clone the control component.
38    pub fn to_control(&self) -> ControlSpec {
39        self.control.clone()
40    }
41}
42
43/// Errors that can occur during theme resolution.
44#[derive(Debug)]
45pub enum ThemeError {
46    /// Theme not found by name.
47    NotFound(String),
48}
49
50impl fmt::Display for ThemeError {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        match self {
53            ThemeError::NotFound(name) => write!(f, "Theme `{}` not found.", name),
54        }
55    }
56}
57
58impl std::error::Error for ThemeError {}
59
60/// Resolves a theme by name (case-insensitive, underscores allowed).
61///
62/// If no name is provided, falls back to default config values.
63///
64/// Returns a triple: `(style, cursor, control)` used in rendering.
65pub fn resolve_theme(
66    name: Option<&str>,
67) -> Result<(StyleSpec, CursorSpec, ControlSpec), ThemeError> {
68    if let Some(name) = name {
69        match get_theme_by_name(name) {
70            Some(theme) => Ok((theme.to_style(), theme.to_cursor(), theme.to_control())),
71            None => Err(ThemeError::NotFound(name.to_string())),
72        }
73    } else {
74        Ok((
75            StyleSpec::default(),
76            CursorSpec::default(),
77            ControlSpec::default(),
78        ))
79    }
80}
81
82// β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€πŸŽ¨ Theme Collection ───────────────
83
84/// Retro green-on-black terminal preset.
85///
86/// Inspired by classic VT100-style terminals with green text on a black background.
87/// Uses the VGA8 font for a highly aligned, pixel-perfect retro aesthetic.
88pub static RETRO_TTY: Lazy<ThemePreset> = Lazy::new(|| ThemePreset {
89    name: "Retro TTY",
90    style: StyleSpec {
91        font_size: 18,
92        font_family: "PxPlus IBM VGA8".into(),
93        text_color: Some("#00FF00".into()),
94        background_color: Some("#000000".into()),
95    },
96    cursor: CursorSpec {
97        char: "|".into(),
98        offset_x: 20,
99        color: Some("#00FF00".into()),
100        blink: true,
101        blink_ms: 800,
102        opacity: 1.0,
103    },
104    control: ControlSpec {
105        frame_delay: Some(60),
106        fade_duration: Some(100),
107        ..Default::default()
108    },
109});
110
111/// DOS Classic theme β€” black background with gray text and Courier font.
112///
113/// Emulates traditional DOS environments, featuring a blinking underscore cursor.
114pub static DOS_CLASSIC: Lazy<ThemePreset> = Lazy::new(|| ThemePreset {
115    name: "DOS Classic",
116    style: StyleSpec {
117        font_size: 18,
118        font_family: "Courier New".into(),
119        text_color: Some("#C0C0C0".into()),
120        background_color: Some("#000000".into()),
121    },
122    cursor: CursorSpec {
123        char: "_".into(),
124        offset_x: 20,
125        color: None,
126        blink: true,
127        blink_ms: 700,
128        opacity: 1.0,
129    },
130    control: ControlSpec {
131        frame_delay: Some(70),
132        fade_duration: Some(100),
133        ..Default::default()
134    },
135});
136
137/// DOS Blue theme β€” blue background with white text.
138///
139/// Simulates classic UIs like `edit.com` or Norton Commander, with a block-style cursor.
140pub static DOS_BLUE: Lazy<ThemePreset> = Lazy::new(|| ThemePreset {
141    name: "DOS Blue",
142    style: StyleSpec {
143        font_size: 18,
144        font_family: "PxPlus IBM VGA8".into(),
145        text_color: Some("#FFFFFF".into()),
146        background_color: Some("#0000AA".into()), // VGA 蓝
147    },
148    cursor: CursorSpec {
149        char: "β–ˆ".into(),
150        offset_x: 20,
151        color: Some("#FFFFFF".into()),
152        blink: true,
153        blink_ms: 650,
154        opacity: 0.9,
155    },
156    control: ControlSpec {
157        frame_delay: Some(80),
158        fade_duration: Some(100),
159        ..Default::default()
160    },
161});
162
163/// Solarized Dark theme β€” low-contrast dark background with muted text colors.
164///
165/// Based on the popular Solarized palette, optimized for readability in low light.
166pub static SOLARIZED_DARK: Lazy<ThemePreset> = Lazy::new(|| ThemePreset {
167    name: "Solarized Dark",
168    style: StyleSpec {
169        font_size: 18,
170        font_family: "Fira Code".into(),
171        text_color: Some("#839496".into()),
172        background_color: Some("#002b36".into()),
173    },
174    cursor: CursorSpec {
175        char: "▏".into(),
176        offset_x: 20,
177        color: Some("#93a1a1".into()),
178        blink: true,
179        blink_ms: 850,
180        opacity: 0.9,
181    },
182    control: ControlSpec {
183        frame_delay: Some(60),
184        fade_duration: Some(100),
185        ..Default::default()
186    },
187});
188
189/// Gruvbox Dark theme β€” warm, earthy tones on a dark background.
190///
191/// Popular among Vim users and terminal customizers. Cursor color matches the accent.
192pub static GRUVBOX_DARK: Lazy<ThemePreset> = Lazy::new(|| ThemePreset {
193    name: "Gruvbox Dark",
194    style: StyleSpec {
195        font_size: 18,
196        font_family: "Source Code Pro".into(),
197        text_color: Some("#ebdbb2".into()),
198        background_color: Some("#282828".into()),
199    },
200    cursor: CursorSpec {
201        char: "▍".into(),
202        offset_x: 20,
203        color: Some("#fe8019".into()),
204        blink: true,
205        blink_ms: 750,
206        opacity: 1.0,
207    },
208    control: ControlSpec {
209        frame_delay: Some(60),
210        fade_duration: Some(100),
211        ..Default::default()
212    },
213});
214
215/// Light Shell theme β€” white background with soft gray text.
216///
217/// A clean, modern preset ideal for light-themed terminals or documentation demos.
218pub static LIGHT_SHELL: Lazy<ThemePreset> = Lazy::new(|| ThemePreset {
219    name: "Light Shell",
220    style: StyleSpec {
221        font_size: 18,
222        font_family: "Monospace".into(),
223        text_color: Some("#444444".into()),
224        background_color: Some("#ffffff".into()),
225    },
226    cursor: CursorSpec {
227        char: "|".into(),
228        offset_x: 20,
229        color: Some("#888888".into()),
230        blink: true,
231        blink_ms: 1000,
232        opacity: 0.7,
233    },
234    control: ControlSpec {
235        frame_delay: Some(60),
236        fade_duration: Some(100),
237        ..Default::default()
238    },
239});
240
241/// Solarized Light theme β€” balanced warm colors on a light background.
242///
243/// Designed for daylight use, retaining the Solarized visual philosophy.
244pub static SOLARIZED_LIGHT: Lazy<ThemePreset> = Lazy::new(|| ThemePreset {
245    name: "Solarized Light",
246    style: StyleSpec {
247        font_size: 18,
248        font_family: "Monospace".into(),
249        text_color: Some("#586e75".into()),
250        background_color: Some("#fdf6e3".into()),
251    },
252    cursor: CursorSpec {
253        char: "|".into(),
254        offset_x: 20,
255        color: Some("#657b83".into()),
256        blink: true,
257        blink_ms: 850,
258        opacity: 0.9,
259    },
260    control: ControlSpec {
261        frame_delay: Some(60),
262        fade_duration: Some(100),
263        ..Default::default()
264    },
265});
266
267/// Neon Night theme β€” dark background with bright magenta text.
268///
269/// Mimics neon signage aesthetics, suitable for retro-futuristic demos.
270pub static NEON_NIGHT: Lazy<ThemePreset> = Lazy::new(|| ThemePreset {
271    name: "Neon Night",
272    style: StyleSpec {
273        font_size: 18,
274        font_family: "Cascadia Mono".into(),
275        text_color: Some("#FF00FF".into()),
276        background_color: Some("#0f0f1a".into()),
277    },
278    cursor: CursorSpec {
279        char: "﹏".into(),
280        offset_x: 20,
281        color: Some("#FF00FF".into()),
282        blink: true,
283        blink_ms: 650,
284        opacity: 0.9,
285    },
286    control: ControlSpec {
287        frame_delay: Some(75),
288        fade_duration: Some(100),
289        ..Default::default()
290    },
291});
292
293/// Poke Terminal β€” inspired by GameBoy or PokΓ©mon-style interfaces.
294///
295/// Golden yellow on dark gray, with a lightning-shaped cursor for added personality.
296pub static POKE_TERMINAL: Lazy<ThemePreset> = Lazy::new(|| ThemePreset {
297    name: "Poke Terminal",
298    style: StyleSpec {
299        font_size: 18,
300        font_family: "Monospace".into(),
301        text_color: Some("#FFD700".into()),
302        background_color: Some("#2b2b2b".into()),
303    },
304    cursor: CursorSpec {
305        char: "⚑".into(), // ζˆ–θ€… "β—‰" / "∎"
306        offset_x: 30,
307        color: Some("#FFD700".into()),
308        blink: true,
309        blink_ms: 800,
310        opacity: 1.0,
311    },
312    control: ControlSpec {
313        frame_delay: Some(50),
314        fade_duration: Some(100),
315        ..Default::default()
316    },
317});
318
319/// Returns a list of all built-in theme presets.
320///
321/// Useful for CLI completions, GUI dropdowns, or documentation generation.
322pub fn all_presets() -> Vec<&'static ThemePreset> {
323    vec![
324        &RETRO_TTY,
325        &DOS_CLASSIC,
326        &DOS_BLUE,
327        &SOLARIZED_DARK,
328        &GRUVBOX_DARK,
329        &LIGHT_SHELL,
330        &SOLARIZED_LIGHT,
331        &NEON_NIGHT,
332        &POKE_TERMINAL,
333    ]
334}
335
336/// Looks up a theme by its display name (case-insensitive).
337///
338/// Accepts name normalization (`"Solarized_Light"` β†’ `"Solarized Light"`).
339/// Returns `None` if no match is found.
340pub fn get_theme_by_name(name: &str) -> Option<&'static ThemePreset> {
341    let normalized = name.to_lowercase().replace('_', " ");
342    all_presets()
343        .into_iter()
344        .find(|preset| preset.name.to_lowercase() == normalized)
345}