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