1use crate::palette::{Colors, Palette};
57use crate::palettes::shell;
58use crate::theme::SalsaTheme;
59use ratatui::style::{Color, Style};
60use std::borrow::Cow;
61use std::error::Error;
62use std::fmt::{Display, Formatter};
63use std::io;
64use std::io::ErrorKind;
65use std::sync::atomic::{AtomicBool, Ordering};
66
67pub mod palette;
68pub mod theme;
69
70pub mod palettes {
72 pub mod core;
73 pub mod dark;
74 pub mod light;
75 pub mod shell;
76}
77
78pub mod themes {
79 mod dark;
80 mod fallback;
81 mod light;
82 mod shell;
83
84 pub use dark::create_dark;
86 pub use light::create_light;
88 pub use shell::create_shell;
92
93 pub use fallback::create_fallback;
99}
100
101pub struct WidgetStyle;
136
137impl WidgetStyle {
138 pub const BUTTON: &'static str = "button";
139 pub const CALENDAR: &'static str = "calendar";
140 pub const CHECKBOX: &'static str = "checkbox";
141 pub const CHOICE: &'static str = "choice";
142 pub const CLIPPER: &'static str = "clipper";
143 #[cfg(feature = "color_input")]
144 pub const COLOR_INPUT: &'static str = "color-input";
145 pub const COMBOBOX: &'static str = "combobox";
146 pub const DIALOG_FRAME: &'static str = "dialog-frame";
147 pub const FILE_DIALOG: &'static str = "file-dialog";
148 pub const FORM: &'static str = "form";
149 pub const LINE_NR: &'static str = "line-nr";
150 pub const LIST: &'static str = "list";
151 pub const MENU: &'static str = "menu";
152 pub const MONTH: &'static str = "month";
153 pub const MSG_DIALOG: &'static str = "msg-dialog";
154 pub const PARAGRAPH: &'static str = "paragraph";
155 pub const RADIO: &'static str = "radio";
156 pub const SCROLL: &'static str = "scroll";
157 pub const SCROLL_DIALOG: &'static str = "scroll.dialog";
158 pub const SCROLL_POPUP: &'static str = "scroll.popup";
159 pub const SHADOW: &'static str = "shadow";
160 pub const SLIDER: &'static str = "slider";
161 pub const SPLIT: &'static str = "split";
162 pub const STATUSLINE: &'static str = "statusline";
163 pub const TABBED: &'static str = "tabbed";
164 pub const TABLE: &'static str = "table";
165 pub const TEXT: &'static str = "text";
166 pub const TEXTAREA: &'static str = "textarea";
167 pub const TEXTVIEW: &'static str = "textview";
168 pub const VIEW: &'static str = "view";
169}
170
171pub trait StyleName {
185 const LABEL_FG: &'static str = "label-fg";
186 const INPUT: &'static str = "input";
187 const INPUT_FOCUS: &'static str = "text-focus";
188 const INPUT_SELECT: &'static str = "text-select";
189 const FOCUS: &'static str = "focus";
190 const SELECT: &'static str = "select";
191 const DISABLED: &'static str = "disabled";
192 const INVALID: &'static str = "invalid";
193
194 const TITLE: &'static str = "title";
195 const HEADER: &'static str = "header";
196 const FOOTER: &'static str = "footer";
197
198 const HOVER: &'static str = "hover";
199 const SHADOWS: &'static str = "shadows";
200
201 const WEEK_HEADER_FG: &'static str = "week-header-fg";
202 const MONTH_HEADER_FG: &'static str = "month-header-fg";
203
204 const KEY_BINDING: &'static str = "key-binding";
205 const BUTTON_BASE: &'static str = "button-base";
206 const MENU_BASE: &'static str = "menu-base";
207 const STATUS_BASE: &'static str = "status-base";
208
209 const CONTAINER_BASE: &'static str = "container-base";
210 const CONTAINER_BORDER_FG: &'static str = "container-border-fg";
211 const CONTAINER_ARROW_FG: &'static str = "container-arrows-fg";
212
213 const DOCUMENT_BASE: &'static str = "document-base";
214 const DOCUMENT_BORDER_FG: &'static str = "document-border-fg";
215 const DOCUMENT_ARROW_FG: &'static str = "document-arrows-fg";
216
217 const POPUP_BASE: &'static str = "popup-base";
218 const POPUP_BORDER_FG: &'static str = "popup-border-fg";
219 const POPUP_ARROW_FG: &'static str = "popup-arrow-fg";
220
221 const DIALOG_BASE: &'static str = "dialog-base";
222 const DIALOG_BORDER_FG: &'static str = "dialog-border-fg";
223 const DIALOG_ARROW_FG: &'static str = "dialog-arrow-fg";
224}
225impl StyleName for Style {}
226
227pub trait RatWidgetColor {
241 const LABEL_FG: &'static str = "label.fg";
242 const INPUT_BG: &'static str = "input.bg";
243 const INPUT_FOCUS_BG: &'static str = "input-focus.bg";
244 const INPUT_SELECT_BG: &'static str = "input-select.bg";
245 const FOCUS_BG: &'static str = "focus.bg";
246 const SELECT_BG: &'static str = "select.bg";
247 const DISABLED_BG: &'static str = "disabled.bg";
248 const INVALID_BG: &'static str = "invalid.bg";
249
250 const TITLE_FG: &'static str = "title.fg";
251 const TITLE_BG: &'static str = "title.bg";
252 const HEADER_FG: &'static str = "header.fg";
253 const HEADER_BG: &'static str = "header.bg";
254 const FOOTER_FG: &'static str = "footer.fg";
255 const FOOTER_BG: &'static str = "footer.bg";
256
257 const HOVER_BG: &'static str = "hover.bg";
258 const BUTTON_BASE_BG: &'static str = "button-base.bg";
259 const KEY_BINDING_BG: &'static str = "key-binding.bg";
260 const MENU_BASE_BG: &'static str = "menu-base.bg";
261 const STATUS_BASE_BG: &'static str = "status-base.bg";
262 const SHADOW_BG: &'static str = "shadow.bg";
263
264 const WEEK_HEADER_FG: &'static str = "week-header.fg";
265 const MONTH_HEADER_FG: &'static str = "month-header.fg";
266
267 const CONTAINER_BASE_BG: &'static str = "container-base.bg";
268 const CONTAINER_BORDER_FG: &'static str = "container-border.fg";
269 const CONTAINER_ARROW_FG: &'static str = "container-arrow.fg";
270 const DOCUMENT_BASE_BG: &'static str = "document-base.bg";
271 const DOCUMENT_BORDER_FG: &'static str = "document-border.fg";
272 const DOCUMENT_ARROW_FG: &'static str = "document-arrow.fg";
273 const POPUP_BASE_BG: &'static str = "popup-base.bg";
274 const POPUP_BORDER_FG: &'static str = "popup-border.fg";
275 const POPUP_ARROW_FG: &'static str = "popup-arrow.fg";
276 const DIALOG_BASE_BG: &'static str = "dialog-base.bg";
277 const DIALOG_BORDER_FG: &'static str = "dialog-border.fg";
278 const DIALOG_ARROW_FG: &'static str = "dialog-arrow.fg";
279}
280impl RatWidgetColor for Color {}
281
282static LOG_DEFINES: AtomicBool = AtomicBool::new(false);
283
284pub fn log_style_define(log: bool) {
287 LOG_DEFINES.store(log, Ordering::Release);
288}
289
290fn is_log_style_define() -> bool {
291 LOG_DEFINES.load(Ordering::Acquire)
292}
293
294#[derive(Debug)]
295pub struct LoadPaletteErr(u8);
296
297impl Display for LoadPaletteErr {
298 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
299 write!(f, "load palette failed: {}", self.0)
300 }
301}
302
303impl Error for LoadPaletteErr {}
304
305pub fn store_palette(pal: &Palette, mut buf: impl io::Write) -> Result<(), io::Error> {
307 writeln!(buf, "[theme]")?;
308 writeln!(buf, "name={}", pal.theme_name)?;
309 writeln!(buf, "theme={}", pal.theme)?;
310 writeln!(buf, "")?;
311 writeln!(buf, "[palette]")?;
312 writeln!(buf, "name={}", pal.name)?;
313 writeln!(buf, "docs={}", pal.doc.replace('\n', "\\n"))?;
314 writeln!(buf, "generator={}", pal.generator)?;
315 writeln!(buf, "")?;
316 writeln!(buf, "[color]")?;
317 for c in Colors::array() {
318 writeln!(
319 buf,
320 "{}={}, {}",
321 *c, pal.color[*c as usize][0], pal.color[*c as usize][3]
322 )?;
323 }
324 writeln!(buf, "")?;
325 writeln!(buf, "[reference]")?;
326 for (r, i) in pal.aliased.as_ref() {
327 writeln!(buf, "{}={}", r, i)?;
328 }
329 Ok(())
330}
331
332pub fn load_palette(mut r: impl io::Read) -> Result<Palette, io::Error> {
334 let mut buf = String::new();
335 r.read_to_string(&mut buf)?;
336
337 enum S {
338 Start,
339 Theme,
340 Palette,
341 Color,
342 Reference,
343 Fail(u8),
344 }
345
346 let mut pal = Palette::default();
347 let mut dark = 63u8;
348
349 let mut state = S::Start;
350 'm: for l in buf.lines() {
351 let l = l.trim();
352 match state {
353 S::Start => {
354 if l == "[theme]" {
355 state = S::Theme;
356 } else if l == "[palette]" {
357 state = S::Palette;
358 } else {
359 state = S::Fail(1);
360 break 'm;
361 }
362 }
363 S::Theme => {
364 if l == "[palette]" {
365 state = S::Palette;
366 } else if l.is_empty() || l.starts_with("#") {
367 } else if l.starts_with("name") {
369 if let Some(s) = l.split('=').nth(1) {
370 pal.theme_name = Cow::Owned(s.trim().to_string());
371 }
372 } else if l.starts_with("theme") {
373 if let Some(s) = l.split('=').nth(1) {
374 pal.theme = Cow::Owned(s.trim().to_string());
375 }
376 } else {
377 state = S::Fail(2);
378 break 'm;
379 }
380 }
381 S::Palette => {
382 if l == "[color]" {
383 state = S::Color;
384 } else if l.is_empty() || l.starts_with("#") {
385 } else if l.starts_with("name") {
387 if let Some(s) = l.split('=').nth(1) {
388 pal.name = Cow::Owned(s.trim().to_string());
389 }
390 } else if l.starts_with("docs") {
391 if let Some(s) = l.split('=').nth(1) {
392 let doc = s.trim().replace("\\n", "\n");
393 pal.doc = Cow::Owned(doc);
394 }
395 } else if l.starts_with("generator") {
396 if let Some(s) = l.split('=').nth(1) {
397 pal.generator = Cow::Owned(s.trim().to_string());
398 if s.starts_with("light-dark") {
399 if let Some(s) = l.split(':').nth(1) {
400 dark = s.trim().parse::<u8>().unwrap_or(63);
401 }
402 }
403 }
404 } else if l.starts_with("dark") {
405 if let Some(s) = l.split('=').nth(1) {
406 if let Ok(v) = s.trim().parse::<u8>() {
407 dark = v;
408 } else {
409 }
411 }
412 } else {
413 state = S::Fail(3);
414 break 'm;
415 }
416 }
417 S::Color => {
418 if l == "[reference]" {
419 state = S::Reference;
420 } else if l.is_empty() || l.starts_with("#") {
421 } else {
423 let mut kv = l.split('=');
424 let cn = if let Some(v) = kv.next() {
425 let Ok(c) = v.trim().parse::<palette::Colors>() else {
426 state = S::Fail(4);
427 break 'm;
428 };
429 c
430 } else {
431 state = S::Fail(5);
432 break 'm;
433 };
434 let (c0, c3) = if let Some(v) = kv.next() {
435 let mut vv = v.split(',');
436 let c0 = if let Some(v) = vv.next() {
437 let Ok(v) = v.trim().parse::<Color>() else {
438 state = S::Fail(6);
439 break 'm;
440 };
441 v
442 } else {
443 state = S::Fail(7);
444 break 'm;
445 };
446 let c3 = if let Some(v) = vv.next() {
447 let Ok(v) = v.trim().parse::<Color>() else {
448 state = S::Fail(8);
449 break 'm;
450 };
451 v
452 } else {
453 state = S::Fail(9);
454 break 'm;
455 };
456 (c0, c3)
457 } else {
458 state = S::Fail(10);
459 break 'm;
460 };
461
462 if cn == Colors::TextLight || cn == Colors::TextDark {
463 pal.color[cn as usize] =
464 Palette::interpolatec2(c0, c3, Color::default(), Color::default())
465 } else {
466 pal.color[cn as usize] = Palette::interpolatec(c0, c3, dark);
467 }
468 }
469 }
470 S::Reference => {
471 let mut kv = l.split('=');
472 let rn = if let Some(v) = kv.next() {
473 v
474 } else {
475 state = S::Fail(11);
476 break 'm;
477 };
478 let ci = if let Some(v) = kv.next() {
479 if let Ok(ci) = v.parse::<palette::ColorIdx>() {
480 ci
481 } else {
482 state = S::Fail(12);
483 break 'm;
484 }
485 } else {
486 state = S::Fail(13);
487 break 'm;
488 };
489 pal.add_aliased(rn, ci);
490 }
491 S::Fail(_) => {
492 unreachable!()
493 }
494 }
495 }
496
497 match state {
498 S::Fail(n) => Err(io::Error::new(ErrorKind::Other, LoadPaletteErr(n))),
499 S::Start => Err(io::Error::new(ErrorKind::Other, LoadPaletteErr(100))),
500 S::Theme => Err(io::Error::new(ErrorKind::Other, LoadPaletteErr(101))),
501 S::Palette => Err(io::Error::new(ErrorKind::Other, LoadPaletteErr(102))),
502 S::Color | S::Reference => Ok(pal),
503 }
504}
505
506pub fn create_palette_theme(pal: Palette) -> Result<SalsaTheme, Palette> {
508 match pal.theme.as_ref() {
509 "Dark" => Ok(themes::create_dark(pal)),
510 "Light" => Ok(themes::create_light(pal)),
511 "Shell" => Ok(themes::create_shell(pal)),
512 _ => Err(pal),
513 }
514}
515
516static THEMES: &'static [&'static str] = &[
517 "Imperial",
518 "Radium",
519 "Tundra",
520 "Ocean",
521 "Monochrome",
522 "Black&White",
523 "Monekai",
524 "Solarized",
525 "OxoCarbon",
526 "EverForest",
527 "Nord",
528 "Rust",
529 "Material",
530 "Tailwind",
531 "VSCode",
532 "Reds",
533 "Imperial Light",
535 "EverForest Light",
536 "Tailwind Light",
537 "Rust Light",
538 "SunriseBreeze Light",
539 "Imperial Shell",
541 "Radium Shell",
542 "Tundra Shell",
543 "Ocean Shell",
544 "Monochrome Shell",
545 "Black&White Shell",
546 "Monekai Shell",
547 "Solarized Shell",
548 "OxoCarbon Shell",
549 "EverForest Shell",
550 "Nord Shell",
551 "Rust Shell",
552 "Material Shell",
553 "Tailwind Shell",
554 "VSCode Shell",
555 "Reds Shell",
556 "Shell",
558 "Blackout",
559 "Fallback",
560];
561
562#[deprecated(
564 since = "4.1.0",
565 note = "there is no separation between themes and palettes any more. use salsa_themes()"
566)]
567pub fn salsa_palettes() -> Vec<&'static str> {
568 let mut r = Vec::new();
569 for v in THEMES {
570 r.push(*v);
571 }
572 r
573}
574
575#[deprecated(since = "4.1.0", note = "use create_salsa_palette() instead")]
585pub fn create_palette(name: &str) -> Option<Palette> {
586 create_salsa_palette(name)
587}
588
589pub fn create_salsa_palette(name: &str) -> Option<Palette> {
599 use crate::palettes::core;
600 use crate::palettes::dark;
601 use crate::palettes::light;
602 match name {
603 "Imperial" => Some(dark::IMPERIAL),
604 "Radium" => Some(dark::RADIUM),
605 "Tundra" => Some(dark::TUNDRA),
606 "Ocean" => Some(dark::OCEAN),
607 "Monochrome" => Some(dark::MONOCHROME),
608 "Black&White" => Some(dark::BLACK_WHITE),
609 "Monekai" => Some(dark::MONEKAI),
610 "Solarized" => Some(dark::SOLARIZED),
611 "OxoCarbon" => Some(dark::OXOCARBON),
612 "EverForest" => Some(dark::EVERFOREST),
613 "Nord" => Some(dark::NORD),
614 "Rust" => Some(dark::RUST),
615 "Material" => Some(dark::MATERIAL),
616 "Tailwind" => Some(dark::TAILWIND),
617 "VSCode" => Some(dark::VSCODE),
618 "Reds" => Some(dark::REDS),
619
620 "Imperial Light" => Some(light::IMPERIAL_LIGHT),
621 "EverForest Light" => Some(light::EVERFOREST_LIGHT),
622 "Tailwind Light" => Some(light::TAILWIND_LIGHT),
623 "Rust Light" => Some(light::RUST_LIGHT),
624 "SunriseBreeze Light" => Some(light::SUNRISEBREEZE_LIGHT),
625
626 "Imperial Shell" => Some(shell::IMPERIAL_SHELL),
627 "Radium Shell" => Some(shell::RADIUM_SHELL),
628 "Tundra Shell" => Some(shell::TUNDRA_SHELL),
629 "Ocean Shell" => Some(shell::OCEAN_SHELL),
630 "Monochrome Shell" => Some(shell::MONOCHROME_SHELL),
631 "Black&White Shell" => Some(shell::BLACK_WHITE_SHELL),
632 "Monekai Shell" => Some(shell::MONEKAI_SHELL),
633 "Solarized Shell" => Some(shell::SOLARIZED_SHELL),
634 "OxoCarbon Shell" => Some(shell::OXOCARBON_SHELL),
635 "EverForest Shell" => Some(shell::EVERFOREST_SHELL),
636 "Nord Shell" => Some(shell::NORD_SHELL),
637 "Rust Shell" => Some(shell::RUST_SHELL),
638 "Material Shell" => Some(shell::MATERIAL_SHELL),
639 "Tailwind Shell" => Some(shell::TAILWIND_SHELL),
640 "VSCode Shell" => Some(shell::VSCODE_SHELL),
641 "Reds Shell" => Some(shell::REDS_SHELL),
642
643 "Shell" => Some(core::SHELL),
644 "Blackout" => Some(core::BLACKOUT),
645 "Fallback" => Some(core::FALLBACK),
646 _ => None,
647 }
648}
649
650pub fn salsa_themes() -> Vec<&'static str> {
652 let mut r = Vec::new();
653 for v in THEMES {
654 r.push(*v);
655 }
656 r
657}
658
659#[deprecated(since = "4.1.0", note = "use create_salsa_theme() instead")]
660pub fn create_theme(theme_name: &str) -> SalsaTheme {
661 create_salsa_theme(theme_name)
662}
663
664pub fn create_salsa_theme(theme_name: &str) -> SalsaTheme {
679 if let Some(pal) = create_salsa_palette(theme_name) {
680 match pal.theme.as_ref() {
681 "Dark" => themes::create_dark(pal),
682 "Light" => themes::create_light(pal),
683 "Shell" => themes::create_shell(pal),
684 "Fallback" => themes::create_fallback(pal),
685 _ => themes::create_shell(palettes::core::SHELL),
686 }
687 } else {
688 themes::create_shell(palettes::core::SHELL)
689 }
690}