Skip to main content

aethermap_gui/
gui.rs

1use crate::theme::{aether_dark, aether_light};
2use crate::views;
3use iced::{
4    widget::{column, container, horizontal_rule, row, scrollable, vertical_rule},
5    Application, Command, Element, Length, Subscription, Theme,
6};
7
8// Import custom widgets
9use aethermap_common::{
10    AnalogMode, CameraOutputMode, DeviceCapabilities, DeviceInfo, DeviceType, LayerConfigInfo,
11    LayerMode, LedPattern, LedZone, MacroEntry, MacroSettings, RemapEntry, RemapProfileInfo,
12};
13use std::collections::{HashMap, HashSet, VecDeque};
14use std::path::PathBuf;
15use std::time::{Duration, Instant};
16
17// Import focus_tracker types - need to use path from lib.rs root
18// Since we're in gui.rs (a module of aethermap_gui library),
19// we access sibling modules via super:: or direct path when in closures
20
21// Razer brand colors (for future custom theming)
22// const RAZER_GREEN: Color = Color::from_rgb(0.267, 0.839, 0.173); // #44D62C
23// const RAZER_GREEN_DIM: Color = Color::from_rgb(0.176, 0.561, 0.118); // #2D8F1E
24// const BG_DEEP: Color = Color::from_rgb(0.051, 0.051, 0.051); // #0D0D0D
25// const BG_SURFACE: Color = Color::from_rgb(0.102, 0.102, 0.102); // #1A1A1A
26// const BG_ELEVATED: Color = Color::from_rgb(0.141, 0.141, 0.141); // #242424
27// const TEXT_PRIMARY: Color = Color::WHITE;
28// const TEXT_SECONDARY: Color = Color::from_rgb(0.702, 0.702, 0.702); // #B3B3B3
29// const TEXT_MUTED: Color = Color::from_rgb(0.400, 0.400, 0.400); // #666666
30// const DANGER_RED: Color = Color::from_rgb(1.0, 0.231, 0.188); // #FF3B30
31// const WARNING_YELLOW: Color = Color::from_rgb(1.0, 0.722, 0.0); // #FFB800
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum Tab {
35    Devices,
36    Macros,
37    Profiles,
38}
39
40#[derive(Debug, Clone)]
41pub struct Notification {
42    pub message: String,
43    pub is_error: bool,
44    pub timestamp: Instant,
45}
46
47pub use views::keypad::{azeron_keypad_layout, KeypadButton};
48
49pub use views::auto_switch::{AutoSwitchRule, AutoSwitchRulesView};
50
51pub use views::hotkeys::{HotkeyBinding, HotkeyBindingsView};
52
53pub use views::analog::{
54    AnalogCalibrationView, CalibrationConfig, DeadzoneShape, SensitivityCurve,
55};
56
57pub use views::led::LedState;
58
59pub struct State {
60    pub devices: Vec<DeviceInfo>,
61    pub macros: Vec<MacroEntry>,
62    pub selected_device: Option<usize>,
63    pub status: String,
64    pub status_history: VecDeque<String>,
65    pub loading: bool,
66    pub recording: bool,
67    pub recording_macro_name: Option<String>,
68    pub daemon_connected: bool,
69    pub new_macro_name: String,
70    pub socket_path: PathBuf,
71    pub recently_updated_macros: HashMap<String, Instant>,
72    pub grabbed_devices: HashSet<String>,
73    pub profile_name: String,
74    pub active_tab: Tab,
75    pub notifications: VecDeque<Notification>,
76    pub recording_pulse: bool,
77    /// Available profiles per device (device_id -> profile names)
78    pub device_profiles: HashMap<String, Vec<String>>,
79    /// Active profile per device (device_id -> profile name)
80    pub active_profiles: HashMap<String, String>,
81    /// Available remap profiles per device (device_path -> profile info)
82    pub remap_profiles: HashMap<String, Vec<RemapProfileInfo>>,
83    /// Active remap profile per device (device_path -> profile name)
84    pub active_remap_profiles: HashMap<String, String>,
85    /// Active remaps per device (device_path -> remap entries)
86    pub active_remaps: HashMap<String, (String, Vec<RemapEntry>)>,
87    /// Azeron keypad layout for selected device
88    pub keypad_layout: Vec<KeypadButton>,
89    /// Current device path being viewed in keypad layout
90    pub keypad_view_device: Option<String>,
91    /// Selected button for remapping (index into keypad_layout)
92    pub selected_button: Option<usize>,
93    /// Device capabilities for current selection
94    pub device_capabilities: Option<DeviceCapabilities>,
95    /// Active layer per device (device_id -> active_layer_id)
96    pub active_layers: HashMap<String, usize>,
97    /// Layer configurations per device (device_id -> layers)
98    pub layer_configs: HashMap<String, Vec<LayerConfigInfo>>,
99    /// Layer configuration dialog state (device_id, layer_id, name, mode)
100    pub layer_config_dialog: Option<(String, usize, String, LayerMode)>,
101    /// D-pad mode per device (device_id -> mode)
102    pub analog_dpad_modes: HashMap<String, String>,
103    /// Per-axis deadzone values (device_id -> (x_percentage, y_percentage))
104    pub analog_deadzones_xy: HashMap<String, (u8, u8)>,
105    /// Per-axis outer deadzone values (device_id -> (x_percentage, y_percentage))
106    pub analog_outer_deadzones_xy: HashMap<String, (u8, u8)>,
107    /// LED configuration state per device (device_id -> LedState)
108    pub led_states: HashMap<String, LedState>,
109    /// LED configuration dialog open for device
110    pub led_config_device: Option<String>,
111    /// Currently selected LED zone for color editing
112    pub selected_led_zone: Option<LedZone>,
113    /// Pending color picker values (r, g, b) before application
114    pub pending_led_color: Option<(u8, u8, u8)>,
115    /// Current focused application ID (for auto-switch rule creation)
116    pub current_focus: Option<String>,
117    /// Focus tracking is active
118    pub focus_tracking_active: bool,
119    /// Auto-switch rules view (open when configuring auto-profile switching)
120    pub auto_switch_view: Option<AutoSwitchRulesView>,
121    /// Hotkey bindings view (open when configuring hotkeys)
122    pub hotkey_view: Option<HotkeyBindingsView>,
123    /// Analog calibration view (open when configuring analog stick)
124    pub analog_calibration_view: Option<AnalogCalibrationView>,
125    /// Global macro timing and jitter settings
126    pub macro_settings: MacroSettings,
127    /// Current UI theme (Adaptive COSMIC)
128    pub current_theme: Theme,
129}
130
131impl Default for State {
132    fn default() -> Self {
133        let socket_path = if cfg!(target_os = "linux") {
134            PathBuf::from("/run/aethermap/aethermap.sock")
135        } else if cfg!(target_os = "macos") {
136            PathBuf::from("/tmp/aethermap.sock")
137        } else {
138            std::env::temp_dir().join("aethermap.sock")
139        };
140        State {
141            devices: Vec::new(),
142            macros: Vec::new(),
143            selected_device: None,
144            status: "Initializing...".to_string(),
145            status_history: VecDeque::with_capacity(10),
146            loading: false,
147            recording: false,
148            recording_macro_name: None,
149            daemon_connected: false,
150            new_macro_name: String::new(),
151            socket_path,
152            recently_updated_macros: HashMap::new(),
153            grabbed_devices: HashSet::new(),
154            profile_name: "default".to_string(),
155            active_tab: Tab::Devices,
156            notifications: VecDeque::with_capacity(5),
157            recording_pulse: false,
158            device_profiles: HashMap::new(),
159            active_profiles: HashMap::new(),
160            remap_profiles: HashMap::new(),
161            active_remap_profiles: HashMap::new(),
162            active_remaps: HashMap::new(),
163            keypad_layout: Vec::new(),
164            keypad_view_device: None,
165            selected_button: None,
166            device_capabilities: None,
167            active_layers: HashMap::new(),
168            layer_configs: HashMap::new(),
169            layer_config_dialog: None,
170            analog_dpad_modes: HashMap::new(),
171            analog_deadzones_xy: HashMap::new(),
172            analog_outer_deadzones_xy: HashMap::new(),
173            led_states: HashMap::new(),
174            led_config_device: None,
175            selected_led_zone: None,
176            pending_led_color: None,
177            current_focus: None,
178            focus_tracking_active: false,
179            auto_switch_view: None,
180            hotkey_view: None,
181            analog_calibration_view: None,
182            macro_settings: MacroSettings {
183                latency_offset_ms: 0,
184                jitter_pct: 0.0,
185                capture_mouse: false,
186            },
187            current_theme: aether_dark(),
188        }
189    }
190}
191
192#[derive(Debug, Clone)]
193pub enum Message {
194    // Navigation
195    SwitchTab(Tab),
196    ThemeChanged(iced::Theme),
197
198    // Device Management
199    LoadDevices,
200    DevicesLoaded(Result<Vec<DeviceInfo>, String>),
201    GrabDevice(String),
202    UngrabDevice(String),
203    DeviceGrabbed(Result<String, String>),
204    DeviceUngrabbed(Result<String, String>),
205    SelectDevice(usize),
206
207    // Macro Recording
208    UpdateMacroName(String),
209    StartRecording,
210    StopRecording,
211    RecordingStarted(Result<String, String>),
212    RecordingStopped(Result<MacroEntry, String>),
213
214    // Macro Management
215    LoadMacros,
216    MacrosLoaded(Result<Vec<MacroEntry>, String>),
217    LoadMacroSettings,
218    MacroSettingsLoaded(Result<MacroSettings, String>),
219    SetMacroSettings(MacroSettings),
220    LatencyChanged(u32),
221    JitterChanged(f32),
222    CaptureMouseToggled(bool),
223    PlayMacro(String),
224    MacroPlayed(Result<String, String>),
225    DeleteMacro(String),
226    MacroDeleted(Result<String, String>),
227
228    // Profile Management
229    UpdateProfileName(String),
230    SaveProfile,
231    ProfileSaved(Result<(String, usize), String>),
232    LoadProfile,
233    ProfileLoaded(Result<(String, usize), String>),
234
235    // Device Profile Management
236    LoadDeviceProfiles(String),
237    DeviceProfilesLoaded(String, Result<Vec<String>, String>),
238    ActivateProfile(String, String),
239    ProfileActivated(String, String),
240    DeactivateProfile(String),
241    ProfileDeactivated(String),
242    ProfileError(String),
243
244    // Remap Profile Management
245    LoadRemapProfiles(String),
246    RemapProfilesLoaded(String, Result<Vec<RemapProfileInfo>, String>),
247    ActivateRemapProfile(String, String),
248    RemapProfileActivated(String, String),
249    DeactivateRemapProfile(String),
250    RemapProfileDeactivated(String),
251    LoadActiveRemaps(String),
252    ActiveRemapsLoaded(String, Result<Option<(String, Vec<RemapEntry>)>, String>),
253
254    // Status
255    CheckDaemonConnection,
256    DaemonStatusChanged(bool),
257
258    // UI
259    TickAnimations,
260    ShowNotification(String, bool), // (message, is_error)
261
262    // Mouse Event Recording
263    RecordMouseEvent {
264        event_type: String,
265        button: Option<u16>,
266        x: i32,
267        y: i32,
268        delta: i32,
269    },
270
271    // Keypad Remapping
272    /// Show keypad remapping view for a device
273    ShowKeypadView(String),
274    /// Select a keypad button for remapping
275    SelectKeypadButton(String),
276    /// Load device capabilities for keypad view
277    DeviceCapabilitiesLoaded(String, Result<DeviceCapabilities, String>),
278
279    // Layer Management
280    /// Layer state changed (device_id, layer_id)
281    LayerStateChanged(String, usize),
282    /// Request layer configuration for a device
283    LayerConfigRequested(String),
284    /// Request activation of a layer (device_id, layer_id, mode)
285    LayerActivateRequested(String, usize, LayerMode),
286    /// Layer configuration updated (device_id, config)
287    LayerConfigUpdated(String, LayerConfigInfo),
288    /// Open layer config dialog for editing
289    OpenLayerConfigDialog(String, usize),
290    /// Update layer name in dialog
291    LayerConfigNameChanged(String),
292    /// Update layer mode in dialog
293    LayerConfigModeChanged(LayerMode),
294    /// Save layer config from dialog
295    SaveLayerConfig,
296    /// Cancel layer config dialog
297    CancelLayerConfig,
298    /// Periodic refresh of layer states
299    RefreshLayers,
300    /// Layer list loaded from daemon (device_id, layers)
301    LayerListLoaded(String, Vec<LayerConfigInfo>),
302
303    // D-pad Mode Management
304    /// Request D-pad mode for a device
305    AnalogDpadModeRequested(String),
306    /// D-pad mode loaded (device_id, mode)
307    AnalogDpadModeLoaded(String, String),
308    /// Set D-pad mode (device_id, mode)
309    SetAnalogDpadMode(String, String),
310    /// D-pad mode set result
311    AnalogDpadModeSet(Result<(), String>),
312
313    // Per-Axis Deadzone Management
314    /// Request per-axis deadzone for a device
315    AnalogDeadzoneXYRequested(String),
316    /// Per-axis deadzone loaded (device_id, (x_pct, y_pct))
317    AnalogDeadzoneXYLoaded(String, (u8, u8)),
318    /// Set per-axis deadzone (device_id, x_pct, y_pct)
319    SetAnalogDeadzoneXY(String, u8, u8),
320    /// Per-axis deadzone set result
321    AnalogDeadzoneXYSet(Result<(), String>),
322    /// Request per-axis outer deadzone for a device
323    AnalogOuterDeadzoneXYRequested(String),
324    /// Per-axis outer deadzone loaded (device_id, (x_pct, y_pct))
325    AnalogOuterDeadzoneXYLoaded(String, (u8, u8)),
326    /// Set per-axis outer deadzone (device_id, x_pct, y_pct)
327    SetAnalogOuterDeadzoneXY(String, u8, u8),
328    /// Per-axis outer deadzone set result
329    AnalogOuterDeadzoneXYSet(Result<(), String>),
330
331    // LED Configuration Management
332    /// Open LED configuration dialog for device
333    OpenLedConfig(String),
334    /// Close LED configuration dialog
335    CloseLedConfig,
336    /// Select LED zone for color editing
337    SelectLedZone(LedZone),
338    /// Set LED color (device_id, zone, red, green, blue)
339    SetLedColor(String, LedZone, u8, u8, u8),
340    /// LED color set result
341    LedColorSet(Result<(), String>),
342    /// Set LED brightness (device_id, zone_opt, brightness)
343    SetLedBrightness(String, Option<LedZone>, u8),
344    /// LED brightness set result
345    LedBrightnessSet(Result<(), String>),
346    /// Set LED pattern (device_id, pattern)
347    SetLedPattern(String, LedPattern),
348    /// LED pattern set result
349    LedPatternSet(Result<(), String>),
350    /// Request LED state refresh for device
351    RefreshLedState(String),
352    /// LED state loaded (device_id, colors)
353    LedStateLoaded(String, Result<HashMap<LedZone, (u8, u8, u8)>, String>),
354    /// RGB slider changed (red, green, blue)
355    LedSliderChanged(u8, u8, u8),
356
357    // Focus Tracking
358    /// Start focus tracking after daemon connection confirmed
359    StartFocusTracking,
360    /// Focus tracking started successfully
361    FocusTrackingStarted(Result<bool, String>),
362    /// Focus change event received from tracker
363    FocusChanged(String, Option<String>), // (app_id, window_title)
364
365    // Auto-Switch Rules Management
366    /// Open auto-switch rules view for a device
367    ShowAutoSwitchRules(String),
368    /// Close auto-switch rules view
369    CloseAutoSwitchRules,
370    /// Load auto-switch rules for a device
371    LoadAutoSwitchRules(String),
372    /// Auto-switch rules loaded (device_id, rules)
373    AutoSwitchRulesLoaded(Result<Vec<AutoSwitchRule>, String>),
374    /// Start editing a rule (index in list)
375    EditAutoSwitchRule(usize),
376    /// Update new rule app_id input
377    AutoSwitchAppIdChanged(String),
378    /// Update new rule profile_name input
379    AutoSwitchProfileNameChanged(String),
380    /// Update new rule layer_id input
381    AutoSwitchLayerIdChanged(String),
382    /// Use current focused app as app_id
383    AutoSwitchUseCurrentApp,
384    /// Save the current rule (add or update)
385    SaveAutoSwitchRule,
386    /// Delete a rule
387    DeleteAutoSwitchRule(usize),
388
389    // Hotkey Bindings Management
390    /// Open hotkey bindings view for a device
391    ShowHotkeyBindings(String),
392    /// Close hotkey bindings view
393    CloseHotkeyBindings,
394    /// Load hotkey bindings for a device
395    LoadHotkeyBindings(String),
396    /// Hotkey bindings loaded result
397    HotkeyBindingsLoaded(Result<Vec<HotkeyBinding>, String>),
398    /// Start editing a binding (index in list)
399    EditHotkeyBinding(usize),
400    /// Toggle modifier checkbox (modifier_name)
401    ToggleHotkeyModifier(String),
402    /// Update new binding key input
403    HotkeyKeyChanged(String),
404    /// Update new binding profile_name input
405    HotkeyProfileNameChanged(String),
406    /// Update new binding layer_id input
407    HotkeyLayerIdChanged(String),
408    /// Save the current binding (add or update)
409    SaveHotkeyBinding,
410    /// Delete a binding
411    DeleteHotkeyBinding(usize),
412    /// Hotkey bindings updated after delete
413    HotkeyBindingsUpdated(Vec<HotkeyBinding>),
414
415    // Analog Calibration Management
416    /// Open analog calibration view for a device and layer
417    OpenAnalogCalibration {
418        device_id: String,
419        layer_id: usize,
420    },
421    /// Analog calibration field changed
422    AnalogDeadzoneChanged(f32),
423    AnalogDeadzoneShapeChanged(DeadzoneShape),
424    AnalogSensitivityChanged(f32),
425    AnalogSensitivityCurveChanged(SensitivityCurve),
426    AnalogRangeMinChanged(i32),
427    AnalogRangeMaxChanged(i32),
428    AnalogInvertXToggled(bool),
429    AnalogInvertYToggled(bool),
430    /// Analog mode changed
431    AnalogModeChanged(AnalogMode),
432    /// Camera output mode changed
433    CameraModeChanged(CameraOutputMode),
434    /// Apply calibration changes
435    ApplyAnalogCalibration,
436    /// Analog calibration loaded
437    AnalogCalibrationLoaded(Result<aethermap_common::AnalogCalibrationConfig, String>),
438    /// Analog calibration applied
439    AnalogCalibrationApplied(Result<(), String>),
440    /// Close analog calibration view
441    CloseAnalogCalibration,
442    /// Analog input updated (streaming from daemon)
443    AnalogInputUpdated(f32, f32), // (x, y)
444}
445
446// Reserved for future use
447#[allow(dead_code)]
448pub enum _FutureMessage {
449    DismissNotification,
450}
451
452impl Application for State {
453    type Message = Message;
454    type Theme = Theme;
455    type Executor = iced::executor::Default;
456    type Flags = ();
457
458    fn new(_flags: ()) -> (Self, Command<Message>) {
459        let initial_state = State::default();
460        let initial_commands = Command::batch([
461            Command::perform(async { Message::CheckDaemonConnection }, |msg| msg),
462            Command::perform(async { Message::LoadDevices }, |msg| msg),
463            Command::perform(async { Message::LoadMacroSettings }, |msg| msg),
464        ]);
465        (initial_state, initial_commands)
466    }
467
468    fn title(&self) -> String {
469        String::from("Aethermap")
470    }
471
472    fn theme(&self) -> Theme {
473        self.current_theme.clone()
474    }
475
476    fn update(&mut self, message: Message) -> Command<Message> {
477        match message {
478            Message::ThemeChanged(theme) => {
479                self.current_theme = theme;
480                Command::none()
481            }
482            Message::SwitchTab(tab) => {
483                self.active_tab = tab;
484                Command::none()
485            }
486            Message::SelectDevice(idx) => {
487                self.selected_device = Some(idx);
488                // Load analog settings for the selected device if it has analog stick
489                if let Some(device) = self.devices.get(idx) {
490                    let device_id = format!("{:04x}:{:04x}", device.vendor_id, device.product_id);
491                    if device.device_type == DeviceType::Gamepad
492                        || device.device_type == DeviceType::Keypad
493                    {
494                        let device_id_clone1 = device_id.clone();
495                        let device_id_clone2 = device_id.clone();
496                        let device_id_clone3 = device_id.clone();
497                        return Command::batch(vec![
498                            Command::none(),
499                            Command::perform(async move { device_id_clone1 }, |id| {
500                                Message::AnalogDpadModeRequested(id)
501                            }),
502                            Command::perform(async move { device_id_clone2 }, |id| {
503                                Message::AnalogDeadzoneXYRequested(id)
504                            }),
505                            Command::perform(async move { device_id_clone3 }, |id| {
506                                Message::AnalogOuterDeadzoneXYRequested(id)
507                            }),
508                        ]);
509                    }
510                }
511                Command::none()
512            }
513            Message::CheckDaemonConnection => {
514                let socket_path = self.socket_path.clone();
515                Command::perform(
516                    async move {
517                        let client = crate::ipc::IpcClient::new(socket_path);
518                        client.connect().await.is_ok()
519                    },
520                    Message::DaemonStatusChanged,
521                )
522            }
523            Message::DaemonStatusChanged(connected) => {
524                self.daemon_connected = connected;
525                if connected {
526                    self.add_notification("Connected to daemon", false);
527                    // Start focus tracking after successful daemon connection
528                    Command::perform(async { Message::StartFocusTracking }, |msg| msg)
529                } else {
530                    self.add_notification("Daemon not running - start aethermapd", true);
531                    Command::none()
532                }
533            }
534            Message::StartFocusTracking => {
535                // Spawn async task to initialize and start focus tracking
536                // We create a simple check for portal availability
537                Command::perform(
538                    async move {
539                        // Check if WAYLAND_DISPLAY is set (basic portal check)
540                        let wayland_available = std::env::var("WAYLAND_DISPLAY").is_ok();
541                        if wayland_available {
542                            tracing::info!("Focus tracking available (Wayland detected)");
543                        } else {
544                            tracing::warn!("Focus tracking unavailable (not on Wayland)");
545                        }
546                        wayland_available
547                    },
548                    |available| Message::FocusTrackingStarted(Ok(available)),
549                )
550            }
551            Message::FocusTrackingStarted(Ok(available)) => {
552                self.focus_tracking_active = available;
553                if available {
554                    self.add_notification("Focus tracking enabled", false);
555                } else {
556                    self.add_notification(
557                        "Focus tracking unavailable (portal not connected)",
558                        true,
559                    );
560                }
561                Command::none()
562            }
563            Message::FocusTrackingStarted(Err(e)) => {
564                self.add_notification(&format!("Focus tracking error: {}", e), true);
565                self.focus_tracking_active = false;
566                Command::none()
567            }
568            Message::FocusChanged(app_id, window_title) => {
569                // Update current focus for auto-switch rule creation UI
570                self.current_focus = Some(app_id.clone());
571                // Send focus change to daemon for auto-profile switching
572                let socket_path = self.socket_path.clone();
573                Command::perform(
574                    async move {
575                        let client = crate::ipc::IpcClient::new(socket_path);
576                        client.send_focus_change(app_id, window_title).await
577                    },
578                    |result| match result {
579                        Ok(()) => Message::TickAnimations, // Silent success
580                        Err(e) => Message::ProfileError(format!("Focus change failed: {}", e)),
581                    },
582                )
583            }
584
585            // Auto-Switch Rules Management
586            Message::ShowAutoSwitchRules(device_id) => {
587                crate::handlers::auto_switch::show(self, device_id)
588            }
589            Message::CloseAutoSwitchRules => crate::handlers::auto_switch::close(self),
590            Message::LoadAutoSwitchRules(_device_id) => crate::handlers::auto_switch::load(self),
591            Message::AutoSwitchRulesLoaded(Ok(rules)) => {
592                crate::handlers::auto_switch::loaded(self, rules)
593            }
594            Message::AutoSwitchRulesLoaded(Err(error)) => {
595                crate::handlers::auto_switch::load_error(self, error)
596            }
597            Message::EditAutoSwitchRule(index) => crate::handlers::auto_switch::edit(self, index),
598            Message::AutoSwitchAppIdChanged(value) => {
599                crate::handlers::auto_switch::app_id_changed(self, value)
600            }
601            Message::AutoSwitchProfileNameChanged(value) => {
602                crate::handlers::auto_switch::profile_name_changed(self, value)
603            }
604            Message::AutoSwitchLayerIdChanged(value) => {
605                crate::handlers::auto_switch::layer_id_changed(self, value)
606            }
607            Message::AutoSwitchUseCurrentApp => crate::handlers::auto_switch::use_current_app(self),
608            Message::SaveAutoSwitchRule => crate::handlers::auto_switch::save(self),
609            Message::DeleteAutoSwitchRule(index) => {
610                crate::handlers::auto_switch::delete(self, index)
611            }
612
613            // Hotkey Bindings Management
614            Message::ShowHotkeyBindings(device_id) => {
615                crate::handlers::hotkeys::show(self, device_id)
616            }
617            Message::CloseHotkeyBindings => crate::handlers::hotkeys::close(self),
618            Message::LoadHotkeyBindings(device_id) => {
619                crate::handlers::hotkeys::load(self, device_id)
620            }
621            Message::HotkeyBindingsLoaded(Ok(bindings)) => {
622                crate::handlers::hotkeys::loaded(self, bindings)
623            }
624            Message::HotkeyBindingsLoaded(Err(error)) => {
625                crate::handlers::hotkeys::load_error(self, error)
626            }
627            Message::EditHotkeyBinding(index) => crate::handlers::hotkeys::edit(self, index),
628            Message::ToggleHotkeyModifier(modifier) => {
629                crate::handlers::hotkeys::toggle_modifier(self, modifier)
630            }
631            Message::HotkeyKeyChanged(value) => crate::handlers::hotkeys::key_changed(self, value),
632            Message::HotkeyProfileNameChanged(value) => {
633                crate::handlers::hotkeys::profile_name_changed(self, value)
634            }
635            Message::HotkeyLayerIdChanged(value) => {
636                crate::handlers::hotkeys::layer_id_changed(self, value)
637            }
638            Message::SaveHotkeyBinding => crate::handlers::hotkeys::save(self),
639            Message::DeleteHotkeyBinding(index) => crate::handlers::hotkeys::delete(self, index),
640            Message::HotkeyBindingsUpdated(bindings) => {
641                crate::handlers::hotkeys::bindings_updated(self, bindings)
642            }
643
644            // Analog Calibration Management
645            Message::OpenAnalogCalibration {
646                device_id,
647                layer_id,
648            } => crate::handlers::analog::open(self, device_id, layer_id),
649            Message::AnalogCalibrationLoaded(Ok(calibration)) => {
650                crate::handlers::analog::loaded(self, calibration)
651            }
652            Message::AnalogCalibrationLoaded(Err(error)) => {
653                crate::handlers::analog::load_error(self, error)
654            }
655            Message::AnalogDeadzoneChanged(value) => {
656                crate::handlers::analog::deadzone_changed(self, value)
657            }
658            Message::AnalogDeadzoneShapeChanged(shape) => {
659                crate::handlers::analog::deadzone_shape_changed(self, shape)
660            }
661            Message::AnalogSensitivityChanged(value) => {
662                crate::handlers::analog::sensitivity_changed(self, value)
663            }
664            Message::AnalogSensitivityCurveChanged(curve) => {
665                crate::handlers::analog::sensitivity_curve_changed(self, curve)
666            }
667            Message::AnalogRangeMinChanged(value) => {
668                crate::handlers::analog::range_min_changed(self, value)
669            }
670            Message::AnalogRangeMaxChanged(value) => {
671                crate::handlers::analog::range_max_changed(self, value)
672            }
673            Message::AnalogInvertXToggled(checked) => {
674                crate::handlers::analog::invert_x_toggled(self, checked)
675            }
676            Message::AnalogInvertYToggled(checked) => {
677                crate::handlers::analog::invert_y_toggled(self, checked)
678            }
679            Message::AnalogModeChanged(mode) => {
680                crate::handlers::analog::analog_mode_changed(self, mode)
681            }
682            Message::CameraModeChanged(mode) => {
683                crate::handlers::analog::camera_mode_changed(self, mode)
684            }
685            Message::ApplyAnalogCalibration => crate::handlers::analog::apply(self),
686            Message::AnalogCalibrationApplied(Ok(())) => crate::handlers::analog::applied_ok(self),
687            Message::AnalogCalibrationApplied(Err(error)) => {
688                crate::handlers::analog::applied_error(self, error)
689            }
690            Message::CloseAnalogCalibration => crate::handlers::analog::close(self),
691            Message::AnalogInputUpdated(x, y) => crate::handlers::analog::input_updated(self, x, y),
692
693            Message::LoadDevices => {
694                let socket_path = self.socket_path.clone();
695                self.loading = true;
696                Command::perform(
697                    async move {
698                        let client = crate::ipc::IpcClient::new(socket_path);
699                        client.get_devices().await.map_err(|e| e.to_string())
700                    },
701                    Message::DevicesLoaded,
702                )
703            }
704            Message::DevicesLoaded(Ok(devices)) => {
705                let count = devices.len();
706                self.devices = devices;
707                self.loading = false;
708                self.add_notification(&format!("Found {} devices", count), false);
709                Command::perform(async { Message::LoadMacros }, |msg| msg)
710            }
711            Message::DevicesLoaded(Err(e)) => {
712                self.loading = false;
713                self.add_notification(&format!("Error: {}", e), true);
714                Command::none()
715            }
716            Message::LoadMacros => crate::handlers::macros::load(self),
717            Message::MacrosLoaded(Ok(macros)) => crate::handlers::macros::loaded(self, macros),
718            Message::MacrosLoaded(Err(e)) => crate::handlers::macros::load_error(self, e),
719            Message::LoadMacroSettings => crate::handlers::macros::load_settings(self),
720            Message::MacroSettingsLoaded(Ok(settings)) => {
721                crate::handlers::macros::settings_loaded(self, settings)
722            }
723            Message::MacroSettingsLoaded(Err(e)) => {
724                crate::handlers::macros::settings_load_error(self, e)
725            }
726            Message::SetMacroSettings(settings) => {
727                crate::handlers::macros::set_settings(self, settings)
728            }
729            Message::LatencyChanged(ms) => crate::handlers::macros::latency_changed(self, ms),
730            Message::JitterChanged(pct) => crate::handlers::macros::jitter_changed(self, pct),
731            Message::CaptureMouseToggled(enabled) => {
732                crate::handlers::macros::capture_mouse_toggled(self, enabled)
733            }
734            Message::PlayMacro(macro_name) => crate::handlers::macros::play(self, macro_name),
735            Message::MacroPlayed(Ok(name)) => crate::handlers::macros::played_ok(self, name),
736            Message::MacroPlayed(Err(e)) => crate::handlers::macros::played_error(self, e),
737            Message::UpdateMacroName(name) => crate::handlers::macros::update_name(self, name),
738            Message::UpdateProfileName(name) => {
739                crate::handlers::macros::update_profile_name(self, name)
740            }
741            Message::StartRecording => crate::handlers::macros::start_recording(self),
742            Message::RecordingStarted(Ok(name)) => {
743                crate::handlers::macros::recording_started_ok(self, name)
744            }
745            Message::RecordingStarted(Err(e)) => {
746                crate::handlers::macros::recording_started_error(self, e)
747            }
748            Message::StopRecording => crate::handlers::macros::stop_recording(self),
749            Message::RecordingStopped(Ok(macro_entry)) => {
750                crate::handlers::macros::recording_stopped_ok(self, macro_entry)
751            }
752            Message::RecordingStopped(Err(e)) => {
753                crate::handlers::macros::recording_stopped_error(self, e)
754            }
755            Message::DeleteMacro(macro_name) => crate::handlers::macros::delete(self, macro_name),
756            Message::MacroDeleted(Ok(name)) => crate::handlers::macros::deleted_ok(self, name),
757            Message::MacroDeleted(Err(e)) => crate::handlers::macros::deleted_error(self, e),
758            Message::SaveProfile => {
759                if self.profile_name.trim().is_empty() {
760                    self.add_notification("Enter a profile name", true);
761                    return Command::none();
762                }
763                let socket_path = self.socket_path.clone();
764                let name = self.profile_name.clone();
765                Command::perform(
766                    async move {
767                        let client = crate::ipc::IpcClient::new(socket_path);
768                        client.save_profile(&name).await.map_err(|e| e.to_string())
769                    },
770                    Message::ProfileSaved,
771                )
772            }
773            Message::ProfileSaved(Ok((name, count))) => {
774                self.add_notification(&format!("Saved '{}' ({} macros)", name, count), false);
775                Command::none()
776            }
777            Message::ProfileSaved(Err(e)) => {
778                self.add_notification(&format!("Save failed: {}", e), true);
779                Command::none()
780            }
781            Message::LoadProfile => {
782                if self.profile_name.trim().is_empty() {
783                    self.add_notification("Enter a profile name to load", true);
784                    return Command::none();
785                }
786                let socket_path = self.socket_path.clone();
787                let name = self.profile_name.clone();
788                Command::perform(
789                    async move {
790                        let client = crate::ipc::IpcClient::new(socket_path);
791                        client.load_profile(&name).await.map_err(|e| e.to_string())
792                    },
793                    Message::ProfileLoaded,
794                )
795            }
796            Message::ProfileLoaded(Ok((name, count))) => {
797                self.add_notification(&format!("Loaded '{}' ({} macros)", name, count), false);
798                Command::perform(async { Message::LoadMacros }, |msg| msg)
799            }
800            Message::ProfileLoaded(Err(e)) => {
801                self.add_notification(&format!("Load failed: {}", e), true);
802                Command::none()
803            }
804            Message::TickAnimations => {
805                let now = Instant::now();
806                self.recently_updated_macros
807                    .retain(|_, timestamp| now.duration_since(*timestamp) < Duration::from_secs(3));
808                self.recording_pulse = !self.recording_pulse;
809                // Auto-dismiss old notifications
810                while let Some(notif) = self.notifications.front() {
811                    if now.duration_since(notif.timestamp) > Duration::from_secs(5) {
812                        self.notifications.pop_front();
813                    } else {
814                        break;
815                    }
816                }
817                Command::none()
818            }
819            Message::ShowNotification(message, is_error) => {
820                self.add_notification(&message, is_error);
821                Command::none()
822            }
823            Message::GrabDevice(device_path) => {
824                let socket_path = self.socket_path.clone();
825                let path_clone = device_path.clone();
826                Command::perform(
827                    async move {
828                        let client = crate::ipc::IpcClient::new(socket_path);
829                        client
830                            .grab_device(&path_clone)
831                            .await
832                            .map(|_| path_clone)
833                            .map_err(|e| e.to_string())
834                    },
835                    Message::DeviceGrabbed,
836                )
837            }
838            Message::UngrabDevice(device_path) => {
839                let socket_path = self.socket_path.clone();
840                let path_clone = device_path.clone();
841                Command::perform(
842                    async move {
843                        let client = crate::ipc::IpcClient::new(socket_path);
844                        client
845                            .ungrab_device(&path_clone)
846                            .await
847                            .map(|_| path_clone)
848                            .map_err(|e| e.to_string())
849                    },
850                    Message::DeviceUngrabbed,
851                )
852            }
853            Message::DeviceGrabbed(Ok(device_path)) => {
854                self.grabbed_devices.insert(device_path.clone());
855                if let Some(idx) = self
856                    .devices
857                    .iter()
858                    .position(|d| d.path.to_string_lossy() == device_path)
859                {
860                    self.selected_device = Some(idx);
861                }
862                self.add_notification("Device grabbed - ready for recording", false);
863                Command::none()
864            }
865            Message::DeviceGrabbed(Err(e)) => {
866                self.add_notification(&format!("Grab failed: {}", e), true);
867                Command::none()
868            }
869            Message::DeviceUngrabbed(Ok(device_path)) => {
870                self.grabbed_devices.remove(&device_path);
871                self.add_notification("Device released", false);
872                Command::none()
873            }
874            Message::DeviceUngrabbed(Err(e)) => {
875                self.add_notification(&format!("Release failed: {}", e), true);
876                Command::none()
877            }
878            Message::LoadDeviceProfiles(device_id) => {
879                let socket_path = self.socket_path.clone();
880                let id = device_id.clone();
881                Command::perform(
882                    async move {
883                        let client = crate::ipc::IpcClient::new(socket_path);
884                        (id.clone(), client.get_device_profiles(id).await)
885                    },
886                    |(device_id, result)| {
887                        Message::DeviceProfilesLoaded(device_id, result.map_err(|e| e.to_string()))
888                    },
889                )
890            }
891            Message::DeviceProfilesLoaded(device_id, Ok(profiles)) => {
892                self.device_profiles.insert(device_id.clone(), profiles);
893                self.add_notification(
894                    &format!(
895                        "Loaded {} profiles for {}",
896                        self.device_profiles
897                            .get(&device_id)
898                            .map(|p| p.len())
899                            .unwrap_or(0),
900                        device_id
901                    ),
902                    false,
903                );
904                Command::none()
905            }
906            Message::DeviceProfilesLoaded(_device_id, Err(e)) => {
907                self.add_notification(&format!("Failed to load device profiles: {}", e), true);
908                Command::none()
909            }
910            Message::ActivateProfile(device_id, profile_name) => {
911                let socket_path = self.socket_path.clone();
912                let id = device_id.clone();
913                let name = profile_name.clone();
914                Command::perform(
915                    async move {
916                        let client = crate::ipc::IpcClient::new(socket_path);
917                        client.activate_profile(id.clone(), name.clone()).await
918                    },
919                    move |result| match result {
920                        Ok(()) => Message::ProfileActivated(device_id, profile_name),
921                        Err(e) => {
922                            Message::ProfileError(format!("Failed to activate profile: {}", e))
923                        }
924                    },
925                )
926            }
927            Message::ProfileActivated(device_id, profile_name) => {
928                self.active_profiles
929                    .insert(device_id.clone(), profile_name.clone());
930                self.add_notification(
931                    &format!("Activated profile '{}' on {}", profile_name, device_id),
932                    false,
933                );
934                Command::none()
935            }
936            Message::DeactivateProfile(device_id) => {
937                let socket_path = self.socket_path.clone();
938                let id = device_id.clone();
939                Command::perform(
940                    async move {
941                        let client = crate::ipc::IpcClient::new(socket_path);
942                        client.deactivate_profile(id.clone()).await
943                    },
944                    move |result| match result {
945                        Ok(()) => Message::ProfileDeactivated(device_id),
946                        Err(e) => {
947                            Message::ProfileError(format!("Failed to deactivate profile: {}", e))
948                        }
949                    },
950                )
951            }
952            Message::ProfileDeactivated(device_id) => {
953                self.active_profiles.remove(&device_id);
954                self.add_notification(&format!("Deactivated profile on {}", device_id), false);
955                Command::none()
956            }
957            Message::ProfileError(msg) => {
958                self.add_notification(&msg, true);
959                Command::none()
960            }
961            Message::LoadRemapProfiles(device_path) => {
962                let socket_path = self.socket_path.clone();
963                let path = device_path.clone();
964                Command::perform(
965                    async move {
966                        let client = crate::ipc::IpcClient::new(socket_path);
967                        (path.clone(), client.list_remap_profiles(&path).await)
968                    },
969                    |(device_path, result)| {
970                        Message::RemapProfilesLoaded(device_path, result.map_err(|e| e.to_string()))
971                    },
972                )
973            }
974            Message::RemapProfilesLoaded(device_path, Ok(profiles)) => {
975                self.remap_profiles.insert(device_path.clone(), profiles);
976                self.add_notification(
977                    &format!(
978                        "Loaded {} remap profiles for {}",
979                        self.remap_profiles
980                            .get(&device_path)
981                            .map(|p| p.len())
982                            .unwrap_or(0),
983                        device_path
984                    ),
985                    false,
986                );
987                Command::none()
988            }
989            Message::RemapProfilesLoaded(_device_path, Err(e)) => {
990                self.add_notification(&format!("Failed to load remap profiles: {}", e), true);
991                Command::none()
992            }
993            Message::ActivateRemapProfile(device_path, profile_name) => {
994                let socket_path = self.socket_path.clone();
995                let path = device_path.clone();
996                let name = profile_name.clone();
997                Command::perform(
998                    async move {
999                        let client = crate::ipc::IpcClient::new(socket_path);
1000                        client.activate_remap_profile(&path, &name).await
1001                    },
1002                    move |result| match result {
1003                        Ok(()) => Message::RemapProfileActivated(device_path, profile_name),
1004                        Err(e) => Message::ProfileError(format!(
1005                            "Failed to activate remap profile: {}",
1006                            e
1007                        )),
1008                    },
1009                )
1010            }
1011            Message::RemapProfileActivated(device_path, profile_name) => {
1012                self.active_remap_profiles
1013                    .insert(device_path.clone(), profile_name.clone());
1014                self.add_notification(
1015                    &format!(
1016                        "Activated remap profile '{}' on {}",
1017                        profile_name, device_path
1018                    ),
1019                    false,
1020                );
1021                // Refresh active remaps after activation
1022                Command::perform(async move { device_path.clone() }, |path| {
1023                    Message::LoadActiveRemaps(path)
1024                })
1025            }
1026            Message::DeactivateRemapProfile(device_path) => {
1027                let socket_path = self.socket_path.clone();
1028                let path = device_path.clone();
1029                Command::perform(
1030                    async move {
1031                        let client = crate::ipc::IpcClient::new(socket_path);
1032                        client.deactivate_remap_profile(&path).await
1033                    },
1034                    move |result| match result {
1035                        Ok(()) => Message::RemapProfileDeactivated(device_path),
1036                        Err(e) => Message::ProfileError(format!(
1037                            "Failed to deactivate remap profile: {}",
1038                            e
1039                        )),
1040                    },
1041                )
1042            }
1043            Message::RemapProfileDeactivated(device_path) => {
1044                self.active_remap_profiles.remove(&device_path);
1045                self.active_remaps.remove(&device_path);
1046                self.add_notification(
1047                    &format!("Deactivated remap profile on {}", device_path),
1048                    false,
1049                );
1050                Command::none()
1051            }
1052            Message::LoadActiveRemaps(device_path) => {
1053                let socket_path = self.socket_path.clone();
1054                let path = device_path.clone();
1055                Command::perform(
1056                    async move {
1057                        let client = crate::ipc::IpcClient::new(socket_path);
1058                        (path.clone(), client.get_active_remaps(&path).await)
1059                    },
1060                    |(device_path, result)| {
1061                        Message::ActiveRemapsLoaded(device_path, result.map_err(|e| e.to_string()))
1062                    },
1063                )
1064            }
1065            Message::ActiveRemapsLoaded(device_path, Ok(Some((profile_name, remaps)))) => {
1066                self.active_remaps
1067                    .insert(device_path.clone(), (profile_name, remaps));
1068                Command::none()
1069            }
1070            Message::ActiveRemapsLoaded(device_path, Ok(None)) => {
1071                self.active_remaps.remove(&device_path);
1072                Command::none()
1073            }
1074            Message::ActiveRemapsLoaded(_device_path, Err(e)) => {
1075                self.add_notification(&format!("Failed to load active remaps: {}", e), true);
1076                Command::none()
1077            }
1078            Message::RecordMouseEvent {
1079                event_type,
1080                button,
1081                x,
1082                y,
1083                delta,
1084            } => {
1085                // Mouse events are captured by daemon during recording via device grab
1086                // This handler is for GUI-side mouse event logging
1087                if self.recording {
1088                    // Log the mouse event for debugging/confirmation
1089                    let event_desc = match event_type.as_str() {
1090                        "button_press" => format!("Mouse button {} pressed", button.unwrap_or(0)),
1091                        "button_release" => {
1092                            format!("Mouse button {} released", button.unwrap_or(0))
1093                        }
1094                        "movement" => format!("Mouse moved to ({}, {})", x, y),
1095                        "scroll" => format!("Mouse scrolled {}", delta),
1096                        _ => format!("Unknown mouse event: {}", event_type),
1097                    };
1098                    // Update status to show mouse event was captured
1099                    self.status = event_desc;
1100                }
1101                Command::none()
1102            }
1103            Message::ShowKeypadView(device_path) => {
1104                // Empty string means back button was pressed - clear keypad view
1105                if device_path.is_empty() {
1106                    self.device_capabilities = None;
1107                    self.keypad_layout.clear();
1108                    self.keypad_view_device = None;
1109                    self.selected_button = None;
1110                    return Command::none();
1111                }
1112                // Store the device path for keypad view
1113                self.keypad_view_device = Some(device_path.clone());
1114                // Query device capabilities and load keypad layout
1115                let socket_path = self.socket_path.clone();
1116                let path_clone = device_path.clone();
1117                Command::perform(
1118                    async move {
1119                        let client = crate::ipc::IpcClient::new(socket_path);
1120                        (
1121                            path_clone.clone(),
1122                            client.get_device_capabilities(&path_clone).await,
1123                        )
1124                    },
1125                    |(device_path, result)| {
1126                        Message::DeviceCapabilitiesLoaded(
1127                            device_path,
1128                            result.map_err(|e| e.to_string()),
1129                        )
1130                    },
1131                )
1132            }
1133            Message::DeviceCapabilitiesLoaded(device_path, Ok(capabilities)) => {
1134                self.device_capabilities = Some(capabilities);
1135                self.keypad_layout = azeron_keypad_layout();
1136                // Load current remappings and update button.current_remap
1137                if let Some((profile_name, remaps)) = self.active_remaps.get(&device_path) {
1138                    for remap in remaps {
1139                        if let Some(button) = self
1140                            .keypad_layout
1141                            .iter_mut()
1142                            .find(|b| b.id == remap.from_key)
1143                        {
1144                            button.current_remap = Some(remap.to_key.clone());
1145                        }
1146                    }
1147                    self.add_notification(
1148                        &format!("Loaded remaps from profile '{}'", profile_name),
1149                        false,
1150                    );
1151                }
1152                // Switch to Devices tab to show keypad view
1153                self.active_tab = Tab::Devices;
1154                Command::none()
1155            }
1156            Message::DeviceCapabilitiesLoaded(_device_path, Err(e)) => {
1157                self.add_notification(&format!("Failed to load device capabilities: {}", e), true);
1158                Command::none()
1159            }
1160            Message::SelectKeypadButton(button_id) => {
1161                self.selected_button = self.keypad_layout.iter().position(|b| b.id == button_id);
1162                self.status = format!(
1163                    "Selected button: {} - Configure remapping in device profile",
1164                    button_id
1165                );
1166                Command::none()
1167            }
1168            Message::LayerStateChanged(device_id, layer_id) => {
1169                self.active_layers.insert(device_id, layer_id);
1170                Command::none()
1171            }
1172            Message::LayerConfigRequested(device_id) => {
1173                let socket_path = self.socket_path.clone();
1174                let id = device_id.clone();
1175                Command::perform(
1176                    async move {
1177                        let client = crate::ipc::IpcClient::new(socket_path);
1178                        (id.clone(), client.list_layers(&id).await)
1179                    },
1180                    |(device_id, result)| match result {
1181                        Ok(layers) => {
1182                            // Store layers and trigger UI refresh
1183                            // We'll emit LayerStateChanged for the active layer
1184                            if let Some(active_layer) = layers.first() {
1185                                Message::LayerStateChanged(device_id, active_layer.layer_id)
1186                            } else {
1187                                Message::TickAnimations // No-op refresh
1188                            }
1189                        }
1190                        Err(e) => Message::ProfileError(format!("Failed to load layers: {}", e)),
1191                    },
1192                )
1193            }
1194            Message::LayerActivateRequested(device_id, layer_id, mode) => {
1195                let socket_path = self.socket_path.clone();
1196                let id = device_id.clone();
1197                Command::perform(
1198                    async move {
1199                        let client = crate::ipc::IpcClient::new(socket_path);
1200                        client.activate_layer(&id, layer_id, mode).await
1201                    },
1202                    move |result| match result {
1203                        Ok(()) => Message::LayerStateChanged(device_id, layer_id),
1204                        Err(e) => Message::ProfileError(format!("Failed to activate layer: {}", e)),
1205                    },
1206                )
1207            }
1208            Message::LayerConfigUpdated(device_id, config) => {
1209                let socket_path = self.socket_path.clone();
1210                let id = device_id.clone();
1211                let layer_id = config.layer_id;
1212                let name = config.name.clone();
1213                let mode = config.mode;
1214                Command::perform(
1215                    async move {
1216                        let client = crate::ipc::IpcClient::new(socket_path);
1217                        client.set_layer_config(&id, layer_id, name, mode).await
1218                    },
1219                    move |result| match result {
1220                        Ok(()) => {
1221                            // Refresh layer list after config update
1222                            Message::LayerConfigRequested(device_id)
1223                        }
1224                        Err(e) => {
1225                            Message::ProfileError(format!("Failed to update layer config: {}", e))
1226                        }
1227                    },
1228                )
1229            }
1230            Message::OpenLayerConfigDialog(device_id, layer_id) => {
1231                // Get current layer config if available
1232                let current_name = self
1233                    .layer_configs
1234                    .get(&device_id)
1235                    .and_then(|layers| layers.iter().find(|l| l.layer_id == layer_id))
1236                    .map(|l| l.name.clone())
1237                    .unwrap_or_else(|| format!("Layer {}", layer_id));
1238
1239                let current_mode = self
1240                    .layer_configs
1241                    .get(&device_id)
1242                    .and_then(|layers| layers.iter().find(|l| l.layer_id == layer_id))
1243                    .map(|l| l.mode)
1244                    .unwrap_or(LayerMode::Hold);
1245
1246                self.layer_config_dialog = Some((device_id, layer_id, current_name, current_mode));
1247                Command::none()
1248            }
1249            Message::LayerConfigNameChanged(name) => {
1250                if let Some((device_id, layer_id, _, mode)) = self.layer_config_dialog.take() {
1251                    self.layer_config_dialog = Some((device_id, layer_id, name, mode));
1252                }
1253                Command::none()
1254            }
1255            Message::LayerConfigModeChanged(mode) => {
1256                if let Some((device_id, layer_id, name, _)) = self.layer_config_dialog.take() {
1257                    self.layer_config_dialog = Some((device_id, layer_id, name, mode));
1258                }
1259                Command::none()
1260            }
1261            Message::SaveLayerConfig => {
1262                if let Some((device_id, layer_id, name, mode)) = self.layer_config_dialog.take() {
1263                    let config = LayerConfigInfo {
1264                        layer_id,
1265                        name: name.clone(),
1266                        mode,
1267                        remap_count: 0,
1268                        led_color: (0, 0, 255), // Default blue - TODO: allow GUI configuration
1269                        led_zone: None,         // Default zone - TODO: allow GUI configuration
1270                    };
1271                    // Return LayerConfigUpdated message to handle the async save
1272                    Command::perform(async move { (device_id, config) }, |(device_id, config)| {
1273                        Message::LayerConfigUpdated(device_id, config)
1274                    })
1275                } else {
1276                    Command::none()
1277                }
1278            }
1279            Message::CancelLayerConfig => {
1280                self.layer_config_dialog = None;
1281                Command::none()
1282            }
1283            Message::RefreshLayers => {
1284                // Periodic refresh of layer states for all devices
1285                let mut commands = Vec::new();
1286
1287                // Request layer configuration refresh for devices that have profiles loaded
1288                for device_id in self.device_profiles.keys() {
1289                    let device_id = device_id.clone();
1290                    let socket_path = self.socket_path.clone();
1291                    commands.push(Command::perform(
1292                        async move {
1293                            let client = crate::ipc::IpcClient::new(socket_path);
1294                            (device_id.clone(), client.list_layers(&device_id).await)
1295                        },
1296                        |(device_id, result)| match result {
1297                            Ok(layers) => {
1298                                // Store layers and update active layer
1299                                Message::LayerListLoaded(device_id, layers)
1300                            }
1301                            Err(_) => Message::TickAnimations, // Silent fail on refresh
1302                        },
1303                    ));
1304                }
1305
1306                // Also refresh active layer states
1307                for device_id in self.active_layers.keys().cloned().collect::<Vec<_>>() {
1308                    let device_id = device_id.clone();
1309                    let socket_path = self.socket_path.clone();
1310                    commands.push(Command::perform(
1311                        async move {
1312                            let client = crate::ipc::IpcClient::new(socket_path);
1313                            (device_id.clone(), client.get_active_layer(&device_id).await)
1314                        },
1315                        |(device_id, result)| match result {
1316                            Ok(Some(layer_id)) => Message::LayerStateChanged(device_id, layer_id),
1317                            _ => Message::TickAnimations,
1318                        },
1319                    ));
1320                }
1321
1322                Command::batch(commands)
1323            }
1324            Message::LayerListLoaded(device_id, layers) => {
1325                self.layer_configs.insert(device_id.clone(), layers);
1326                Command::none()
1327            }
1328
1329            Message::AnalogDpadModeRequested(device_id) => {
1330                let socket_path = self.socket_path.clone();
1331                let device_id_clone = device_id.clone();
1332                Command::perform(
1333                    async move {
1334                        let client = crate::ipc::IpcClient::new(socket_path);
1335                        client.get_analog_dpad_mode(&device_id_clone).await
1336                    },
1337                    move |result| match result {
1338                        Ok(mode) => Message::AnalogDpadModeLoaded(device_id, mode),
1339                        Err(e) => {
1340                            eprintln!("Failed to get D-pad mode: {}", e);
1341                            Message::TickAnimations // Silent fail
1342                        }
1343                    },
1344                )
1345            }
1346
1347            Message::AnalogDpadModeLoaded(device_id, mode) => {
1348                self.analog_dpad_modes.insert(device_id, mode);
1349                Command::none()
1350            }
1351
1352            Message::SetAnalogDpadMode(device_id, mode) => {
1353                let socket_path = self.socket_path.clone();
1354                let device_id_clone = device_id.clone();
1355                Command::perform(
1356                    async move {
1357                        let client = crate::ipc::IpcClient::new(socket_path);
1358                        client.set_analog_dpad_mode(&device_id_clone, &mode).await
1359                    },
1360                    |result| match result {
1361                        Ok(_) => Message::AnalogDpadModeSet(Ok(())),
1362                        Err(e) => Message::AnalogDpadModeSet(Err(e)),
1363                    },
1364                )
1365            }
1366
1367            Message::AnalogDpadModeSet(result) => {
1368                match result {
1369                    Ok(_) => {
1370                        // Success - D-pad mode updated
1371                        Command::none()
1372                    }
1373                    Err(e) => {
1374                        eprintln!("Failed to set D-pad mode: {}", e);
1375                        // Could show a toast notification here
1376                        Command::none()
1377                    }
1378                }
1379            }
1380
1381            // Per-Axis Deadzone handlers
1382            Message::AnalogDeadzoneXYRequested(device_id) => {
1383                let socket_path = self.socket_path.clone();
1384                let device_id_clone = device_id.clone();
1385                Command::perform(
1386                    async move {
1387                        let client = crate::ipc::IpcClient::new(socket_path);
1388                        client.get_analog_deadzone_xy(&device_id_clone).await
1389                    },
1390                    move |result| match result {
1391                        Ok((x_pct, y_pct)) => {
1392                            Message::AnalogDeadzoneXYLoaded(device_id, (x_pct, y_pct))
1393                        }
1394                        Err(e) => {
1395                            eprintln!("Failed to get per-axis deadzone: {}", e);
1396                            Message::TickAnimations // Silent fail
1397                        }
1398                    },
1399                )
1400            }
1401
1402            Message::AnalogDeadzoneXYLoaded(device_id, (x_pct, y_pct)) => {
1403                self.analog_deadzones_xy.insert(device_id, (x_pct, y_pct));
1404                Command::none()
1405            }
1406
1407            Message::SetAnalogDeadzoneXY(device_id, x_pct, y_pct) => {
1408                let socket_path = self.socket_path.clone();
1409                Command::perform(
1410                    async move {
1411                        let client = crate::ipc::IpcClient::new(socket_path);
1412                        client
1413                            .set_analog_deadzone_xy(&device_id, x_pct, y_pct)
1414                            .await
1415                    },
1416                    |result| match result {
1417                        Ok(_) => Message::AnalogDeadzoneXYSet(Ok(())),
1418                        Err(e) => Message::AnalogDeadzoneXYSet(Err(e)),
1419                    },
1420                )
1421            }
1422
1423            Message::AnalogDeadzoneXYSet(result) => {
1424                match result {
1425                    Ok(_) => {
1426                        // Success - per-axis deadzone updated
1427                        Command::none()
1428                    }
1429                    Err(e) => {
1430                        eprintln!("Failed to set per-axis deadzone: {}", e);
1431                        self.add_notification(&format!("Failed to set deadzone: {}", e), true);
1432                        Command::none()
1433                    }
1434                }
1435            }
1436
1437            // Per-Axis Outer Deadzone handlers
1438            Message::AnalogOuterDeadzoneXYRequested(device_id) => {
1439                let socket_path = self.socket_path.clone();
1440                let device_id_clone = device_id.clone();
1441                Command::perform(
1442                    async move {
1443                        let client = crate::ipc::IpcClient::new(socket_path);
1444                        client.get_analog_outer_deadzone_xy(&device_id_clone).await
1445                    },
1446                    move |result| match result {
1447                        Ok((x_pct, y_pct)) => {
1448                            Message::AnalogOuterDeadzoneXYLoaded(device_id, (x_pct, y_pct))
1449                        }
1450                        Err(e) => {
1451                            eprintln!("Failed to get per-axis outer deadzone: {}", e);
1452                            Message::TickAnimations // Silent fail
1453                        }
1454                    },
1455                )
1456            }
1457
1458            Message::AnalogOuterDeadzoneXYLoaded(device_id, (x_pct, y_pct)) => {
1459                self.analog_outer_deadzones_xy
1460                    .insert(device_id, (x_pct, y_pct));
1461                Command::none()
1462            }
1463
1464            Message::SetAnalogOuterDeadzoneXY(device_id, x_pct, y_pct) => {
1465                let socket_path = self.socket_path.clone();
1466                Command::perform(
1467                    async move {
1468                        let client = crate::ipc::IpcClient::new(socket_path);
1469                        client
1470                            .set_analog_outer_deadzone_xy(&device_id, x_pct, y_pct)
1471                            .await
1472                    },
1473                    |result| match result {
1474                        Ok(_) => Message::AnalogOuterDeadzoneXYSet(Ok(())),
1475                        Err(e) => Message::AnalogOuterDeadzoneXYSet(Err(e)),
1476                    },
1477                )
1478            }
1479
1480            Message::AnalogOuterDeadzoneXYSet(result) => {
1481                match result {
1482                    Ok(_) => {
1483                        // Success - per-axis outer deadzone updated
1484                        Command::none()
1485                    }
1486                    Err(e) => {
1487                        eprintln!("Failed to set per-axis outer deadzone: {}", e);
1488                        self.add_notification(
1489                            &format!("Failed to set outer deadzone: {}", e),
1490                            true,
1491                        );
1492                        Command::none()
1493                    }
1494                }
1495            }
1496
1497            // LED Configuration handlers
1498            Message::OpenLedConfig(device_id) => crate::handlers::led::open(self, device_id),
1499            Message::CloseLedConfig => crate::handlers::led::close(self),
1500            Message::SelectLedZone(zone) => crate::handlers::led::select_zone(self, zone),
1501            Message::RefreshLedState(device_id) => crate::handlers::led::refresh(self, device_id),
1502            Message::LedStateLoaded(device_id, result) => {
1503                crate::handlers::led::state_loaded(self, device_id, result)
1504            }
1505            Message::SetLedColor(device_id, zone, red, green, blue) => {
1506                crate::handlers::led::set_color(self, device_id, zone, red, green, blue)
1507            }
1508            Message::LedColorSet(result) => crate::handlers::led::color_set(self, result),
1509            Message::SetLedBrightness(device_id, zone, brightness) => {
1510                crate::handlers::led::set_brightness(self, device_id, zone, brightness)
1511            }
1512            Message::LedBrightnessSet(result) => crate::handlers::led::brightness_set(self, result),
1513            Message::SetLedPattern(device_id, pattern) => {
1514                crate::handlers::led::set_pattern(self, device_id, pattern)
1515            }
1516            Message::LedPatternSet(result) => crate::handlers::led::pattern_set(self, result),
1517            Message::LedSliderChanged(red, green, blue) => {
1518                crate::handlers::led::slider_changed(self, red, green, blue)
1519            }
1520        }
1521    }
1522
1523    fn view(&self) -> Element<'_, Message> {
1524        let sidebar = self.view_sidebar();
1525        let main_content = self.view_main_content();
1526        let status_bar = self.view_status_bar();
1527
1528        let main_layout = row![
1529            sidebar,
1530            vertical_rule(1),
1531            column![main_content, horizontal_rule(1), status_bar,].height(Length::Fill)
1532        ];
1533
1534        let base: Element<'_, Message> = container(main_layout)
1535            .width(Length::Fill)
1536            .height(Length::Fill)
1537            .into();
1538
1539        // Show layer config dialog overlay if active
1540        if let Some(dialog) = views::devices::layer_config_dialog(self) {
1541            container(column![base, dialog,].height(Length::Fill))
1542                .width(Length::Fill)
1543                .height(Length::Fill)
1544                .into()
1545        } else if let Some(led_dialog) = self.view_led_config() {
1546            // Show LED config dialog overlay if active
1547            container(column![base, led_dialog,].height(Length::Fill))
1548                .width(Length::Fill)
1549                .height(Length::Fill)
1550                .into()
1551        } else if let Some(calib_dialog) = self.view_analog_calibration() {
1552            // Show analog calibration dialog overlay if active
1553            container(column![base, calib_dialog,].height(Length::Fill))
1554                .width(Length::Fill)
1555                .height(Length::Fill)
1556                .into()
1557        } else {
1558            base
1559        }
1560    }
1561
1562    fn subscription(&self) -> Subscription<Message> {
1563        let timer = iced::time::every(Duration::from_millis(500)).map(|_| Message::TickAnimations);
1564
1565        // Periodic layer state refresh (every 2 seconds)
1566        let layer_refresh =
1567            iced::time::every(Duration::from_secs(2)).map(|_| Message::RefreshLayers);
1568
1569        // Subscribe to mouse events only when recording
1570        // Note: In iced 0.12, mouse events are handled via the runtime event stream
1571        // The actual mouse event capture for macros happens at the daemon level via evdev
1572        // This subscription tracks recording state for UI updates only
1573        let mouse_events = iced::event::listen_with(|event, _status| {
1574            match event {
1575                iced::Event::Mouse(iced::mouse::Event::ButtonPressed(
1576                    iced::mouse::Button::Left,
1577                )) => {
1578                    Some(Message::RecordMouseEvent {
1579                        event_type: "button_press".to_string(),
1580                        button: Some(0x110), // BTN_LEFT in evdev
1581                        x: 0,
1582                        y: 0,
1583                        delta: 0,
1584                    })
1585                }
1586                iced::Event::Mouse(iced::mouse::Event::ButtonPressed(
1587                    iced::mouse::Button::Right,
1588                )) => {
1589                    Some(Message::RecordMouseEvent {
1590                        event_type: "button_press".to_string(),
1591                        button: Some(0x111), // BTN_RIGHT in evdev
1592                        x: 0,
1593                        y: 0,
1594                        delta: 0,
1595                    })
1596                }
1597                iced::Event::Mouse(iced::mouse::Event::ButtonPressed(
1598                    iced::mouse::Button::Middle,
1599                )) => {
1600                    Some(Message::RecordMouseEvent {
1601                        event_type: "button_press".to_string(),
1602                        button: Some(0x112), // BTN_MIDDLE in evdev
1603                        x: 0,
1604                        y: 0,
1605                        delta: 0,
1606                    })
1607                }
1608                iced::Event::Mouse(iced::mouse::Event::ButtonReleased(_)) => {
1609                    Some(Message::RecordMouseEvent {
1610                        event_type: "button_release".to_string(),
1611                        button: Some(0),
1612                        x: 0,
1613                        y: 0,
1614                        delta: 0,
1615                    })
1616                }
1617                iced::Event::Mouse(iced::mouse::Event::WheelScrolled { delta }) => {
1618                    let scroll_delta = match delta {
1619                        iced::mouse::ScrollDelta::Lines { y, .. } => y as i32,
1620                        iced::mouse::ScrollDelta::Pixels { y, .. } => y as i32,
1621                    };
1622                    Some(Message::RecordMouseEvent {
1623                        event_type: "scroll".to_string(),
1624                        button: None,
1625                        x: 0,
1626                        y: 0,
1627                        delta: scroll_delta,
1628                    })
1629                }
1630                iced::Event::Mouse(iced::mouse::Event::CursorMoved { .. }) => {
1631                    // Note: Cursor movement is tracked but may be sampled at reduced rate
1632                    Some(Message::RecordMouseEvent {
1633                        event_type: "movement".to_string(),
1634                        button: None,
1635                        x: 0,
1636                        y: 0,
1637                        delta: 0,
1638                    })
1639                }
1640                _ => None,
1641            }
1642        });
1643
1644        // Only enable mouse event subscription during recording
1645        let mouse_subscription = if self.recording {
1646            mouse_events
1647        } else {
1648            Subscription::none()
1649        };
1650
1651        let theme_subscription = iced::subscription::unfold(
1652            "ashpd-theme",
1653            None,
1654            |state: Option<
1655                iced::futures::stream::BoxStream<'static, ashpd::desktop::settings::ColorScheme>,
1656            >| async move {
1657                use ashpd::desktop::settings::{ColorScheme, Settings};
1658                use iced::futures::StreamExt;
1659
1660                let mut stream = match state {
1661                    Some(s) => s,
1662                    None => {
1663                        let settings = match Settings::new().await {
1664                            Ok(s) => s,
1665                            Err(_) => return iced::futures::future::pending().await,
1666                        };
1667                        let initial = settings
1668                            .color_scheme()
1669                            .await
1670                            .unwrap_or(ColorScheme::NoPreference);
1671                        let theme = match initial {
1672                            ColorScheme::PreferDark => aether_dark(),
1673                            ColorScheme::PreferLight => aether_light(),
1674                            ColorScheme::NoPreference => aether_dark(),
1675                        };
1676
1677                        let s = match settings.receive_color_scheme_changed().await {
1678                            Ok(s) => s,
1679                            Err(_) => return (Message::ThemeChanged(theme), None),
1680                        };
1681                        return (Message::ThemeChanged(theme), Some(s.boxed()));
1682                    }
1683                };
1684
1685                if let Some(scheme) = stream.next().await {
1686                    let theme = match scheme {
1687                        ColorScheme::PreferDark => aether_dark(),
1688                        ColorScheme::PreferLight => aether_light(),
1689                        ColorScheme::NoPreference => aether_dark(),
1690                    };
1691                    (Message::ThemeChanged(theme), Some(stream))
1692                } else {
1693                    iced::futures::future::pending().await
1694                }
1695            },
1696        );
1697
1698        Subscription::batch(vec![
1699            timer,
1700            layer_refresh,
1701            mouse_subscription,
1702            theme_subscription,
1703        ])
1704    }
1705}
1706
1707impl State {
1708    pub(crate) fn add_notification(&mut self, message: &str, is_error: bool) {
1709        self.notifications.push_back(Notification {
1710            message: message.to_string(),
1711            is_error,
1712            timestamp: Instant::now(),
1713        });
1714        self.status = message.to_string();
1715        self.status_history.push_back(message.to_string());
1716        if self.status_history.len() > 10 {
1717            self.status_history.pop_front();
1718        }
1719        if self.notifications.len() > 5 {
1720            self.notifications.pop_front();
1721        }
1722    }
1723
1724    fn view_sidebar(&self) -> Element<'_, Message> {
1725        views::sidebar::view(self)
1726    }
1727
1728    fn view_main_content(&self) -> Element<'_, Message> {
1729        let content = match self.active_tab {
1730            Tab::Devices => self.view_devices_tab(),
1731            Tab::Macros => self.view_macros_tab(),
1732            Tab::Profiles => self.view_profiles_tab(),
1733        };
1734
1735        container(scrollable(content))
1736            .width(Length::Fill)
1737            .height(Length::Fill)
1738            .padding(24)
1739            .into()
1740    }
1741
1742    fn view_devices_tab(&self) -> Element<'_, Message> {
1743        views::devices::view_devices_tab(self)
1744    }
1745
1746    fn view_macros_tab(&self) -> Element<'_, Message> {
1747        views::macros::view(self)
1748    }
1749
1750    fn view_profiles_tab(&self) -> Element<'_, Message> {
1751        views::profiles::view_profiles_tab(self)
1752    }
1753
1754    fn view_status_bar(&self) -> Element<'_, Message> {
1755        views::status_bar::view(self)
1756    }
1757
1758    /// View layer indicator for a device
1759    ///
1760    /// Displays the active layer name/ID for the given device.
1761    /// Shows "Layer N: {name}" format with Primary style for visibility.
1762    /// Get current color for a zone, with default fallback
1763    pub fn view_led_config(&self) -> Option<Element<'_, Message>> {
1764        views::led::view(self)
1765    }
1766
1767    /// View analog calibration dialog
1768    ///
1769    /// Displays modal dialog for analog stick calibration with deadzone,
1770    /// sensitivity, range, and inversion controls.
1771    pub fn view_analog_calibration(&self) -> Option<Element<'_, Message>> {
1772        views::analog::overlay_view(self)
1773    }
1774}