systemprompt_logging/services/cli/
display.rs1use 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}