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}