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 | 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}