Skip to main content

prettyping_rs/render/
palette.rs

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}