Skip to main content

rusty_rich/
style.rs

1//! Text style — equivalent to Rich's `style.py`.
2//!
3//! A Style combines a foreground color, background color, and a set of
4//! boolean text attributes (bold, italic, underline, etc.).
5
6use std::fmt;
7use std::hash::{Hash, Hasher};
8use std::sync::atomic::{AtomicU32, Ordering};
9
10use crate::color::Color;
11
12static NEXT_ID: AtomicU32 = AtomicU32::new(0);
13
14// ---------------------------------------------------------------------------
15// Style attributes — bit flags
16// ---------------------------------------------------------------------------
17
18/// Bit flags for text attributes.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub struct Attributes(u32);
21
22impl Attributes {
23    pub const BOLD: u32 = 1 << 0;
24    pub const DIM: u32 = 1 << 1;
25    pub const ITALIC: u32 = 1 << 2;
26    pub const UNDERLINE: u32 = 1 << 3;
27    pub const BLINK: u32 = 1 << 4;
28    pub const REVERSE: u32 = 1 << 5;
29    pub const STRIKE: u32 = 1 << 6;
30    pub const UNDERLINE2: u32 = 1 << 7;
31    pub const FRAME: u32 = 1 << 8;
32    pub const ENCIRCLE: u32 = 1 << 9;
33    pub const OVERLINE: u32 = 1 << 10;
34    pub const BLINK2: u32 = 1 << 11;
35    pub const CONCEAL: u32 = 1 << 12;
36
37    pub const fn empty() -> Self {
38        Self(0)
39    }
40
41    pub fn set(&mut self, bit: u32, value: bool) {
42        if value {
43            self.0 |= bit;
44        } else {
45            self.0 &= !bit;
46        }
47    }
48
49    pub fn get(&self, bit: u32) -> bool {
50        self.0 & bit != 0
51    }
52
53    pub const fn bits(&self) -> u32 {
54        self.0
55    }
56}
57
58/// All 13 style attribute bits in order (for iteration).
59pub const STYLE_BITS: &[u32] = &[
60    Attributes::BOLD, Attributes::DIM, Attributes::ITALIC,
61    Attributes::UNDERLINE, Attributes::BLINK, Attributes::REVERSE,
62    Attributes::STRIKE, Attributes::UNDERLINE2, Attributes::FRAME,
63    Attributes::ENCIRCLE, Attributes::OVERLINE, Attributes::BLINK2,
64    Attributes::CONCEAL,
65];
66
67impl fmt::Display for Attributes {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        let mut parts: Vec<&str> = Vec::new();
70        if self.get(Self::BOLD) { parts.push("bold"); }
71        if self.get(Self::DIM) { parts.push("dim"); }
72        if self.get(Self::ITALIC) { parts.push("italic"); }
73        if self.get(Self::UNDERLINE) { parts.push("underline"); }
74        if self.get(Self::BLINK) { parts.push("blink"); }
75        if self.get(Self::REVERSE) { parts.push("reverse"); }
76        if self.get(Self::CONCEAL) { parts.push("conceal"); }
77        if self.get(Self::STRIKE) { parts.push("strike"); }
78        if self.get(Self::OVERLINE) { parts.push("overline"); }
79        if parts.is_empty() {
80            write!(f, "none")
81        } else {
82            write!(f, "{}", parts.join(" "))
83        }
84    }
85}
86
87// ---------------------------------------------------------------------------
88// Style
89// ---------------------------------------------------------------------------
90
91/// A terminal style.
92///
93/// Supports foreground color, background color, attributes, and an optional
94/// hyperlink. Attributes use a three-state system: set to `true`, set to
95/// `false`, or not set (`None`).
96#[derive(Debug, Clone)]
97pub struct Style {
98    pub(crate) color: Option<Color>,
99    pub(crate) bgcolor: Option<Color>,
100    pub(crate) attributes: Attributes,
101    /// Which attribute bits have been explicitly set (vs inherited).
102    pub(crate) set_attributes: u32,
103    pub(crate) link: Option<String>,
104    pub(crate) link_id: u32,
105    pub(crate) is_null: bool,
106    /// Arbitrary metadata attached to this style.
107    pub(crate) meta: Option<Vec<u8>>,
108}
109
110impl Style {
111    // -- constructors -------------------------------------------------------
112
113    /// Create a null (empty) style.
114    pub fn null() -> Self {
115        Self {
116            color: None,
117            bgcolor: None,
118            attributes: Attributes::empty(),
119            set_attributes: 0,
120            link: None,
121            link_id: 0,
122            is_null: true,
123            meta: None,
124        }
125    }
126
127    /// Create a new style with optional settings.
128    pub fn new() -> Self {
129        Self {
130            color: None,
131            bgcolor: None,
132            attributes: Attributes::empty(),
133            set_attributes: 0,
134            link: None,
135            link_id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
136            is_null: false,
137            meta: None,
138        }
139    }
140
141    /// Builder: set foreground color.
142    pub fn color(mut self, color: impl Into<Option<Color>>) -> Self {
143        self.color = color.into();
144        self
145    }
146
147    /// Builder: set background color.
148    pub fn bgcolor(mut self, bgcolor: impl Into<Option<Color>>) -> Self {
149        self.bgcolor = bgcolor.into();
150        self
151    }
152
153    /// Builder: set bold.
154    pub fn bold(mut self, value: bool) -> Self {
155        self.set_attributes |= Attributes::BOLD;
156        self.attributes.set(Attributes::BOLD, value);
157        self
158    }
159
160    /// Builder: set dim.
161    pub fn dim(mut self, value: bool) -> Self {
162        self.set_attributes |= Attributes::DIM;
163        self.attributes.set(Attributes::DIM, value);
164        self
165    }
166
167    /// Builder: set italic.
168    pub fn italic(mut self, value: bool) -> Self {
169        self.set_attributes |= Attributes::ITALIC;
170        self.attributes.set(Attributes::ITALIC, value);
171        self
172    }
173
174    /// Builder: set underline.
175    pub fn underline(mut self, value: bool) -> Self {
176        self.set_attributes |= Attributes::UNDERLINE;
177        self.attributes.set(Attributes::UNDERLINE, value);
178        self
179    }
180
181    /// Builder: set blink.
182    pub fn blink(mut self, value: bool) -> Self {
183        self.set_attributes |= Attributes::BLINK;
184        self.attributes.set(Attributes::BLINK, value);
185        self
186    }
187
188    /// Builder: set reverse.
189    pub fn reverse(mut self, value: bool) -> Self {
190        self.set_attributes |= Attributes::REVERSE;
191        self.attributes.set(Attributes::REVERSE, value);
192        self
193    }
194
195    /// Builder: set strikethrough.
196    pub fn strike(mut self, value: bool) -> Self {
197        self.set_attributes |= Attributes::STRIKE;
198        self.attributes.set(Attributes::STRIKE, value);
199        self
200    }
201
202    /// Builder: set blink2 (rapid blink).
203    pub fn blink2(mut self, value: bool) -> Self {
204        self.set_attributes |= Attributes::BLINK2;
205        self.attributes.set(Attributes::BLINK2, value);
206        self
207    }
208
209    /// Builder: set conceal.
210    pub fn conceal(mut self, value: bool) -> Self {
211        self.set_attributes |= Attributes::CONCEAL;
212        self.attributes.set(Attributes::CONCEAL, value);
213        self
214    }
215
216    /// Builder: set double underline.
217    pub fn underline2(mut self, value: bool) -> Self {
218        self.set_attributes |= Attributes::UNDERLINE2;
219        self.attributes.set(Attributes::UNDERLINE2, value);
220        self
221    }
222
223    /// Builder: set frame.
224    pub fn frame(mut self, value: bool) -> Self {
225        self.set_attributes |= Attributes::FRAME;
226        self.attributes.set(Attributes::FRAME, value);
227        self
228    }
229
230    /// Builder: set encircle.
231    pub fn encircle(mut self, value: bool) -> Self {
232        self.set_attributes |= Attributes::ENCIRCLE;
233        self.attributes.set(Attributes::ENCIRCLE, value);
234        self
235    }
236
237    /// Builder: set overline.
238    pub fn overline(mut self, value: bool) -> Self {
239        self.set_attributes |= Attributes::OVERLINE;
240        self.attributes.set(Attributes::OVERLINE, value);
241        self
242    }
243
244    /// Return a copy with foreground and background colors stripped.
245    pub fn without_color(&self) -> Self {
246        let mut s = self.clone();
247        s.color = None;
248        s.bgcolor = None;
249        s
250    }
251
252    /// Return a style with the background color set to the foreground color,
253    /// useful for background-only rendering.
254    pub fn background_style(&self) -> Self {
255        let mut s = Self::new();
256        s.bgcolor = self.color.clone();
257        s
258    }
259
260    /// Returns true if the background is not set (transparent).
261    pub fn transparent_background(&self) -> bool {
262        self.bgcolor.is_none()
263    }
264
265    /// Builder: set link.
266    pub fn link(mut self, url: impl Into<String>) -> Self {
267        self.link = Some(url.into());
268        self.link_id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
269        self
270    }
271
272    /// Builder: set style from a string (e.g. "bold red on blue").
273    pub fn from_str(definition: &str) -> Self {
274        let mut style = Self::new();
275        for part in definition.split_whitespace() {
276            match part {
277                "bold" | "b" => { style.set_attributes |= Attributes::BOLD; style.attributes.set(Attributes::BOLD, true); }
278                "dim" | "d" => { style.set_attributes |= Attributes::DIM; style.attributes.set(Attributes::DIM, true); }
279                "italic" | "i" => { style.set_attributes |= Attributes::ITALIC; style.attributes.set(Attributes::ITALIC, true); }
280                "underline" | "u" => { style.set_attributes |= Attributes::UNDERLINE; style.attributes.set(Attributes::UNDERLINE, true); }
281                "blink" => { style.set_attributes |= Attributes::BLINK; style.attributes.set(Attributes::BLINK, true); }
282                "reverse" | "r" => { style.set_attributes |= Attributes::REVERSE; style.attributes.set(Attributes::REVERSE, true); }
283                "strike" | "s" => { style.set_attributes |= Attributes::STRIKE; style.attributes.set(Attributes::STRIKE, true); }
284                "not bold" | "!bold" | "nobold" => { style.set_attributes |= Attributes::BOLD; style.attributes.set(Attributes::BOLD, false); }
285                "not italic" | "!italic" | "noitalic" => { style.set_attributes |= Attributes::ITALIC; style.attributes.set(Attributes::ITALIC, false); }
286                "not underline" | "!underline" | "nounderline" => { style.set_attributes |= Attributes::UNDERLINE; style.attributes.set(Attributes::UNDERLINE, false); }
287                "none" | "default" => {}
288                "on" => { /* "on <color>" handled below */ }
289                part if part.starts_with("on ") => {
290                    if let Ok(c) = Color::parse(&part[3..]) {
291                        style.bgcolor = Some(c);
292                    }
293                }
294                part if part.starts_with("link=") => {
295                    style.link = Some(part[5..].to_string());
296                }
297                part => {
298                    // Try as color name
299                    if let Ok(c) = Color::parse(part) {
300                        if style.bgcolor.is_some() && style.color.is_none() {
301                            // We already saw "on" — don't overwrite fg
302                        } else {
303                            style.color = Some(c);
304                        }
305                    }
306                }
307            }
308        }
309        style
310    }
311
312    // -- queries ------------------------------------------------------------
313
314    pub fn is_null(&self) -> bool {
315        self.is_null
316    }
317
318    pub fn is_plain(&self) -> bool {
319        self.color.is_none()
320            && self.bgcolor.is_none()
321            && self.set_attributes == 0
322            && self.link.is_none()
323    }
324
325    /// Check if the bold attribute is explicitly set to `true`.
326    pub fn get_bold(&self) -> Option<bool> {
327        if self.set_attributes & Attributes::BOLD != 0 {
328            Some(self.attributes.get(Attributes::BOLD))
329        } else {
330            None
331        }
332    }
333
334    /// Merge two styles: `self` is the base, `other` overrides.
335    pub fn combine(&self, other: &Style) -> Style {
336        if other.is_null {
337            return self.clone();
338        }
339        if self.is_null {
340            return other.clone();
341        }
342
343        let mut combined = self.clone();
344        if other.color.is_some() {
345            combined.color = other.color.clone();
346        }
347        if other.bgcolor.is_some() {
348            combined.bgcolor = other.bgcolor.clone();
349        }
350        // Attributes: other's set bits override self (3-state cascade)
351        for &bit in STYLE_BITS {
352            if other.set_attributes & bit != 0 {
353                combined.set_attributes |= bit;
354                combined.attributes.set(bit, other.attributes.get(bit));
355            }
356        }
357        if other.link.is_some() {
358            combined.link = other.link.clone();
359            combined.link_id = other.link_id;
360        }
361        if other.meta.is_some() {
362            combined.meta = other.meta.clone();
363        }
364        combined.is_null = false;
365        combined
366    }
367
368    /// Render this style as ANSI SGR escape sequences.
369    pub fn to_ansi(&self) -> String {
370        if self.is_null {
371            return String::new();
372        }
373        let mut codes: Vec<String> = Vec::new();
374
375        // Foreground color
376        if let Some(ref c) = self.color {
377            match c.color_type {
378                crate::color::ColorType::Default => codes.push("39".into()),
379                crate::color::ColorType::Standard => {
380                    if let Some(n) = c.number {
381                        if n < 8 {
382                            codes.push((30 + n).to_string());
383                        } else {
384                            codes.push((82 + n).to_string()); // 90-97 for bright
385                        }
386                    }
387                }
388                crate::color::ColorType::EightBit => {
389                    if let Some(n) = c.number {
390                        codes.push(format!("38;5;{n}"));
391                    }
392                }
393                crate::color::ColorType::TrueColor => {
394                    if let Some((r, g, b)) = c.triplet {
395                        codes.push(format!("38;2;{r};{g};{b}"));
396                    }
397                }
398            }
399        }
400
401        // Background color
402        if let Some(ref c) = self.bgcolor {
403            match c.color_type {
404                crate::color::ColorType::Default => codes.push("49".into()),
405                crate::color::ColorType::Standard => {
406                    if let Some(n) = c.number {
407                        if n < 8 {
408                            codes.push((40 + n).to_string());
409                        } else {
410                            codes.push((92 + n).to_string()); // 100-107
411                        }
412                    }
413                }
414                crate::color::ColorType::EightBit => {
415                    if let Some(n) = c.number {
416                        codes.push(format!("48;5;{n}"));
417                    }
418                }
419                crate::color::ColorType::TrueColor => {
420                    if let Some((r, g, b)) = c.triplet {
421                        codes.push(format!("48;2;{r};{g};{b}"));
422                    }
423                }
424            }
425        }
426
427        // Attributes
428        if self.set_attributes & Attributes::BOLD != 0 {
429            codes.push(if self.attributes.get(Attributes::BOLD) { "1" } else { "22" }.into());
430        }
431        if self.set_attributes & Attributes::DIM != 0 {
432            codes.push(if self.attributes.get(Attributes::DIM) { "2" } else { "22" }.into());
433        }
434        if self.set_attributes & Attributes::ITALIC != 0 {
435            codes.push(if self.attributes.get(Attributes::ITALIC) { "3" } else { "23" }.into());
436        }
437        if self.set_attributes & Attributes::UNDERLINE != 0 {
438            codes.push(if self.attributes.get(Attributes::UNDERLINE) { "4" } else { "24" }.into());
439        }
440        if self.set_attributes & Attributes::BLINK != 0 {
441            codes.push(if self.attributes.get(Attributes::BLINK) { "5" } else { "25" }.into());
442        }
443        if self.set_attributes & Attributes::REVERSE != 0 {
444            codes.push(if self.attributes.get(Attributes::REVERSE) { "7" } else { "27" }.into());
445        }
446        if self.set_attributes & Attributes::CONCEAL != 0 {
447            codes.push(if self.attributes.get(Attributes::CONCEAL) { "8" } else { "28" }.into());
448        }
449        if self.set_attributes & Attributes::STRIKE != 0 {
450            codes.push(if self.attributes.get(Attributes::STRIKE) { "9" } else { "29" }.into());
451        }
452        if self.set_attributes & Attributes::CONCEAL != 0 {
453            codes.push(if self.attributes.get(Attributes::CONCEAL) { "8" } else { "28" }.into());
454        }
455        if self.set_attributes & Attributes::UNDERLINE2 != 0 {
456            codes.push(if self.attributes.get(Attributes::UNDERLINE2) { "21" } else { "24" }.into());
457        }
458        if self.set_attributes & Attributes::BLINK2 != 0 {
459            codes.push(if self.attributes.get(Attributes::BLINK2) { "6" } else { "25" }.into());
460        }
461        if self.set_attributes & Attributes::FRAME != 0 {
462            codes.push(if self.attributes.get(Attributes::FRAME) { "51" } else { "54" }.into());
463        }
464        if self.set_attributes & Attributes::ENCIRCLE != 0 {
465            codes.push(if self.attributes.get(Attributes::ENCIRCLE) { "52" } else { "54" }.into());
466        }
467        if self.set_attributes & Attributes::OVERLINE != 0 {
468            codes.push(if self.attributes.get(Attributes::OVERLINE) { "53" } else { "55" }.into());
469        }
470
471        if codes.is_empty() {
472            String::new()
473        } else {
474            format!("\x1b[{}m", codes.join(";"))
475        }
476    }
477
478    /// Return the ANSI reset sequence needed to turn off this style.
479    pub fn reset_ansi(&self) -> &'static str {
480        "\x1b[0m"
481    }
482}
483
484impl Default for Style {
485    fn default() -> Self {
486        Self::new()
487    }
488}
489
490impl PartialEq for Style {
491    fn eq(&self, other: &Self) -> bool {
492        self.color == other.color
493            && self.bgcolor == other.bgcolor
494            && self.attributes == other.attributes
495            && self.set_attributes == other.set_attributes
496            && self.link == other.link
497    }
498}
499
500impl Eq for Style {}
501
502impl Hash for Style {
503    fn hash<H: Hasher>(&self, state: &mut H) {
504        self.color.hash(state);
505        self.bgcolor.hash(state);
506        self.attributes.hash(state);
507        self.set_attributes.hash(state);
508        self.link.hash(state);
509    }
510}
511
512impl fmt::Display for Style {
513    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
514        if self.is_null {
515            return write!(f, "null");
516        }
517        let mut parts: Vec<String> = Vec::new();
518        if let Some(ref c) = self.color {
519            parts.push(c.to_string());
520        }
521        if let Some(ref c) = self.bgcolor {
522            parts.push(format!("on {}", c));
523        }
524        let attrs = self.attributes.to_string();
525        if attrs != "none" {
526            parts.push(attrs);
527        }
528        if parts.is_empty() {
529            write!(f, "none")
530        } else {
531            write!(f, "{}", parts.join(" "))
532        }
533    }
534}
535
536/// Convenience type alias.
537pub type StyleType = Style;
538
539// ---------------------------------------------------------------------------
540// StyleStack — a stack of styles (for nested markup)
541// ---------------------------------------------------------------------------
542
543/// A stack of styles, used when rendering nested markup.
544#[derive(Debug, Clone)]
545pub struct StyleStack {
546    stack: Vec<Style>,
547    default_style: Style,
548}
549
550impl StyleStack {
551    pub fn new(default_style: Style) -> Self {
552        Self {
553            stack: Vec::new(),
554            default_style,
555        }
556    }
557
558    /// Get the current (combined) style.
559    pub fn current(&self) -> Style {
560        let mut combined = self.default_style.clone();
561        for s in &self.stack {
562            combined = combined.combine(s);
563        }
564        combined
565    }
566
567    /// Push a style onto the stack.
568    pub fn push(&mut self, style: Style) {
569        self.stack.push(style);
570    }
571
572    /// Pop the top style.
573    pub fn pop(&mut self) -> Option<Style> {
574        self.stack.pop()
575    }
576
577    /// Get the depth.
578    pub fn len(&self) -> usize {
579        self.stack.len()
580    }
581
582    pub fn is_empty(&self) -> bool {
583        self.stack.is_empty()
584    }
585}
586
587#[cfg(test)]
588mod tests {
589    use super::*;
590
591    #[test]
592    fn test_style_parse() {
593        let s = Style::from_str("bold red");
594        assert_eq!(s.get_bold(), Some(true));
595        assert!(s.color.is_some());
596    }
597
598    #[test]
599    fn test_style_combine() {
600        let base = Style::from_str("red");
601        let over = Style::from_str("bold");
602        let combined = base.combine(&over);
603        assert_eq!(combined.get_bold(), Some(true));
604        assert!(combined.color.is_some());
605    }
606
607    #[test]
608    fn test_ansi_output() {
609        let s = Style::new().color(Color::parse("red").unwrap()).bold(true);
610        let ansi = s.to_ansi();
611        assert!(ansi.contains("31")); // red foreground
612        assert!(ansi.contains("1"));  // bold
613    }
614}