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