Skip to main content

systemprompt_logging/services/cli/
display.rs

1#![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}