1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2pub enum SymbolColor {
3 Default,
4 Green,
5 Yellow,
6 Red,
7 YellowOnGreen,
8 RedOnYellow,
9}
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct PaletteItem {
13 pub ch: char,
14 pub color: SymbolColor,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct Palette {
19 items: Vec<PaletteItem>,
20 pub rtt_min: u32,
21 pub rtt_max: u32,
22}
23
24impl Palette {
25 #[must_use]
26 pub fn from_flags(
27 unicode: bool,
28 color: bool,
29 multicolor: bool,
30 rttmin: Option<u32>,
31 rttmax: Option<u32>,
32 ) -> Self {
33 let multi = color && multicolor;
34
35 let (mut items, mut rtt_min, mut rtt_max) = if unicode {
36 if multi {
37 (unicode_multicolor_items(), 10, 230)
38 } else {
39 (unicode_simple_items(color), 25, 175)
40 }
41 } else if multi {
42 (ascii_multicolor_items(), 20, 220)
43 } else {
44 (ascii_simple_items(color), 75, 225)
45 };
46
47 if let (Some(min), Some(max)) = (rttmin, rttmax) {
48 rtt_min = min;
49 rtt_max = max;
50 } else if let Some(min) = rttmin {
51 rtt_min = min;
52 let span = u32::try_from(items.len().saturating_sub(1)).unwrap_or(u32::MAX);
53 rtt_max = min.saturating_mul(span.max(1));
54 } else if let Some(max) = rttmax {
55 rtt_max = max;
56 let span = u32::try_from(items.len().saturating_sub(1))
57 .unwrap_or(1)
58 .max(1);
59 rtt_min = max / span;
60 }
61
62 if rtt_max <= rtt_min {
63 rtt_max = rtt_min.saturating_add(1);
64 }
65
66 if !color {
67 for item in &mut items {
68 item.color = SymbolColor::Default;
69 }
70 }
71
72 Self {
73 items,
74 rtt_min,
75 rtt_max,
76 }
77 }
78
79 #[must_use]
80 pub fn len(&self) -> usize {
81 self.items.len()
82 }
83
84 #[must_use]
85 pub fn is_empty(&self) -> bool {
86 self.items.is_empty()
87 }
88
89 #[must_use]
90 pub fn item_for_rtt(&self, rtt_ms: u32) -> PaletteItem {
91 if rtt_ms < self.rtt_min {
92 return self.items[0];
93 }
94 if rtt_ms >= self.rtt_max {
95 return *self.items.last().unwrap_or(&self.items[0]);
96 }
97
98 let len = self.items.len();
99 if len <= 2 {
100 return self.items[0];
101 }
102
103 let numerator = u64::from(rtt_ms.saturating_sub(self.rtt_min));
104 let range = u64::from(self.rtt_max.saturating_sub(self.rtt_min)).max(1);
105 let bins = u64::try_from(len.saturating_sub(2)).unwrap_or(0);
106 let idx = 1 + usize::try_from((numerator.saturating_mul(bins)) / range).unwrap_or(0);
107 self.items[idx.min(len - 1)]
108 }
109
110 #[must_use]
111 pub fn legend_line(&self) -> String {
112 if self.items.len() <= 1 {
113 return String::new();
114 }
115
116 let mut out = String::new();
117 out.push_str(&format!("0 {}", self.items[0].ch));
118
119 let len = self.items.len();
120 for index in 1..len {
121 let lower_bound = self.rtt_min.saturating_add(
122 (u64::from(index as u32 - 1).saturating_mul(u64::from(self.rtt_range()))
123 / u64::from((len as u32).saturating_sub(2).max(1))) as u32,
124 );
125 out.push(' ');
126 out.push_str(&format!("{} {}", lower_bound, self.items[index].ch));
127 }
128
129 out.push_str(" inf");
130 out
131 }
132
133 #[must_use]
134 pub fn legend_line_painted(&self) -> String {
135 if self.items.len() <= 1 {
136 return String::new();
137 }
138
139 let mut out = String::new();
140 out.push_str("0 ");
141 out.push_str(&self.paint(self.items[0]));
142
143 let len = self.items.len();
144 for index in 1..len {
145 let lower_bound = self.rtt_min.saturating_add(
146 (u64::from(index as u32 - 1).saturating_mul(u64::from(self.rtt_range()))
147 / u64::from((len as u32).saturating_sub(2).max(1))) as u32,
148 );
149 out.push(' ');
150 out.push_str(&lower_bound.to_string());
151 out.push(' ');
152 out.push_str(&self.paint(self.items[index]));
153 }
154
155 out.push_str(" inf");
156 out
157 }
158
159 #[must_use]
160 pub fn paint(&self, item: PaletteItem) -> String {
161 match item.color {
162 SymbolColor::Default => item.ch.to_string(),
163 SymbolColor::Green => format!("\x1b[0;32m{}\x1b[0m", item.ch),
164 SymbolColor::Yellow => format!("\x1b[0;33m{}\x1b[0m", item.ch),
165 SymbolColor::Red => format!("\x1b[0;31m{}\x1b[0m", item.ch),
166 SymbolColor::YellowOnGreen => format!("\x1b[42;33m{}\x1b[0m", item.ch),
167 SymbolColor::RedOnYellow => format!("\x1b[43;31m{}\x1b[0m", item.ch),
168 }
169 }
170
171 #[must_use]
172 pub fn rtt_range(&self) -> u32 {
173 self.rtt_max.saturating_sub(self.rtt_min).max(1)
174 }
175}
176
177fn unicode_multicolor_items() -> Vec<PaletteItem> {
178 let chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
179 let mut items = Vec::with_capacity(24);
180
181 for ch in chars {
182 items.push(PaletteItem {
183 ch,
184 color: SymbolColor::Green,
185 });
186 }
187 for ch in chars {
188 items.push(PaletteItem {
189 ch,
190 color: SymbolColor::YellowOnGreen,
191 });
192 }
193 for ch in chars {
194 items.push(PaletteItem {
195 ch,
196 color: SymbolColor::RedOnYellow,
197 });
198 }
199
200 items
201}
202
203fn unicode_simple_items(color: bool) -> Vec<PaletteItem> {
204 let chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
205 chars
206 .into_iter()
207 .map(|ch| PaletteItem {
208 ch,
209 color: if color {
210 SymbolColor::Green
211 } else {
212 SymbolColor::Default
213 },
214 })
215 .collect()
216}
217
218fn ascii_multicolor_items() -> Vec<PaletteItem> {
219 let greens = ['_', '.', 'o', 'O'];
220 let yellows = ['_', '.', 'o', 'O'];
221 let reds = ['_', '.', 'o', 'O'];
222
223 let mut items = Vec::with_capacity(12);
224 for ch in greens {
225 items.push(PaletteItem {
226 ch,
227 color: SymbolColor::Green,
228 });
229 }
230 for ch in yellows {
231 items.push(PaletteItem {
232 ch,
233 color: SymbolColor::Yellow,
234 });
235 }
236 for ch in reds {
237 items.push(PaletteItem {
238 ch,
239 color: SymbolColor::Red,
240 });
241 }
242
243 items
244}
245
246fn ascii_simple_items(color: bool) -> Vec<PaletteItem> {
247 ['_', '.', 'o', 'O']
248 .into_iter()
249 .map(|ch| PaletteItem {
250 ch,
251 color: if color {
252 SymbolColor::Green
253 } else {
254 SymbolColor::Default
255 },
256 })
257 .collect()
258}
259
260#[cfg(test)]
261mod tests {
262 use super::Palette;
263
264 #[test]
265 fn ascii_simple_defaults_match_expected_ranges() {
266 let palette = Palette::from_flags(false, false, false, None, None);
267 assert_eq!(palette.len(), 4);
268 assert_eq!(palette.rtt_min, 75);
269 assert_eq!(palette.rtt_max, 225);
270
271 assert_eq!(palette.item_for_rtt(10).ch, '_');
272 assert_eq!(palette.item_for_rtt(90).ch, '.');
273 assert_eq!(palette.item_for_rtt(170).ch, 'o');
274 assert_eq!(palette.item_for_rtt(260).ch, 'O');
275 }
276
277 #[test]
278 fn explicit_rtt_bounds_override_defaults() {
279 let palette = Palette::from_flags(true, true, true, Some(50), Some(100));
280 assert_eq!(palette.rtt_min, 50);
281 assert_eq!(palette.rtt_max, 100);
282 }
283}