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