scope/display/theme/
text.rs1use super::decision::{color_enabled, color_enabled_stderr};
10use super::{AnsiColor, Role, Tone, tone};
11
12const CSI: &str = "\x1b[";
13const SGR_END: &str = "m";
14const RESET_ALL: u8 = 0;
15const BOLD: u8 = 1;
16const DIM: u8 = 2;
17const UNDERLINE: u8 = 4;
18const REVERSE: u8 = 7;
19const DEFAULT_FOREGROUND: u8 = 39;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct Painter {
25 enabled: bool,
26}
27
28impl Painter {
29 pub const fn new(enabled: bool) -> Self {
32 Self { enabled }
33 }
34
35 pub fn stdout() -> Self {
37 Self::new(color_enabled())
38 }
39
40 pub fn stderr() -> Self {
42 Self::new(color_enabled_stderr())
43 }
44
45 pub fn paint(self, role: Role, text: &str) -> String {
47 self.paint_tone(tone(role), text)
48 }
49
50 pub fn paint_strong(self, role: Role, text: &str) -> String {
52 self.paint_tone(tone(role).strong(), text)
53 }
54
55 pub fn paint_tone(self, tone: Tone, text: &str) -> String {
58 if self.enabled {
59 render(tone, text)
60 } else {
61 text.to_string()
62 }
63 }
64}
65
66pub fn paint(role: Role, text: &str) -> String {
68 Painter::stdout().paint(role, text)
69}
70
71fn render(tone: Tone, text: &str) -> String {
73 let effects = [
74 (tone.reverse, REVERSE),
75 (tone.underline, UNDERLINE),
76 (tone.dim, DIM),
77 (tone.bold, BOLD),
78 ];
79 let mut out = text.to_string();
80 for (_, code) in effects.into_iter().filter(|(on, _)| *on) {
81 out = wrap(code, &out, RESET_ALL);
82 }
83 match tone.color {
84 Some(color) => wrap(foreground_code(color), &out, DEFAULT_FOREGROUND),
85 None => out,
86 }
87}
88
89fn wrap(start: u8, text: &str, end: u8) -> String {
90 format!("{CSI}{start}{SGR_END}{text}{CSI}{end}{SGR_END}")
91}
92
93const fn foreground_code(color: AnsiColor) -> u8 {
95 match color {
96 AnsiColor::Black => 30,
97 AnsiColor::Red => 31,
98 AnsiColor::Green => 32,
99 AnsiColor::Yellow => 33,
100 AnsiColor::Blue => 34,
101 AnsiColor::Magenta => 35,
102 AnsiColor::Cyan => 36,
103 AnsiColor::White => 37,
104 AnsiColor::BrightBlack => 90,
105 AnsiColor::BrightRed => 91,
106 AnsiColor::BrightGreen => 92,
107 AnsiColor::BrightYellow => 93,
108 AnsiColor::BrightBlue => 94,
109 AnsiColor::BrightMagenta => 95,
110 AnsiColor::BrightCyan => 96,
111 AnsiColor::BrightWhite => 97,
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 const ON: Painter = Painter::new(true);
120 const OFF: Painter = Painter::new(false);
121
122 #[test]
123 fn color_off_leaves_every_role_as_plain_text() {
124 for role in Role::ALL {
126 assert_eq!(OFF.paint(role, "x"), "x", "{role:?}");
127 assert_eq!(OFF.paint_strong(role, "x"), "x", "{role:?}");
128 }
129 }
130
131 #[test]
132 fn color_on_marks_every_role_but_neutral() {
133 for role in Role::ALL {
134 let painted = ON.paint(role, "x");
135 assert_eq!(painted == "x", role == Role::Neutral, "{role:?}");
136 }
137 }
138
139 #[test]
140 fn a_color_closes_with_default_foreground_and_effects_reset_inside_it() {
141 assert_eq!(ON.paint(Role::Accent, "x"), "\x1b[36mx\x1b[39m");
142 assert_eq!(ON.paint(Role::Secondary, "x"), "\x1b[2mx\x1b[0m");
143 assert_eq!(
144 ON.paint_strong(Role::Err, "x"),
145 "\x1b[31m\x1b[1mx\x1b[0m\x1b[39m"
146 );
147 assert_eq!(
148 ON.paint(Role::Selected, "x"),
149 "\x1b[36m\x1b[7mx\x1b[0m\x1b[39m"
150 );
151 }
152
153 #[test]
154 fn bright_colors_use_the_high_intensity_codes() {
155 let tone = Tone {
156 color: Some(AnsiColor::BrightBlack),
157 ..Tone::PLAIN
158 };
159 assert_eq!(ON.paint_tone(tone, "x"), "\x1b[90mx\x1b[39m");
160 }
161}