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
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}