Skip to main content

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}