Skip to main content

systemprompt_logging/services/cli/
theme.rs

1//! Colour, icon, and styling primitives for CLI output.
2//!
3//! [`Theme`] resolves an [`IconType`]/[`ColorType`] to a styled object;
4//! [`BrandColors`], [`Colors`], and [`Icons`] supply the underlying palette and
5//! glyphs, with terminal-fallback variants for status and message levels.
6
7use console::{Emoji, StyledObject, style};
8
9pub use super::types::{
10    ActionType, ColorType, EmphasisType, IconType, ItemStatus, MessageLevel, ModuleType,
11};
12
13#[derive(Debug, Copy, Clone)]
14pub struct BrandColors;
15
16impl BrandColors {
17    pub fn primary<D: std::fmt::Display>(text: D) -> StyledObject<D> {
18        style(text).color256(208)
19    }
20
21    pub fn primary_bold<D: std::fmt::Display>(text: D) -> StyledObject<D> {
22        style(text).color256(208).bold()
23    }
24
25    pub fn white_bold<D: std::fmt::Display>(text: D) -> StyledObject<D> {
26        style(text).white().bold()
27    }
28
29    pub fn white<D: std::fmt::Display>(text: D) -> StyledObject<D> {
30        style(text).white()
31    }
32
33    pub fn dim<D: std::fmt::Display>(text: D) -> StyledObject<D> {
34        style(text).dim()
35    }
36
37    pub fn highlight<D: std::fmt::Display>(text: D) -> StyledObject<D> {
38        style(text).cyan()
39    }
40
41    pub fn running<D: std::fmt::Display>(text: D) -> StyledObject<D> {
42        style(text).green()
43    }
44
45    pub fn stopped<D: std::fmt::Display>(text: D) -> StyledObject<D> {
46        style(text).red()
47    }
48
49    pub fn starting<D: std::fmt::Display>(text: D) -> StyledObject<D> {
50        style(text).yellow()
51    }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum ServiceStatus {
56    Running,
57    Stopped,
58    Starting,
59    Failed,
60    Unknown,
61}
62
63impl ServiceStatus {
64    pub const fn symbol(&self) -> &'static str {
65        match self {
66            Self::Running => "●",
67            Self::Stopped => "○",
68            Self::Starting => "◐",
69            Self::Failed => "✗",
70            Self::Unknown => "?",
71        }
72    }
73
74    pub const fn text(&self) -> &'static str {
75        match self {
76            Self::Running => "Running",
77            Self::Stopped => "Stopped",
78            Self::Starting => "Starting",
79            Self::Failed => "Failed",
80            Self::Unknown => "Unknown",
81        }
82    }
83}
84
85#[derive(Debug, Copy, Clone)]
86pub struct Icons;
87
88impl Icons {
89    pub const CHECKMARK: Emoji<'static, 'static> = Emoji("✓", "✓");
90    pub const WARNING: Emoji<'static, 'static> = Emoji("⚠", "!");
91    pub const ERROR: Emoji<'static, 'static> = Emoji("✗", "X");
92    pub const INFO: Emoji<'static, 'static> = Emoji("ℹ", "i");
93
94    pub const PACKAGE: Emoji<'static, 'static> = Emoji("📦", "[MOD]");
95    pub const SCHEMA: Emoji<'static, 'static> = Emoji("📄", "[SCHEMA]");
96    pub const SEED: Emoji<'static, 'static> = Emoji("🌱", "[SEED]");
97    pub const CONFIG: Emoji<'static, 'static> = Emoji("⚙", "[CONFIG]");
98
99    pub const ARROW: Emoji<'static, 'static> = Emoji("→", "->");
100    pub const UPDATE: Emoji<'static, 'static> = Emoji("🔄", "[UPDATE]");
101    pub const INSTALL: Emoji<'static, 'static> = Emoji("📥", "[INSTALL]");
102    pub const PAUSE: Emoji<'static, 'static> = Emoji("⏸", "[PAUSED]");
103
104    pub const fn for_module_type(module_type: ModuleType) -> Emoji<'static, 'static> {
105        match module_type {
106            ModuleType::Schema => Self::SCHEMA,
107            ModuleType::Seed => Self::SEED,
108            ModuleType::Module => Self::PACKAGE,
109            ModuleType::Configuration => Self::CONFIG,
110        }
111    }
112
113    pub const fn for_status(status: ItemStatus) -> Emoji<'static, 'static> {
114        match status {
115            ItemStatus::Valid | ItemStatus::Applied => Self::CHECKMARK,
116            ItemStatus::Missing | ItemStatus::Pending => Self::WARNING,
117            ItemStatus::Failed => Self::ERROR,
118            ItemStatus::Disabled => Self::PAUSE,
119        }
120    }
121
122    pub const fn for_message_level(level: MessageLevel) -> Emoji<'static, 'static> {
123        match level {
124            MessageLevel::Success => Self::CHECKMARK,
125            MessageLevel::Warning => Self::WARNING,
126            MessageLevel::Error => Self::ERROR,
127            MessageLevel::Info => Self::INFO,
128        }
129    }
130}
131
132#[derive(Debug, Copy, Clone)]
133pub struct Colors;
134
135impl Colors {
136    pub fn success<D: std::fmt::Display>(text: D) -> StyledObject<D> {
137        style(text).green()
138    }
139
140    pub fn warning<D: std::fmt::Display>(text: D) -> StyledObject<D> {
141        style(text).yellow()
142    }
143
144    pub fn error<D: std::fmt::Display>(text: D) -> StyledObject<D> {
145        style(text).red()
146    }
147
148    pub fn info<D: std::fmt::Display>(text: D) -> StyledObject<D> {
149        style(text).cyan()
150    }
151
152    pub fn highlight<D: std::fmt::Display>(text: D) -> StyledObject<D> {
153        style(text).bold().cyan()
154    }
155
156    pub fn dim<D: std::fmt::Display>(text: D) -> StyledObject<D> {
157        style(text).dim()
158    }
159
160    pub fn bold<D: std::fmt::Display>(text: D) -> StyledObject<D> {
161        style(text).bold()
162    }
163
164    pub fn underlined<D: std::fmt::Display>(text: D) -> StyledObject<D> {
165        style(text).bold().underlined()
166    }
167
168    pub fn for_status<D: std::fmt::Display>(text: D, status: ItemStatus) -> StyledObject<D> {
169        match status {
170            ItemStatus::Valid | ItemStatus::Applied => Self::success(text),
171            ItemStatus::Missing | ItemStatus::Pending => Self::warning(text),
172            ItemStatus::Failed => Self::error(text),
173            ItemStatus::Disabled => Self::dim(text),
174        }
175    }
176
177    pub fn for_message_level<D: std::fmt::Display>(
178        text: D,
179        level: MessageLevel,
180    ) -> StyledObject<D> {
181        match level {
182            MessageLevel::Success => Self::success(text),
183            MessageLevel::Warning => Self::warning(text),
184            MessageLevel::Error => Self::error(text),
185            MessageLevel::Info => Self::info(text),
186        }
187    }
188}
189
190#[derive(Debug, Clone, Copy)]
191pub struct Theme;
192
193impl Theme {
194    pub fn icon(icon_type: impl Into<IconType>) -> Emoji<'static, 'static> {
195        match icon_type.into() {
196            IconType::Status(status) => Icons::for_status(status),
197            IconType::Module(module_type) => Icons::for_module_type(module_type),
198            IconType::Message(level) => Icons::for_message_level(level),
199            IconType::Action(action) => match action {
200                ActionType::Install => Icons::INSTALL,
201                ActionType::Update => Icons::UPDATE,
202                ActionType::Arrow => Icons::ARROW,
203            },
204        }
205    }
206
207    pub fn color<D: std::fmt::Display>(
208        text: D,
209        color_type: impl Into<ColorType>,
210    ) -> StyledObject<D> {
211        match color_type.into() {
212            ColorType::Status(status) => Colors::for_status(text, status),
213            ColorType::Message(level) => Colors::for_message_level(text, level),
214            ColorType::Emphasis(emphasis) => match emphasis {
215                EmphasisType::Highlight => Colors::highlight(text),
216                EmphasisType::Dim => Colors::dim(text),
217                EmphasisType::Bold => Colors::bold(text),
218                EmphasisType::Underlined => Colors::underlined(text),
219            },
220        }
221    }
222}