Skip to main content

modde_ui/views/
diagnostics.rs

1use 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/// A single diagnostic finding.
10#[derive(Debug, Clone)]
11pub struct DiagnosticEntry {
12    pub severity: DiagnosticSeverity,
13    pub message: String,
14}
15
16/// Staged profile integrity results included in the diagnostics report.
17#[derive(Debug, Clone, Default)]
18pub struct IntegritySummary {
19    pub ok_count: usize,
20    pub broken_symlinks: Vec<PathBuf>,
21}
22
23/// Combined diagnostics and integrity report for the active profile.
24#[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/// Severity levels for diagnostics.
33#[derive(Debug, Clone)]
34pub enum DiagnosticSeverity {
35    Info,
36    Warning,
37    Error,
38}
39
40/// State machine for the diagnostics view.
41#[derive(Debug, Clone, Default)]
42pub enum DiagnosticsState {
43    #[default]
44    Idle,
45    Running,
46    Error(String),
47    Complete(DiagnosticsReport),
48}
49
50/// Render the diagnostics view.
51pub 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}