1use urushi::{ComponentRole, TextStyle, Theme, View};
4
5use crate::{Summary, SummaryPresentation, Warning, WarningPresentation};
6
7const CLI_ROLE_COUNT: usize = 4;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum CliRole {
12 Body,
13 Muted,
14 Accent,
15 Warning,
16}
17
18impl CliRole {
19 const fn index(self) -> usize {
20 match self {
21 Self::Body => 0,
22 Self::Muted => 1,
23 Self::Accent => 2,
24 Self::Warning => 3,
25 }
26 }
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct CliTheme {
32 styles: [TextStyle; CLI_ROLE_COUNT],
33 summary_presentation: SummaryPresentation,
34 warning_presentation: WarningPresentation,
35}
36
37impl CliTheme {
38 pub fn from_theme(theme: &Theme) -> Self {
40 let styles = [
41 theme.text_style(ComponentRole::Body),
42 theme.text_style(ComponentRole::Muted),
43 theme.text_style(ComponentRole::Accent),
44 TextStyle::new().foreground(theme.tokens().warning),
45 ];
46 let summary_presentation = SummaryPresentation::new(
47 styles[CliRole::Muted.index()].clone(),
48 styles[CliRole::Accent.index()].clone(),
49 styles[CliRole::Body.index()].clone(),
50 );
51 let warning_presentation = WarningPresentation::new(
52 styles[CliRole::Muted.index()].clone(),
53 styles[CliRole::Warning.index()].clone(),
54 styles[CliRole::Body.index()].clone(),
55 );
56 Self {
57 styles,
58 summary_presentation,
59 warning_presentation,
60 }
61 }
62
63 pub fn get_style(&self, role: CliRole) -> &TextStyle {
65 &self.styles[role.index()]
66 }
67
68 pub fn summary_presentation(&self) -> &SummaryPresentation {
70 &self.summary_presentation
71 }
72
73 pub fn warning_presentation(&self) -> &WarningPresentation {
75 &self.warning_presentation
76 }
77
78 pub fn summary(&self, summary: &Summary) -> View {
80 self.summary_presentation.compose(summary)
81 }
82
83 pub fn warning(&self, warning: &Warning) -> View {
85 self.warning_presentation.compose(warning)
86 }
87
88 #[must_use]
90 pub fn style(mut self, role: CliRole, style: TextStyle) -> Self {
91 self.styles[role.index()] = style.clone();
92 self.summary_presentation.set_style(role, style.clone());
93 self.warning_presentation.set_style(role, style);
94 self
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use urushi::{Color, RenderSettings, SemanticTokens, StyledText, render_text};
102
103 fn render_style(style: &TextStyle, text: &str) -> String {
104 let settings = RenderSettings::all();
105 render_text(&StyledText::new(text, style.clone()), &settings)
106 }
107
108 fn theme() -> Theme {
109 Theme::from_tokens(SemanticTokens {
110 text: Color::Ansi(1),
111 text_muted: Color::Ansi(2),
112 background: Color::Ansi(3),
113 surface: Color::Ansi(4),
114 accent: Color::Ansi(5),
115 accent_text: Color::Ansi(6),
116 success: Color::Ansi(7),
117 warning: Color::Ansi(8),
118 error: Color::Ansi(9),
119 border: Color::Ansi(10),
120 })
121 }
122
123 #[test]
124 fn derives_cli_roles_from_the_core_theme() {
125 let cli = CliTheme::from_theme(&theme());
126
127 assert_eq!(
128 render_style(cli.get_style(CliRole::Body), "x"),
129 "\x1b[31mx\x1b[0m"
130 );
131 assert_eq!(
132 render_style(cli.get_style(CliRole::Muted), "x"),
133 "\x1b[2;32mx\x1b[0m"
134 );
135 assert_eq!(
136 render_style(cli.get_style(CliRole::Accent), "x"),
137 "\x1b[1;35mx\x1b[0m"
138 );
139 assert_eq!(
140 render_style(cli.get_style(CliRole::Warning), "x"),
141 "\x1b[90mx\x1b[0m"
142 );
143 }
144
145 #[test]
146 fn shortcuts_delegate_to_the_canonical_presentations() {
147 let theme = theme();
148 let cli = CliTheme::from_theme(&theme);
149 let summary = Summary::new("Done");
150 let warning = Warning::new("Caution", "Careful");
151
152 assert_eq!(
153 cli.summary(&summary),
154 cli.summary_presentation().compose(&summary)
155 );
156 assert_eq!(
157 cli.warning(&warning),
158 cli.warning_presentation().compose(&warning)
159 );
160 }
161
162 #[test]
163 fn role_styles_are_replaceable() {
164 let theme = theme();
165 let base = CliTheme::from_theme(&theme);
166 let changed_role = base
167 .clone()
168 .style(CliRole::Muted, TextStyle::new().underlined());
169 assert_eq!(
170 changed_role.get_style(CliRole::Muted),
171 &TextStyle::new().underlined()
172 );
173 assert_ne!(
174 changed_role.summary(&Summary::new("Done")),
175 base.summary(&Summary::new("Done"))
176 );
177 }
178}