Skip to main content

aethermap_gui/
ipc.rs

1//! IPC module for the aethermap GUI
2//!
3//! This module provides a simplified interface for the GUI to communicate
4//! with the aethermap daemon using the common IPC client.
5
6use aethermap_common::{ipc_client, DeviceCapabilities, DeviceInfo, LayerConfigInfo, LayerMode, LedPattern, LedZone, MacroEntry, Request, Response, AnalogCalibrationConfig};
7use std::collections::HashMap;
8use std::path::PathBuf;
9
10/// Simplified IPC client for the GUI
11pub struct GuiIpcClient {
12    socket_path: PathBuf,
13}
14
15impl GuiIpcClient {
16    /// Create a new GUI IPC client with the specified socket path
17    pub fn new(socket_path: PathBuf) -> Self {
18        Self { socket_path }
19    }
20
21    /// Connect to the daemon
22    pub async fn connect(&self) -> Result<(), String> {
23        match ipc_client::is_daemon_running(Some(&self.socket_path)).await {
24            true => Ok(()),
25            false => Err("Daemon is not running".to_string()),
26        }
27    }
28
29    /// Get list of available devices
30    pub async fn get_devices(&self) -> Result<Vec<DeviceInfo>, String> {
31        let request = Request::GetDevices;
32        match ipc_client::send_to_path(&request, &self.socket_path).await {
33            Ok(Response::Devices(devices)) => Ok(devices),
34            Ok(_) => Err("Unexpected response".to_string()),
35            Err(e) => Err(format!("Failed to get devices: {}", e)),
36        }
37    }
38
39    /// Get list of configured macros
40    pub async fn list_macros(&self) -> Result<Vec<MacroEntry>, String> {
41        let request = Request::ListMacros;
42        match ipc_client::send_to_path(&request, &self.socket_path).await {
43            Ok(Response::Macros(macros)) => Ok(macros),
44            Ok(_) => Err("Unexpected response".to_string()),
45            Err(e) => Err(format!("Failed to list macros: {}", e)),
46        }
47    }
48
49    /// Start recording a macro for a device
50    pub async fn start_recording_macro(&self, device_path: &str, name: &str, capture_mouse: bool) -> Result<(), String> {
51        let request = Request::RecordMacro {
52            device_path: device_path.to_string(),
53            name: name.to_string(),
54            capture_mouse,
55        };
56        match ipc_client::send_to_path(&request, &self.socket_path).await {
57            Ok(Response::RecordingStarted { .. }) => Ok(()),
58            Ok(_) => Err("Unexpected response".to_string()),
59            Err(e) => Err(format!("Failed to start recording: {}", e)),
60        }
61    }
62
63    /// Stop recording a macro
64    pub async fn stop_recording_macro(&self) -> Result<MacroEntry, String> {
65        let request = Request::StopRecording;
66        match ipc_client::send_to_path(&request, &self.socket_path).await {
67            Ok(Response::RecordingStopped { macro_entry }) => Ok(macro_entry),
68            Ok(_) => Err("Unexpected response".to_string()),
69            Err(e) => Err(format!("Failed to stop recording: {}", e)),
70        }
71    }
72
73    /// Delete a macro by name
74    pub async fn delete_macro(&self, name: &str) -> Result<(), String> {
75        let request = Request::DeleteMacro {
76            name: name.to_string(),
77        };
78        match ipc_client::send_to_path(&request, &self.socket_path).await {
79            Ok(Response::Ack) => Ok(()),
80            Ok(_) => Err("Unexpected response".to_string()),
81            Err(e) => Err(format!("Failed to delete macro: {}", e)),
82        }
83    }
84
85    /// Test a macro execution
86    pub async fn test_macro(&self, name: &str) -> Result<(), String> {
87        let request = Request::TestMacro {
88            name: name.to_string(),
89        };
90        match ipc_client::send_to_path(&request, &self.socket_path).await {
91            Ok(Response::Ack) => Ok(()),
92            Ok(_) => Err("Unexpected response".to_string()),
93            Err(e) => Err(format!("Failed to test macro: {}", e)),
94        }
95    }
96
97    /// Save current macros to a profile
98    pub async fn save_profile(&self, name: &str) -> Result<(String, usize), String> {
99        let request = Request::SaveProfile {
100            name: name.to_string(),
101        };
102        match ipc_client::send_to_path(&request, &self.socket_path).await {
103            Ok(Response::ProfileSaved { name, macros_count }) => Ok((name, macros_count)),
104            Ok(_) => Err("Unexpected response".to_string()),
105            Err(e) => Err(format!("Failed to save profile: {}", e)),
106        }
107    }
108
109    /// Load macros from a profile
110    pub async fn load_profile(&self, name: &str) -> Result<(String, usize), String> {
111        let request = Request::LoadProfile {
112            name: name.to_string(),
113        };
114        match ipc_client::send_to_path(&request, &self.socket_path).await {
115            Ok(Response::ProfileLoaded { name, macros_count }) => Ok((name, macros_count)),
116            Ok(_) => Err("Unexpected response".to_string()),
117            Err(e) => Err(format!("Failed to load profile: {}", e)),
118        }
119    }
120
121    /// Grab a device exclusively for input interception
122    pub async fn grab_device(&self, device_path: &str) -> Result<(), String> {
123        let request = Request::GrabDevice {
124            device_path: device_path.to_string(),
125        };
126        match ipc_client::send_to_path(&request, &self.socket_path).await {
127            Ok(Response::Ack) => Ok(()),
128            Ok(_) => Err("Unexpected response".to_string()),
129            Err(e) => Err(format!("Failed to grab device: {}", e)),
130        }
131    }
132
133    /// Release exclusive access to a device
134    pub async fn ungrab_device(&self, device_path: &str) -> Result<(), String> {
135        let request = Request::UngrabDevice {
136            device_path: device_path.to_string(),
137        };
138        match ipc_client::send_to_path(&request, &self.socket_path).await {
139            Ok(Response::Ack) => Ok(()),
140            Ok(_) => Err("Unexpected response".to_string()),
141            Err(e) => Err(format!("Failed to ungrab device: {}", e)),
142        }
143    }
144
145    /// Get available profiles for a specific device
146    ///
147    /// # Arguments
148    ///
149    /// * `device_id` - Device identifier in vendor:product format (e.g., "1532:0220")
150    ///
151    /// # Returns
152    ///
153    /// * `Ok(Vec<String>)` - List of available profile names
154    /// * `Err(String)` - IPC communication error
155    pub async fn get_device_profiles(
156        &self,
157        device_id: String,
158    ) -> Result<Vec<String>, String> {
159        let request = Request::GetDeviceProfiles { device_id };
160
161        match ipc_client::send_to_path(&request, &self.socket_path).await {
162            Ok(Response::DeviceProfiles { profiles, .. }) => Ok(profiles),
163            Ok(Response::Error(msg)) => Err(msg),
164            Ok(_) => Err("Unexpected response".to_string()),
165            Err(e) => Err(format!("Failed to get device profiles: {}", e)),
166        }
167    }
168
169    /// Activate a remap profile for a device
170    ///
171    /// # Arguments
172    ///
173    /// * `device_id` - Device identifier in vendor:product format
174    /// * `profile_name` - Name of the profile to activate
175    ///
176    /// # Returns
177    ///
178    /// * `Ok(())` - Profile activated successfully
179    /// * `Err(String)` - IPC communication error
180    pub async fn activate_profile(
181        &self,
182        device_id: String,
183        profile_name: String,
184    ) -> Result<(), String> {
185        let request = Request::ActivateProfile {
186            device_id,
187            profile_name,
188        };
189
190        match ipc_client::send_to_path(&request, &self.socket_path).await {
191            Ok(Response::ProfileActivated { .. }) => Ok(()),
192            Ok(Response::Error(msg)) => Err(msg),
193            Ok(_) => Err("Unexpected response".to_string()),
194            Err(e) => Err(format!("Failed to activate profile: {}", e)),
195        }
196    }
197
198    /// Deactivate the current remap profile for a device
199    ///
200    /// # Arguments
201    ///
202    /// * `device_id` - Device identifier in vendor:product format
203    ///
204    /// # Returns
205    ///
206    /// * `Ok(())` - Profile deactivated successfully
207    /// * `Err(String)` - IPC communication error
208    pub async fn deactivate_profile(
209        &self,
210        device_id: String,
211    ) -> Result<(), String> {
212        let request = Request::DeactivateProfile { device_id };
213
214        match ipc_client::send_to_path(&request, &self.socket_path).await {
215            Ok(Response::ProfileDeactivated { .. }) => Ok(()),
216            Ok(Response::Error(msg)) => Err(msg),
217            Ok(_) => Err("Unexpected response".to_string()),
218            Err(e) => Err(format!("Failed to deactivate profile: {}", e)),
219        }
220    }
221
222    /// Get the currently active profile for a device
223    ///
224    /// # Arguments
225    ///
226    /// * `device_id` - Device identifier in vendor:product format
227    ///
228    /// # Returns
229    ///
230    /// * `Ok(Option<String>)` - Active profile name or None
231    /// * `Err(String)` - IPC communication error
232    pub async fn get_active_profile(
233        &self,
234        device_id: String,
235    ) -> Result<Option<String>, String> {
236        let request = Request::GetActiveProfile { device_id };
237
238        match ipc_client::send_to_path(&request, &self.socket_path).await {
239            Ok(Response::ActiveProfile { profile_name, .. }) => Ok(profile_name),
240            Ok(Response::Error(msg)) => Err(msg),
241            Ok(_) => Err("Unexpected response".to_string()),
242            Err(e) => Err(format!("Failed to get active profile: {}", e)),
243        }
244    }
245
246    /// Get active remaps for a device
247    ///
248    /// # Arguments
249    ///
250    /// * `device_path` - Full path to the device (e.g., "/dev/input/event0")
251    ///
252    /// # Returns
253    ///
254    /// * `Ok(Option<(String, Vec<RemapEntry>)>)` - Active profile name and remap entries, or None if no profile active
255    /// * `Err(String)` - IPC communication error
256    pub async fn get_active_remaps(
257        &self,
258        device_path: &str,
259    ) -> Result<Option<(String, Vec<aethermap_common::RemapEntry>)>, String> {
260        let request = Request::GetActiveRemaps {
261            device_path: device_path.to_string(),
262        };
263
264        match ipc_client::send_to_path(&request, &self.socket_path).await {
265            Ok(Response::ActiveRemaps { profile_name, remaps, .. }) => {
266                if let Some(name) = profile_name {
267                    Ok(Some((name, remaps)))
268                } else {
269                    Ok(None)
270                }
271            }
272            Ok(Response::Error(msg)) => Err(msg),
273            Ok(_) => Err("Unexpected response".to_string()),
274            Err(e) => Err(format!("Failed to get active remaps: {}", e)),
275        }
276    }
277
278    /// List available remap profiles for a device
279    ///
280    /// # Arguments
281    ///
282    /// * `device_path` - Full path to the device
283    ///
284    /// # Returns
285    ///
286    /// * `Ok(Vec<aethermap_common::RemapProfileInfo>)` - List of available profile info
287    /// * `Err(String)` - IPC communication error
288    pub async fn list_remap_profiles(
289        &self,
290        device_path: &str,
291    ) -> Result<Vec<aethermap_common::RemapProfileInfo>, String> {
292        let request = Request::ListRemapProfiles {
293            device_path: device_path.to_string(),
294        };
295
296        match ipc_client::send_to_path(&request, &self.socket_path).await {
297            Ok(Response::RemapProfiles { profiles, .. }) => Ok(profiles),
298            Ok(Response::Error(msg)) => Err(msg),
299            Ok(_) => Err("Unexpected response".to_string()),
300            Err(e) => Err(format!("Failed to list remap profiles: {}", e)),
301        }
302    }
303
304    /// Activate a remap profile for a device
305    ///
306    /// # Arguments
307    ///
308    /// * `device_path` - Full path to the device
309    /// * `profile_name` - Name of the profile to activate
310    ///
311    /// # Returns
312    ///
313    /// * `Ok(())` - Profile activated successfully
314    /// * `Err(String)` - IPC communication error
315    pub async fn activate_remap_profile(
316        &self,
317        device_path: &str,
318        profile_name: &str,
319    ) -> Result<(), String> {
320        let request = Request::ActivateRemapProfile {
321            device_path: device_path.to_string(),
322            profile_name: profile_name.to_string(),
323        };
324
325        match ipc_client::send_to_path(&request, &self.socket_path).await {
326            Ok(Response::RemapProfileActivated { .. }) => Ok(()),
327            Ok(Response::Error(msg)) => Err(msg),
328            Ok(_) => Err("Unexpected response".to_string()),
329            Err(e) => Err(format!("Failed to activate remap profile: {}", e)),
330        }
331    }
332
333    /// Deactivate the current remap profile for a device
334    ///
335    /// # Arguments
336    ///
337    /// * `device_path` - Full path to the device
338    ///
339    /// # Returns
340    ///
341    /// * `Ok(())` - Profile deactivated successfully
342    /// * `Err(String)` - IPC communication error
343    pub async fn deactivate_remap_profile(
344        &self,
345        device_path: &str,
346    ) -> Result<(), String> {
347        let request = Request::DeactivateRemapProfile {
348            device_path: device_path.to_string(),
349        };
350
351        match ipc_client::send_to_path(&request, &self.socket_path).await {
352            Ok(Response::RemapProfileDeactivated { .. }) => Ok(()),
353            Ok(Response::Error(msg)) => Err(msg),
354            Ok(_) => Err("Unexpected response".to_string()),
355            Err(e) => Err(format!("Failed to deactivate remap profile: {}", e)),
356        }
357    }
358
359    /// Get device capabilities and features
360    ///
361    /// # Arguments
362    ///
363    /// * `device_path` - Full path to the device
364    ///
365    /// # Returns
366    ///
367    /// * `Ok(DeviceCapabilities)` - Device capabilities including button count, hat switch, analog stick
368    /// * `Err(String)` - IPC communication error
369    pub async fn get_device_capabilities(
370        &self,
371        device_path: &str,
372    ) -> Result<DeviceCapabilities, String> {
373        let request = Request::GetDeviceCapabilities {
374            device_path: device_path.to_string(),
375        };
376
377        match ipc_client::send_to_path(&request, &self.socket_path).await {
378            Ok(Response::DeviceCapabilities { capabilities, .. }) => Ok(capabilities),
379            Ok(Response::Error(msg)) => Err(msg),
380            Ok(_) => Err("Unexpected response".to_string()),
381            Err(e) => Err(format!("Failed to get device capabilities: {}", e)),
382        }
383    }
384
385    /// Get the currently active layer for a device
386    ///
387    /// # Arguments
388    ///
389    /// * `device_id` - Device identifier in vendor:product format (e.g., "1532:0220")
390    ///
391    /// # Returns
392    ///
393    /// * `Ok(Option<usize>)` - Active layer ID (Some) or None if no layer active
394    /// * `Err(String)` - IPC communication error
395    pub async fn get_active_layer(
396        &self,
397        device_id: &str,
398    ) -> Result<Option<usize>, String> {
399        let request = Request::GetActiveLayer {
400            device_id: device_id.to_string(),
401        };
402
403        match ipc_client::send_to_path(&request, &self.socket_path).await {
404            Ok(Response::ActiveLayer { layer_id, .. }) => Ok(Some(layer_id)),
405            Ok(Response::Error(msg)) => Err(msg),
406            Ok(_) => Err("Unexpected response".to_string()),
407            Err(e) => Err(format!("Failed to get active layer: {}", e)),
408        }
409    }
410
411    /// Set layer configuration for a device
412    ///
413    /// # Arguments
414    ///
415    /// * `device_id` - Device identifier in vendor:product format
416    /// * `layer_id` - Layer ID to configure
417    /// * `name` - Human-readable layer name
418    /// * `mode` - Layer activation mode (Hold or Toggle)
419    ///
420    /// # Returns
421    ///
422    /// * `Ok(())` - Layer configuration updated successfully
423    /// * `Err(String)` - IPC communication error
424    pub async fn set_layer_config(
425        &self,
426        device_id: &str,
427        layer_id: usize,
428        name: String,
429        mode: LayerMode,
430    ) -> Result<(), String> {
431        let config = LayerConfigInfo {
432            layer_id,
433            name: name.clone(),
434            mode,
435            remap_count: 0, // Remaps are managed separately via RemapEngine
436            led_color: (0, 0, 255), // Default blue - TODO: allow GUI configuration
437            led_zone: None, // Default zone - TODO: allow GUI configuration
438        };
439
440        let request = Request::SetLayerConfig {
441            device_id: device_id.to_string(),
442            layer_id,
443            config,
444        };
445
446        match ipc_client::send_to_path(&request, &self.socket_path).await {
447            Ok(Response::LayerConfigured { .. }) => Ok(()),
448            Ok(Response::Error(msg)) => Err(msg),
449            Ok(_) => Err("Unexpected response".to_string()),
450            Err(e) => Err(format!("Failed to set layer config: {}", e)),
451        }
452    }
453
454    /// Activate a layer for a device
455    ///
456    /// # Arguments
457    ///
458    /// * `device_id` - Device identifier in vendor:product format
459    /// * `layer_id` - Layer ID to activate
460    /// * `mode` - Layer activation mode (Hold or Toggle)
461    ///
462    /// # Returns
463    ///
464    /// * `Ok(())` - Layer activated successfully
465    /// * `Err(String)` - IPC communication error
466    pub async fn activate_layer(
467        &self,
468        device_id: &str,
469        layer_id: usize,
470        mode: LayerMode,
471    ) -> Result<(), String> {
472        let request = Request::ActivateLayer {
473            device_id: device_id.to_string(),
474            layer_id,
475            mode,
476        };
477
478        match ipc_client::send_to_path(&request, &self.socket_path).await {
479            Ok(Response::LayerConfigured { .. }) => Ok(()),
480            Ok(Response::Error(msg)) => Err(msg),
481            Ok(_) => Err("Unexpected response".to_string()),
482            Err(e) => Err(format!("Failed to activate layer: {}", e)),
483        }
484    }
485
486    /// List all configured layers for a device
487    ///
488    /// # Arguments
489    ///
490    /// * `device_id` - Device identifier in vendor:product format
491    ///
492    /// # Returns
493    ///
494    /// * `Ok(Vec<LayerConfigInfo>)` - List of layer configurations
495    /// * `Err(String)` - IPC communication error
496    pub async fn list_layers(
497        &self,
498        device_id: &str,
499    ) -> Result<Vec<LayerConfigInfo>, String> {
500        let request = Request::ListLayers {
501            device_id: device_id.to_string(),
502        };
503
504        match ipc_client::send_to_path(&request, &self.socket_path).await {
505            Ok(Response::LayerList { layers, .. }) => Ok(layers),
506            Ok(Response::Error(msg)) => Err(msg),
507            Ok(_) => Err("Unexpected response".to_string()),
508            Err(e) => Err(format!("Failed to list layers: {}", e)),
509        }
510    }
511
512    /// Set D-pad emulation mode for a device
513    ///
514    /// # Arguments
515    ///
516    /// * `device_id` - Device identifier in vendor:product format
517    /// * `mode` - D-pad mode: "disabled", "eight_way", or "four_way"
518    ///
519    /// # Returns
520    ///
521    /// * `Ok(())` - D-pad mode set successfully
522    /// * `Err(String)` - IPC communication error
523    pub async fn set_analog_dpad_mode(
524        &self,
525        device_id: &str,
526        mode: &str,
527    ) -> Result<(), String> {
528        let request = Request::SetAnalogDpadMode {
529            device_id: device_id.to_string(),
530            mode: mode.to_string(),
531        };
532
533        match ipc_client::send_to_path(&request, &self.socket_path).await {
534            Ok(Response::AnalogDpadModeSet { .. }) => Ok(()),
535            Ok(Response::Error(msg)) => Err(msg),
536            Ok(_) => Err("Unexpected response".to_string()),
537            Err(e) => Err(format!("Failed to set D-pad mode: {}", e)),
538        }
539    }
540
541    /// Get D-pad emulation mode for a device
542    ///
543    /// # Arguments
544    ///
545    /// * `device_id` - Device identifier in vendor:product format
546    ///
547    /// # Returns
548    ///
549    /// * `Ok(String)` - D-pad mode: "disabled", "eight_way", or "four_way"
550    /// * `Err(String)` - IPC communication error
551    pub async fn get_analog_dpad_mode(
552        &self,
553        device_id: &str,
554    ) -> Result<String, String> {
555        let request = Request::GetAnalogDpadMode {
556            device_id: device_id.to_string(),
557        };
558
559        match ipc_client::send_to_path(&request, &self.socket_path).await {
560            Ok(Response::AnalogDpadMode { mode, .. }) => Ok(mode),
561            Ok(Response::Error(msg)) => Err(msg),
562            Ok(_) => Err("Unexpected response".to_string()),
563            Err(e) => Err(format!("Failed to get D-pad mode: {}", e)),
564        }
565    }
566
567    /// Set per-axis analog deadzone for a device
568    ///
569    /// # Arguments
570    ///
571    /// * `device_id` - Device identifier in vendor:product format
572    /// * `x_percentage` - X-axis deadzone percentage (0-100)
573    /// * `y_percentage` - Y-axis deadzone percentage (0-100)
574    ///
575    /// # Returns
576    ///
577    /// * `Ok(())` - Per-axis deadzone set successfully
578    /// * `Err(String)` - IPC communication error
579    pub async fn set_analog_deadzone_xy(
580        &self,
581        device_id: &str,
582        x_percentage: u8,
583        y_percentage: u8,
584    ) -> Result<(), String> {
585        let request = Request::SetAnalogDeadzoneXY {
586            device_id: device_id.to_string(),
587            x_percentage,
588            y_percentage,
589        };
590
591        match ipc_client::send_to_path(&request, &self.socket_path).await {
592            Ok(Response::AnalogDeadzoneXYSet { .. }) => Ok(()),
593            Ok(Response::Error(msg)) => Err(msg),
594            Ok(_) => Err("Unexpected response".to_string()),
595            Err(e) => Err(format!("Failed to set per-axis deadzone: {}", e)),
596        }
597    }
598
599    /// Get per-axis analog deadzone for a device
600    ///
601    /// # Arguments
602    ///
603    /// * `device_id` - Device identifier in vendor:product format
604    ///
605    /// # Returns
606    ///
607    /// * `Ok((u8, u8))` - X and Y deadzone percentages (0-100 each)
608    /// * `Err(String)` - IPC communication error
609    pub async fn get_analog_deadzone_xy(
610        &self,
611        device_id: &str,
612    ) -> Result<(u8, u8), String> {
613        let request = Request::GetAnalogDeadzoneXY {
614            device_id: device_id.to_string(),
615        };
616
617        match ipc_client::send_to_path(&request, &self.socket_path).await {
618            Ok(Response::AnalogDeadzoneXY { x_percentage, y_percentage, .. }) => {
619                Ok((x_percentage, y_percentage))
620            }
621            Ok(Response::Error(msg)) => Err(msg),
622            Ok(_) => Err("Unexpected response".to_string()),
623            Err(e) => Err(format!("Failed to get per-axis deadzone: {}", e)),
624        }
625    }
626
627    /// Set per-axis outer deadzone (max clamp) for a device
628    ///
629    /// # Arguments
630    ///
631    /// * `device_id` - Device identifier in vendor:product format
632    /// * `x_percentage` - X-axis outer deadzone percentage (0-100)
633    /// * `y_percentage` - Y-axis outer deadzone percentage (0-100)
634    ///
635    /// # Returns
636    ///
637    /// * `Ok(())` - Per-axis outer deadzone set successfully
638    /// * `Err(String)` - IPC communication error
639    pub async fn set_analog_outer_deadzone_xy(
640        &self,
641        device_id: &str,
642        x_percentage: u8,
643        y_percentage: u8,
644    ) -> Result<(), String> {
645        let request = Request::SetAnalogOuterDeadzoneXY {
646            device_id: device_id.to_string(),
647            x_percentage,
648            y_percentage,
649        };
650
651        match ipc_client::send_to_path(&request, &self.socket_path).await {
652            Ok(Response::AnalogOuterDeadzoneXYSet { .. }) => Ok(()),
653            Ok(Response::Error(msg)) => Err(msg),
654            Ok(_) => Err("Unexpected response".to_string()),
655            Err(e) => Err(format!("Failed to set per-axis outer deadzone: {}", e)),
656        }
657    }
658
659    /// Get per-axis outer deadzone for a device
660    ///
661    /// # Arguments
662    ///
663    /// * `device_id` - Device identifier in vendor:product format
664    ///
665    /// # Returns
666    ///
667    /// * `Ok((u8, u8))` - X and Y outer deadzone percentages (0-100 each)
668    /// * `Err(String)` - IPC communication error
669    pub async fn get_analog_outer_deadzone_xy(
670        &self,
671        device_id: &str,
672    ) -> Result<(u8, u8), String> {
673        let request = Request::GetAnalogOuterDeadzoneXY {
674            device_id: device_id.to_string(),
675        };
676
677        match ipc_client::send_to_path(&request, &self.socket_path).await {
678            Ok(Response::AnalogOuterDeadzoneXY { x_percentage, y_percentage, .. }) => {
679                Ok((x_percentage, y_percentage))
680            }
681            Ok(Response::Error(msg)) => Err(msg),
682            Ok(_) => Err("Unexpected response".to_string()),
683            Err(e) => Err(format!("Failed to get per-axis outer deadzone: {}", e)),
684        }
685    }
686
687    /// Set LED color for a specific zone
688    ///
689    /// # Arguments
690    ///
691    /// * `device_id` - Device identifier in vendor:product format
692    /// * `zone` - LED zone to configure (Logo, Keys, Thumbstick, All, Global)
693    /// * `red` - Red component (0-255)
694    /// * `green` - Green component (0-255)
695    /// * `blue` - Blue component (0-255)
696    ///
697    /// # Returns
698    ///
699    /// * `Ok(())` - Color set successfully
700    /// * `Err(String)` - IPC communication error
701    pub async fn set_led_color(
702        &self,
703        device_id: &str,
704        zone: LedZone,
705        red: u8,
706        green: u8,
707        blue: u8,
708    ) -> Result<(), String> {
709        let request = Request::SetLedColor {
710            device_id: device_id.to_string(),
711            zone,
712            red,
713            green,
714            blue,
715        };
716
717        match ipc_client::send_to_path(&request, &self.socket_path).await {
718            Ok(Response::LedColorSet { .. }) => Ok(()),
719            Ok(Response::Error(msg)) => Err(msg),
720            Ok(_) => Err("Unexpected response".to_string()),
721            Err(e) => Err(format!("Failed to set LED color: {}", e)),
722        }
723    }
724
725    /// Get LED color for a specific zone
726    ///
727    /// # Arguments
728    ///
729    /// * `device_id` - Device identifier in vendor:product format
730    /// * `zone` - LED zone to query
731    ///
732    /// # Returns
733    ///
734    /// * `Ok(Option<(u8, u8, u8)>)` - RGB color tuple if set, None if not set
735    /// * `Err(String)` - IPC communication error
736    pub async fn get_led_color(
737        &self,
738        device_id: &str,
739        zone: LedZone,
740    ) -> Result<Option<(u8, u8, u8)>, String> {
741        let request = Request::GetLedColor {
742            device_id: device_id.to_string(),
743            zone,
744        };
745
746        match ipc_client::send_to_path(&request, &self.socket_path).await {
747            Ok(Response::LedColor { color, .. }) => Ok(color),
748            Ok(Response::Error(msg)) => Err(msg),
749            Ok(_) => Err("Unexpected response".to_string()),
750            Err(e) => Err(format!("Failed to get LED color: {}", e)),
751        }
752    }
753
754    /// Get all LED colors for a device
755    ///
756    /// # Arguments
757    ///
758    /// * `device_id` - Device identifier in vendor:product format
759    ///
760    /// # Returns
761    ///
762    /// * `Ok(HashMap<LedZone, (u8, u8, u8)>)` - Map of zones to RGB colors
763    /// * `Err(String)` - IPC communication error
764    pub async fn get_all_led_colors(
765        &self,
766        device_id: &str,
767    ) -> Result<HashMap<LedZone, (u8, u8, u8)>, String> {
768        let request = Request::GetAllLedColors {
769            device_id: device_id.to_string(),
770        };
771
772        match ipc_client::send_to_path(&request, &self.socket_path).await {
773            Ok(Response::AllLedColors { colors, .. }) => Ok(colors),
774            Ok(Response::Error(msg)) => Err(msg),
775            Ok(_) => Err("Unexpected response".to_string()),
776            Err(e) => Err(format!("Failed to get all LED colors: {}", e)),
777        }
778    }
779
780    /// Set LED brightness for a device
781    ///
782    /// # Arguments
783    ///
784    /// * `device_id` - Device identifier in vendor:product format
785    /// * `zone` - LED zone (None = global brightness)
786    /// * `brightness` - Brightness percentage (0-100)
787    ///
788    /// # Returns
789    ///
790    /// * `Ok(())` - Brightness set successfully
791    /// * `Err(String)` - IPC communication error
792    pub async fn set_led_brightness(
793        &self,
794        device_id: &str,
795        zone: Option<LedZone>,
796        brightness: u8,
797    ) -> Result<(), String> {
798        let request = Request::SetLedBrightness {
799            device_id: device_id.to_string(),
800            zone,
801            brightness,
802        };
803
804        match ipc_client::send_to_path(&request, &self.socket_path).await {
805            Ok(Response::LedBrightnessSet { .. }) => Ok(()),
806            Ok(Response::Error(msg)) => Err(msg),
807            Ok(_) => Err("Unexpected response".to_string()),
808            Err(e) => Err(format!("Failed to set LED brightness: {}", e)),
809        }
810    }
811
812    /// Get LED brightness for a device
813    ///
814    /// # Arguments
815    ///
816    /// * `device_id` - Device identifier in vendor:product format
817    /// * `zone` - LED zone (None = global brightness)
818    ///
819    /// # Returns
820    ///
821    /// * `Ok(u8)` - Brightness percentage (0-100)
822    /// * `Err(String)` - IPC communication error
823    pub async fn get_led_brightness(
824        &self,
825        device_id: &str,
826        zone: Option<LedZone>,
827    ) -> Result<u8, String> {
828        let request = Request::GetLedBrightness {
829            device_id: device_id.to_string(),
830            zone,
831        };
832
833        match ipc_client::send_to_path(&request, &self.socket_path).await {
834            Ok(Response::LedBrightness { brightness, .. }) => Ok(brightness),
835            Ok(Response::Error(msg)) => Err(msg),
836            Ok(_) => Err("Unexpected response".to_string()),
837            Err(e) => Err(format!("Failed to get LED brightness: {}", e)),
838        }
839    }
840
841    /// Set LED pattern for a device
842    ///
843    /// # Arguments
844    ///
845    /// * `device_id` - Device identifier in vendor:product format
846    /// * `pattern` - LED pattern (Static, Breathing, Rainbow, RainbowWave)
847    ///
848    /// # Returns
849    ///
850    /// * `Ok(())` - Pattern set successfully
851    /// * `Err(String)` - IPC communication error
852    pub async fn set_led_pattern(
853        &self,
854        device_id: &str,
855        pattern: LedPattern,
856    ) -> Result<(), String> {
857        let request = Request::SetLedPattern {
858            device_id: device_id.to_string(),
859            pattern,
860        };
861
862        match ipc_client::send_to_path(&request, &self.socket_path).await {
863            Ok(Response::LedPatternSet { .. }) => Ok(()),
864            Ok(Response::Error(msg)) => Err(msg),
865            Ok(_) => Err("Unexpected response".to_string()),
866            Err(e) => Err(format!("Failed to set LED pattern: {}", e)),
867        }
868    }
869
870    /// Get LED pattern for a device
871    ///
872    /// # Arguments
873    ///
874    /// * `device_id` - Device identifier in vendor:product format
875    ///
876    /// # Returns
877    ///
878    /// * `Ok(LedPattern)` - Current LED pattern
879    /// * `Err(String)` - IPC communication error
880    pub async fn get_led_pattern(
881        &self,
882        device_id: &str,
883    ) -> Result<LedPattern, String> {
884        let request = Request::GetLedPattern {
885            device_id: device_id.to_string(),
886        };
887
888        match ipc_client::send_to_path(&request, &self.socket_path).await {
889            Ok(Response::LedPattern { pattern, .. }) => Ok(pattern),
890            Ok(Response::Error(msg)) => Err(msg),
891            Ok(_) => Err("Unexpected response".to_string()),
892            Err(e) => Err(format!("Failed to get LED pattern: {}", e)),
893        }
894    }
895
896    /// Send focus change event to daemon for auto-profile switching
897    ///
898    /// # Arguments
899    ///
900    /// * `app_id` - Application identifier (e.g., "org.alacritty", "firefox")
901    /// * `window_title` - Optional window title (may be empty on some compositors)
902    ///
903    /// # Returns
904    ///
905    /// * `Ok(())` - Focus event acknowledged by daemon
906    /// * `Err(String)` - IPC communication error
907    pub async fn send_focus_change(
908        &self,
909        app_id: String,
910        window_title: Option<String>,
911    ) -> Result<(), String> {
912        let request = Request::FocusChanged { app_id, window_title };
913        match ipc_client::send_to_path(&request, &self.socket_path).await {
914            Ok(Response::FocusChangedAck { .. }) => Ok(()),
915            Ok(Response::Error(e)) => Err(e),
916            Ok(other) => Err(format!("Unexpected response: {:?}", other)),
917            Err(e) => Err(format!("Failed to send focus change: {}", e)),
918        }
919    }
920
921    /// Get analog calibration for a device and layer
922    ///
923    /// # Arguments
924    ///
925    /// * `device_id` - Device identifier (e.g., "32b6:12f7")
926    /// * `layer_id` - Layer ID (0=base, 1, 2, ...)
927    ///
928    /// # Returns
929    ///
930    /// * `Ok(AnalogCalibrationConfig)` - Calibration settings
931    /// * `Err(String)` - IPC communication error
932    pub async fn get_analog_calibration(
933        &self,
934        device_id: &str,
935        layer_id: usize,
936    ) -> Result<AnalogCalibrationConfig, String> {
937        let request = Request::GetAnalogCalibration {
938            device_id: device_id.to_string(),
939            layer_id,
940        };
941
942        match ipc_client::send_to_path(&request, &self.socket_path).await {
943            Ok(Response::AnalogCalibration { calibration: Some(cal), .. }) => Ok(cal),
944            Ok(Response::AnalogCalibration { calibration: None, .. }) => {
945                // Return default config
946                Ok(AnalogCalibrationConfig::default())
947            }
948            Ok(Response::Error(msg)) => Err(msg),
949            Ok(_) => Err("Unexpected response".to_string()),
950            Err(e) => Err(format!("Failed to get analog calibration: {}", e)),
951        }
952    }
953
954    /// Set analog calibration for a device and layer
955    ///
956    /// # Arguments
957    ///
958    /// * `device_id` - Device identifier (e.g., "32b6:12f7")
959    /// * `layer_id` - Layer ID (0=base, 1, 2, ...)
960    /// * `calibration` - New calibration settings
961    ///
962    /// # Returns
963    ///
964    /// * `Ok(())` - Calibration updated successfully
965    /// * `Err(String)` - IPC communication or validation error
966    pub async fn set_analog_calibration(
967        &self,
968        device_id: &str,
969        layer_id: usize,
970        calibration: AnalogCalibrationConfig,
971    ) -> Result<(), String> {
972        let request = Request::SetAnalogCalibration {
973            device_id: device_id.to_string(),
974            layer_id,
975            calibration,
976        };
977
978        match ipc_client::send_to_path(&request, &self.socket_path).await {
979            Ok(Response::AnalogCalibrationAck) => Ok(()),
980            Ok(Response::Error(msg)) => Err(msg),
981            Ok(_) => Err("Unexpected response".to_string()),
982            Err(e) => Err(format!("Failed to set analog calibration: {}", e)),
983        }
984    }
985
986    /// Subscribe to real-time analog input updates for a device
987    ///
988    /// # Arguments
989    ///
990    /// * `device_id` - Device identifier (vendor:product format)
991    ///
992    /// # Returns
993    ///
994    /// * `Ok(())` - Subscription successful
995    /// * `Err(String)` - IPC communication error
996    pub async fn subscribe_analog_input(
997        &self,
998        device_id: &str,
999    ) -> Result<(), String> {
1000        let request = Request::SubscribeAnalogInput {
1001            device_id: device_id.to_string(),
1002        };
1003
1004        match ipc_client::send_to_path(&request, &self.socket_path).await {
1005            Ok(Response::AnalogInputSubscribed) => Ok(()),
1006            Ok(Response::Error(msg)) => Err(msg),
1007            Ok(_) => Err("Unexpected response".to_string()),
1008            Err(e) => Err(format!("Failed to subscribe to analog input: {}", e)),
1009        }
1010    }
1011
1012    /// Unsubscribe from analog input updates
1013    ///
1014    /// # Arguments
1015    ///
1016    /// * `device_id` - Device identifier (vendor:product format)
1017    ///
1018    /// # Returns
1019    ///
1020    /// * `Ok(())` - Unsubscription successful
1021    /// * `Err(String)` - IPC communication error
1022    pub async fn unsubscribe_analog_input(
1023        &self,
1024        device_id: &str,
1025    ) -> Result<(), String> {
1026        let request = Request::UnsubscribeAnalogInput {
1027            device_id: device_id.to_string(),
1028        };
1029
1030        match ipc_client::send_to_path(&request, &self.socket_path).await {
1031            Ok(Response::Ack) => Ok(()),
1032            Ok(Response::Error(msg)) => Err(msg),
1033            Ok(_) => Err("Unexpected response".to_string()),
1034            Err(e) => Err(format!("Failed to unsubscribe from analog input: {}", e)),
1035        }
1036    }
1037
1038    /// Get global macro settings
1039    pub async fn get_macro_settings(&self) -> Result<aethermap_common::MacroSettings, String> {
1040        let request = Request::GetMacroSettings;
1041        match ipc_client::send_to_path(&request, &self.socket_path).await {
1042            Ok(Response::MacroSettings(settings)) => Ok(settings),
1043            Ok(Response::Error(msg)) => Err(msg),
1044            Ok(_) => Err("Unexpected response".to_string()),
1045            Err(e) => Err(format!("Failed to get macro settings: {}", e)),
1046        }
1047    }
1048
1049    /// Set global macro settings
1050    pub async fn set_macro_settings(&self, settings: aethermap_common::MacroSettings) -> Result<(), String> {
1051        let request = Request::SetMacroSettings(settings);
1052        match ipc_client::send_to_path(&request, &self.socket_path).await {
1053            Ok(Response::Ack) => Ok(()),
1054            Ok(Response::Error(msg)) => Err(msg),
1055            Ok(_) => Err("Unexpected response".to_string()),
1056            Err(e) => Err(format!("Failed to set macro settings: {}", e)),
1057        }
1058    }
1059}
1060
1061/// Type alias for the IPC client used in the GUI
1062pub type IpcClient = GuiIpcClient;