Skip to main content

systemprompt_logging/services/cli/
display.rs

1//! Display traits and primitives for CLI output.
2//!
3//! Defines the [`Display`]/[`DetailedDisplay`] traits and the [`DisplayUtils`]
4//! helpers (messages, section headers, items, relationships) plus reusable
5//! renderers ([`StatusDisplay`], [`ModuleItemDisplay`], [`CollectionDisplay`]).
6//! All output goes to stderr via this sanctioned display sink.
7
8use std::io::Write;
9
10use crate::services::cli::theme::{
11    ActionType, EmphasisType, IconType, ItemStatus, MessageLevel, ModuleType, Theme,
12};
13
14pub trait Display {
15    fn display(&self);
16}
17
18pub trait DetailedDisplay {
19    fn display_summary(&self);
20    fn display_details(&self);
21}
22
23fn stderr_writeln(args: std::fmt::Arguments<'_>) {
24    let mut stderr = std::io::stderr();
25    writeln!(stderr, "{args}").ok();
26}
27
28#[derive(Debug, Copy, Clone)]
29pub struct DisplayUtils;
30
31impl DisplayUtils {
32    pub fn message(level: MessageLevel, text: &str) {
33        stderr_writeln(format_args!(
34            "{} {}",
35            Theme::icon(level),
36            Theme::color(text, level)
37        ));
38    }
39
40    pub fn section_header(title: &str) {
41        stderr_writeln(format_args!(
42            "\n{}",
43            Theme::color(title, EmphasisType::Underlined)
44        ));
45    }
46
47    pub fn subsection_header(title: &str) {
48        stderr_writeln(format_args!(
49            "\n  {}",
50            Theme::color(title, EmphasisType::Bold)
51        ));
52    }
53
54    pub fn item(icon_type: impl Into<IconType>, name: &str, detail: Option<&str>) {
55        match detail {
56            Some(detail) => stderr_writeln(format_args!(
57                "   {} {} {}",
58                Theme::icon(icon_type),
59                Theme::color(name, EmphasisType::Bold),
60                Theme::color(detail, EmphasisType::Dim)
61            )),
62            None => stderr_writeln(format_args!(
63                "   {} {}",
64                Theme::icon(icon_type),
65                Theme::color(name, EmphasisType::Bold)
66            )),
67        }
68    }
69
70    pub fn relationship(icon_type: impl Into<IconType>, from: &str, to: &str, status: ItemStatus) {
71        stderr_writeln(format_args!(
72            "   {} {} {} {} {}",
73            Theme::icon(icon_type),
74            Theme::color(from, EmphasisType::Highlight),
75            Theme::icon(ActionType::Arrow),
76            Theme::color(to, status),
77            Theme::color(&format!("({})", status_text(status)), EmphasisType::Dim)
78        ));
79    }
80
81    pub fn module_status(module_name: &str, message: &str) {
82        let module_label = format!("Module: {module_name}");
83        stderr_writeln(format_args!(
84            "{} {} {}",
85            Theme::icon(ModuleType::Module),
86            Theme::color(&module_label, EmphasisType::Highlight),
87            Theme::color(message, EmphasisType::Dim)
88        ));
89    }
90
91    pub fn count_message(level: MessageLevel, count: usize, item_type: &str) {
92        let count_label = format!("{} {item_type}", count_text(count, item_type));
93        let count_str = count.to_string();
94        stderr_writeln(format_args!(
95            "   {} {}: {}",
96            Theme::icon(level),
97            count_label,
98            Theme::color(&count_str, level)
99        ));
100    }
101}
102
103#[derive(Debug)]
104pub struct StatusDisplay {
105    pub status: ItemStatus,
106    pub name: String,
107    pub detail: Option<String>,
108}
109
110impl StatusDisplay {
111    pub fn new(status: ItemStatus, name: impl Into<String>) -> Self {
112        Self {
113            status,
114            name: name.into(),
115            detail: None,
116        }
117    }
118
119    #[must_use]
120    pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
121        self.detail = Some(detail.into());
122        self
123    }
124}
125
126impl Display for StatusDisplay {
127    fn display(&self) {
128        DisplayUtils::item(self.status, &self.name, self.detail.as_deref());
129    }
130}
131
132#[derive(Debug)]
133pub struct ModuleItemDisplay {
134    pub module_type: ModuleType,
135    pub file: String,
136    pub target: String,
137    pub status: ItemStatus,
138}
139
140impl ModuleItemDisplay {
141    pub fn new(
142        module_type: ModuleType,
143        file: impl Into<String>,
144        target: impl Into<String>,
145        status: ItemStatus,
146    ) -> Self {
147        Self {
148            module_type,
149            file: file.into(),
150            target: target.into(),
151            status,
152        }
153    }
154}
155
156impl Display for ModuleItemDisplay {
157    fn display(&self) {
158        DisplayUtils::relationship(self.module_type, &self.file, &self.target, self.status);
159    }
160}
161
162#[derive(Debug)]
163pub struct CollectionDisplay<T: Display> {
164    pub title: String,
165    pub items: Vec<T>,
166    pub show_count: bool,
167}
168
169impl<T: Display> CollectionDisplay<T> {
170    pub fn new(title: impl Into<String>, items: Vec<T>) -> Self {
171        Self {
172            title: title.into(),
173            items,
174            show_count: true,
175        }
176    }
177
178    #[must_use]
179    pub const fn without_count(mut self) -> Self {
180        self.show_count = false;
181        self
182    }
183}
184
185impl<T: Display> Display for CollectionDisplay<T> {
186    fn display(&self) {
187        if self.show_count && !self.items.is_empty() {
188            stderr_writeln(format_args!(
189                "\n{} {}:",
190                Theme::color(&self.title, EmphasisType::Bold),
191                Theme::color(&format!("({})", self.items.len()), EmphasisType::Dim)
192            ));
193        } else if !self.items.is_empty() {
194            stderr_writeln(format_args!(
195                "\n{}:",
196                Theme::color(&self.title, EmphasisType::Bold)
197            ));
198        }
199
200        for item in &self.items {
201            item.display();
202        }
203    }
204}
205
206const fn status_text(status: ItemStatus) -> &'static str {
207    match status {
208        ItemStatus::Missing => "missing",
209        ItemStatus::Applied => "applied",
210        ItemStatus::Failed => "failed",
211        ItemStatus::Valid => "valid",
212        ItemStatus::Disabled => "disabled",
213        ItemStatus::Pending => "pending",
214    }
215}
216
217fn count_text(count: usize, item_type: &str) -> &'static str {
218    if count == 1 {
219        match item_type {
220            "schemas" => "Missing schema",
221            "seeds" => "Missing seed",
222            "modules" => "New module",
223            _ => "Missing item",
224        }
225    } else {
226        match item_type {
227            "schemas" => "Missing schemas",
228            "seeds" => "Missing seeds",
229            "modules" => "New modules",
230            _ => "Missing items",
231        }
232    }
233}