tinter/
lib.rs

1/*
2 * Copyright (c) Peter Bjorklund. All rights reserved. https://github.com/swamp/swamp
3 * Licensed under the MIT License. See LICENSE in the project root for license information.
4 */
5use std::fmt::{Display, Formatter};
6use std::io::Write;
7use std::{fmt, io};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Color {
11    Black,
12    Red,
13    Green,
14    Yellow,
15    Blue,
16    Magenta,
17    Cyan,
18    White,
19    BrightBlack,
20    BrightRed,
21    BrightGreen,
22    BrightYellow,
23    BrightBlue,
24    BrightMagenta,
25    BrightCyan,
26    BrightWhite,
27}
28
29impl Color {
30    #[must_use]
31    pub const fn code(&self) -> u8 {
32        match self {
33            Self::Black => 30,
34            Self::Red => 31,
35            Self::Green => 32,
36            Self::Yellow => 33,
37            Self::Blue => 34,
38            Self::Magenta => 35,
39            Self::Cyan => 36,
40            Self::White => 37,
41            Self::BrightBlack => 90,
42            Self::BrightRed => 91,
43            Self::BrightGreen => 92,
44            Self::BrightYellow => 93,
45            Self::BrightBlue => 94,
46            Self::BrightMagenta => 95,
47            Self::BrightCyan => 96,
48            Self::BrightWhite => 97,
49        }
50    }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct Style {
55    fg_color: Option<Color>,
56}
57
58impl Default for Style {
59    fn default() -> Self {
60        Self::new()
61    }
62}
63
64impl Style {
65    #[must_use]
66    pub const fn new() -> Self {
67        Self { fg_color: None }
68    }
69
70    #[must_use]
71    pub const fn fg(mut self, color: Color) -> Self {
72        self.fg_color = Some(color);
73        self
74    }
75}
76
77#[must_use]
78pub const fn style() -> Style {
79    Style::new()
80}
81
82#[must_use]
83pub const fn fg_black() -> Style {
84    style().fg(Color::Black)
85}
86#[must_use]
87pub const fn fg_red() -> Style {
88    style().fg(Color::Red)
89}
90#[must_use]
91pub const fn fg_green() -> Style {
92    style().fg(Color::Green)
93}
94#[must_use]
95pub const fn fg_yellow() -> Style {
96    style().fg(Color::Yellow)
97}
98#[must_use]
99pub const fn fg_blue() -> Style {
100    style().fg(Color::Blue)
101}
102#[must_use]
103pub const fn fg_magenta() -> Style {
104    style().fg(Color::Magenta)
105}
106#[must_use]
107pub const fn fg_cyan() -> Style {
108    style().fg(Color::Cyan)
109}
110#[must_use]
111pub const fn fg_white() -> Style {
112    style().fg(Color::White)
113}
114#[must_use]
115pub const fn fg_bright_black() -> Style {
116    style().fg(Color::BrightBlack)
117}
118#[must_use]
119pub const fn fg_bright_red() -> Style {
120    style().fg(Color::BrightRed)
121}
122#[must_use]
123pub const fn fg_bright_green() -> Style {
124    style().fg(Color::BrightGreen)
125}
126#[must_use]
127pub const fn fg_bright_yellow() -> Style {
128    style().fg(Color::BrightYellow)
129}
130#[must_use]
131pub const fn fg_bright_blue() -> Style {
132    style().fg(Color::BrightBlue)
133}
134#[must_use]
135pub const fn fg_bright_magenta() -> Style {
136    style().fg(Color::BrightMagenta)
137}
138#[must_use]
139pub const fn fg_bright_cyan() -> Style {
140    style().fg(Color::BrightCyan)
141}
142#[must_use]
143pub const fn fg_bright_white() -> Style {
144    style().fg(Color::BrightWhite)
145}
146
147#[allow(clippy::missing_errors_doc)]
148pub fn set_style(writer: &mut dyn Write, style: Style) -> io::Result<()> {
149    write!(writer, "\x1b[0m")?;
150    if let Some(color) = style.fg_color {
151        write!(writer, "\x1b[{}m", color.code())?;
152    }
153    Ok(())
154}
155#[allow(clippy::missing_errors_doc)]
156pub fn reset_style(writer: &mut dyn Write) -> io::Result<()> {
157    write!(writer, "\x1b[0m")
158}
159
160#[allow(clippy::missing_errors_doc)]
161pub fn print_styled<W: Write, T: Display>(
162    writer: &mut W,
163    style: Style,
164    value: T,
165) -> io::Result<()> {
166    set_style(writer, style)?;
167    write!(writer, "{value}")?;
168    reset_style(writer)?;
169    writer.flush()?;
170    Ok(())
171}
172
173#[allow(clippy::missing_errors_doc)]
174pub fn println_styled<W: Write, T: Display>(
175    writer: &mut W,
176    style: Style,
177    value: T,
178) -> io::Result<()> {
179    set_style(writer, style)?;
180    writeln!(writer, "{value}")?;
181    reset_style(writer)?;
182    writer.flush()?;
183    Ok(())
184}
185
186pub struct StyledText<T: Display> {
187    style: Style,
188    value: T,
189}
190
191impl<T: Display> StyledText<T> {
192    pub const fn new(style: Style, value: T) -> Self {
193        Self { style, value }
194    }
195}
196
197impl<T: Display> Display for StyledText<T> {
198    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
199        if let Some(color) = self.style.fg_color {
200            write!(f, "\x1b[{}m", color.code())?;
201        }
202        write!(f, "{}", self.value)?;
203        write!(f, "\x1b[0m")?;
204        Ok(())
205    }
206}
207
208pub const fn color<T: Display>(color: Color, value: T) -> StyledText<T> {
209    StyledText::new(style().fg(color), value)
210}
211
212pub const fn black<T: Display>(value: T) -> StyledText<T> {
213    color(Color::Black, value)
214}
215pub const fn red<T: Display>(value: T) -> StyledText<T> {
216    color(Color::Red, value)
217}
218pub const fn green<T: Display>(value: T) -> StyledText<T> {
219    color(Color::Green, value)
220}
221pub const fn yellow<T: Display>(value: T) -> StyledText<T> {
222    color(Color::Yellow, value)
223}
224pub const fn blue<T: Display>(value: T) -> StyledText<T> {
225    color(Color::Blue, value)
226}
227pub const fn magenta<T: Display>(value: T) -> StyledText<T> {
228    color(Color::Magenta, value)
229}
230pub const fn cyan<T: Display>(value: T) -> StyledText<T> {
231    color(Color::Cyan, value)
232}
233pub const fn white<T: Display>(value: T) -> StyledText<T> {
234    color(Color::White, value)
235}
236pub const fn bright_black<T: Display>(value: T) -> StyledText<T> {
237    color(Color::BrightBlack, value)
238}
239pub const fn bright_red<T: Display>(value: T) -> StyledText<T> {
240    color(Color::BrightRed, value)
241}
242pub const fn bright_green<T: Display>(value: T) -> StyledText<T> {
243    color(Color::BrightGreen, value)
244}
245pub const fn bright_yellow<T: Display>(value: T) -> StyledText<T> {
246    color(Color::BrightYellow, value)
247}
248pub const fn bright_blue<T: Display>(value: T) -> StyledText<T> {
249    color(Color::BrightBlue, value)
250}
251pub const fn bright_magenta<T: Display>(value: T) -> StyledText<T> {
252    color(Color::BrightMagenta, value)
253}
254pub const fn bright_cyan<T: Display>(value: T) -> StyledText<T> {
255    color(Color::BrightCyan, value)
256}
257pub const fn bright_white<T: Display>(value: T) -> StyledText<T> {
258    color(Color::BrightWhite, value)
259}
260
261/// A printer that can toggle color output on or off
262#[derive(Debug, Clone)]
263pub struct Printer {
264    use_color: bool,
265}
266
267impl Default for Printer {
268    fn default() -> Self {
269        Self::new()
270    }
271}
272
273impl Printer {
274    /// Create a new printer with colors enabled
275    #[must_use]
276    pub const fn new() -> Self {
277        Self { use_color: true }
278    }
279
280    /// Create a printer with colors disabled
281    #[must_use]
282    pub const fn without_color() -> Self {
283        Self { use_color: false }
284    }
285
286    /// Create a printer with color detection based on TTY
287    #[must_use]
288    pub fn auto_detect() -> Self {
289        #[cfg(unix)]
290        {
291            use std::io::IsTerminal;
292            Self {
293                use_color: std::io::stdout().is_terminal(),
294            }
295        }
296        #[cfg(not(unix))]
297        {
298            Self { use_color: true }
299        }
300    }
301
302    /// Enable colors
303    pub fn enable_color(&mut self) {
304        self.use_color = true;
305    }
306
307    /// Disable colors
308    pub fn disable_color(&mut self) {
309        self.use_color = false;
310    }
311
312    /// Check if colors are enabled
313    #[must_use]
314    pub const fn colors_enabled(&self) -> bool {
315        self.use_color
316    }
317
318    /// Print styled text, respecting the color setting
319    pub fn print<W: Write, T: Display>(
320        &self,
321        writer: &mut W,
322        style: Style,
323        value: T,
324    ) -> io::Result<()> {
325        if self.use_color {
326            print_styled(writer, style, value)
327        } else {
328            write!(writer, "{value}")?;
329            writer.flush()
330        }
331    }
332
333    /// Print styled text with newline, respecting the color setting
334    pub fn println<W: Write, T: Display>(
335        &self,
336        writer: &mut W,
337        style: Style,
338        value: T,
339    ) -> io::Result<()> {
340        if self.use_color {
341            println_styled(writer, style, value)
342        } else {
343            writeln!(writer, "{value}")?;
344            writer.flush()
345        }
346    }
347
348    /// Create a styled text that respects this printer's color setting
349    #[must_use]
350    pub fn styled<T: Display>(&self, style: Style, value: T) -> PrinterStyledText<T> {
351        PrinterStyledText {
352            style,
353            value,
354            use_color: self.use_color,
355        }
356    }
357
358    /// Convenience method for colored text
359    #[must_use]
360    pub fn color<T: Display>(&self, color: Color, value: T) -> PrinterStyledText<T> {
361        self.styled(style().fg(color), value)
362    }
363}
364
365/// Styled text that respects a specific color setting
366pub struct PrinterStyledText<T: Display> {
367    style: Style,
368    value: T,
369    use_color: bool,
370}
371
372impl<T: Display> Display for PrinterStyledText<T> {
373    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
374        if self.use_color {
375            if let Some(color) = self.style.fg_color {
376                write!(f, "\x1b[{}m", color.code())?;
377            }
378            write!(f, "{}", self.value)?;
379            write!(f, "\x1b[0m")?;
380        } else {
381            write!(f, "{}", self.value)?;
382        }
383        Ok(())
384    }
385}