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
8use 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#[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 pub device_profiles: HashMap<String, Vec<String>>,
79 pub active_profiles: HashMap<String, String>,
81 pub remap_profiles: HashMap<String, Vec<RemapProfileInfo>>,
83 pub active_remap_profiles: HashMap<String, String>,
85 pub active_remaps: HashMap<String, (String, Vec<RemapEntry>)>,
87 pub keypad_layout: Vec<KeypadButton>,
89 pub keypad_view_device: Option<String>,
91 pub selected_button: Option<usize>,
93 pub device_capabilities: Option<DeviceCapabilities>,
95 pub active_layers: HashMap<String, usize>,
97 pub layer_configs: HashMap<String, Vec<LayerConfigInfo>>,
99 pub layer_config_dialog: Option<(String, usize, String, LayerMode)>,
101 pub analog_dpad_modes: HashMap<String, String>,
103 pub analog_deadzones_xy: HashMap<String, (u8, u8)>,
105 pub analog_outer_deadzones_xy: HashMap<String, (u8, u8)>,
107 pub led_states: HashMap<String, LedState>,
109 pub led_config_device: Option<String>,
111 pub selected_led_zone: Option<LedZone>,
113 pub pending_led_color: Option<(u8, u8, u8)>,
115 pub current_focus: Option<String>,
117 pub focus_tracking_active: bool,
119 pub auto_switch_view: Option<AutoSwitchRulesView>,
121 pub hotkey_view: Option<HotkeyBindingsView>,
123 pub analog_calibration_view: Option<AnalogCalibrationView>,
125 pub macro_settings: MacroSettings,
127 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 SwitchTab(Tab),
196 ThemeChanged(iced::Theme),
197
198 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 UpdateMacroName(String),
209 StartRecording,
210 StopRecording,
211 RecordingStarted(Result<String, String>),
212 RecordingStopped(Result<MacroEntry, String>),
213
214 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 UpdateProfileName(String),
230 SaveProfile,
231 ProfileSaved(Result<(String, usize), String>),
232 LoadProfile,
233 ProfileLoaded(Result<(String, usize), String>),
234
235 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 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 CheckDaemonConnection,
256 DaemonStatusChanged(bool),
257
258 TickAnimations,
260 ShowNotification(String, bool), RecordMouseEvent {
264 event_type: String,
265 button: Option<u16>,
266 x: i32,
267 y: i32,
268 delta: i32,
269 },
270
271 ShowKeypadView(String),
274 SelectKeypadButton(String),
276 DeviceCapabilitiesLoaded(String, Result<DeviceCapabilities, String>),
278
279 LayerStateChanged(String, usize),
282 LayerConfigRequested(String),
284 LayerActivateRequested(String, usize, LayerMode),
286 LayerConfigUpdated(String, LayerConfigInfo),
288 OpenLayerConfigDialog(String, usize),
290 LayerConfigNameChanged(String),
292 LayerConfigModeChanged(LayerMode),
294 SaveLayerConfig,
296 CancelLayerConfig,
298 RefreshLayers,
300 LayerListLoaded(String, Vec<LayerConfigInfo>),
302
303 AnalogDpadModeRequested(String),
306 AnalogDpadModeLoaded(String, String),
308 SetAnalogDpadMode(String, String),
310 AnalogDpadModeSet(Result<(), String>),
312
313 AnalogDeadzoneXYRequested(String),
316 AnalogDeadzoneXYLoaded(String, (u8, u8)),
318 SetAnalogDeadzoneXY(String, u8, u8),
320 AnalogDeadzoneXYSet(Result<(), String>),
322 AnalogOuterDeadzoneXYRequested(String),
324 AnalogOuterDeadzoneXYLoaded(String, (u8, u8)),
326 SetAnalogOuterDeadzoneXY(String, u8, u8),
328 AnalogOuterDeadzoneXYSet(Result<(), String>),
330
331 OpenLedConfig(String),
334 CloseLedConfig,
336 SelectLedZone(LedZone),
338 SetLedColor(String, LedZone, u8, u8, u8),
340 LedColorSet(Result<(), String>),
342 SetLedBrightness(String, Option<LedZone>, u8),
344 LedBrightnessSet(Result<(), String>),
346 SetLedPattern(String, LedPattern),
348 LedPatternSet(Result<(), String>),
350 RefreshLedState(String),
352 LedStateLoaded(String, Result<HashMap<LedZone, (u8, u8, u8)>, String>),
354 LedSliderChanged(u8, u8, u8),
356
357 StartFocusTracking,
360 FocusTrackingStarted(Result<bool, String>),
362 FocusChanged(String, Option<String>), ShowAutoSwitchRules(String),
368 CloseAutoSwitchRules,
370 LoadAutoSwitchRules(String),
372 AutoSwitchRulesLoaded(Result<Vec<AutoSwitchRule>, String>),
374 EditAutoSwitchRule(usize),
376 AutoSwitchAppIdChanged(String),
378 AutoSwitchProfileNameChanged(String),
380 AutoSwitchLayerIdChanged(String),
382 AutoSwitchUseCurrentApp,
384 SaveAutoSwitchRule,
386 DeleteAutoSwitchRule(usize),
388
389 ShowHotkeyBindings(String),
392 CloseHotkeyBindings,
394 LoadHotkeyBindings(String),
396 HotkeyBindingsLoaded(Result<Vec<HotkeyBinding>, String>),
398 EditHotkeyBinding(usize),
400 ToggleHotkeyModifier(String),
402 HotkeyKeyChanged(String),
404 HotkeyProfileNameChanged(String),
406 HotkeyLayerIdChanged(String),
408 SaveHotkeyBinding,
410 DeleteHotkeyBinding(usize),
412 HotkeyBindingsUpdated(Vec<HotkeyBinding>),
414
415 OpenAnalogCalibration {
418 device_id: String,
419 layer_id: usize,
420 },
421 AnalogDeadzoneChanged(f32),
423 AnalogDeadzoneShapeChanged(DeadzoneShape),
424 AnalogSensitivityChanged(f32),
425 AnalogSensitivityCurveChanged(SensitivityCurve),
426 AnalogRangeMinChanged(i32),
427 AnalogRangeMaxChanged(i32),
428 AnalogInvertXToggled(bool),
429 AnalogInvertYToggled(bool),
430 AnalogModeChanged(AnalogMode),
432 CameraModeChanged(CameraOutputMode),
434 ApplyAnalogCalibration,
436 AnalogCalibrationLoaded(Result<aethermap_common::AnalogCalibrationConfig, String>),
438 AnalogCalibrationApplied(Result<(), String>),
440 CloseAnalogCalibration,
442 AnalogInputUpdated(f32, f32), }
445
446#[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 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 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 Command::perform(
538 async move {
539 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 self.current_focus = Some(app_id.clone());
571 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, Err(e) => Message::ProfileError(format!("Focus change failed: {}", e)),
581 },
582 )
583 }
584
585 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 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 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 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 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 if self.recording {
1088 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 self.status = event_desc;
1100 }
1101 Command::none()
1102 }
1103 Message::ShowKeypadView(device_path) => {
1104 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 self.keypad_view_device = Some(device_path.clone());
1114 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 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 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 if let Some(active_layer) = layers.first() {
1185 Message::LayerStateChanged(device_id, active_layer.layer_id)
1186 } else {
1187 Message::TickAnimations }
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 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 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), led_zone: None, };
1271 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 let mut commands = Vec::new();
1286
1287 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 Message::LayerListLoaded(device_id, layers)
1300 }
1301 Err(_) => Message::TickAnimations, },
1303 ));
1304 }
1305
1306 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 }
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 Command::none()
1372 }
1373 Err(e) => {
1374 eprintln!("Failed to set D-pad mode: {}", e);
1375 Command::none()
1377 }
1378 }
1379 }
1380
1381 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 }
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 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 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 }
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 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 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 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 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 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 let layer_refresh =
1567 iced::time::every(Duration::from_secs(2)).map(|_| Message::RefreshLayers);
1568
1569 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), 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), 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), 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 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 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 pub fn view_led_config(&self) -> Option<Element<'_, Message>> {
1764 views::led::view(self)
1765 }
1766
1767 pub fn view_analog_calibration(&self) -> Option<Element<'_, Message>> {
1772 views::analog::overlay_view(self)
1773 }
1774}