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    pub fg: Option<Color>,
17    pub bg: Option<Color>,
18    pub bold: bool,
19    pub italic: bool,
20    pub underline: bool,
21    pub dim: bool,
22}
23
24impl Style {
25    pub fn new() -> Self {
26        Self::default()
27    }
28
29    pub fn fg(mut self, color: Color) -> Self {
30        self.fg = Some(color);
31        self
32    }
33
34    pub fn bg(mut self, color: Color) -> Self {
35        self.bg = Some(color);
36        self
37    }
38
39    pub fn bold(mut self) -> Self {
40        self.bold = true;
41        self
42    }
43
44    pub fn italic(mut self) -> Self {
45        self.italic = true;
46        self
47    }
48
49    pub fn underline(mut self) -> Self {
50        self.underline = true;
51        self
52    }
53
54    pub fn dim(mut self) -> Self {
55        self.dim = true;
56        self
57    }
58
59    /// Generate the ANSI escape prefix for this style.
60    pub fn prefix(&self, mode: ColorMode) -> String {
61        let mut parts = Vec::new();
62        if let Some(c) = self.fg {
63            parts.push(ansi::fg(c, mode));
64        }
65        if let Some(c) = self.bg {
66            parts.push(ansi::bg(c, mode));
67        }
68        if self.bold {
69            parts.push("\x1b[1m".to_string());
70        }
71        if self.dim {
72            parts.push("\x1b[2m".to_string());
73        }
74        if self.italic {
75            parts.push("\x1b[3m".to_string());
76        }
77        if self.underline {
78            parts.push("\x1b[4m".to_string());
79        }
80        parts.concat()
81    }
82
83    /// The ANSI reset suffix.
84    pub const fn suffix() -> &'static str {
85        ansi::RESET
86    }
87}
88
89/// Wrap `text` with the ANSI codes for `style`, automatically resetting
90/// at the end. Respects the active color mode.
91pub fn stylize(text: &str, style: &Style) -> String {
92    let mode = ColorMode::detect();
93    format!("{}{}{}", style.prefix(mode), text, Style::suffix())
94}
95
96/// Like [`stylize`], but pads `text` with `pad` spaces on each side.
97pub fn stylize_padded(text: &str, style: &Style, pad: usize) -> String {
98    let padding = " ".repeat(pad);
99    stylize(&format!("{}{}{}", padding, text, padding), style)
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn style_builder() {
108        let s = Style::new().fg(Color::SUNBEAM_ORANGE).bold();
109        assert_eq!(s.fg, Some(Color::SUNBEAM_ORANGE));
110        assert!(s.bold);
111        assert!(!s.italic);
112    }
113
114    #[test]
115    fn stylize_produces_codes() {
116        let s = Style::new().fg(Color::SUNBEAM_ORANGE);
117        let out = stylize("hi", &s);
118        assert!(out.contains("hi"));
119        assert!(out.starts_with('\x1b'));
120        assert!(out.ends_with("\x1b[0m"));
121    }
122
123    #[test]
124    fn stylize_padded_applies_padding() {
125        let s = Style::new().fg(Color::WHITE);
126        let out = stylize_padded("ok", &s, 2);
127        assert!(out.contains("  ok  "));
128    }
129}