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