1use ftui::render::cell::PackedRgba;
16use ftui::{Color, ColorCache, ColorProfile, Style, StyleSheet, Theme};
17
18use crate::ui::components::theme::{self as legacy, ThemePalette, ThemePreset};
19
20const ENV_NO_COLOR: &str = "NO_COLOR";
23const ENV_CASS_NO_COLOR: &str = "CASS_NO_COLOR";
24const ENV_CASS_NO_ICONS: &str = "CASS_NO_ICONS";
25const ENV_CASS_NO_GRADIENT: &str = "CASS_NO_GRADIENT";
26const ENV_CASS_DISABLE_ANIMATIONS: &str = "CASS_DISABLE_ANIMATIONS";
27const ENV_CASS_ANIM: &str = "CASS_ANIM";
28const ENV_CASS_A11Y: &str = "CASS_A11Y";
29
30pub mod style_ids {
34 pub const TEXT_PRIMARY: &str = "text.primary";
36 pub const TEXT_SECONDARY: &str = "text.secondary";
37 pub const TEXT_MUTED: &str = "text.muted";
38 pub const TEXT_DISABLED: &str = "text.disabled";
39
40 pub const ACCENT_PRIMARY: &str = "accent.primary";
42 pub const ACCENT_SECONDARY: &str = "accent.secondary";
43 pub const ACCENT_TERTIARY: &str = "accent.tertiary";
44
45 pub const BG_DEEP: &str = "bg.deep";
47 pub const BG_SURFACE: &str = "bg.surface";
48 pub const BG_HIGHLIGHT: &str = "bg.highlight";
49
50 pub const BORDER: &str = "border";
52 pub const BORDER_FOCUS: &str = "border.focus";
53 pub const BORDER_MINIMAL: &str = "border.minimal";
54 pub const BORDER_EMPHASIZED: &str = "border.emphasized";
55
56 pub const ROLE_USER: &str = "role.user";
58 pub const ROLE_AGENT: &str = "role.agent";
59 pub const ROLE_TOOL: &str = "role.tool";
60 pub const ROLE_SYSTEM: &str = "role.system";
61
62 pub const ROLE_USER_BG: &str = "role.user.bg";
64 pub const ROLE_AGENT_BG: &str = "role.agent.bg";
65 pub const ROLE_TOOL_BG: &str = "role.tool.bg";
66 pub const ROLE_SYSTEM_BG: &str = "role.system.bg";
67
68 pub const STATUS_SUCCESS: &str = "status.success";
70 pub const STATUS_WARNING: &str = "status.warning";
71 pub const STATUS_ERROR: &str = "status.error";
72 pub const STATUS_INFO: &str = "status.info";
73
74 pub const HIGHLIGHT: &str = "highlight";
76 pub const SELECTED: &str = "selected";
77 pub const CHIP: &str = "chip";
78 pub const KBD: &str = "kbd";
79 pub const CODE: &str = "code";
80
81 pub const STRIPE_EVEN: &str = "stripe.even";
83 pub const STRIPE_ODD: &str = "stripe.odd";
84
85 pub const GRADIENT_TOP: &str = "gradient.top";
87 pub const GRADIENT_MID: &str = "gradient.mid";
88 pub const GRADIENT_BOT: &str = "gradient.bot";
89}
90
91#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub struct ThemeFlags {
96 pub no_color: bool,
98 pub no_icons: bool,
100 pub no_gradient: bool,
102 pub no_animations: bool,
104 pub a11y: bool,
106}
107
108impl ThemeFlags {
109 pub fn detect() -> Self {
111 Self {
112 no_color: std::env::var_os(ENV_NO_COLOR).is_some() || env_truthy(ENV_CASS_NO_COLOR),
113 no_icons: env_truthy(ENV_CASS_NO_ICONS),
114 no_gradient: env_truthy(ENV_CASS_NO_GRADIENT),
115 no_animations: env_truthy(ENV_CASS_DISABLE_ANIMATIONS) || env_is(ENV_CASS_ANIM, "0"),
116 a11y: env_truthy(ENV_CASS_A11Y),
117 }
118 }
119
120 pub fn custom(
122 no_color: bool,
123 no_icons: bool,
124 no_gradient: bool,
125 no_animations: bool,
126 a11y: bool,
127 ) -> Self {
128 Self {
129 no_color,
130 no_icons,
131 no_gradient,
132 no_animations,
133 a11y,
134 }
135 }
136
137 pub fn all_enabled() -> Self {
139 Self {
140 no_color: false,
141 no_icons: false,
142 no_gradient: false,
143 no_animations: false,
144 a11y: false,
145 }
146 }
147}
148
149impl Default for ThemeFlags {
150 fn default() -> Self {
151 Self::all_enabled()
152 }
153}
154
155pub struct CassTheme {
163 pub preset: ThemePreset,
165 pub is_dark: bool,
167 pub theme: Theme,
169 pub styles: StyleSheet,
171 pub profile: ColorProfile,
173 pub color_cache: ColorCache,
175 pub flags: ThemeFlags,
177}
178
179impl CassTheme {
180 pub fn from_preset(preset: ThemePreset) -> Self {
182 let flags = ThemeFlags::detect();
183 let profile = if flags.no_color {
184 ColorProfile::Mono
185 } else {
186 ColorProfile::detect()
187 };
188 Self::with_options(preset, profile, flags)
189 }
190
191 pub fn with_options(preset: ThemePreset, profile: ColorProfile, flags: ThemeFlags) -> Self {
193 let palette = preset.to_palette();
194 let is_dark = !matches!(preset, ThemePreset::Daylight | ThemePreset::SolarizedLight);
195 let theme = build_ftui_theme(&palette, is_dark);
196 let styles = build_stylesheet(&palette, is_dark, &flags);
197 let color_cache = ColorCache::new(profile);
198
199 Self {
200 preset,
201 is_dark,
202 theme,
203 styles,
204 profile,
205 color_cache,
206 flags,
207 }
208 }
209
210 pub fn next_preset(&mut self) {
212 self.preset = self.preset.next();
213 self.rebuild();
214 }
215
216 pub fn prev_preset(&mut self) {
218 self.preset = self.preset.prev();
219 self.rebuild();
220 }
221
222 fn rebuild(&mut self) {
224 let palette = self.preset.to_palette();
225 self.is_dark = !matches!(
226 self.preset,
227 ThemePreset::Daylight | ThemePreset::SolarizedLight
228 );
229 self.theme = build_ftui_theme(&palette, self.is_dark);
230 self.styles = build_stylesheet(&palette, self.is_dark, &self.flags);
231 self.color_cache = ColorCache::new(self.profile);
232 }
233
234 pub fn style(&self, name: &str) -> Style {
237 self.styles.get_or_default(name)
238 }
239
240 pub fn compose(&self, names: &[&str]) -> Style {
242 self.styles.compose(names)
243 }
244
245 pub fn downgrade(&mut self, color: Color) -> Color {
247 color.downgrade(self.profile)
248 }
249
250 pub fn legacy_palette(&self) -> ThemePalette {
252 self.preset.to_palette()
253 }
254
255 pub fn show_icons(&self) -> bool {
257 !self.flags.no_icons
258 }
259
260 pub fn show_gradient(&self) -> bool {
262 !self.flags.no_gradient && self.profile.supports_true_color()
263 }
264
265 pub fn show_animations(&self) -> bool {
267 !self.flags.no_animations
268 }
269
270 pub fn a11y_mode(&self) -> bool {
272 self.flags.a11y
273 }
274
275 pub fn agent_icon(&self, agent: &str) -> &'static str {
277 if self.flags.no_icons {
278 ""
279 } else {
280 ThemePalette::agent_icon(agent)
281 }
282 }
283
284 pub fn role_style(&self, role: &str) -> Style {
286 let id = match role.to_lowercase().as_str() {
287 "user" => style_ids::ROLE_USER,
288 "assistant" | "agent" => style_ids::ROLE_AGENT,
289 "tool" => style_ids::ROLE_TOOL,
290 "system" => style_ids::ROLE_SYSTEM,
291 _ => style_ids::TEXT_MUTED,
292 };
293 self.style(id)
294 }
295
296 pub fn role_bg_style(&self, role: &str) -> Style {
298 let id = match role.to_lowercase().as_str() {
299 "user" => style_ids::ROLE_USER_BG,
300 "assistant" | "agent" => style_ids::ROLE_AGENT_BG,
301 "tool" => style_ids::ROLE_TOOL_BG,
302 "system" => style_ids::ROLE_SYSTEM_BG,
303 _ => style_ids::BG_DEEP,
304 };
305 self.style(id)
306 }
307
308 pub fn agent_pane_style(&self, agent: &str) -> (Style, Style) {
310 let pane = ThemePalette::agent_pane(agent);
311 let bg = Style::new().bg(pane.bg);
312 let fg = Style::new().fg(pane.fg).bg(pane.bg);
313 (bg, fg)
314 }
315
316 pub fn stripe_style(&self, row_idx: usize) -> Style {
318 if row_idx.is_multiple_of(2) {
319 self.style(style_ids::STRIPE_EVEN)
320 } else {
321 self.style(style_ids::STRIPE_ODD)
322 }
323 }
324}
325
326impl Default for CassTheme {
327 fn default() -> Self {
328 Self::from_preset(ThemePreset::default())
329 }
330}
331
332fn build_ftui_theme(palette: &ThemePalette, is_dark: bool) -> Theme {
336 let c = |color: PackedRgba| -> Color { color.into() };
338
339 Theme::builder()
340 .primary(c(palette.accent))
341 .secondary(c(palette.accent_alt))
342 .accent(c(palette.accent))
343 .background(c(palette.bg))
344 .surface(c(palette.surface))
345 .overlay(c(palette.surface))
346 .text(c(palette.fg))
347 .text_muted(c(palette.hint))
348 .text_subtle(if is_dark {
349 c(legacy::colors::TEXT_DISABLED)
350 } else {
351 Color::rgb(180, 180, 190)
352 })
353 .success(c(legacy::colors::STATUS_SUCCESS))
354 .warning(c(legacy::colors::STATUS_WARNING))
355 .error(c(legacy::colors::STATUS_ERROR))
356 .info(c(legacy::colors::STATUS_INFO))
357 .border(c(palette.border))
358 .border_focused(c(legacy::colors::BORDER_FOCUS))
359 .selection_bg(if is_dark {
360 c(legacy::colors::BG_HIGHLIGHT)
361 } else {
362 Color::rgb(210, 215, 230)
363 })
364 .selection_fg(c(palette.fg))
365 .scrollbar_track(c(palette.surface))
366 .scrollbar_thumb(c(palette.border))
367 .build()
368}
369
370fn build_stylesheet(palette: &ThemePalette, is_dark: bool, flags: &ThemeFlags) -> StyleSheet {
372 let sheet = StyleSheet::new();
373
374 sheet.define(style_ids::TEXT_PRIMARY, Style::new().fg(palette.fg));
376 sheet.define(
377 style_ids::TEXT_SECONDARY,
378 Style::new().fg(if is_dark {
379 legacy::colors::TEXT_SECONDARY
380 } else {
381 palette.fg
382 }),
383 );
384 sheet.define(style_ids::TEXT_MUTED, Style::new().fg(palette.hint));
385 sheet.define(
386 style_ids::TEXT_DISABLED,
387 Style::new().fg(if is_dark {
388 legacy::colors::TEXT_DISABLED
389 } else {
390 PackedRgba::rgb(180, 180, 190)
391 }),
392 );
393
394 sheet.define(
396 style_ids::ACCENT_PRIMARY,
397 Style::new().fg(palette.accent).bold(),
398 );
399 sheet.define(
400 style_ids::ACCENT_SECONDARY,
401 Style::new().fg(palette.accent_alt),
402 );
403 sheet.define(
404 style_ids::ACCENT_TERTIARY,
405 Style::new().fg(if is_dark {
406 legacy::colors::ACCENT_TERTIARY
407 } else {
408 PackedRgba::rgb(0, 130, 200)
409 }),
410 );
411
412 sheet.define(style_ids::BG_DEEP, Style::new().bg(palette.bg));
414 sheet.define(style_ids::BG_SURFACE, Style::new().bg(palette.surface));
415 sheet.define(
416 style_ids::BG_HIGHLIGHT,
417 Style::new().bg(if is_dark {
418 legacy::colors::BG_HIGHLIGHT
419 } else {
420 PackedRgba::rgb(230, 232, 240)
421 }),
422 );
423
424 sheet.define(style_ids::BORDER, Style::new().fg(palette.border));
426 sheet.define(
427 style_ids::BORDER_FOCUS,
428 Style::new().fg(legacy::colors::BORDER_FOCUS),
429 );
430 sheet.define(
431 style_ids::BORDER_MINIMAL,
432 Style::new().fg(legacy::colors::BORDER_MINIMAL),
433 );
434 sheet.define(
435 style_ids::BORDER_EMPHASIZED,
436 Style::new().fg(legacy::colors::BORDER_EMPHASIZED),
437 );
438
439 sheet.define(style_ids::ROLE_USER, Style::new().fg(palette.user));
441 sheet.define(style_ids::ROLE_AGENT, Style::new().fg(palette.agent));
442 sheet.define(style_ids::ROLE_TOOL, Style::new().fg(palette.tool));
443 sheet.define(style_ids::ROLE_SYSTEM, Style::new().fg(palette.system));
444
445 sheet.define(
447 style_ids::ROLE_USER_BG,
448 Style::new().bg(legacy::colors::ROLE_USER_BG),
449 );
450 sheet.define(
451 style_ids::ROLE_AGENT_BG,
452 Style::new().bg(legacy::colors::ROLE_AGENT_BG),
453 );
454 sheet.define(
455 style_ids::ROLE_TOOL_BG,
456 Style::new().bg(legacy::colors::ROLE_TOOL_BG),
457 );
458 sheet.define(
459 style_ids::ROLE_SYSTEM_BG,
460 Style::new().bg(legacy::colors::ROLE_SYSTEM_BG),
461 );
462
463 sheet.define(
465 style_ids::STATUS_SUCCESS,
466 Style::new().fg(legacy::colors::STATUS_SUCCESS),
467 );
468 sheet.define(
469 style_ids::STATUS_WARNING,
470 Style::new().fg(legacy::colors::STATUS_WARNING),
471 );
472 sheet.define(
473 style_ids::STATUS_ERROR,
474 Style::new().fg(legacy::colors::STATUS_ERROR).bold(),
475 );
476 sheet.define(
477 style_ids::STATUS_INFO,
478 Style::new().fg(legacy::colors::STATUS_INFO),
479 );
480
481 sheet.define(
483 style_ids::HIGHLIGHT,
484 Style::new().fg(palette.bg).bg(palette.accent).bold(),
485 );
486 sheet.define(
487 style_ids::SELECTED,
488 Style::new()
489 .bg(if is_dark {
490 legacy::colors::BG_HIGHLIGHT
491 } else {
492 PackedRgba::rgb(220, 224, 236)
493 })
494 .bold(),
495 );
496 sheet.define(style_ids::CHIP, Style::new().fg(palette.accent_alt).bold());
497 sheet.define(style_ids::KBD, Style::new().fg(palette.accent).bold());
498 sheet.define(
499 style_ids::CODE,
500 Style::new()
501 .fg(if is_dark {
502 legacy::colors::TEXT_SECONDARY
503 } else {
504 palette.fg
505 })
506 .bg(palette.surface),
507 );
508
509 sheet.define(style_ids::STRIPE_EVEN, Style::new().bg(palette.stripe_even));
511 sheet.define(style_ids::STRIPE_ODD, Style::new().bg(palette.stripe_odd));
512
513 if !flags.no_gradient {
515 sheet.define(
516 style_ids::GRADIENT_TOP,
517 Style::new().bg(legacy::colors::GRADIENT_HEADER_TOP),
518 );
519 sheet.define(
520 style_ids::GRADIENT_MID,
521 Style::new().bg(legacy::colors::GRADIENT_HEADER_MID),
522 );
523 sheet.define(
524 style_ids::GRADIENT_BOT,
525 Style::new().bg(legacy::colors::GRADIENT_HEADER_BOT),
526 );
527 }
528
529 sheet
530}
531
532fn env_truthy(name: &str) -> bool {
536 match dotenvy::var(name) {
537 Ok(val) => {
538 let normalized = val.trim().to_ascii_lowercase();
539 !normalized.is_empty()
540 && normalized != "0"
541 && normalized != "false"
542 && normalized != "off"
543 && normalized != "no"
544 }
545 Err(_) => false,
546 }
547}
548
549fn env_is(name: &str, expected: &str) -> bool {
551 dotenvy::var(name).map(|v| v == expected).unwrap_or(false)
552}
553
554pub fn lerp_u8(a: u8, b: u8, t: f32) -> u8 {
558 let t = t.clamp(0.0, 1.0);
559 let result = f32::from(a) * (1.0 - t) + f32::from(b) * t;
560 result.round() as u8
561}
562
563pub fn lerp_color(from: Color, to: Color, progress: f32) -> Color {
567 let from_rgb = from.to_rgb();
568 let to_rgb = to.to_rgb();
569 Color::rgb(
570 lerp_u8(from_rgb.r, to_rgb.r, progress),
571 lerp_u8(from_rgb.g, to_rgb.g, progress),
572 lerp_u8(from_rgb.b, to_rgb.b, progress),
573 )
574}
575
576pub fn dim_color(color: Color, factor: f32) -> Color {
578 let rgb = color.to_rgb();
579 let factor = factor.clamp(0.0, 1.0);
580 Color::rgb(
581 (f32::from(rgb.r) * factor).round() as u8,
582 (f32::from(rgb.g) * factor).round() as u8,
583 (f32::from(rgb.b) * factor).round() as u8,
584 )
585}
586
587#[cfg(test)]
590mod tests {
591 use super::*;
592
593 #[test]
594 fn default_creates_dark_theme() {
595 let theme = CassTheme::default();
596 assert_eq!(theme.preset, ThemePreset::TokyoNight);
597 assert!(theme.is_dark);
598 }
599
600 #[test]
601 fn all_presets_build_without_panic() {
602 let flags = ThemeFlags::all_enabled();
603 for preset in ThemePreset::all() {
604 let _ = CassTheme::with_options(*preset, ColorProfile::TrueColor, flags);
605 }
606 }
607
608 #[test]
609 fn style_sheet_has_core_styles() {
610 let theme = CassTheme::with_options(
611 ThemePreset::TokyoNight,
612 ColorProfile::TrueColor,
613 ThemeFlags::all_enabled(),
614 );
615 assert!(theme.styles.contains(style_ids::TEXT_PRIMARY));
617 assert!(theme.styles.contains(style_ids::ROLE_USER));
618 assert!(theme.styles.contains(style_ids::ROLE_AGENT));
619 assert!(theme.styles.contains(style_ids::BORDER));
620 assert!(theme.styles.contains(style_ids::HIGHLIGHT));
621 assert!(theme.styles.contains(style_ids::STRIPE_EVEN));
622 assert!(theme.styles.contains(style_ids::STRIPE_ODD));
623 assert!(theme.styles.contains(style_ids::STATUS_ERROR));
624 }
625
626 #[test]
627 fn preset_cycling_wraps() {
628 let mut theme = CassTheme::with_options(
629 ThemePreset::Colorblind,
630 ColorProfile::TrueColor,
631 ThemeFlags::all_enabled(),
632 );
633 theme.next_preset();
634 assert_eq!(theme.preset, ThemePreset::TokyoNight);
635 }
636
637 #[test]
638 fn no_color_forces_mono_profile() {
639 let flags = ThemeFlags::custom(true, false, false, false, false);
640 let theme =
641 CassTheme::with_options(ThemePreset::TokyoNight, ColorProfile::TrueColor, flags);
642 assert!(theme.flags.no_color);
645 }
646
647 #[test]
648 fn no_icons_suppresses_agent_icons() {
649 let flags = ThemeFlags::custom(false, true, false, false, false);
650 let theme =
651 CassTheme::with_options(ThemePreset::TokyoNight, ColorProfile::TrueColor, flags);
652 assert_eq!(theme.agent_icon("codex"), "");
653 assert_eq!(theme.agent_icon("claude_code"), "");
654 }
655
656 #[test]
657 fn icons_shown_by_default() {
658 let flags = ThemeFlags::all_enabled();
659 let theme =
660 CassTheme::with_options(ThemePreset::TokyoNight, ColorProfile::TrueColor, flags);
661 assert_eq!(theme.agent_icon("codex"), "\u{25c6}"); }
663
664 #[test]
665 fn role_styles_return_non_default() {
666 let theme = CassTheme::with_options(
667 ThemePreset::TokyoNight,
668 ColorProfile::TrueColor,
669 ThemeFlags::all_enabled(),
670 );
671 let user_style = theme.role_style("user");
672 let agent_style = theme.role_style("assistant");
673 let tool_style = theme.role_style("tool");
674 let system_style = theme.role_style("system");
675 assert!(!user_style.is_empty());
677 assert!(!agent_style.is_empty());
678 assert!(!tool_style.is_empty());
679 assert!(!system_style.is_empty());
680 }
681
682 #[test]
683 fn stripe_alternates() {
684 let theme = CassTheme::with_options(
685 ThemePreset::TokyoNight,
686 ColorProfile::TrueColor,
687 ThemeFlags::all_enabled(),
688 );
689 let even = theme.stripe_style(0);
690 let odd = theme.stripe_style(1);
691 assert_ne!(even, odd);
693 }
694
695 #[test]
696 fn light_theme_has_light_bg() {
697 let theme = CassTheme::with_options(
698 ThemePreset::Daylight,
699 ColorProfile::TrueColor,
700 ThemeFlags::all_enabled(),
701 );
702 assert!(!theme.is_dark);
703 }
704
705 #[test]
706 fn high_contrast_has_core_styles() {
707 let theme = CassTheme::with_options(
708 ThemePreset::HighContrast,
709 ColorProfile::TrueColor,
710 ThemeFlags::all_enabled(),
711 );
712 assert!(theme.styles.contains(style_ids::ROLE_USER));
713 assert!(theme.styles.contains(style_ids::STATUS_ERROR));
714 }
715
716 #[test]
717 fn compose_merges_styles() {
718 let theme = CassTheme::with_options(
719 ThemePreset::TokyoNight,
720 ColorProfile::TrueColor,
721 ThemeFlags::all_enabled(),
722 );
723 let composed = theme.compose(&[style_ids::BG_DEEP, style_ids::TEXT_PRIMARY]);
724 assert!(!composed.is_empty());
726 }
727
728 #[test]
731 fn lerp_u8_extremes() {
732 assert_eq!(lerp_u8(0, 255, 0.0), 0);
733 assert_eq!(lerp_u8(0, 255, 1.0), 255);
734 assert_eq!(lerp_u8(0, 200, 0.5), 100);
735 }
736
737 #[test]
738 fn lerp_u8_clamps() {
739 assert_eq!(lerp_u8(0, 100, -1.0), 0);
740 assert_eq!(lerp_u8(0, 100, 2.0), 100);
741 }
742
743 #[test]
744 fn lerp_color_identity() {
745 let c = Color::rgb(100, 150, 200);
746 let result = lerp_color(c, c, 0.5);
747 assert_eq!(result, c);
748 }
749
750 #[test]
751 fn lerp_color_midpoint() {
752 let from = Color::rgb(0, 0, 0);
753 let to = Color::rgb(200, 100, 50);
754 let mid = lerp_color(from, to, 0.5);
755 let rgb = mid.to_rgb();
756 assert_eq!(rgb.r, 100);
757 assert_eq!(rgb.g, 50);
758 assert_eq!(rgb.b, 25);
759 }
760
761 #[test]
762 fn dim_color_half() {
763 let c = Color::rgb(200, 100, 50);
764 let dimmed = dim_color(c, 0.5);
765 let rgb = dimmed.to_rgb();
766 assert_eq!(rgb.r, 100);
767 assert_eq!(rgb.g, 50);
768 assert_eq!(rgb.b, 25);
769 }
770
771 #[test]
772 fn dim_color_zero_is_black() {
773 let c = Color::rgb(200, 100, 50);
774 let dimmed = dim_color(c, 0.0);
775 let rgb = dimmed.to_rgb();
776 assert_eq!(rgb.r, 0);
777 assert_eq!(rgb.g, 0);
778 assert_eq!(rgb.b, 0);
779 }
780
781 #[test]
782 fn packed_rgba_to_color_round_trips() {
783 let orig = PackedRgba::rgb(42, 84, 168);
784 let ftui_color: Color = orig.into();
785 let rgb = ftui_color.to_rgb();
786 assert_eq!(rgb.r, 42);
787 assert_eq!(rgb.g, 84);
788 assert_eq!(rgb.b, 168);
789 }
790
791 #[test]
792 fn no_gradient_skips_gradient_styles() {
793 let flags = ThemeFlags::custom(false, false, true, false, false);
794 let theme =
795 CassTheme::with_options(ThemePreset::TokyoNight, ColorProfile::TrueColor, flags);
796 assert!(!theme.styles.contains(style_ids::GRADIENT_TOP));
797 assert!(!theme.show_gradient());
798 }
799
800 #[test]
801 fn gradient_present_when_enabled() {
802 let flags = ThemeFlags::all_enabled();
803 let theme =
804 CassTheme::with_options(ThemePreset::TokyoNight, ColorProfile::TrueColor, flags);
805 assert!(theme.styles.contains(style_ids::GRADIENT_TOP));
806 assert!(theme.styles.contains(style_ids::GRADIENT_MID));
807 assert!(theme.styles.contains(style_ids::GRADIENT_BOT));
808 }
809
810 #[test]
811 fn a11y_mode_reports_correctly() {
812 let flags = ThemeFlags::custom(false, false, false, false, true);
813 let theme =
814 CassTheme::with_options(ThemePreset::TokyoNight, ColorProfile::TrueColor, flags);
815 assert!(theme.a11y_mode());
816 }
817
818 #[test]
819 fn theme_flags_default_all_enabled() {
820 let flags = ThemeFlags::default();
821 assert!(!flags.no_color);
822 assert!(!flags.no_icons);
823 assert!(!flags.no_gradient);
824 assert!(!flags.no_animations);
825 assert!(!flags.a11y);
826 }
827
828 #[test]
829 fn legacy_palette_matches_preset() {
830 let theme = CassTheme::with_options(
831 ThemePreset::Nord,
832 ColorProfile::TrueColor,
833 ThemeFlags::all_enabled(),
834 );
835 let palette = theme.legacy_palette();
836 assert_eq!(palette.bg, ThemePalette::nord().bg);
837 }
838}