1use anstyle::{AnsiColor, Color, Style};
17
18#[derive(Debug, Clone, Copy)]
21pub struct Palette;
22
23impl Palette {
24 #[must_use]
26 pub fn key() -> Style {
27 Style::new().bold()
28 }
29
30 #[must_use]
32 pub fn label() -> Style {
33 Style::new().dimmed()
34 }
35
36 #[must_use]
38 pub fn ok() -> Style {
39 Style::new().fg_color(Some(Color::Ansi(AnsiColor::Green)))
40 }
41
42 #[must_use]
44 pub fn warn() -> Style {
45 Style::new().fg_color(Some(Color::Ansi(AnsiColor::Yellow)))
46 }
47
48 #[must_use]
50 pub fn bad() -> Style {
51 Style::new().fg_color(Some(Color::Ansi(AnsiColor::Red)))
52 }
53
54 #[must_use]
56 pub fn url() -> Style {
57 Style::new()
58 .fg_color(Some(Color::Ansi(AnsiColor::Cyan)))
59 .underline()
60 }
61
62 #[must_use]
64 pub fn heading() -> Style {
65 Style::new().bold().underline()
66 }
67
68 #[must_use]
70 pub fn untrusted() -> Style {
71 Style::new().dimmed()
72 }
73}
74
75#[derive(Debug, Clone, Copy)]
77pub struct Painter {
78 enabled: bool,
79}
80
81impl Painter {
82 #[must_use]
84 pub fn colour() -> Self {
85 Self { enabled: true }
86 }
87
88 #[must_use]
90 pub fn plain() -> Self {
91 Self { enabled: false }
92 }
93
94 #[must_use]
96 pub fn for_stream(is_terminal: bool) -> Self {
97 Self {
98 enabled: is_terminal,
99 }
100 }
101
102 #[must_use]
104 pub fn paint(self, text: &str, style: Style) -> String {
105 if !self.enabled {
106 return text.to_owned();
107 }
108 format!("{style}{text}{style:#}")
109 }
110
111 #[must_use]
116 pub fn paint_padded(self, text: &str, width: usize, style: Style) -> String {
117 let visible = text.chars().count();
118 let padding = width.saturating_sub(visible);
119 format!("{}{}", self.paint(text, style), " ".repeat(padding))
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn a_plain_painter_changes_nothing() {
129 assert_eq!(Painter::plain().paint("PROJ-1", Palette::key()), "PROJ-1");
130 }
131
132 #[test]
133 fn a_colour_painter_wraps_and_resets() {
134 let painted = Painter::colour().paint("PROJ-1", Palette::key());
135 assert!(painted.starts_with('\u{1b}'));
136 assert!(painted.contains("PROJ-1"));
137 assert!(painted.ends_with("\u{1b}[0m"));
138 }
139
140 #[test]
143 fn padding_counts_visible_characters_only() {
144 let plain = Painter::plain().paint_padded("PROJ-1", 12, Palette::key());
145 let coloured = Painter::colour().paint_padded("PROJ-1", 12, Palette::key());
146
147 assert_eq!(plain, "PROJ-1 ");
148 assert!(coloured.ends_with(" "));
149 assert_eq!(
150 coloured.matches(' ').count(),
151 plain.matches(' ').count(),
152 "same visible width in both modes"
153 );
154 }
155
156 #[test]
157 fn text_longer_than_the_column_is_not_truncated_by_padding() {
158 assert_eq!(
159 Painter::plain().paint_padded("very-long-key", 4, Palette::key()),
160 "very-long-key"
161 );
162 }
163}