Skip to main content

okf_studio/
theme.rs

1//! The studio's visual vocabulary: one glyph-and-color encoding per semantic
2//! dimension, used identically in every workspace.
3//!
4//! Color is never the only channel: every colored state has a glyph, so the
5//! UI degrades gracefully on monochrome terminals. The palette is drawn from
6//! the terminal's 16 ANSI colors so the user's scheme is respected, and
7//! `NO_COLOR` disables color entirely.
8
9use okf_core::{ActorKind, Status, TrustTier};
10use ratatui::style::{Color, Modifier, Style};
11
12/// Glyph for a plain concept node.
13pub const GLYPH_CONCEPT: &str = "●";
14/// Glyph for an Attested Computation concept.
15pub const GLYPH_COMPUTATION: &str = "⚙";
16/// Glyph for a reserved `index.md` file.
17pub const GLYPH_INDEX: &str = "◈";
18/// Glyph for a reserved `log.md` file.
19pub const GLYPH_LOG: &str = "≡";
20/// Glyph for a parse error or broken target.
21pub const GLYPH_BROKEN: &str = "✗";
22/// Suffix glyph for a stale concept or one carrying diagnostics.
23pub const GLYPH_WARN: &str = "⚠";
24/// Glyph for a concept going stale within 30 days.
25pub const GLYPH_STALE_SOON: &str = "⏳";
26/// Glyph for a fixable diagnostic.
27pub const GLYPH_FIXABLE: &str = "✚";
28/// Glyph for a passing check.
29pub const GLYPH_OK: &str = "✔";
30
31/// Braille spinner frames shown while a reload or apply is in flight.
32pub const SPINNER_FRAMES: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
33
34/// The active theme: whether color is enabled, and the style vocabulary.
35#[derive(Clone, Copy, Debug)]
36pub struct Theme {
37    /// `false` when `NO_COLOR` is set; every style collapses to monochrome.
38    pub color: bool,
39}
40
41impl Default for Theme {
42    fn default() -> Self {
43        Self::from_env()
44    }
45}
46
47impl Theme {
48    /// Builds the theme from the environment, honoring `NO_COLOR`.
49    #[must_use]
50    pub fn from_env() -> Self {
51        Self {
52            color: std::env::var_os("NO_COLOR").is_none_or(|v| v.is_empty()),
53        }
54    }
55
56    /// A theme with color forced on or off (used by tests).
57    #[must_use]
58    pub const fn with_color(color: bool) -> Self {
59        Self { color }
60    }
61
62    fn fg(self, color: Color) -> Style {
63        if self.color {
64            Style::default().fg(color)
65        } else {
66            Style::default()
67        }
68    }
69
70    /// The accent style for chrome highlights (tab bar, headings).
71    #[must_use]
72    pub fn accent(self) -> Style {
73        self.fg(Color::Cyan)
74    }
75
76    /// The style for dimmed, secondary text.
77    #[must_use]
78    pub fn dim(self) -> Style {
79        Style::default().add_modifier(Modifier::DIM)
80    }
81
82    /// The style for errors and broken targets.
83    #[must_use]
84    pub fn error(self) -> Style {
85        self.fg(Color::Red)
86    }
87
88    /// The style for warnings and staleness.
89    #[must_use]
90    pub fn warn(self) -> Style {
91        self.fg(Color::Yellow)
92    }
93
94    /// The style for success verdicts.
95    #[must_use]
96    pub fn ok(self) -> Style {
97        self.fg(Color::Green)
98    }
99
100    /// The style for the selected row / focused element.
101    #[must_use]
102    pub fn selection(self) -> Style {
103        Style::default().add_modifier(Modifier::REVERSED)
104    }
105
106    /// The primary hue for a trust tier, the dimension every workspace colors
107    /// by first.
108    #[must_use]
109    pub fn tier(self, tier: TrustTier) -> Style {
110        match tier {
111            TrustTier::HumanReviewed => self.fg(Color::Green),
112            TrustTier::MachineConfirmed => self.fg(Color::Blue),
113            TrustTier::Unverified => self.fg(Color::DarkGray),
114        }
115    }
116
117    /// The style for an actor string, keyed by its kind.
118    #[must_use]
119    pub fn actor(self, kind: ActorKind) -> Style {
120        match kind {
121            ActorKind::Human => self.fg(Color::Cyan),
122            ActorKind::Process => self.fg(Color::Magenta),
123            ActorKind::Agent => self.fg(Color::Blue),
124            ActorKind::Other => Style::default(),
125        }
126    }
127
128    /// The style for a lifecycle status.
129    #[must_use]
130    pub fn status(self, status: &Status) -> Style {
131        match status {
132            Status::Draft => self.fg(Color::Yellow),
133            Status::Deprecated => self.dim().add_modifier(Modifier::CROSSED_OUT),
134            Status::Stable | Status::Other(_) => Style::default(),
135        }
136    }
137}
138
139/// The glyph for a trust tier: `◆` human-reviewed, `●` machine-confirmed,
140/// `○` unverified.
141#[must_use]
142pub const fn tier_glyph(tier: TrustTier) -> &'static str {
143    match tier {
144        TrustTier::HumanReviewed => "◆",
145        TrustTier::MachineConfirmed => "●",
146        TrustTier::Unverified => "○",
147    }
148}
149
150/// The glyph for a lifecycle status: `●` stable, `◐` draft, `◌` deprecated.
151#[must_use]
152pub const fn status_glyph(status: &Status) -> &'static str {
153    match status {
154        Status::Draft => "◐",
155        Status::Deprecated => "◌",
156        Status::Stable | Status::Other(_) => "●",
157    }
158}