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