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