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(default, skip_serializing_if = "is_false", rename = "reverse")]
37    pub inverse: bool,
38    /// ANSI strikethrough (SGR 9).
39    #[serde(default, skip_serializing_if = "is_false", rename = "strikethrough")]
40    pub strike: bool,
41}
42
43impl Style {
44    pub const fn plain() -> Self {
45        Self {
46            fg: None,
47            bg: None,
48            dim: false,
49            bold: false,
50            italic: false,
51            underline: false,
52            blink: false,
53            inverse: false,
54            strike: false,
55        }
56    }
57
58    /// Overlay `top` style on this style.
59    ///
60    /// Semantics (kept intentionally small and deterministic):
61    /// - `fg` / `bg`: `top` wins when present; otherwise the base value remains.
62    /// - boolean flags (`dim`, `bold`, `italic`, `underline`, `blink`, `inverse`, `strike`): logical OR.
63    #[must_use]
64    pub const fn overlay(self, top: Style) -> Style {
65        Style {
66            fg: match top.fg {
67                Some(v) => Some(v),
68                None => self.fg,
69            },
70            bg: match top.bg {
71                Some(v) => Some(v),
72                None => self.bg,
73            },
74            dim: self.dim || top.dim,
75            bold: self.bold || top.bold,
76            italic: self.italic || top.italic,
77            underline: self.underline || top.underline,
78            blink: self.blink || top.blink,
79            inverse: self.inverse || top.inverse,
80            strike: self.strike || top.strike,
81        }
82    }
83}