Skip to main content

rusty_rich/
style.rs

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