Skip to main content

tca_ratatui/
styleset.rs

1//! A StyleSet is a collection of Ratatui Styles created from a TcaTheme.
2//!
3//! There are a number of preset styles covering many TUI needs, and more can
4//! be added as needed for a given application.
5use std::collections::HashMap;
6
7use ratatui::style::Style;
8
9use crate::TcaTheme;
10
11/// The main StyleSet struct with predefined styles as named fields
12#[derive(Debug, Clone)]
13pub struct StyleSet {
14    /// Human-readable theme name.
15    pub name: String,
16    /// Theme author name or contact. Empty string if not specified.
17    pub author: String,
18    /// `true` for dark themes, `false` for light themes.
19    pub is_dark: bool,
20    /// The primary style for normal text and other elements.
21    pub primary: Style,
22    /// A secondary style, for sidebars or other secondary elements.
23    pub secondary: Style,
24    /// A muted style for disabled/deselected elements.
25    pub muted: Style,
26    /// A style for borders and other decorative elements.
27    pub border: Style,
28    /// A style for disabled borders and other decorative elements.
29    pub border_muted: Style,
30    /// A style for selected text or elements.
31    pub selection: Style,
32    /// A cursor style
33    pub cursor: Style,
34    /// A disabled cursor style
35    pub cursor_muted: Style,
36    /// A style for errors
37    pub error: Style,
38    /// A style for warnings
39    pub warning: Style,
40    /// A style for informative text or elements such as spinners
41    pub info: Style,
42    /// It works!
43    pub success: Style,
44    /// A style for highlighted information or elements
45    pub highlight: Style,
46    /// URLs or other links
47    pub link: Style,
48    // User-defined extensions
49    custom: HashMap<String, Style>,
50}
51
52impl StyleSet {
53    /// Create a `StyleSet` from a theme name, with reasonable fallbacks.
54    ///
55    /// Accepts any common case format: `"Nord Dark"`, `"nord-dark"`, `"NordDark"`.
56    #[cfg(feature = "fs")]
57    pub fn from_name(name: &str) -> Self {
58        TcaTheme::from_name(name).into()
59    }
60
61    /// Create a light `StyleSet` from the user's configured light default.
62    ///
63    /// Fallback order:
64    /// 1. User configured light theme.
65    /// 2. Built-in default light theme.
66    #[cfg(feature = "fs")]
67    pub fn from_default_light_cfg() -> Self {
68        TcaTheme::from_default_light_cfg().into()
69    }
70
71    /// Add a custom style by name
72    pub fn insert_custom(&mut self, key: impl Into<String>, style: Style) {
73        self.custom.insert(key.into(), style);
74    }
75
76    /// Get a custom style by name
77    pub fn get_custom(&self, key: &str) -> Option<&Style> {
78        self.custom.get(key)
79    }
80}
81
82impl From<TcaTheme> for StyleSet {
83    fn from(value: TcaTheme) -> Self {
84        StyleSet {
85            name: value.meta.name.clone(),
86            author: value.meta.author.clone(),
87            is_dark: value.meta.dark,
88            primary: Style::default()
89                .bg(value.ui.bg_primary)
90                .fg(value.ui.fg_primary),
91            secondary: Style::default()
92                .bg(value.ui.bg_secondary)
93                .fg(value.ui.fg_secondary),
94            muted: Style::default()
95                .bg(value.ui.bg_primary)
96                .fg(value.ui.fg_muted),
97            border: Style::default()
98                .bg(value.ui.bg_primary)
99                .fg(value.ui.border_primary),
100            border_muted: Style::default()
101                .bg(value.ui.bg_primary)
102                .fg(value.ui.border_muted),
103            selection: Style::default()
104                .bg(value.ui.selection_bg)
105                .fg(value.ui.selection_fg),
106            cursor: Style::default()
107                .bg(value.ui.bg_primary)
108                .fg(value.ui.cursor_primary),
109            cursor_muted: Style::default()
110                .bg(value.ui.bg_primary)
111                .fg(value.ui.cursor_muted),
112            error: Style::default()
113                .bg(value.ui.bg_primary)
114                .fg(value.semantic.error),
115            warning: Style::default()
116                .bg(value.ui.bg_primary)
117                .fg(value.semantic.warning),
118            info: Style::default()
119                .bg(value.ui.bg_primary)
120                .fg(value.semantic.info),
121            success: Style::default()
122                .bg(value.ui.bg_primary)
123                .fg(value.semantic.success),
124            highlight: Style::default()
125                .bg(value.ui.bg_primary)
126                .fg(value.semantic.highlight),
127            link: Style::default()
128                .bg(value.ui.bg_primary)
129                .fg(value.semantic.link),
130            custom: HashMap::default(),
131        }
132    }
133}
134
135impl From<&TcaTheme> for StyleSet {
136    fn from(value: &TcaTheme) -> Self {
137        value.clone().into()
138    }
139}
140
141/// Returns the user's configured default theme as a `StyleSet`, falling back to the built-in default.
142///
143/// Reads `$XDG_CONFIG_HOME/tca/tca.toml` if the `fs` feature is enabled.
144/// For a guaranteed no-I/O default, convert directly from a `BuiltinTheme`.
145#[cfg(feature = "fs")]
146impl Default for StyleSet {
147    fn default() -> Self {
148        TcaTheme::default().into()
149    }
150}
151
152/// A cycling cursor over a collection of [`StyleSet`]s, backed by a [`TcaThemeCursor`].
153///
154/// Converts [`TcaTheme`] to [`StyleSet`] on access, so the full theme collection
155/// is held in memory as themes (not pre-converted styles).
156#[cfg(feature = "fs")]
157#[derive(Debug)]
158pub struct StyleSetCursor(crate::theme::TcaThemeCursor);
159
160#[cfg(feature = "fs")]
161impl StyleSetCursor {
162    /// Create a cursor from an arbitrary iterator of [`TcaTheme`]s.
163    pub fn new(themes: impl IntoIterator<Item = TcaTheme>) -> Self {
164        Self(crate::theme::TcaThemeCursor::new(themes))
165    }
166
167    /// All built-in themes.
168    pub fn with_builtins() -> Self {
169        Self(crate::theme::TcaThemeCursor::with_builtins())
170    }
171
172    /// User-installed themes only.
173    pub fn with_user_themes() -> Self {
174        Self(crate::theme::TcaThemeCursor::with_user_themes())
175    }
176
177    /// Built-ins + user themes.
178    pub fn with_all_themes() -> Self {
179        Self(crate::theme::TcaThemeCursor::with_all_themes())
180    }
181
182    /// The current [`StyleSet`] without advancing the cursor.
183    pub fn peek(&self) -> Option<StyleSet> {
184        self.0.peek().map(Into::into)
185    }
186
187    /// Advance to the next theme (wrapping) and return it as a [`StyleSet`].
188    #[allow(clippy::should_implement_trait)]
189    pub fn next(&mut self) -> Option<StyleSet> {
190        self.0.next().map(Into::into)
191    }
192
193    /// Move to the previous theme (wrapping) and return it as a [`StyleSet`].
194    pub fn prev(&mut self) -> Option<StyleSet> {
195        self.0.prev().map(Into::into)
196    }
197
198    /// Move the cursor to the theme matching `name` (slug-insensitive).
199    pub fn set_current(&mut self, name: &str) -> Option<StyleSet> {
200        self.0.set_current(name).map(Into::into)
201    }
202
203    /// Number of themes in the cursor.
204    pub fn len(&self) -> usize {
205        self.0.len()
206    }
207
208    /// Returns `true` if the cursor contains no themes.
209    pub fn is_empty(&self) -> bool {
210        self.0.is_empty()
211    }
212}