statusline/
style.rs

1use std::fmt::{Display, Formatter, Result as FmtResult};
2
3const INVISIBLE_START: &str = "\x01";
4const INVISIBLE_END: &str = "\x02";
5const ESC: &str = "\x1b";
6const CSI: &str = "\x1b[";
7const RESET: &str = "\x1b[0m";
8const BEL: &str = "\x07";
9const HSV_COLOR_TABLE: [(u8, u8, u8); 24] = [
10    (255, 0, 0),
11    (255, 85, 0),
12    (255, 128, 0),
13    (255, 170, 0),
14    (255, 213, 0),
15    (255, 255, 0),
16    (213, 255, 0),
17    (170, 255, 0),
18    (128, 255, 0),
19    (0, 255, 85),
20    (0, 255, 128),
21    (0, 255, 170),
22    (0, 255, 213),
23    (0, 213, 255),
24    (0, 128, 255),
25    (0, 85, 255),
26    (128, 0, 255),
27    (170, 0, 255),
28    (213, 0, 255),
29    (255, 0, 255),
30    (255, 0, 212),
31    (255, 0, 170),
32    (255, 0, 128),
33    (255, 0, 85),
34];
35
36enum StyleKind {
37    Title,
38    Bold,
39    Italic,
40    Color8(usize),
41    TrueColor(u8, u8, u8),
42    ResetEnd,
43    Invisible,
44    Visible,
45    Boxed,
46    Rounded,
47    CursorHorizontalAbsolute(usize),
48    CursorPreviousLine(i32),
49    CursorSaveRestore,
50    ClearLine,
51    NewlineJoin(String),
52}
53
54/// Styled "string"-like object
55///
56/// Can be used instead of raw ANSI sequences because of cleaner interface.
57///
58/// This type is a wrapper around any object which implements [Display] trait --- usually some
59/// kind of strings or other simple ogjects. It wraps one style change at a time, making
60/// it possible to chain styles to apply them from the innermost to the outermost.
61///
62/// All the magic happens in [Style] trait.
63///
64/// ```
65/// use statusline::Style;
66///
67/// let hello = "Hello world!";
68/// let styled = hello.boxed().red().bold().with_reset().to_string();
69/// //                ^^^^^^^^------=======.............
70/// assert_eq!("\x1b[1m\x1b[31m[Hello world!]\x1b[0m", styled);
71/// //          =======--------^            ^.......
72/// ```
73///
74/// This wrapper applies styles only when formatted to string. Formatting results are never saved
75/// and every call to `<Styled as ToString>::to_string()` will format the result one more time,
76/// which may lead to different results for special types like the one which tells the exact
77/// moment of time at the `std::fmt::Display::fmt` call.
78pub struct Styled<'a, T: Display + ?Sized> {
79    style: StyleKind,
80    value: &'a T,
81}
82
83impl<T: Display + ?Sized> Display for Styled<'_, T> {
84    fn fmt(&self, f: &mut Formatter) -> FmtResult {
85        match &self.style {
86            StyleKind::Title => write!(f, "{ESC}]0;{}{BEL}", self.value),
87            StyleKind::Bold => write!(f, "{CSI}1m{}", self.value),
88            StyleKind::Italic => write!(f, "{CSI}3m{}", self.value),
89            StyleKind::Color8(index) => write!(f, "{CSI}{}m{}", index + 31, self.value),
90            StyleKind::TrueColor(r, g, b) => {
91                write!(f, "{CSI}38;2;{r};{g};{b}m{}", self.value)
92            }
93            StyleKind::ResetEnd => write!(f, "{}{RESET}", self.value),
94            StyleKind::Invisible => write!(f, "{INVISIBLE_START}{}{INVISIBLE_END}", self.value),
95            StyleKind::Visible => write!(f, "{INVISIBLE_END}{}{INVISIBLE_START}", self.value),
96            StyleKind::Boxed => write!(f, "[{}]", self.value),
97            StyleKind::Rounded => write!(f, "({})", self.value),
98            StyleKind::CursorHorizontalAbsolute(n) => write!(f, "{CSI}{n}G{}", self.value),
99            StyleKind::CursorPreviousLine(n) => write!(f, "{CSI}{n}A{CSI}G{}", self.value),
100            StyleKind::CursorSaveRestore => write!(f, "{CSI}s{}{CSI}u", self.value),
101            StyleKind::ClearLine => write!(f, "{CSI}0K{}", self.value),
102            StyleKind::NewlineJoin(s) => write!(f, "{}\n{s}", self.value),
103        }
104    }
105}
106
107/// Styling functions for function chaining
108///
109/// ```
110/// use statusline::Style;
111///
112/// let hello = "Hello world!";
113/// let styled = hello.boxed().red().bold().with_reset().to_string();
114/// //                ^^^^^^^^------=======.............
115/// assert_eq!("\x1b[1m\x1b[31m[Hello world!]\x1b[0m", styled);
116/// //          =======--------^            ^.......
117/// ```
118///
119/// Every chained function call a new [Styled] object wraps the previous result like a cabbage.
120pub trait Style: Display {
121    /// Format as a title for terminal
122    ///
123    /// ```
124    /// use statusline::Style;
125    /// assert_eq!("\x1b]0;yuki@reimu: /home/yuki\x07", "yuki@reimu: /home/yuki".as_title().to_string());
126    /// ```
127    fn as_title(&self) -> Styled<Self> {
128        Styled {
129            style: StyleKind::Title,
130            value: self,
131        }
132    }
133
134    /// Prepend bold style. Colors from 16-color palette may shift a bit
135    ///
136    /// ```
137    /// use statusline::Style;
138    /// assert_eq!("\x1b[1mBOLD text", "BOLD text".bold().to_string());
139    /// ```
140    fn bold(&self) -> Styled<Self> {
141        Styled {
142            style: StyleKind::Bold,
143            value: self,
144        }
145    }
146
147    /// Prepend italic style
148    ///
149    /// ```
150    /// use statusline::Style;
151    /// assert_eq!("\x1b[3mItalic text", "Italic text".italic().to_string());
152    /// ```
153    fn italic(&self) -> Styled<Self> {
154        Styled {
155            style: StyleKind::Italic,
156            value: self,
157        }
158    }
159
160    /// Use colors from 16-color palette, dark version (0 for CSI 31 thru 6 for CSI 37, CSI 30 is black which
161    /// is useless)
162    fn low_color(&self, index: usize) -> Styled<Self> {
163        Styled {
164            style: StyleKind::Color8(index),
165            value: self,
166        }
167    }
168
169    /// Use true color. Note that some terminals lack true color support and will approximate
170    /// the result with colors they do support. This may lead to text being completely unreadable.
171    ///
172    /// However, since most GUI terminal emulators in linux do support true color display no worry
173    /// is usually needed. Just use it as-is
174    fn true_color(&self, red: u8, green: u8, blue: u8) -> Styled<Self> {
175        Styled {
176            style: StyleKind::TrueColor(red, green, blue),
177            value: self,
178        }
179    }
180
181    /// Wrap into "readline invisible" characters, for PS1 output or some other strange things
182    ///
183    /// ```
184    /// use statusline::Style;
185    /// assert_eq!("\x01invis\x02", "invis".invisible().to_string());
186    /// ```
187    fn invisible(&self) -> Styled<Self> {
188        Styled {
189            style: StyleKind::Invisible,
190            value: self,
191        }
192    }
193
194    /// Wrap into "readline invisible" but reverse --- for making surroundings invisible.
195    ///
196    /// ```
197    /// use statusline::Style;
198    /// assert_eq!("\x01\x1b[31m\x02Visible\x01\x1b[0m\x02",
199    ///     "Visible".visible().red().with_reset().invisible().to_string());
200    /// ```
201    fn visible(&self) -> Styled<Self> {
202        Styled {
203            style: StyleKind::Visible,
204            value: self,
205        }
206    }
207
208    /// Add "reset colors and boldness" to the end
209    ///
210    /// ```
211    /// use statusline::Style;
212    /// assert_eq!("\x1b[31mRED\x1b[0mnormal", "RED".red().with_reset().to_string() + "normal");
213    /// ```
214    fn with_reset(&self) -> Styled<Self> {
215        Styled {
216            style: StyleKind::ResetEnd,
217            value: self,
218        }
219    }
220
221    /// Wrap into square brackets
222    ///
223    /// ```
224    /// use statusline::Style;
225    /// assert_eq!("[nya]", "nya".boxed().to_string());
226    /// ```
227    fn boxed(&self) -> Styled<Self> {
228        Styled {
229            style: StyleKind::Boxed,
230            value: self,
231        }
232    }
233
234    /// Wrap into round brackets
235    ///
236    /// ```
237    /// use statusline::Style;
238    /// assert_eq!("(nyaah~)", "nyaah~".rounded().to_string());
239    /// ```
240    fn rounded(&self) -> Styled<Self> {
241        Styled {
242            style: StyleKind::Rounded,
243            value: self,
244        }
245    }
246
247    /// Set cursor position, the horizontal part, with absolute value. Coordinates are counted
248    /// from 1, from line start to line end, which may seem counter-intuitive
249    fn horizontal_absolute(&self, pos: usize) -> Styled<Self> {
250        Styled {
251            style: StyleKind::CursorHorizontalAbsolute(pos),
252            value: self,
253        }
254    }
255
256    /// Move cursor to the beginning of line which is `count` lines above the current one
257    fn prev_line(&self, count: i32) -> Styled<Self> {
258        Styled {
259            style: StyleKind::CursorPreviousLine(count),
260            value: self,
261        }
262    }
263
264    /// Wrap into cursor saver --- for example for outputting PS1 above the PS1 "line"
265    fn save_restore(&self) -> Styled<Self> {
266        Styled {
267            style: StyleKind::CursorSaveRestore,
268            value: self,
269        }
270    }
271
272    /// Prepends line cleaner
273    fn clear_till_end(&self) -> Styled<Self> {
274        Styled {
275            style: StyleKind::ClearLine,
276            value: self,
277        }
278    }
279
280    /// Join current line with fixed one with newline
281    fn join_lf(&self, s: String) -> Styled<Self> {
282        Styled {
283            style: StyleKind::NewlineJoin(s),
284            value: self,
285        }
286    }
287
288    /// Red color from 16-color palette (CSI 31)
289    fn red(&self) -> Styled<Self> {
290        self.low_color(0)
291    }
292
293    /// Green color from 16-color palette (CSI 32)
294    fn green(&self) -> Styled<Self> {
295        self.low_color(1)
296    }
297
298    /// Yellow color from 16-color palette (CSI 33)
299    fn yellow(&self) -> Styled<Self> {
300        self.low_color(2)
301    }
302
303    /// Blue color from 16-color palette (CSI 34)
304    fn blue(&self) -> Styled<Self> {
305        self.low_color(3)
306    }
307
308    /// Purple color from 16-color palette (CSI 35)
309    fn purple(&self) -> Styled<Self> {
310        self.low_color(4)
311    }
312
313    /// Cyan color from 16-color palette (CSI 36)
314    fn cyan(&self) -> Styled<Self> {
315        self.low_color(5)
316    }
317
318    /// Light gray color from 16-color palette (CSI 37)
319    fn light_gray(&self) -> Styled<Self> {
320        self.low_color(6)
321    }
322
323    /// Pink color (true)
324    fn pink(&self) -> Styled<Self> {
325        self.true_color(255, 100, 203)
326    }
327
328    /// Light green color (true)
329    fn light_green(&self) -> Styled<Self> {
330        self.true_color(100, 255, 100)
331    }
332
333    /// Light red color (true)
334    fn light_red(&self) -> Styled<Self> {
335        self.true_color(255, 80, 100)
336    }
337
338    /// Gray color (true)
339    fn gray(&self) -> Styled<Self> {
340        self.true_color(128, 128, 128)
341    }
342
343    /// String autocolorizer.
344    ///
345    /// Colors `self` with a "random" color associated with given string `with`.
346    ///
347    /// |`with` value|Resulting color     |
348    /// |------------|--------------------|
349    /// |`="root"`   | Red                |
350    /// |other       | Some non-red color |
351    ///
352    /// There are 24 different colors
353    fn colorize_with(&self, with: &str) -> Styled<Self> {
354        if with == "root" {
355            self.red()
356        } else {
357            let idx = polyhash(with, 23, 179, with.len()) + 1;
358            let (r, g, b) = HSV_COLOR_TABLE[idx];
359            self.true_color(r, g, b)
360        }
361    }
362}
363
364/// All types which can be displayed can be styled too
365impl<T: Display + ?Sized> Style for T {}
366
367fn polyhash(s: &str, m: usize, p: usize, h_init: usize) -> usize {
368    let mut h = h_init % m;
369    for by in s.bytes() {
370        h = (h * p + by as usize) % m;
371    }
372    h
373}