flashkraft_gui/core/message.rs
1//! Message - Events in The Elm Architecture
2//!
3//! This module defines all possible messages (events) that can occur
4//! in the FlashKraft application. Messages are the only way to trigger
5//! state changes, making the application predictable and debuggable.
6
7use std::path::PathBuf;
8
9use crate::domain::DriveInfo;
10use iced::Theme;
11
12/// All possible messages in the application
13///
14/// Messages represent events that can occur, either from user interactions
15/// or as results of asynchronous operations (Commands).
16#[derive(Debug, Clone)]
17pub enum Message {
18 // ========================================================================
19 // User Interaction Messages
20 // ========================================================================
21 /// User clicked the "Select Image" button
22 SelectImageClicked,
23
24 /// User clicked the "Refresh Drives" button
25 RefreshDrivesClicked,
26
27 /// A USB device was connected or disconnected — triggers re-enumeration.
28 ///
29 /// Emitted by the hotplug subscription in [`crate::core::state`] and
30 /// handled by immediately re-running drive detection, exactly as if the
31 /// user had pressed Refresh.
32 UsbHotplugDetected,
33
34 /// User clicked on a specific target drive
35 TargetDriveClicked(DriveInfo),
36
37 /// User clicked to open the device selection view
38 OpenDeviceSelection,
39
40 /// User clicked to close the device selection view
41 CloseDeviceSelection,
42
43 /// User clicked the "Flash" button
44 FlashClicked,
45
46 /// User clicked Flash but the process is not privileged — attempt to
47 /// re-exec via pkexec / sudo so the user gets a password prompt, then
48 /// restart the app with the same selections intact.
49 EscalateAndFlash,
50
51 /// User clicked the "Reset" button (start over)
52 ResetClicked,
53
54 /// User clicked the "Cancel" button
55 CancelClicked,
56
57 /// User clicked "Cancel" during flash operation
58 CancelFlash,
59
60 // ========================================================================
61 // Animation Messages
62 // ========================================================================
63 /// Animation tick for progress bar effects
64 AnimationTick,
65
66 // ========================================================================
67 // Async Result Messages
68 // ========================================================================
69 /// Result from async image file selection
70 ///
71 /// Contains `Some(path)` if user selected a file, `None` if cancelled
72 ImageSelected(Option<PathBuf>),
73
74 /// Result from async drive detection
75 ///
76 /// Contains a list of detected drives
77 DrivesRefreshed(Vec<DriveInfo>),
78
79 /// Progress update from flash subscription
80 ///
81 /// Contains (progress 0.0-1.0, bytes_written, speed_mb_per_sec)
82 FlashProgressUpdate(f32, u64, f32),
83
84 /// Verification read-back progress update.
85 ///
86 /// Contains (overall 0.0–1.0 across both passes, phase, bytes_read, total_bytes, speed_mb_s)
87 VerifyProgressUpdate(f32, &'static str, u64, u64, f32),
88
89 /// Status message from flash operation
90 Status(String),
91
92 /// Result from async flash operation
93 ///
94 /// Contains `Ok(())` on success or `Err(message)` on failure
95 FlashCompleted(Result<(), String>),
96
97 /// User changed the application theme
98 ThemeChanged(Theme),
99}
100
101// ---------------------------------------------------------------------------
102// Tests
103// ---------------------------------------------------------------------------
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use std::path::PathBuf;
109
110 // ── Debug + Clone ────────────────────────────────────────────────────
111
112 #[test]
113 fn message_implements_debug() {
114 let msg = Message::SelectImageClicked;
115 // Debug formatting should not panic.
116 let debug_str = format!("{:?}", msg);
117 assert!(!debug_str.is_empty());
118 }
119
120 #[test]
121 fn message_implements_clone() {
122 let msg = Message::AnimationTick;
123 let cloned = msg.clone();
124 // Both should produce identical debug output.
125 assert_eq!(format!("{:?}", msg), format!("{:?}", cloned));
126 }
127
128 // ── Unit variant construction ────────────────────────────────────────
129
130 #[test]
131 fn select_image_clicked_can_be_constructed() {
132 let _msg = Message::SelectImageClicked;
133 }
134
135 #[test]
136 fn animation_tick_can_be_constructed() {
137 let _msg = Message::AnimationTick;
138 }
139
140 // ── Tuple variant construction ───────────────────────────────────────
141
142 #[test]
143 fn flash_progress_update_holds_values() {
144 let msg = Message::FlashProgressUpdate(0.5, 1024, 10.0);
145 match msg {
146 Message::FlashProgressUpdate(progress, bytes, speed) => {
147 assert!((progress - 0.5).abs() < f32::EPSILON);
148 assert_eq!(bytes, 1024);
149 assert!((speed - 10.0).abs() < f32::EPSILON);
150 }
151 _ => panic!("wrong variant"),
152 }
153 }
154
155 #[test]
156 fn flash_completed_ok() {
157 let msg = Message::FlashCompleted(Ok(()));
158 match msg {
159 Message::FlashCompleted(Ok(())) => {} // success
160 _ => panic!("expected FlashCompleted(Ok(()))"),
161 }
162 }
163
164 #[test]
165 fn flash_completed_err() {
166 let msg = Message::FlashCompleted(Err("test".into()));
167 match msg {
168 Message::FlashCompleted(Err(e)) => assert_eq!(e, "test"),
169 _ => panic!("expected FlashCompleted(Err(..))"),
170 }
171 }
172
173 #[test]
174 fn status_holds_string() {
175 let msg = Message::Status("test".into());
176 match msg {
177 Message::Status(s) => assert_eq!(s, "test"),
178 _ => panic!("expected Status"),
179 }
180 }
181
182 // ── ThemeChanged ─────────────────────────────────────────────────────
183
184 #[test]
185 fn theme_changed_holds_theme() {
186 let msg = Message::ThemeChanged(Theme::Light);
187 match msg {
188 Message::ThemeChanged(t) => {
189 // Just verify the variant can hold a theme.
190 let _ = format!("{:?}", t);
191 }
192 _ => panic!("expected ThemeChanged"),
193 }
194 }
195
196 // ── ImageSelected ────────────────────────────────────────────────────
197
198 #[test]
199 fn image_selected_none() {
200 let msg = Message::ImageSelected(None);
201 match msg {
202 Message::ImageSelected(None) => {} // ok
203 _ => panic!("expected ImageSelected(None)"),
204 }
205 }
206
207 #[test]
208 fn image_selected_some_path() {
209 let msg = Message::ImageSelected(Some(PathBuf::from("/test")));
210 match msg {
211 Message::ImageSelected(Some(p)) => assert_eq!(p, PathBuf::from("/test")),
212 _ => panic!("expected ImageSelected(Some(..))"),
213 }
214 }
215}