modde_ui/views/
diagnostics.rs1use crate::views::selectable_text::text;
2use iced::widget::{button, column, container, row, scrollable};
3use iced::{Alignment, Element, Length, color};
4use std::path::PathBuf;
5
6use crate::action_button::{ButtonAction, DescribedButtonExt};
7use crate::app::Message;
8
9#[derive(Debug, Clone)]
11pub struct DiagnosticEntry {
12 pub severity: DiagnosticSeverity,
13 pub message: String,
14}
15
16#[derive(Debug, Clone, Default)]
18pub struct IntegritySummary {
19 pub ok_count: usize,
20 pub broken_symlinks: Vec<PathBuf>,
21}
22
23#[derive(Debug, Clone)]
25pub struct DiagnosticsReport {
26 pub profile_name: String,
27 pub game_id: String,
28 pub entries: Vec<DiagnosticEntry>,
29 pub integrity: IntegritySummary,
30}
31
32#[derive(Debug, Clone)]
34pub enum DiagnosticSeverity {
35 Info,
36 Warning,
37 Error,
38}
39
40#[derive(Debug, Clone, Default)]
42pub enum DiagnosticsState {
43 #[default]
44 Idle,
45 Running,
46 Error(String),
47 Complete(DiagnosticsReport),
48}
49
50pub fn view(state: &DiagnosticsState) -> Element<'_, Message> {
52 let running = matches!(state, DiagnosticsState::Running);
53
54 let title_bar = row![
55 text("Diagnostics").size(20),
56 iced::widget::space::horizontal(),
57 button(
58 text(if running {
59 "Running..."
60 } else {
61 "Run Diagnostics"
62 })
63 .size(14)
64 )
65 .style(button::primary)
66 .padding([6, 14])
67 .on_action_maybe(
68 (!running).then_some(ButtonAction::RunDiagnostics),
69 "Diagnostics are already running.",
70 ),
71 ]
72 .align_y(Alignment::Center);
73
74 let content: Element<Message> = match state {
75 DiagnosticsState::Idle => {
76 container(text("Diagnostics run automatically when this view opens.").size(14))
77 .padding(20)
78 .width(Length::Fill)
79 .center_x(Length::Fill)
80 .into()
81 }
82
83 DiagnosticsState::Running => container(text("Running diagnostics...").size(14))
84 .padding(20)
85 .width(Length::Fill)
86 .center_x(Length::Fill)
87 .into(),
88
89 DiagnosticsState::Error(message) => {
90 container(text(message).size(14).color(color!(0xFF4444)))
91 .padding(20)
92 .width(Length::Fill)
93 .center_x(Length::Fill)
94 .into()
95 }
96
97 DiagnosticsState::Complete(report) => {
98 let broken_count = report.integrity.broken_symlinks.len();
99 if report.entries.is_empty() && broken_count == 0 {
100 let content = column![
101 text(format!(
102 "Profile: {} ({})",
103 report.profile_name, report.game_id
104 ))
105 .size(12),
106 text(format!("{} staged file(s) OK", report.integrity.ok_count))
107 .size(14)
108 .color(color!(0x88CC88)),
109 text("No issues found!").size(14).color(color!(0x88CC88)),
110 ]
111 .spacing(8);
112
113 container(content)
114 .padding(20)
115 .width(Length::Fill)
116 .center_x(Length::Fill)
117 .into()
118 } else {
119 let rows = report
120 .entries
121 .iter()
122 .fold(column![].spacing(4), |col, entry| {
123 let (icon, icon_color) = match entry.severity {
124 DiagnosticSeverity::Info => ("INFO", color!(0x88AACC)),
125 DiagnosticSeverity::Warning => ("WARN", color!(0xFFAA44)),
126 DiagnosticSeverity::Error => ("ERR ", color!(0xFF4444)),
127 };
128
129 let entry_row = row![
130 text(icon)
131 .size(12)
132 .color(icon_color)
133 .width(Length::Fixed(40.0)),
134 text(&entry.message).size(13).width(Length::Fill),
135 ]
136 .spacing(8)
137 .padding([4, 8]);
138
139 col.push(entry_row)
140 });
141
142 let summary = {
143 let errors = report
144 .entries
145 .iter()
146 .filter(|e| matches!(e.severity, DiagnosticSeverity::Error))
147 .count();
148 let warnings = report
149 .entries
150 .iter()
151 .filter(|e| matches!(e.severity, DiagnosticSeverity::Warning))
152 .count();
153 let infos = report
154 .entries
155 .iter()
156 .filter(|e| matches!(e.severity, DiagnosticSeverity::Info))
157 .count();
158 text(format!(
159 "{} ({}) - {errors} error(s), {warnings} warning(s), {infos} info(s), {broken_count} broken symlink(s)",
160 report.profile_name, report.game_id
161 ))
162 .size(12)
163 };
164
165 let integrity = if report.integrity.broken_symlinks.is_empty() {
166 column![
167 text(format!(
168 "Integrity: {} staged file(s) OK",
169 report.integrity.ok_count
170 ))
171 .size(14)
172 .color(color!(0x88CC88))
173 ]
174 } else {
175 let symlinks = report
176 .integrity
177 .broken_symlinks
178 .iter()
179 .fold(column![].spacing(2), |col, path| {
180 col.push(text(path.display().to_string()).size(12))
181 });
182 column![
183 text(format!(
184 "Integrity: {} staged file(s) OK, {} broken symlink(s)",
185 report.integrity.ok_count,
186 report.integrity.broken_symlinks.len()
187 ))
188 .size(14)
189 .color(color!(0xFF8844)),
190 container(symlinks)
191 .padding(8)
192 .width(Length::Fill)
193 .style(container::rounded_box),
194 ]
195 .spacing(6)
196 };
197
198 column![
199 summary,
200 integrity,
201 scrollable(rows.padding(8)).height(Length::Fill),
202 ]
203 .spacing(8)
204 .into()
205 }
206 }
207 };
208
209 column![title_bar, iced::widget::rule::horizontal(1), content]
210 .spacing(8)
211 .padding(16)
212 .width(Length::Fill)
213 .height(Length::Fill)
214 .into()
215}