1use crate::theme::Category;
48use ratatui::style::{Color, Style};
49use std::borrow::Cow;
50use std::collections::HashMap;
51use std::error::Error;
52use std::fmt::{Display, Formatter};
53use std::io;
54use std::io::ErrorKind;
55use std::sync::OnceLock;
56use std::sync::atomic::{AtomicBool, Ordering};
57
58pub mod palette;
59pub mod theme;
60
61pub mod palettes {
63 pub mod core;
64 pub mod dark;
65 pub mod light;
66}
67
68pub mod themes {
69 mod core;
70 mod dark;
71 mod fallback;
72 mod shell;
73
74 pub use core::create_core;
77 pub use dark::create_dark;
79 pub use fallback::create_fallback;
85 pub use shell::create_shell;
89}
90
91pub struct WidgetStyle;
126
127impl WidgetStyle {
128 pub const BUTTON: &'static str = "button";
129 pub const CALENDAR: &'static str = "calendar";
130 pub const CHECKBOX: &'static str = "checkbox";
131 pub const CHOICE: &'static str = "choice";
132 pub const CLIPPER: &'static str = "clipper";
133 #[cfg(feature = "color_input")]
134 pub const COLOR_INPUT: &'static str = "color-input";
135 pub const COMBOBOX: &'static str = "combobox";
136 pub const DIALOG_FRAME: &'static str = "dialog-frame";
137 pub const FILE_DIALOG: &'static str = "file-dialog";
138 pub const FORM: &'static str = "form";
139 pub const LINE_NR: &'static str = "line-nr";
140 pub const LIST: &'static str = "list";
141 pub const MENU: &'static str = "menu";
142 pub const MONTH: &'static str = "month";
143 pub const MSG_DIALOG: &'static str = "msg-dialog";
144 pub const PARAGRAPH: &'static str = "paragraph";
145 pub const RADIO: &'static str = "radio";
146 pub const SCROLL: &'static str = "scroll";
147 pub const SCROLL_DIALOG: &'static str = "scroll.dialog";
148 pub const SCROLL_POPUP: &'static str = "scroll.popup";
149 pub const SHADOW: &'static str = "shadow";
150 pub const SLIDER: &'static str = "slider";
151 pub const SPLIT: &'static str = "split";
152 pub const STATUSLINE: &'static str = "statusline";
153 pub const TABBED: &'static str = "tabbed";
154 pub const TABLE: &'static str = "table";
155 pub const TEXT: &'static str = "text";
156 pub const TEXTAREA: &'static str = "textarea";
157 pub const TEXTVIEW: &'static str = "textview";
158 pub const VIEW: &'static str = "view";
159}
160
161pub trait StyleName {
175 const LABEL_FG: &'static str = "label-fg";
176 const INPUT: &'static str = "input";
177 const INPUT_FOCUS: &'static str = "text-focus";
178 const INPUT_SELECT: &'static str = "text-select";
179 const FOCUS: &'static str = "focus";
180 const SELECT: &'static str = "select";
181 const DISABLED: &'static str = "disabled";
182 const INVALID: &'static str = "invalid";
183
184 const TITLE: &'static str = "title";
185 const HEADER: &'static str = "header";
186 const FOOTER: &'static str = "footer";
187
188 const HOVER: &'static str = "hover";
189 const SHADOWS: &'static str = "shadows";
190
191 const WEEK_HEADER_FG: &'static str = "week-header-fg";
192 const MONTH_HEADER_FG: &'static str = "month-header-fg";
193
194 const KEY_BINDING: &'static str = "key-binding";
195 const BUTTON_BASE: &'static str = "button-base";
196 const MENU_BASE: &'static str = "menu-base";
197 const STATUS_BASE: &'static str = "status-base";
198
199 const CONTAINER_BASE: &'static str = "container-base";
200 const CONTAINER_BORDER_FG: &'static str = "container-border-fg";
201 const CONTAINER_ARROW_FG: &'static str = "container-arrows-fg";
202
203 const DOCUMENT_BASE: &'static str = "document-base";
204 const DOCUMENT_BORDER_FG: &'static str = "document-border-fg";
205 const DOCUMENT_ARROW_FG: &'static str = "document-arrows-fg";
206
207 const POPUP_BASE: &'static str = "popup-base";
208 const POPUP_BORDER_FG: &'static str = "popup-border-fg";
209 const POPUP_ARROW_FG: &'static str = "popup-arrow-fg";
210
211 const DIALOG_BASE: &'static str = "dialog-base";
212 const DIALOG_BORDER_FG: &'static str = "dialog-border-fg";
213 const DIALOG_ARROW_FG: &'static str = "dialog-arrow-fg";
214}
215impl StyleName for Style {}
216
217pub trait RatWidgetColor {
231 const LABEL_FG: &'static str = "label.fg";
232 const INPUT_BG: &'static str = "input.bg";
233 const INPUT_FOCUS_BG: &'static str = "input-focus.bg";
234 const INPUT_SELECT_BG: &'static str = "input-select.bg";
235 const FOCUS_BG: &'static str = "focus.bg";
236 const SELECT_BG: &'static str = "select.bg";
237 const DISABLED_BG: &'static str = "disabled.bg";
238 const INVALID_BG: &'static str = "invalid.bg";
239
240 const TITLE_FG: &'static str = "title.fg";
241 const TITLE_BG: &'static str = "title.bg";
242 const HEADER_FG: &'static str = "header.fg";
243 const HEADER_BG: &'static str = "header.bg";
244 const FOOTER_FG: &'static str = "footer.fg";
245 const FOOTER_BG: &'static str = "footer.bg";
246
247 const HOVER_BG: &'static str = "hover.bg";
248 const BUTTON_BASE_BG: &'static str = "button-base.bg";
249 const KEY_BINDING_BG: &'static str = "key-binding.bg";
250 const MENU_BASE_BG: &'static str = "menu-base.bg";
251 const STATUS_BASE_BG: &'static str = "status-base.bg";
252 const SHADOW_BG: &'static str = "shadow.bg";
253
254 const WEEK_HEADER_FG: &'static str = "week-header.fg";
255 const MONTH_HEADER_FG: &'static str = "month-header.fg";
256
257 const CONTAINER_BASE_BG: &'static str = "container-base.bg";
258 const CONTAINER_BORDER_FG: &'static str = "container-border.fg";
259 const CONTAINER_ARROW_FG: &'static str = "container-arrow.fg";
260 const DOCUMENT_BASE_BG: &'static str = "document-base.bg";
261 const DOCUMENT_BORDER_FG: &'static str = "document-border.fg";
262 const DOCUMENT_ARROW_FG: &'static str = "document-arrow.fg";
263 const POPUP_BASE_BG: &'static str = "popup-base.bg";
264 const POPUP_BORDER_FG: &'static str = "popup-border.fg";
265 const POPUP_ARROW_FG: &'static str = "popup-arrow.fg";
266 const DIALOG_BASE_BG: &'static str = "dialog-base.bg";
267 const DIALOG_BORDER_FG: &'static str = "dialog-border.fg";
268 const DIALOG_ARROW_FG: &'static str = "dialog-arrow.fg";
269}
270impl RatWidgetColor for Color {}
271
272static LOG_DEFINES: AtomicBool = AtomicBool::new(false);
273
274pub fn log_style_define(log: bool) {
277 LOG_DEFINES.store(log, Ordering::Release);
278}
279
280fn is_log_style_define() -> bool {
281 LOG_DEFINES.load(Ordering::Acquire)
282}
283
284const PALETTE_DEF: &str = include_str!("themes.ini");
285
286#[derive(Debug)]
287struct Def {
288 palette: Vec<&'static str>,
289 theme: Vec<&'static str>,
290 theme_init: HashMap<&'static str, (&'static str, &'static str)>,
291}
292
293static THEMES: OnceLock<Def> = OnceLock::new();
294
295fn init_themes() -> Def {
296 let mut palette = Vec::new();
297 let mut theme = Vec::new();
298 let mut theme_init = HashMap::new();
299
300 for l in PALETTE_DEF.lines() {
301 if !l.contains('=') {
302 continue;
303 }
304
305 let mut it = l.split(['=', ',']);
306 let Some(name) = it.next() else {
307 continue;
308 };
309 let Some(cat) = it.next() else {
310 continue;
311 };
312 let Some(pal) = it.next() else {
313 continue;
314 };
315 let name = name.trim();
316 let cat = cat.trim();
317 let pal = pal.trim();
318
319 if pal != "None" {
320 if !palette.contains(&pal) {
321 palette.push(pal);
322 }
323 }
324 if name != "Blackout" && name != "Fallback" {
325 if !theme.contains(&name) {
326 theme.push(name);
327 }
328 }
329 theme_init.insert(name, (cat, pal));
330 }
331
332 let d = Def {
333 palette,
334 theme,
335 theme_init,
336 };
337 d
338}
339
340pub fn salsa_palettes() -> Vec<&'static str> {
342 let themes = THEMES.get_or_init(init_themes);
343 themes.palette.clone()
344}
345
346#[derive(Debug)]
347pub struct LoadPaletteErr(u8);
348
349impl Display for LoadPaletteErr {
350 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
351 write!(f, "load palette failed: {}", self.0)
352 }
353}
354
355impl Error for LoadPaletteErr {}
356
357pub fn load_palette(mut r: impl std::io::Read) -> Result<palette::Palette, std::io::Error> {
359 let mut buf = String::new();
360 r.read_to_string(&mut buf)?;
361
362 enum S {
363 Start,
364 Recognize,
365 Color,
366 Reference,
367 Fail(u8),
368 }
369
370 let mut pal = palette::Palette::default();
371 let mut dark = 63u8;
372
373 let mut state = S::Start;
374 'm: for l in buf.lines() {
375 let l = l.trim();
376 match state {
377 S::Start => {
378 if l.trim() == "[palette]" {
379 state = S::Recognize;
380 } else {
381 state = S::Fail(1);
382 break 'm;
383 }
384 }
385 S::Recognize => {
386 if l == "[color]" {
387 state = S::Color;
388 } else if l.is_empty() || l.starts_with("#") {
389 } else if l.starts_with("name=") {
391 if let Some(name_str) = l.split('=').nth(1) {
392 pal.name = Cow::Owned(name_str.to_string());
393 }
394 } else if l.starts_with("docs=") {
395 } else if l.starts_with("dark") {
397 if let Some(dark_str) = l.split('=').nth(1) {
398 if let Ok(v) = dark_str.parse::<u8>() {
399 dark = v;
400 } else {
401 }
403 }
404 } else {
405 state = S::Fail(2);
406 break 'm;
407 }
408 }
409 S::Color => {
410 if l == "[reference]" {
411 state = S::Reference;
412 } else if l.is_empty() || l.starts_with("#") {
413 } else {
415 let mut kv = l.split('=');
416 let cn = if let Some(v) = kv.next() {
417 let Ok(c) = v.trim().parse::<palette::Colors>() else {
418 state = S::Fail(3);
419 break 'm;
420 };
421 c
422 } else {
423 state = S::Fail(4);
424 break 'm;
425 };
426 let (c0, c3) = if let Some(v) = kv.next() {
427 let mut vv = v.split(',');
428 let c0 = if let Some(v) = vv.next() {
429 let Ok(v) = v.trim().parse::<Color>() else {
430 state = S::Fail(5);
431 break 'm;
432 };
433 v
434 } else {
435 state = S::Fail(6);
436 break 'm;
437 };
438 let c3 = if let Some(v) = vv.next() {
439 let Ok(v) = v.trim().parse::<Color>() else {
440 state = S::Fail(7);
441 break 'm;
442 };
443 v
444 } else {
445 state = S::Fail(8);
446 break 'm;
447 };
448 (c0, c3)
449 } else {
450 state = S::Fail(9);
451 break 'm;
452 };
453
454 if cn == palette::Colors::TextLight || cn == palette::Colors::TextDark {
455 pal.color[cn as usize] = palette::Palette::interpolatec2(
456 c0,
457 c3,
458 Color::default(),
459 Color::default(),
460 )
461 } else {
462 pal.color[cn as usize] = palette::Palette::interpolatec(c0, c3, dark);
463 }
464 }
465 }
466 S::Reference => {
467 let mut kv = l.split('=');
468 let rn = if let Some(v) = kv.next() {
469 v
470 } else {
471 state = S::Fail(9);
472 break 'm;
473 };
474 let ci = if let Some(v) = kv.next() {
475 if let Ok(ci) = v.parse::<palette::ColorIdx>() {
476 ci
477 } else {
478 state = S::Fail(10);
479 break 'm;
480 }
481 } else {
482 state = S::Fail(11);
483 break 'm;
484 };
485 pal.add_aliased(rn, ci);
486 }
487 S::Fail(_) => {
488 unreachable!()
489 }
490 }
491 }
492
493 match state {
494 S::Fail(n) => Err(io::Error::new(ErrorKind::Other, LoadPaletteErr(n))),
495 S::Start => Err(io::Error::new(ErrorKind::Other, LoadPaletteErr(100))),
496 S::Recognize => Err(io::Error::new(ErrorKind::Other, LoadPaletteErr(101))),
497 S::Color | S::Reference => Ok(pal),
498 }
499}
500
501pub fn create_palette(name: &str) -> Option<palette::Palette> {
511 use crate::palettes::core;
512 use crate::palettes::dark;
513 use crate::palettes::light;
514 match name {
515 "Imperial" => Some(dark::IMPERIAL),
516 "Radium" => Some(dark::RADIUM),
517 "Tundra" => Some(dark::TUNDRA),
518 "Ocean" => Some(dark::OCEAN),
519 "Monochrome" => Some(dark::MONOCHROME),
520 "Black&White" => Some(dark::BLACK_WHITE),
521 "Monekai" => Some(dark::MONEKAI),
522 "Solarized" => Some(dark::SOLARIZED),
523 "OxoCarbon" => Some(dark::OXOCARBON),
524 "EverForest" => Some(dark::EVERFOREST),
525 "Nord" => Some(dark::NORD),
526 "Rust" => Some(dark::RUST),
527 "Material" => Some(dark::MATERIAL),
528 "Tailwind" => Some(dark::TAILWIND),
529 "VSCode" => Some(dark::VSCODE),
530
531 "Reds" => Some(dark::REDS),
532 "Blackout" => Some(dark::BLACKOUT),
533 "Shell" => Some(core::SHELL),
534
535 "Imperial Light" => Some(light::IMPERIAL_LIGHT),
536 "EverForest Light" => Some(light::EVERFOREST_LIGHT),
537 "Tailwind Light" => Some(light::TAILWIND_LIGHT),
538 "Rust Light" => Some(light::RUST_LIGHT),
539 "SunriseBreeze Light" => Some(light::SUNRISEBREEZE_LIGHT),
540 _ => None,
541 }
542}
543
544pub fn salsa_themes() -> Vec<&'static str> {
546 let themes = THEMES.get_or_init(init_themes);
547 themes.theme.clone()
548}
549
550pub fn create_theme(theme: &str) -> theme::SalsaTheme {
565 let themes = THEMES.get_or_init(init_themes);
566 let Some(def) = themes.theme_init.get(&theme) else {
567 if cfg!(debug_assertions) {
568 panic!("no theme {:?}", theme);
569 } else {
570 return themes::create_core(theme);
571 }
572 };
573 match def {
574 ("dark", p) => {
575 let Some(pal) = create_palette(*p) else {
576 if cfg!(debug_assertions) {
577 panic!("no palette {:?}", *p);
578 } else {
579 return themes::create_core(theme);
580 }
581 };
582 themes::create_dark(theme, pal)
583 }
584 ("light", p) => {
585 let Some(pal) = create_palette(*p) else {
586 if cfg!(debug_assertions) {
587 panic!("no palette {:?}", *p);
588 } else {
589 return themes::create_core(theme);
590 }
591 };
592 let mut theme = themes::create_dark(theme, pal);
595 theme.cat = Category::Light;
596 theme
597 }
598 ("shell", p) => {
599 let Some(pal) = create_palette(*p) else {
600 if cfg!(debug_assertions) {
601 panic!("no palette {:?}", *p);
602 } else {
603 return themes::create_core(theme);
604 }
605 };
606 themes::create_shell(theme, pal)
607 }
608 ("core", _) => themes::create_core(theme),
609 ("blackout", _) => themes::create_dark(theme, palettes::dark::BLACKOUT),
610 ("fallback", _) => themes::create_fallback(theme, palettes::dark::REDS),
611 _ => {
612 if cfg!(debug_assertions) {
613 panic!("no theme {:?}", theme);
614 } else {
615 themes::create_core(theme)
616 }
617 }
618 }
619}