rat_theme4/
lib.rs

1//!
2//! SalsaTheme provides a styling system for ratatui apps.
3//!
4//! It has a simple flat naming scheme.
5//!
6//! But it can store
7//! * [ratatui Style](ratatui::style::Style)
8//! * composite styles as used by [rat-widget](rat_widget).
9//!   eg [CheckboxStyle](rat_widget::checkbox::CheckboxStyle)
10//! * practically anything else.
11//!
12//! ## Naming styles
13//!
14//! * It has an extension trait for [Style](ratatui::style::Style) that
15//!   adds constants for known styles. In the same manner you can add your
16//!   application specific styles and have them with code completion.
17//!
18//! * For [rat-widget](rat_widget) composite style it defines an anchor struct
19//!   [WidgetStyle] that performs the same purpose.
20//!
21//! ## Usage
22//!
23//! ```rust
24//! # use ratatui::buffer::Buffer;
25//! # use ratatui::layout::Rect;
26//! # use ratatui::style::Style;
27//! # use ratatui::widgets::StatefulWidget;
28//! # use rat_theme4::theme::{SalsaTheme};
29//! # use rat_theme4::{StyleName, WidgetStyle};
30//! # use rat_theme4::palettes::dark::BLACKOUT;
31//! # use rat_widget::checkbox::{Checkbox, CheckboxState, CheckboxStyle};
32//! # let theme = SalsaTheme::default();
33//! # let area = Rect::default();
34//! # let mut buf = Buffer::default();
35//! # let buf = &mut buf;
36//! # let mut state = CheckboxState::default();
37//!
38//! // ratatui Style
39//! let s = theme.style::<Style>(Style::SELECT);
40//!
41//! // composite style
42//! Checkbox::new()
43//!     .styles(theme.style(WidgetStyle::CHECKBOX))
44//!     .render(area, buf, &mut state);
45//! ```
46
47use ratatui::style::{Color, Style};
48use std::collections::HashMap;
49use std::sync::OnceLock;
50use std::sync::atomic::{AtomicBool, Ordering};
51
52pub mod palette;
53pub mod theme;
54
55/// Currently shipped palettes.
56pub mod palettes {
57    pub mod core;
58    pub mod dark;
59    pub mod light;
60}
61
62pub mod themes {
63    mod core;
64    mod dark;
65    mod fallback;
66    mod shell;
67
68    /// Create a `core` theme that acts as a fallback.
69    /// It uses the SHELL palette and set almost no backgrounds.
70    pub use core::create_core;
71    /// Creates a `dark` theme.
72    pub use dark::create_dark;
73    /// Create the `fallback` theme.
74    /// This is more for testing widgets than anything else.
75    /// It just uses `Default::default()` for any style.
76    /// This helps to check if a widget is still functional
77    /// if no styling is applied.
78    pub use fallback::create_fallback;
79    /// Creates a `shell` theme. This uses the dark palettes,
80    /// but sets almost no backgrounds. Instead, it lets the
81    /// terminal background shine.
82    pub use shell::create_shell;
83}
84
85/// Anchor struct for the names of composite styles used
86/// by rat-widget's.
87///
88/// Use as
89/// ```rust
90/// # use ratatui::style::Style;
91/// # use rat_theme4::theme::{SalsaTheme};
92/// # use rat_theme4::{ StyleName, WidgetStyle};
93/// # use rat_theme4::palettes::dark::BLACKOUT;
94/// # use rat_widget::checkbox::CheckboxStyle;
95/// # let theme = SalsaTheme::default();
96///
97/// let s: CheckboxStyle = theme.style(WidgetStyle::CHECKBOX);
98/// ```
99/// or more likely
100/// ```rust
101/// # use ratatui::buffer::Buffer;
102/// # use ratatui::layout::Rect;
103/// # use ratatui::style::Style;
104/// # use ratatui::widgets::StatefulWidget;
105/// # use rat_theme4::theme::{SalsaTheme};
106/// # use rat_theme4::{ StyleName, WidgetStyle};
107/// # use rat_theme4::palettes::dark::BLACKOUT;
108/// # use rat_widget::checkbox::{Checkbox, CheckboxState, CheckboxStyle};
109/// # let theme = SalsaTheme::default();
110/// # let area = Rect::default();
111/// # let mut buf = Buffer::default();
112/// # let buf = &mut buf;
113/// # let mut state = CheckboxState::default();
114///
115/// Checkbox::new()
116///     .styles(theme.style(WidgetStyle::CHECKBOX))
117///     .render(area, buf, &mut state);
118/// ```
119pub struct WidgetStyle;
120
121impl WidgetStyle {
122    pub const BUTTON: &'static str = "button";
123    pub const CALENDAR: &'static str = "calendar";
124    pub const CHECKBOX: &'static str = "checkbox";
125    pub const CHOICE: &'static str = "choice";
126    pub const CLIPPER: &'static str = "clipper";
127    pub const COLOR_INPUT: &'static str = "color-input";
128    pub const COMBOBOX: &'static str = "combobox";
129    pub const DIALOG_FRAME: &'static str = "dialog-frame";
130    pub const FILE_DIALOG: &'static str = "file-dialog";
131    pub const FORM: &'static str = "form";
132    pub const LINE_NR: &'static str = "line-nr";
133    pub const LIST: &'static str = "list";
134    pub const MENU: &'static str = "menu";
135    pub const MONTH: &'static str = "month";
136    pub const MSG_DIALOG: &'static str = "msg-dialog";
137    pub const PARAGRAPH: &'static str = "paragraph";
138    pub const RADIO: &'static str = "radio";
139    pub const SCROLL: &'static str = "scroll";
140    pub const SCROLL_DIALOG: &'static str = "scroll.dialog";
141    pub const SCROLL_POPUP: &'static str = "scroll.popup";
142    pub const SHADOW: &'static str = "shadow";
143    pub const SLIDER: &'static str = "slider";
144    pub const SPLIT: &'static str = "split";
145    pub const STATUSLINE: &'static str = "statusline";
146    pub const TABBED: &'static str = "tabbed";
147    pub const TABLE: &'static str = "table";
148    pub const TEXT: &'static str = "text";
149    pub const TEXTAREA: &'static str = "textarea";
150    pub const TEXTVIEW: &'static str = "textview";
151    pub const VIEW: &'static str = "view";
152}
153
154/// Extension trait for [Style](ratatui::style::Style) that defines
155/// some standard names used by rat-theme/rat-widget
156///
157/// Use as
158/// ```rust
159/// # use ratatui::style::Style;
160/// # use rat_theme4::theme::{SalsaTheme};
161/// # use rat_theme4::{ StyleName, WidgetStyle};
162/// # use rat_theme4::palettes::dark::BLACKOUT;
163/// # let theme = SalsaTheme::default();
164///
165/// let s: Style = theme.style(Style::INPUT);
166/// ```
167pub trait StyleName {
168    const LABEL_FG: &'static str = "label-fg";
169    const INPUT: &'static str = "input";
170    const FOCUS: &'static str = "focus";
171    const SELECT: &'static str = "select";
172    const DISABLED: &'static str = "disabled";
173    const INVALID: &'static str = "invalid";
174    const HOVER: &'static str = "hover";
175    const TITLE: &'static str = "title";
176    const HEADER: &'static str = "header";
177    const FOOTER: &'static str = "footer";
178    const SHADOWS: &'static str = "shadows";
179    const WEEK_HEADER_FG: &'static str = "week-header-fg";
180    const MONTH_HEADER_FG: &'static str = "month-header-fg";
181    const TEXT_FOCUS: &'static str = "text-focus";
182    const TEXT_SELECT: &'static str = "text-select";
183    const KEY_BINDING: &'static str = "key-binding";
184    const BUTTON_BASE: &'static str = "button-base";
185    const MENU_BASE: &'static str = "menu-base";
186    const STATUS_BASE: &'static str = "status-base";
187
188    const CONTAINER_BASE: &'static str = "container-base";
189    const CONTAINER_BORDER_FG: &'static str = "container-border-fg";
190    const CONTAINER_ARROW_FG: &'static str = "container-arrows-fg";
191
192    const POPUP_BASE: &'static str = "popup-base";
193    const POPUP_BORDER_FG: &'static str = "popup-border-fg";
194    const POPUP_ARROW_FG: &'static str = "popup-arrow-fg";
195
196    const DIALOG_BASE: &'static str = "dialog-base";
197    const DIALOG_BORDER_FG: &'static str = "dialog-border-fg";
198    const DIALOG_ARROW_FG: &'static str = "dialog-arrow-fg";
199}
200impl StyleName for Style {}
201
202///
203/// Extension trait for [Color](ratatui::style::Color) that defines
204/// standard names used by rat-theme to define color-aliases.
205///
206/// Use as
207/// ```rust
208/// # use ratatui::style::{Style, Color};
209/// # use rat_theme4::theme::{SalsaTheme};
210/// # use rat_theme4::RatWidgetColor;
211/// # let theme = SalsaTheme::default();
212///
213/// let c: Color = theme.p.color_alias(Color::LABEL_FG);
214/// ```
215pub trait RatWidgetColor {
216    const LABEL_FG: &'static str = "label.fg";
217    const INPUT_BG: &'static str = "input.bg";
218    const FOCUS_BG: &'static str = "focus.bg";
219    const SELECT_BG: &'static str = "select.bg";
220    const DISABLED_BG: &'static str = "disabled.bg";
221    const INVALID_BG: &'static str = "invalid.bg";
222    const HOVER_BG: &'static str = "hover.bg";
223    const TITLE_FG: &'static str = "title.fg";
224    const TITLE_BG: &'static str = "title.bg";
225    const HEADER_FG: &'static str = "header.fg";
226    const HEADER_BG: &'static str = "header.bg";
227    const FOOTER_FG: &'static str = "footer.fg";
228    const FOOTER_BG: &'static str = "footer.bg";
229    const SHADOW_BG: &'static str = "shadow.bg";
230    const WEEK_HEADER_FG: &'static str = "week-header.fg";
231    const MONTH_HEADER_FG: &'static str = "month-header.fg";
232    const TEXT_FOCUS_BG: &'static str = "text-focus.bg";
233    const TEXT_SELECT_BG: &'static str = "text-select.bg";
234    const BUTTON_BASE_BG: &'static str = "button-base.bg";
235    const MENU_BASE_BG: &'static str = "menu-base.bg";
236    const KEY_BINDING_BG: &'static str = "key-binding.bg";
237    const STATUS_BASE_BG: &'static str = "status-base.bg";
238    const CONTAINER_BASE_BG: &'static str = "container-base.bg";
239    const CONTAINER_BORDER_FG: &'static str = "container-border.fg";
240    const CONTAINER_ARROW_FG: &'static str = "container-arrow.fg";
241    const POPUP_BASE_BG: &'static str = "popup-base.bg";
242    const POPUP_BORDER_FG: &'static str = "popup-border.fg";
243    const POPUP_ARROW_FG: &'static str = "popup-arrow.fg";
244    const DIALOG_BASE_BG: &'static str = "dialog-base.bg";
245    const DIALOG_BORDER_FG: &'static str = "dialog-border.fg";
246    const DIALOG_ARROW_FG: &'static str = "dialog-arrow.fg";
247}
248impl RatWidgetColor for Color {}
249
250static LOG_DEFINES: AtomicBool = AtomicBool::new(false);
251
252/// Log style definition.
253/// May help debugging styling problems ...
254pub fn log_style_define(log: bool) {
255    LOG_DEFINES.store(log, Ordering::Release);
256}
257
258fn is_log_style_define() -> bool {
259    LOG_DEFINES.load(Ordering::Acquire)
260}
261
262const PALETTE_DEF: &str = include_str!("themes.ini");
263
264#[derive(Debug)]
265struct Def {
266    palette: Vec<&'static str>,
267    theme: Vec<&'static str>,
268    theme_init: HashMap<&'static str, (&'static str, &'static str)>,
269}
270
271static THEMES: OnceLock<Def> = OnceLock::new();
272
273fn init_themes() -> Def {
274    let mut palette = Vec::new();
275    let mut theme = Vec::new();
276    let mut theme_init = HashMap::new();
277
278    for l in PALETTE_DEF.lines() {
279        if !l.contains('=') {
280            continue;
281        }
282
283        let mut it = l.split(['=', ',']);
284        let Some(name) = it.next() else {
285            continue;
286        };
287        let Some(cat) = it.next() else {
288            continue;
289        };
290        let Some(pal) = it.next() else {
291            continue;
292        };
293        let name = name.trim();
294        let cat = cat.trim();
295        let pal = pal.trim();
296
297        if pal != "None" {
298            if !palette.contains(&pal) {
299                palette.push(pal);
300            }
301        }
302        if name != "Blackout" && name != "Fallback" {
303            if !theme.contains(&name) {
304                theme.push(name);
305            }
306        }
307        theme_init.insert(name, (cat, pal));
308    }
309
310    let d = Def {
311        palette,
312        theme,
313        theme_init,
314    };
315    d
316}
317
318/// All defined color palettes.
319pub fn salsa_palettes() -> Vec<&'static str> {
320    let themes = THEMES.get_or_init(init_themes);
321    themes.palette.clone()
322}
323
324/// Create one of the defined palettes.
325///
326/// The available palettes can be queried by [salsa_palettes].
327///
328/// Currently known: Imperial, Radium, Tundra, Ocean, Monochrome,
329/// Black&White, Monekai, Solarized, OxoCarbon, EverForest,
330/// Nord, Rust, Material, Tailwind, VSCode, Reds, Blackout,
331/// Shell, Imperial Light, EverForest Light, Tailwind Light,
332/// Rust Light.
333pub fn create_palette(name: &str) -> Option<palette::Palette> {
334    use crate::palettes::core;
335    use crate::palettes::dark;
336    use crate::palettes::light;
337    match name {
338        "Imperial" => Some(dark::IMPERIAL),
339        "Radium" => Some(dark::RADIUM),
340        "Tundra" => Some(dark::TUNDRA),
341        "Ocean" => Some(dark::OCEAN),
342        "Monochrome" => Some(dark::MONOCHROME),
343        "Black&White" => Some(dark::BLACK_WHITE),
344        "Monekai" => Some(dark::MONEKAI),
345        "Solarized" => Some(dark::SOLARIZED),
346        "OxoCarbon" => Some(dark::OXOCARBON),
347        "EverForest" => Some(dark::EVERFOREST),
348        "Nord" => Some(dark::NORD),
349        "Rust" => Some(dark::RUST),
350        "Material" => Some(dark::MATERIAL),
351        "Tailwind" => Some(dark::TAILWIND),
352        "VSCode" => Some(dark::VSCODE),
353        "Reds" => Some(dark::REDS),
354        "Blackout" => Some(dark::BLACKOUT),
355        "Shell" => Some(core::SHELL),
356        "Imperial Light" => Some(light::IMPERIAL),
357        "EverForest Light" => Some(light::EVERFOREST),
358        "Tailwind Light" => Some(light::TAILWIND),
359        "Rust Light" => Some(light::RUST),
360        _ => None,
361    }
362}
363
364/// All defined rat-salsa themes.
365pub fn salsa_themes() -> Vec<&'static str> {
366    let themes = THEMES.get_or_init(init_themes);
367    themes.theme.clone()
368}
369
370/// Create one of the defined themes.
371///
372/// The available themes can be queried by [salsa_themes].
373///
374/// Currently known: Imperial Dark, Radium Dark, Tundra Dark,
375/// Ocean Dark, Monochrome Dark, Black&White Dark, Monekai Dark,
376/// Solarized Dark, OxoCarbon Dark, EverForest Dark, Nord Dark,
377/// Rust Dark, Material Dark, Tailwind Dark, VSCode Dark,
378/// Imperial Light, EverForest Light, Tailwind Light, Rust Light,
379/// Imperial Shell, Radium Shell, Tundra Shell, Ocean Shell,
380/// Monochrome Shell, Black&White Shell, Monekai Shell,
381/// Solarized Shell, OxoCarbon Shell, EverForest Shell, Nord Shell,
382/// Rust Shell, Material Shell, Tailwind Shell, VSCode Shell,
383/// Shell, Blackout and Fallback.
384pub fn create_theme(theme: &str) -> theme::SalsaTheme {
385    let themes = THEMES.get_or_init(init_themes);
386    let Some(def) = themes.theme_init.get(&theme) else {
387        if cfg!(debug_assertions) {
388            panic!("no theme {:?}", theme);
389        } else {
390            return themes::create_core(theme);
391        }
392    };
393    match def {
394        ("dark", p) => {
395            let Some(pal) = create_palette(*p) else {
396                if cfg!(debug_assertions) {
397                    panic!("no palette {:?}", *p);
398                } else {
399                    return themes::create_core(theme);
400                }
401            };
402            themes::create_dark(theme, pal)
403        }
404        ("light", p) => {
405            let Some(pal) = create_palette(*p) else {
406                if cfg!(debug_assertions) {
407                    panic!("no palette {:?}", *p);
408                } else {
409                    return themes::create_core(theme);
410                }
411            };
412            // currently no difference, just a different
413            // set of color palettes
414            themes::create_dark(theme, pal)
415        }
416        ("shell", p) => {
417            let Some(pal) = create_palette(*p) else {
418                if cfg!(debug_assertions) {
419                    panic!("no palette {:?}", *p);
420                } else {
421                    return themes::create_core(theme);
422                }
423            };
424            themes::create_shell(theme, pal)
425        }
426        ("core", _) => themes::create_core(theme),
427        ("blackout", _) => themes::create_dark(theme, palettes::dark::BLACKOUT),
428        ("fallback", _) => themes::create_fallback(theme, palettes::dark::REDS),
429        _ => {
430            if cfg!(debug_assertions) {
431                panic!("no theme {:?}", theme);
432            } else {
433                themes::create_core(theme)
434            }
435        }
436    }
437}