Skip to main content

termgrid_core/
style.rs

1use serde::{Deserialize, Serialize};
2
3fn is_false(v: &bool) -> bool {
4    !*v
5}
6
7/// Helper for serde `skip_serializing_if` on op `style` fields.
8pub fn is_plain_style(s: &Style) -> bool {
9    *s == Style::plain()
10}
11
12/// A minimal style model compatible with classic ANSI SGR.
13///
14/// This intentionally stays small; higher layers can extend or map their own
15/// style semantics onto this.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
17pub struct Style {
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    pub fg: Option<u8>,
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub bg: Option<u8>,
22    /// ANSI "faint" (SGR 2). Often rendered as dim/low intensity.
23    #[serde(default, skip_serializing_if = "is_false")]
24    pub dim: bool,
25    #[serde(default, skip_serializing_if = "is_false")]
26    pub bold: bool,
27    /// ANSI italic (SGR 3). Not universally supported, but common in modern terminals.
28    #[serde(default, skip_serializing_if = "is_false")]
29    pub italic: bool,
30    #[serde(default, skip_serializing_if = "is_false")]
31    pub underline: bool,
32    /// ANSI blink (SGR 5). Often disabled by terminals; modeled for completeness.
33    #[serde(default, skip_serializing_if = "is_false")]
34    pub blink: bool,
35    /// ANSI reverse video (SGR 7).
36    #[serde(
37        default,
38        skip_serializing_if = "is_false",
39        rename = "reverse",
40        alias = "inverse"
41    )]
42    pub inverse: bool,
43    /// ANSI strikethrough (SGR 9).
44    #[serde(
45        default,
46        skip_serializing_if = "is_false",
47        rename = "strikethrough",
48        alias = "strike"
49    )]
50    pub strike: bool,
51}
52
53impl Style {
54    pub const fn plain() -> Self {
55        Self {
56            fg: None,
57            bg: None,
58            dim: false,
59            bold: false,
60            italic: false,
61            underline: false,
62            blink: false,
63            inverse: false,
64            strike: false,
65        }
66    }
67
68    /// Overlay `top` style on this style.
69    ///
70    /// Semantics (kept intentionally small and deterministic):
71    /// - `fg` / `bg`: `top` wins when present; otherwise the base value remains.
72    /// - boolean flags (`dim`, `bold`, `italic`, `underline`, `blink`, `inverse`, `strike`): logical OR.
73    #[must_use]
74    pub const fn overlay(self, top: Style) -> Style {
75        Style {
76            fg: match top.fg {
77                Some(v) => Some(v),
78                None => self.fg,
79            },
80            bg: match top.bg {
81                Some(v) => Some(v),
82                None => self.bg,
83            },
84            dim: self.dim || top.dim,
85            bold: self.bold || top.bold,
86            italic: self.italic || top.italic,
87            underline: self.underline || top.underline,
88            blink: self.blink || top.blink,
89            inverse: self.inverse || top.inverse,
90            strike: self.strike || top.strike,
91        }
92    }
93}