Skip to main content

photon_ui/theme/
style.rs

1//! Composite text styles for terminal rendering.
2//!
3//! A [`Style`] bundles foreground color, background color, and text
4//! attributes (bold, italic, underline, dim) into a single unit that
5//! can be applied to strings via [`stylize`].
6
7use super::{
8    Color,
9    ColorMode,
10    ansi,
11};
12
13/// A terminal text style: colors + attributes.
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
15pub struct Style {
16    /// Foreground color.
17    pub fg: Option<Color>,
18    /// Background color.
19    pub bg: Option<Color>,
20    /// Bold text attribute.
21    pub bold: bool,
22    /// Italic text attribute.
23    pub italic: bool,
24    /// Underline text attribute.
25    pub underline: bool,
26    /// Dim / faint text attribute.
27    pub dim: bool,
28}
29
30impl Style {
31    /// Create a new default style.
32    pub fn new() -> Self {
33        Self::default()
34    }
35
36    /// Set the foreground color.
37    pub fn fg(mut self, color: Color) -> Self {
38        self.fg = Some(color);
39        self
40    }
41
42    /// Set the background color.
43    pub fn bg(mut self, color: Color) -> Self {
44        self.bg = Some(color);
45        self
46    }
47
48    /// Enable bold text.
49    pub fn bold(mut self) -> Self {
50        self.bold = true;
51        self
52    }
53
54    /// Enable italic text.
55    pub fn italic(mut self) -> Self {
56        self.italic = true;
57        self
58    }
59
60    /// Enable underlined text.
61    pub fn underline(mut self) -> Self {
62        self.underline = true;
63        self
64    }
65
66    /// Enable dim text.
67    pub fn dim(mut self) -> Self {
68        self.dim = true;
69        self
70    }
71
72    /// Generate the ANSI escape prefix for this style.
73    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    /// The ANSI reset suffix.
97    pub const fn suffix() -> &'static str {
98        ansi::RESET
99    }
100}
101
102/// Wrap `text` with the ANSI codes for `style`, automatically resetting
103/// at the end. Respects the active color mode.
104pub fn stylize(text: &str, style: &Style) -> String {
105    let mode = ColorMode::detect();
106    format!("{}{}{}", style.prefix(mode), text, Style::suffix())
107}
108
109/// Like [`stylize`], but pads `text` with `pad` spaces on each side.
110pub fn stylize_padded(text: &str, style: &Style, pad: usize) -> String {
111    let padding = " ".repeat(pad);
112    stylize(&format!("{}{}{}", padding, text, padding), style)
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn style_builder() {
121        let s = Style::new().fg(Color::SUNBEAM_ORANGE).bold();
122        assert_eq!(s.fg, Some(Color::SUNBEAM_ORANGE));
123        assert!(s.bold);
124        assert!(!s.italic);
125    }
126
127    #[test]
128    fn stylize_produces_codes() {
129        let s = Style::new().fg(Color::SUNBEAM_ORANGE);
130        let out = stylize("hi", &s);
131        assert!(out.contains("hi"));
132        assert!(out.starts_with('\x1b'));
133        assert!(out.ends_with("\x1b[0m"));
134    }
135
136    #[test]
137    fn stylize_padded_applies_padding() {
138        let s = Style::new().fg(Color::WHITE);
139        let out = stylize_padded("ok", &s, 2);
140        assert!(out.contains("  ok  "));
141    }
142}