1use super::{
8 Color,
9 ColorMode,
10 ansi,
11};
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
15pub struct Style {
16 pub fg: Option<Color>,
18 pub bg: Option<Color>,
20 pub bold: bool,
22 pub italic: bool,
24 pub underline: bool,
26 pub dim: bool,
28}
29
30impl Style {
31 pub fn new() -> Self {
33 Self::default()
34 }
35
36 pub fn fg(mut self, color: Color) -> Self {
38 self.fg = Some(color);
39 self
40 }
41
42 pub fn bg(mut self, color: Color) -> Self {
44 self.bg = Some(color);
45 self
46 }
47
48 pub fn bold(mut self) -> Self {
50 self.bold = true;
51 self
52 }
53
54 pub fn italic(mut self) -> Self {
56 self.italic = true;
57 self
58 }
59
60 pub fn underline(mut self) -> Self {
62 self.underline = true;
63 self
64 }
65
66 pub fn dim(mut self) -> Self {
68 self.dim = true;
69 self
70 }
71
72 pub fn prefix(&self, mode: ColorMode) -> String {
74 let mut parts = Vec::new();
75 if let Some(c) = self.fg {
76 parts.push(ansi::fg(c, mode));
77 }
78 if let Some(c) = self.bg {
79 parts.push(ansi::bg(c, mode));
80 }
81 if self.bold {
82 parts.push("\x1b[1m".to_string());
83 }
84 if self.dim {
85 parts.push("\x1b[2m".to_string());
86 }
87 if self.italic {
88 parts.push("\x1b[3m".to_string());
89 }
90 if self.underline {
91 parts.push("\x1b[4m".to_string());
92 }
93 parts.concat()
94 }
95
96 pub fn suffix(&self) -> &'static str {
103 if self.bold || self.dim {
104 "\x1b[22m\x1b[0m"
105 } else {
106 ansi::RESET
107 }
108 }
109}
110
111pub fn stylize(text: &str, style: &Style) -> String {
114 let mode = ColorMode::detect();
115 format!("{}{}{}", style.prefix(mode), text, style.suffix())
116}
117
118pub fn stylize_padded(text: &str, style: &Style, pad: usize) -> String {
120 let padding = " ".repeat(pad);
121 stylize(&format!("{}{}{}", padding, text, padding), style)
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn style_builder() {
130 let s = Style::new().fg(Color::SUNBEAM_ORANGE).bold();
131 assert_eq!(s.fg, Some(Color::SUNBEAM_ORANGE));
132 assert!(s.bold);
133 assert!(!s.italic);
134 }
135
136 #[test]
137 fn stylize_produces_codes() {
138 let s = Style::new().fg(Color::SUNBEAM_ORANGE);
139 let out = stylize("hi", &s);
140 assert!(out.contains("hi"));
141 assert!(out.starts_with('\x1b'));
142 assert!(out.ends_with("\x1b[0m"));
143 }
144
145 #[test]
146 fn stylize_padded_applies_padding() {
147 let s = Style::new().fg(Color::WHITE);
148 let out = stylize_padded("ok", &s, 2);
149 assert!(out.contains(" ok "));
150 }
151
152 #[test]
155 fn stylize_bold_emits_bold_off() {
156 let s = Style::new().bold();
157 let out = stylize("hi", &s);
158 assert!(out.ends_with("\x1b[22m\x1b[0m"));
159 }
160
161 #[test]
163 fn stylize_non_bold_uses_plain_reset() {
164 let s = Style::new().fg(Color::WHITE);
165 let out = stylize("hi", &s);
166 assert!(out.ends_with("\x1b[0m"));
167 assert!(!out.contains("\x1b[22m"));
168 }
169
170 #[test]
171 fn style_italic_prefix_and_suffix() {
172 let s = Style::new().italic();
173 let out = stylize("hi", &s);
174 assert!(out.contains("\x1b[3m"));
175 assert!(out.ends_with("\x1b[0m"));
176 }
177
178 #[test]
179 fn style_dim_prefix_and_suffix() {
180 let s = Style::new().dim();
181 let out = stylize("hi", &s);
182 assert!(out.contains("\x1b[2m"));
183 assert!(out.ends_with("\x1b[22m\x1b[0m"));
184 }
185
186 #[test]
187 fn style_full_prefix() {
188 let s = Style::new()
189 .fg(Color::WHITE)
190 .bg(Color::BLACK)
191 .bold()
192 .italic()
193 .underline()
194 .dim();
195 let prefix = s.prefix(ColorMode::TrueColor);
196 assert!(prefix.contains("\x1b[38;2;255;255;255m"));
197 assert!(prefix.contains("\x1b[48;2;0;0;0m"));
198 assert!(prefix.contains("\x1b[1m"));
199 assert!(prefix.contains("\x1b[2m"));
200 assert!(prefix.contains("\x1b[3m"));
201 assert!(prefix.contains("\x1b[4m"));
202 }
203}