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 | DiagnosticsState::Running => {
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::Error(message) => {
84 container(text(message).size(14).color(color!(0xFF4444)))
85 .padding(20)
86 .width(Length::Fill)
87 .center_x(Length::Fill)
88 .into()
89 }
90
91 DiagnosticsState::Complete(report) => {
92 let broken_count = report.integrity.broken_symlinks.len();
93 if report.entries.is_empty() && broken_count == 0 {
94 let content = column![
95 text(format!(
96 "Profile: {} ({})",
97 report.profile_name, report.game_id
98 ))
99 .size(12),
100 text(format!("{} staged file(s) OK", report.integrity.ok_count))
101 .size(14)
102 .color(color!(0x88CC88)),
103 text("No issues found!").size(14).color(color!(0x88CC88)),
104 ]
105 .spacing(8);
106
107 container(content)
108 .padding(20)
109 .width(Length::Fill)
110 .center_x(Length::Fill)
111 .into()
112 } else {
113 let rows = report
114 .entries
115 .iter()
116 .fold(column![].spacing(4), |col, entry| {
117 let (icon, icon_color) = match entry.severity {
118 DiagnosticSeverity::Info => ("INFO", color!(0x88AACC)),
119 DiagnosticSeverity::Warning => ("WARN", color!(0xFFAA44)),
120 DiagnosticSeverity::Error => ("ERR ", color!(0xFF4444)),
121 };
122
123 let entry_row = row![
124 text(icon)
125 .size(12)
126 .color(icon_color)
127 .width(Length::Fixed(40.0)),
128 text(&entry.message).size(13).width(Length::Fill),
129 ]
130 .spacing(8)
131 .padding([4, 8]);
132
133 col.push(entry_row)
134 });
135
136 let summary = {
137 let errors = report
138 .entries
139 .iter()
140 .filter(|e| matches!(e.severity, DiagnosticSeverity::Error))
141 .count();
142 let warnings = report
143 .entries
144 .iter()
145 .filter(|e| matches!(e.severity, DiagnosticSeverity::Warning))
146 .count();
147 let infos = report
148 .entries
149 .iter()
150 .filter(|e| matches!(e.severity, DiagnosticSeverity::Info))
151 .count();
152 text(format!(
153 "{} ({}) - {errors} error(s), {warnings} warning(s), {infos} info(s), {broken_count} broken symlink(s)",
154 report.profile_name, report.game_id
155 ))
156 .size(12)
157 };
158
159 let integrity = if report.integrity.broken_symlinks.is_empty() {
160 column![
161 text(format!(
162 "Integrity: {} staged file(s) OK",
163 report.integrity.ok_count
164 ))
165 .size(14)
166 .color(color!(0x88CC88))
167 ]
168 } else {
169 let symlinks = report
170 .integrity
171 .broken_symlinks
172 .iter()
173 .fold(column![].spacing(2), |col, path| {
174 col.push(text(path.display().to_string()).size(12))
175 });
176 column![
177 text(format!(
178 "Integrity: {} staged file(s) OK, {} broken symlink(s)",
179 report.integrity.ok_count,
180 report.integrity.broken_symlinks.len()
181 ))
182 .size(14)
183 .color(color!(0xFF8844)),
184 container(symlinks)
185 .padding(8)
186 .width(Length::Fill)
187 .style(container::rounded_box),
188 ]
189 .spacing(6)
190 };
191
192 column![
193 summary,
194 integrity,
195 scrollable(rows.padding(8)).height(Length::Fill),
196 ]
197 .spacing(8)
198 .into()
199 }
200 }
201 };
202
203 column![title_bar, iced::widget::rule::horizontal(1), content]
204 .spacing(8)
205 .padding(16)
206 .width(Length::Fill)
207 .height(Length::Fill)
208 .into()
209}