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