Skip to main content

systemprompt_logging/services/cli/
display.rs

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