taba_cli/tui/theme/
mod.rs1use ratatui::style::{Color, Modifier, Style};
6
7use crate::tui::api::Severity;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct Theme {
11 pub color: bool,
12 pub ascii: bool,
13}
14
15impl Default for Theme {
16 fn default() -> Theme {
17 Theme {
18 color: true,
19 ascii: false,
20 }
21 }
22}
23
24impl Theme {
25 pub fn from_env(env: &dyn Fn(&str) -> Option<String>) -> Theme {
28 let set = |name: &str| env(name).is_some_and(|v| !v.is_empty());
29 let utf8 = ["LC_ALL", "LC_CTYPE", "LANG"]
30 .iter()
31 .find_map(|name| env(name).filter(|v| !v.is_empty()))
32 .is_none_or(|locale| {
33 let upper = locale.to_ascii_uppercase();
34 upper.contains("UTF-8") || upper.contains("UTF8")
35 });
36 Theme {
37 color: !set("NO_COLOR"),
38 ascii: set("TABA_CLI_ASCII") || !utf8,
39 }
40 }
41
42 fn paint(self, color: Color) -> Style {
43 if self.color {
44 Style::default().fg(color)
45 } else {
46 Style::default()
47 }
48 }
49
50 pub fn ok(self) -> Style {
51 self.paint(Color::Green)
52 }
53
54 pub fn warning(self) -> Style {
55 self.paint(Color::Yellow)
56 }
57
58 pub fn critical(self) -> Style {
59 self.paint(Color::Red).add_modifier(Modifier::BOLD)
60 }
61
62 pub fn dim(self) -> Style {
63 if self.color {
64 Style::default().fg(Color::DarkGray)
65 } else {
66 Style::default().add_modifier(Modifier::DIM)
67 }
68 }
69
70 pub fn accent(self) -> Style {
71 self.paint(Color::Cyan)
72 }
73
74 pub fn title(self) -> Style {
75 Style::default().add_modifier(Modifier::BOLD)
76 }
77
78 pub fn key(self) -> Style {
79 self.paint(Color::Cyan).add_modifier(Modifier::BOLD)
80 }
81
82 pub fn selected(self) -> Style {
84 let base = Style::default().add_modifier(Modifier::BOLD);
85 if self.color {
86 base.bg(Color::Indexed(238))
87 } else {
88 base.add_modifier(Modifier::UNDERLINED)
89 }
90 }
91
92 pub fn severity(self, severity: Severity) -> Style {
93 match severity {
94 Severity::Ok => self.ok(),
95 Severity::Warning => self.warning(),
96 Severity::Critical => self.critical(),
97 }
98 }
99
100 pub fn icon(self, severity: Severity) -> &'static str {
102 match (severity, self.ascii) {
103 (Severity::Ok, false) => "✔",
104 (Severity::Warning, false) => "▲",
105 (Severity::Critical, false) => "✖",
106 (Severity::Ok, true) => "[ok]",
107 (Severity::Warning, true) => "[!]",
108 (Severity::Critical, true) => "[x]",
109 }
110 }
111
112 pub fn selector(self) -> &'static str {
113 if self.ascii {
114 "> "
115 } else {
116 "▶ "
117 }
118 }
119
120 pub fn offline(self) -> &'static str {
121 if self.ascii {
122 "[x]"
123 } else {
124 "✖"
125 }
126 }
127
128 pub fn refresh(self) -> &'static str {
129 if self.ascii {
130 "~"
131 } else {
132 "⟳"
133 }
134 }
135
136 pub fn bar(self, ratio: f64, width: usize) -> String {
138 let filled = ((ratio.clamp(0.0, 1.0) * width as f64).round() as usize).min(width);
139 let (on, off) = if self.ascii {
140 ('#', '-')
141 } else {
142 ('█', '░')
143 };
144 std::iter::repeat_n(on, filled)
145 .chain(std::iter::repeat_n(off, width - filled))
146 .collect()
147 }
148
149 pub fn marker(self, index: usize) -> &'static str {
151 const FANCY: [&str; 4] = ["●", "◆", "▲", "■"];
152 const PLAIN: [&str; 4] = ["*", "+", "o", "#"];
153 if self.ascii {
154 PLAIN[index % 4]
155 } else {
156 FANCY[index % 4]
157 }
158 }
159
160 pub fn series(self, index: usize) -> Style {
162 const COLORS: [Color; 6] = [
163 Color::Cyan,
164 Color::Yellow,
165 Color::Green,
166 Color::Magenta,
167 Color::LightBlue,
168 Color::LightRed,
169 ];
170 self.paint(COLORS[index % COLORS.len()])
171 }
172
173 pub fn arrow_up(self) -> &'static str {
174 if self.ascii {
175 "^"
176 } else {
177 "↑"
178 }
179 }
180}
181
182#[cfg(test)]
183mod tests;