Skip to main content

aethermap_common/
lib.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::path::PathBuf;
4
5// Re-export common dependencies
6pub use serde;
7pub use bincode;
8pub use tokio;
9pub use tracing;
10
11// IPC client module
12pub mod ipc_client;
13
14/// Device type classification based on input capabilities
15#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
16pub enum DeviceType {
17    /// Keyboard device (has EV_KEY with key codes)
18    Keyboard,
19    /// Mouse or pointing device (has EV_REL or mouse buttons)
20    Mouse,
21    /// Gamepad or joystick (has gamepad buttons, may have EV_ABS)
22    Gamepad,
23    /// Keypad device (many keys + possibly analog stick, e.g., Azeron Cyborg)
24    Keypad,
25    /// Other or unknown input device
26    Other,
27}
28
29impl fmt::Display for DeviceType {
30    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
31        match self {
32            DeviceType::Keyboard => write!(f, "Keyboard"),
33            DeviceType::Mouse => write!(f, "Mouse"),
34            DeviceType::Gamepad => write!(f, "Gamepad"),
35            DeviceType::Keypad => write!(f, "Keypad"),
36            DeviceType::Other => write!(f, "Other"),
37        }
38    }
39}
40
41/// Information about a connected input device
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
43pub struct DeviceInfo {
44    pub name: String,
45    pub path: PathBuf,
46    pub vendor_id: u16,
47    pub product_id: u16,
48    pub phys: String,
49    pub device_type: DeviceType,
50}
51
52impl fmt::Display for DeviceInfo {
53    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
54        write!(f, "{} (VID: {:04X}, PID: {:04X}, Type: {})",
55               self.name, self.vendor_id, self.product_id, self.device_type)
56    }
57}
58
59/// Represents a key combination for macro triggers
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
61pub struct KeyCombo {
62    pub keys: Vec<u16>, // Key codes
63    pub modifiers: Vec<u16>, // Modifier key codes
64}
65
66/// Global hotkey binding for manual profile switching
67///
68/// Defines a keyboard shortcut that triggers profile or layer activation.
69/// Hotkeys are checked at the daemon level before remap processing.
70#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
71pub struct HotkeyBinding {
72    /// Modifier keys (Ctrl, Alt, Shift, Super)
73    ///
74    /// Accepted values: "ctrl", "alt", "shift", "super" (case-insensitive)
75    pub modifiers: Vec<String>,
76
77    /// Trigger key (number 1-9 for profile switching)
78    ///
79    /// Common values: "1"-"9" for profile slots, or any key name like "f1", "esc"
80    pub key: String,
81
82    /// Profile to activate when hotkey pressed
83    pub profile_name: String,
84
85    /// Device to apply to (None = all devices)
86    ///
87    /// If set, only this device_id (vendor:product format) will switch profiles.
88    pub device_id: Option<String>,
89
90    /// Layer to activate (None = profile default)
91    ///
92    /// If set, activates the specified layer after switching profiles.
93    pub layer_id: Option<usize>,
94}
95
96/// Auto-profile switching rule based on window focus
97///
98/// Defines automatic profile switching when specific applications gain focus.
99/// Rules are evaluated in order with first-match-wins semantics.
100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
101pub struct AutoSwitchRule {
102    /// Application identifier to match (e.g., "org.alacritty", "firefox", "*")
103    ///
104    /// - "*" acts as wildcard matching any app (useful for default profile)
105    /// - Can match prefix (e.g., "org.mozilla." matches any Firefox window)
106    /// - Can match suffix (e.g., ".firefox" matches Firefox app)
107    pub app_id: String,
108
109    /// Profile name to activate when this app has focus
110    pub profile_name: String,
111
112    /// Device ID to apply profile to (vendor:product format)
113    ///
114    /// If None, applies to all devices. Use this for per-device auto-switching.
115    pub device_id: Option<String>,
116
117    /// Layer ID to activate (0 = base, 1+ = additional layers)
118    ///
119    /// If None, uses profile's default layer (typically base layer 0).
120    pub layer_id: Option<usize>,
121}
122
123/// Different actions that can be executed by a macro
124#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
125pub enum Action {
126    /// Key press with optional key code
127    KeyPress(u16),
128    /// Key release
129    KeyRelease(u16),
130    /// Delay in milliseconds
131    Delay(u32),
132    /// Execute a command
133    Execute(String),
134    /// Type a string
135    Type(String),
136    /// Mouse button press
137    MousePress(u16),
138    /// Mouse button release
139    MouseRelease(u16),
140    /// Mouse move relative
141    MouseMove(i32, i32),
142    /// Mouse scroll
143    MouseScroll(i32),
144    /// Analog stick movement with normalized value
145    /// axis_code: 61000-61005 (ABS_X, ABS_Y, etc.)
146    /// normalized: -1.0 to 1.0 (device-independent)
147    AnalogMove {
148        axis_code: u16,
149        normalized: f32,
150    },
151}
152
153impl fmt::Display for Action {
154    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
155        match self {
156            Action::KeyPress(code) => write!(f, "KeyPress({})", code),
157            Action::KeyRelease(code) => write!(f, "KeyRelease({})", code),
158            Action::Delay(ms) => write!(f, "Delay({}ms)", ms),
159            Action::Execute(cmd) => write!(f, "Execute({})", cmd),
160            Action::Type(text) => write!(f, "Type({})", text),
161            Action::MousePress(btn) => write!(f, "MousePress({})", btn),
162            Action::MouseRelease(btn) => write!(f, "MouseRelease({})", btn),
163            Action::MouseMove(x, y) => write!(f, "MouseMove({}, {})", x, y),
164            Action::MouseScroll(amount) => write!(f, "MouseScroll({})", amount),
165            Action::AnalogMove { axis_code, normalized } => {
166                let axis_name = match axis_code {
167                    61000 => "X",
168                    61001 => "Y",
169                    61002 => "Z",
170                    61003 => "RX",
171                    61004 => "RY",
172                    61005 => "RZ",
173                    _ => "UNKNOWN",
174                };
175                write!(f, "Analog({}, {}={:.2})", axis_name, axis_code, normalized)
176            }
177        }
178    }
179}
180
181/// Macro definition with name, trigger combo, and actions
182#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
183pub struct MacroSettings {
184    pub latency_offset_ms: u32,
185    pub jitter_pct: f32,
186    pub capture_mouse: bool,
187}
188
189/// Macro definition with name, trigger combo, and actions
190#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
191pub struct MacroEntry {
192    pub name: String,
193    pub trigger: KeyCombo,
194    pub actions: Vec<Action>,
195    pub device_id: Option<String>, // Optional device restriction
196    pub enabled: bool,
197    #[serde(default)]
198    pub humanize: bool,
199    #[serde(default)]
200    pub capture_mouse: bool,
201}
202
203/// Information about a remap profile for listing
204#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
205pub struct RemapProfileInfo {
206    /// Profile name
207    pub name: String,
208    /// Human-readable description
209    pub description: Option<String>,
210    /// Number of remaps in this profile
211    pub remap_count: usize,
212}
213
214/// A single key remapping entry
215#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
216pub struct RemapEntry {
217    /// Source key (the key being remapped)
218    pub from_key: String,
219    /// Target key (what the source key becomes)
220    pub to_key: String,
221}
222
223/// Device capability information
224///
225/// This structure provides detailed capability information for a device,
226/// allowing the GUI to enable/disable relevant UI elements based on actual
227/// device hardware capabilities.
228#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
229pub struct DeviceCapabilities {
230    /// Device has analog stick (absolute X/Y axes)
231    pub has_analog_stick: bool,
232
233    /// Device has hat switch (D-pad with ABS_HAT0X/ABS_HAT0Y)
234    pub has_hat_switch: bool,
235
236    /// Number of joystick buttons (BTN_JOYSTICK range)
237    pub joystick_button_count: usize,
238
239    /// LED zones available (empty if none, populated in Phase 12)
240    pub led_zones: Vec<String>,
241}
242
243/// Layer activation mode
244///
245/// Determines how a layer becomes active and inactive.
246#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
247#[serde(rename_all = "lowercase")]
248pub enum LayerMode {
249    /// Layer is active while a modifier key is held
250    ///
251    /// When the modifier key is released, the layer deactivates.
252    /// This is the typical behavior for "layer shift" keys.
253    Hold,
254
255    /// Layer toggles on/off with each press
256    ///
257    /// First press activates the layer, second press deactivates it.
258    /// This is useful for "layer lock" functionality.
259    Toggle,
260}
261
262impl Default for LayerMode {
263    fn default() -> Self {
264        LayerMode::Hold
265    }
266}
267
268impl fmt::Display for LayerMode {
269    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
270        match self {
271            LayerMode::Hold => write!(f, "hold"),
272            LayerMode::Toggle => write!(f, "toggle"),
273        }
274    }
275}
276
277/// Common layer configuration for IPC
278///
279/// This structure provides the complete layer configuration including LED colors
280/// for IPC communication between daemon and GUI.
281#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
282pub struct CommonLayerConfig {
283    /// Layer ID (0 = base, 1+ = additional layers)
284    #[serde(default)]
285    pub layer_id: usize,
286
287    /// Human-readable layer name (e.g., "Base", "Gaming", "Work")
288    #[serde(default)]
289    pub name: String,
290
291    /// How this layer is activated (hold or toggle)
292    #[serde(default)]
293    pub mode: LayerMode,
294
295    /// LED color for this layer (RGB)
296    #[serde(default = "default_layer_color")]
297    pub led_color: (u8, u8, u8),
298
299    /// LED zone to display layer color
300    #[serde(default)]
301    pub led_zone: Option<LedZone>,
302}
303
304/// Default layer color (blue for layer 1, can be customized)
305fn default_layer_color() -> (u8, u8, u8) {
306    (0, 0, 255)  // Blue
307}
308
309/// Layer configuration information for IPC
310///
311/// This structure provides layer configuration details for GUI display
312/// and modification via IPC.
313#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
314pub struct LayerConfigInfo {
315    /// Layer ID (0 = base, 1+ = additional layers)
316    pub layer_id: usize,
317
318    /// Human-readable layer name (e.g., "Base", "Gaming", "Work")
319    pub name: String,
320
321    /// How this layer is activated (hold or toggle)
322    pub mode: LayerMode,
323
324    /// Number of remappings configured for this layer
325    pub remap_count: usize,
326
327    /// LED color for this layer (RGB)
328    #[serde(default = "default_layer_color")]
329    pub led_color: (u8, u8, u8),
330
331    /// LED zone to display layer color
332    #[serde(default)]
333    pub led_zone: Option<LedZone>,
334}
335
336impl Default for DeviceCapabilities {
337    fn default() -> Self {
338        Self {
339            has_analog_stick: false,
340            has_hat_switch: false,
341            joystick_button_count: 0,
342            led_zones: Vec::new(),
343        }
344    }
345}
346
347/// Analog calibration configuration for IPC
348///
349/// This structure provides a simplified version of AnalogCalibration for IPC
350/// communication between daemon and GUI. It uses string representations for
351/// enum values to avoid circular dependencies.
352#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
353pub struct AnalogCalibrationConfig {
354    /// Deadzone radius (0.0 to 1.0)
355    pub deadzone: f32,
356
357    /// Deadzone shape: "circular" or "square"
358    pub deadzone_shape: String,
359
360    /// Sensitivity curve: "linear", "quadratic", or "exponential"
361    pub sensitivity: String,
362
363    /// Sensitivity multiplier (0.1 to 5.0)
364    pub sensitivity_multiplier: f32,
365
366    /// Minimum output value (typically -32768)
367    pub range_min: i32,
368
369    /// Maximum output value (typically 32767)
370    pub range_max: i32,
371
372    /// Invert X axis
373    pub invert_x: bool,
374
375    /// Invert Y axis
376    pub invert_y: bool,
377
378    /// Exponential curve exponent (only used when sensitivity is "exponential")
379    #[serde(default = "default_exponent")]
380    pub exponent: f32,
381
382    /// Analog output mode (Wasd, Mouse, Camera, etc.)
383    #[serde(default)]
384    pub analog_mode: AnalogMode,
385
386    /// Camera output sub-mode (only used when analog_mode is Camera)
387    #[serde(default)]
388    pub camera_output_mode: Option<CameraOutputMode>,
389}
390
391fn default_exponent() -> f32 {
392    2.0
393}
394
395impl Default for AnalogCalibrationConfig {
396    fn default() -> Self {
397        Self {
398            deadzone: 0.15,
399            deadzone_shape: "circular".to_string(),
400            sensitivity: "linear".to_string(),
401            sensitivity_multiplier: 1.0,
402            range_min: -32768,
403            range_max: 32767,
404            invert_x: false,
405            invert_y: false,
406            exponent: 2.0,
407            analog_mode: AnalogMode::Disabled,
408            camera_output_mode: None,
409        }
410    }
411}
412
413/// IPC Requests from GUI to Daemon
414#[derive(Debug, Clone, Serialize, Deserialize)]
415pub enum Request {
416    /// List all available devices
417    GetDevices,
418
419    /// Set a macro for a device
420    SetMacro {
421        device_path: String,
422        macro_entry: MacroEntry,
423    },
424
425    /// List all configured macros
426    ListMacros,
427
428    /// Delete a macro by name
429    DeleteMacro {
430        name: String,
431    },
432
433    /// Reload configuration from disk
434    ReloadConfig,
435
436    /// Set LED color for a device
437    LedSet {
438        device_path: String,
439        color: (u8, u8, u8), // RGB
440    },
441
442    /// Start recording a macro
443    RecordMacro {
444        device_path: String,
445        name: String,
446        capture_mouse: bool,
447    },
448
449    /// Stop recording a macro
450    StopRecording,
451
452    /// Test a macro execution
453    TestMacro {
454        name: String,
455    },
456
457    /// Get daemon status and version
458    GetStatus,
459
460    /// Save current macros to a profile
461    SaveProfile {
462        name: String,
463    },
464
465    /// Load macros from a profile
466    LoadProfile {
467        name: String,
468    },
469
470    /// List available profiles
471    ListProfiles,
472
473    /// Delete a profile
474    DeleteProfile {
475        name: String,
476    },
477
478    /// Generate an authentication token
479    GenerateToken {
480        client_id: String,
481    },
482
483    /// Authenticate with a token
484    Authenticate {
485        token: String,
486    },
487
488    /// Execute a macro by name
489    ExecuteMacro {
490        name: String,
491    },
492
493    /// Grab a device exclusively for input interception
494    GrabDevice {
495        device_path: String,
496    },
497
498    /// Release exclusive access to a device
499    UngrabDevice {
500        device_path: String,
501    },
502
503    /// Get available profiles for a specific device
504    GetDeviceProfiles {
505        device_id: String,  // vendor:product format
506    },
507
508    /// Activate a remap profile for a device
509    ActivateProfile {
510        device_id: String,    // vendor:product format
511        profile_name: String,
512    },
513
514    /// Deactivate the current remap profile for a device
515    DeactivateProfile {
516        device_id: String,    // vendor:product format
517    },
518
519    /// Get the currently active profile for a device
520    GetActiveProfile {
521        device_id: String,    // vendor:product format
522    },
523
524    /// Query active remap configuration for a device
525    GetActiveRemaps {
526        device_path: String,
527    },
528
529    /// List available remap profiles for a device
530    ListRemapProfiles {
531        device_path: String,
532    },
533
534    /// Activate a remap profile for a device
535    ActivateRemapProfile {
536        device_path: String,
537        profile_name: String,
538    },
539
540    /// Deactivate current remap profile for a device
541    DeactivateRemapProfile {
542        device_path: String,
543    },
544
545    /// Get device capabilities and features
546    GetDeviceCapabilities {
547        device_path: String,
548    },
549
550    /// Get the currently active layer for a device
551    GetActiveLayer {
552        device_id: String,
553    },
554
555    /// Set layer configuration for a device
556    SetLayerConfig {
557        device_id: String,
558        layer_id: usize,
559        config: LayerConfigInfo,
560    },
561
562    /// Activate a layer for a device with specified mode
563    ActivateLayer {
564        device_id: String,
565        layer_id: usize,
566        mode: LayerMode,
567    },
568
569    /// List all configured layers for a device
570    ListLayers {
571        device_id: String,
572    },
573
574    /// Set analog sensitivity for a device
575    SetAnalogSensitivity {
576        device_id: String,
577        sensitivity: f32,  // 0.1-5.0 range
578    },
579
580    /// Get analog sensitivity for a device
581    GetAnalogSensitivity {
582        device_id: String,
583    },
584
585    /// Set analog response curve for a device
586    SetAnalogResponseCurve {
587        device_id: String,
588        curve: String,  // "linear" or "exponential" or "exponential(<exponent>)"
589    },
590
591    /// Get analog response curve for a device
592    GetAnalogResponseCurve {
593        device_id: String,
594    },
595
596    /// Set analog deadzone for a device (both X and Y axes)
597    SetAnalogDeadzone {
598        device_id: String,
599        percentage: u8,  // 0-100
600    },
601
602    /// Get analog deadzone for a device (returns X-axis percentage)
603    GetAnalogDeadzone {
604        device_id: String,
605    },
606
607    /// Set per-axis analog deadzone for a device
608    SetAnalogDeadzoneXY {
609        device_id: String,
610        x_percentage: u8,  // 0-100
611        y_percentage: u8,  // 0-100
612    },
613
614    /// Get per-axis analog deadzone for a device
615    GetAnalogDeadzoneXY {
616        device_id: String,
617    },
618
619    /// Set per-axis outer deadzone (max clamp) for a device
620    SetAnalogOuterDeadzoneXY {
621        device_id: String,
622        x_percentage: u8,  // 0-100
623        y_percentage: u8,  // 0-100
624    },
625
626    /// Get per-axis outer deadzone for a device
627    GetAnalogOuterDeadzoneXY {
628        device_id: String,
629    },
630
631    /// Set D-pad emulation mode for a device
632    SetAnalogDpadMode {
633        device_id: String,
634        mode: String,  // "disabled", "eight_way", "four_way"
635    },
636
637    /// Get D-pad emulation mode for a device
638    GetAnalogDpadMode {
639        device_id: String,
640    },
641
642    /// Set LED color for a specific zone
643    SetLedColor {
644        device_id: String,
645        zone: LedZone,
646        red: u8,
647        green: u8,
648        blue: u8,
649    },
650
651    /// Get LED color for a specific zone
652    GetLedColor {
653        device_id: String,
654        zone: LedZone,
655    },
656
657    /// Get all LED colors for a device
658    GetAllLedColors {
659        device_id: String,
660    },
661
662    /// Set LED brightness for a device (global or per-zone)
663    SetLedBrightness {
664        device_id: String,
665        zone: Option<LedZone>,  // None = global brightness
666        brightness: u8,  // 0-100
667    },
668
669    /// Get LED brightness for a device
670    GetLedBrightness {
671        device_id: String,
672        zone: Option<LedZone>,
673    },
674
675    /// Set LED pattern for a device
676    SetLedPattern {
677        device_id: String,
678        pattern: LedPattern,
679    },
680
681    /// Get LED pattern for a device
682    GetLedPattern {
683        device_id: String,
684    },
685
686    /// Notify daemon that window focus changed (for auto-profile switching)
687    FocusChanged {
688        app_id: String,              // e.g., "org.alacritty", "firefox"
689        window_title: Option<String>, // May be empty on some compositors
690    },
691
692    /// Register a global hotkey binding
693    RegisterHotkey {
694        device_id: String,
695        binding: HotkeyBinding,
696    },
697
698    /// List all registered hotkey bindings for a device
699    ListHotkeys {
700        device_id: String,
701    },
702
703    /// Remove a hotkey binding
704    RemoveHotkey {
705        device_id: String,
706        key: String,
707        modifiers: Vec<String>,
708    },
709
710    /// Set global auto-switch rules for profile switching
711    SetAutoSwitchRules {
712        rules: Vec<AutoSwitchRule>,
713    },
714
715    /// Get all auto-switch rules
716    GetAutoSwitchRules,
717
718    /// Get analog calibration for a device and layer
719    GetAnalogCalibration {
720        device_id: String,
721        layer_id: usize,
722    },
723
724    /// Set analog calibration for a device and layer
725    SetAnalogCalibration {
726        device_id: String,
727        layer_id: usize,
728        calibration: AnalogCalibrationConfig,
729    },
730
731    /// Subscribe to analog input updates for a device
732    SubscribeAnalogInput {
733        device_id: String,
734    },
735
736    /// Unsubscribe from analog input updates
737    UnsubscribeAnalogInput {
738        device_id: String,
739    },
740
741    /// Set global macro timing and jitter settings
742    SetMacroSettings(MacroSettings),
743
744    /// Get current global macro settings
745    GetMacroSettings,
746}
747
748/// Analog output mode for stick behavior
749///
750/// Determines how analog stick input is converted to output events.
751/// Used in LayerConfig to specify per-layer analog mode selection.
752#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
753#[serde(rename_all = "lowercase")]
754pub enum AnalogMode {
755    /// No output (analog disabled)
756    Disabled,
757    /// D-pad mode - 8-way directional keys (arrows)
758    Dpad,
759    /// Gamepad mode - Xbox 360 compatible axis output
760    Gamepad,
761    /// Camera mode - scroll or key repeat
762    Camera,
763    /// Mouse mode - velocity-based cursor movement
764    Mouse,
765    /// WASD mode - directional keys (WASD)
766    Wasd,
767}
768
769impl fmt::Display for AnalogMode {
770    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
771        match self {
772            AnalogMode::Disabled => write!(f, "Disabled"),
773            AnalogMode::Dpad => write!(f, "D-pad (Arrows)"),
774            AnalogMode::Gamepad => write!(f, "Gamepad"),
775            AnalogMode::Camera => write!(f, "Camera"),
776            AnalogMode::Mouse => write!(f, "Mouse"),
777            AnalogMode::Wasd => write!(f, "WASD"),
778        }
779    }
780}
781
782impl Default for AnalogMode {
783    fn default() -> Self {
784        AnalogMode::Disabled
785    }
786}
787
788impl AnalogMode {
789    /// All analog modes for pick_list widget
790    pub const ALL: [AnalogMode; 6] = [
791        AnalogMode::Disabled,
792        AnalogMode::Dpad,
793        AnalogMode::Gamepad,
794        AnalogMode::Wasd,
795        AnalogMode::Mouse,
796        AnalogMode::Camera,
797    ];
798}
799
800/// Camera mode output type
801///
802/// Controls how analog stick input is converted in Camera mode.
803/// Used in LayerConfig to specify camera output behavior per layer.
804#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
805#[serde(rename_all = "lowercase")]
806pub enum CameraOutputMode {
807    /// Emit REL_WHEEL events for scrolling
808    Scroll,
809    /// Emit key repeat events (PageUp/PageDown/arrows)
810    Keys,
811}
812
813impl fmt::Display for CameraOutputMode {
814    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
815        match self {
816            CameraOutputMode::Scroll => write!(f, "Scroll"),
817            CameraOutputMode::Keys => write!(f, "Key Repeat"),
818        }
819    }
820}
821
822impl Default for CameraOutputMode {
823    fn default() -> Self {
824        CameraOutputMode::Scroll
825    }
826}
827
828impl CameraOutputMode {
829    /// All camera output modes for pick_list widget
830    pub const ALL: [CameraOutputMode; 2] = [
831        CameraOutputMode::Scroll,
832        CameraOutputMode::Keys,
833    ];
834}
835
836/// LED pattern types for visual effects
837#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
838pub enum LedPattern {
839    /// Static solid colors (no animation)
840    Static,
841    /// Breathing pattern - fades colors in/out
842    Breathing,
843    /// Rainbow pattern - cycles through colors
844    Rainbow,
845    /// Rainbow wave - wave effect across zones
846    RainbowWave,
847}
848
849/// LED zones on devices with configurable RGB lighting
850#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
851pub enum LedZone {
852    /// Side LED (single LED on Azeron Cyborg 2)
853    Side,
854    /// Logo LED (top of device) - legacy, may map to Side on some devices
855    Logo,
856    /// Main key cluster LEDs - legacy, may map to Side on some devices
857    Keys,
858    /// Thumbstick or analog stick LED ring - legacy, may map to Side on some devices
859    Thumbstick,
860    /// All zones at once
861    All,
862    /// Global setting
863    Global,
864}
865
866/// Status information structure
867#[derive(Debug, Clone, Serialize, Deserialize)]
868pub struct StatusInfo {
869    pub version: String,
870    pub uptime_seconds: u64,
871    pub devices_count: usize,
872    pub macros_count: usize,
873}
874
875/// IPC Responses from Daemon to GUI
876#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
877pub enum Response {
878    /// List of discovered devices
879    Devices(Vec<DeviceInfo>),
880
881    /// List of configured macros
882    Macros(Vec<MacroEntry>),
883
884    /// Acknowledgment of successful operation
885    Ack,
886
887    /// Status information
888    Status {
889        version: String,
890        uptime_seconds: u64,
891        devices_count: usize,
892        macros_count: usize,
893    },
894
895    /// Notification that recording has started
896    RecordingStarted {
897        device_path: String,
898        name: String,
899    },
900
901    /// Notification that recording has stopped
902    RecordingStopped {
903        macro_entry: MacroEntry,
904    },
905
906    /// List of available profiles
907    Profiles(Vec<String>),
908
909    /// Profile load confirmation
910    ProfileLoaded {
911        name: String,
912        macros_count: usize,
913    },
914
915    /// Profile save confirmation
916    ProfileSaved {
917        name: String,
918        macros_count: usize,
919    },
920
921    /// Error response
922    Error(String),
923
924    /// Authentication token
925    Token(String),
926
927    /// Authentication successful
928    Authenticated,
929
930    /// List of available profiles for a device
931    DeviceProfiles {
932        device_id: String,
933        profiles: Vec<String>,
934    },
935
936    /// Profile activation confirmation
937    ProfileActivated {
938        device_id: String,
939        profile_name: String,
940    },
941
942    /// Profile deactivation confirmation
943    ProfileDeactivated {
944        device_id: String,
945    },
946
947    /// Current active profile for a device
948    ActiveProfile {
949        device_id: String,
950        profile_name: Option<String>,
951    },
952
953    /// Active remap configuration
954    ActiveRemaps {
955        device_path: String,
956        profile_name: Option<String>,
957        remaps: Vec<RemapEntry>,
958    },
959
960    /// List of available profiles
961    RemapProfiles {
962        device_path: String,
963        profiles: Vec<RemapProfileInfo>,
964    },
965
966    /// Remap profile activation confirmation
967    RemapProfileActivated {
968        device_path: String,
969        profile_name: String,
970    },
971
972    /// Remap profile deactivation confirmation
973    RemapProfileDeactivated {
974        device_path: String,
975    },
976
977    /// Device capability information
978    DeviceCapabilities {
979        device_path: String,
980        capabilities: DeviceCapabilities,
981    },
982
983    /// Current active layer for a device
984    ActiveLayer {
985        device_id: String,
986        layer_id: usize,
987        layer_name: String,
988    },
989
990    /// Layer configuration confirmation
991    LayerConfigured {
992        device_id: String,
993        layer_id: usize,
994    },
995
996    /// List of configured layers for a device
997    LayerList {
998        device_id: String,
999        layers: Vec<LayerConfigInfo>,
1000    },
1001
1002    /// Analog sensitivity set confirmation
1003    AnalogSensitivitySet {
1004        device_id: String,
1005        sensitivity: f32,
1006    },
1007
1008    /// Analog sensitivity response
1009    AnalogSensitivity {
1010        device_id: String,
1011        sensitivity: f32,
1012    },
1013
1014    /// Analog response curve set confirmation
1015    AnalogResponseCurveSet {
1016        device_id: String,
1017        curve: String,
1018    },
1019
1020    /// Analog response curve response
1021    AnalogResponseCurve {
1022        device_id: String,
1023        curve: String,
1024    },
1025
1026    /// Analog deadzone set confirmation
1027    AnalogDeadzoneSet {
1028        device_id: String,
1029        percentage: u8,
1030    },
1031
1032    /// Analog deadzone response
1033    AnalogDeadzone {
1034        device_id: String,
1035        percentage: u8,
1036    },
1037
1038    /// Per-axis deadzone set confirmation
1039    AnalogDeadzoneXYSet {
1040        device_id: String,
1041        x_percentage: u8,
1042        y_percentage: u8,
1043    },
1044
1045    /// Per-axis deadzone response
1046    AnalogDeadzoneXY {
1047        device_id: String,
1048        x_percentage: u8,
1049        y_percentage: u8,
1050    },
1051
1052    /// Per-axis outer deadzone set confirmation
1053    AnalogOuterDeadzoneXYSet {
1054        device_id: String,
1055        x_percentage: u8,
1056        y_percentage: u8,
1057    },
1058
1059    /// Per-axis outer deadzone response
1060    AnalogOuterDeadzoneXY {
1061        device_id: String,
1062        x_percentage: u8,
1063        y_percentage: u8,
1064    },
1065
1066    /// D-pad mode set confirmation
1067    AnalogDpadModeSet {
1068        device_id: String,
1069        mode: String,
1070    },
1071
1072    /// D-pad mode response
1073    AnalogDpadMode {
1074        device_id: String,
1075        mode: String,
1076    },
1077
1078    /// LED color set confirmation
1079    LedColorSet {
1080        device_id: String,
1081        zone: LedZone,
1082        color: (u8, u8, u8),
1083    },
1084
1085    /// LED color response
1086    LedColor {
1087        device_id: String,
1088        zone: LedZone,
1089        color: Option<(u8, u8, u8)>,
1090    },
1091
1092    /// All LED colors response
1093    AllLedColors {
1094        device_id: String,
1095        colors: std::collections::HashMap<LedZone, (u8, u8, u8)>,
1096    },
1097
1098    /// LED brightness set confirmation
1099    LedBrightnessSet {
1100        device_id: String,
1101        zone: Option<LedZone>,
1102        brightness: u8,
1103    },
1104
1105    /// LED brightness response
1106    LedBrightness {
1107        device_id: String,
1108        zone: Option<LedZone>,
1109        brightness: u8,
1110    },
1111
1112    /// LED pattern set confirmation
1113    LedPatternSet {
1114        device_id: String,
1115        pattern: LedPattern,
1116    },
1117
1118    /// LED pattern response
1119    LedPattern {
1120        device_id: String,
1121        pattern: LedPattern,
1122    },
1123
1124    /// Acknowledgment of focus change event
1125    FocusChangedAck {
1126        app_id: String,
1127    },
1128
1129    /// Hotkey registration successful
1130    HotkeyRegistered {
1131        device_id: String,
1132        key: String,
1133        modifiers: Vec<String>,
1134    },
1135
1136    /// List of hotkey bindings for a device
1137    HotkeyList {
1138        device_id: String,
1139        bindings: Vec<HotkeyBinding>,
1140    },
1141
1142    /// Hotkey removal successful
1143    HotkeyRemoved {
1144        device_id: String,
1145        key: String,
1146        modifiers: Vec<String>,
1147    },
1148
1149    /// Auto-switch rules acknowledgment
1150    AutoSwitchRulesAck,
1151
1152    /// Auto-switch rules response
1153    AutoSwitchRules {
1154        rules: Vec<AutoSwitchRule>,
1155    },
1156
1157    /// Analog calibration response
1158    AnalogCalibration {
1159        device_id: String,
1160        layer_id: usize,
1161        calibration: Option<AnalogCalibrationConfig>,
1162    },
1163
1164    /// Analog calibration acknowledgment
1165    AnalogCalibrationAck,
1166
1167    /// Analog input update (streamed to subscribers)
1168    AnalogInputUpdate {
1169        device_id: String,
1170        axis_x: f32,  // -1.0 to 1.0
1171        axis_y: f32,  // -1.0 to 1.0
1172    },
1173
1174    /// Analog subscription acknowledgment
1175    AnalogInputSubscribed,
1176
1177    /// Global macro settings response
1178    MacroSettings(MacroSettings),
1179}
1180
1181/// Profile structure for organizing macros
1182#[derive(Debug, Clone, Serialize, Deserialize)]
1183pub struct Profile {
1184    pub name: String,
1185    pub macros: std::collections::HashMap<String, MacroEntry>,
1186}
1187
1188/// Serialization helpers for the IPC protocol
1189pub fn serialize<T: Serialize>(msg: &T) -> Vec<u8> {
1190    bincode::serialize(msg).unwrap_or_else(|e| {
1191        tracing::error!("Failed to serialize message: {:?}", e);
1192        Vec::new()
1193    })
1194}
1195
1196pub fn deserialize<'a, T: Deserialize<'a>>(bytes: &'a [u8]) -> Result<T, bincode::Error> {
1197    bincode::deserialize(bytes)
1198}
1199
1200#[cfg(test)]
1201mod tests {
1202    use super::*;
1203
1204    #[test]
1205    fn test_macro_settings_serialization() {
1206        let settings = MacroSettings {
1207            latency_offset_ms: 10,
1208            jitter_pct: 0.05,
1209            capture_mouse: false,
1210        };
1211
1212        let serialized = serialize(&settings);
1213        let deserialized: MacroSettings = deserialize(&serialized).unwrap();
1214        assert_eq!(deserialized, settings);
1215    }
1216
1217    #[test]
1218    fn test_ipc_serialization() {
1219        let request = Request::GetDevices;
1220        let serialized = serialize(&request);
1221        let deserialized: Request = deserialize(&serialized).unwrap();
1222        assert!(matches!(deserialized, Request::GetDevices));
1223    }
1224
1225    #[test]
1226    fn test_macro_entry_serialization() {
1227        let macro_entry = MacroEntry {
1228            name: "Test Macro".to_string(),
1229            trigger: KeyCombo {
1230                keys: vec![30, 40], // A and D keys
1231                modifiers: vec![29], // Ctrl key
1232            },
1233            actions: vec![
1234                Action::KeyPress(30),
1235                Action::Delay(100),
1236                Action::KeyRelease(30),
1237            ],
1238            device_id: Some("test_device".to_string()),
1239            enabled: true,
1240            humanize: false,
1241            capture_mouse: false,
1242        };
1243
1244        let serialized = serialize(&macro_entry);
1245        let deserialized: MacroEntry = deserialize(&serialized).unwrap();
1246        assert_eq!(deserialized.name, "Test Macro");
1247        assert_eq!(deserialized.trigger.keys, vec![30, 40]);
1248    }
1249
1250    #[test]
1251    fn test_profile_ipc_serialization() {
1252        let request = Request::GetDeviceProfiles {
1253            device_id: "1532:0220".to_string(),
1254        };
1255        let serialized = serialize(&request);
1256        let deserialized: Request = deserialize(&serialized).unwrap();
1257        assert!(matches!(deserialized, Request::GetDeviceProfiles { .. }));
1258
1259        let response = Response::DeviceProfiles {
1260            device_id: "1532:0220".to_string(),
1261            profiles: vec!["gaming".to_string(), "work".to_string()],
1262        };
1263        let serialized = serialize(&response);
1264        let deserialized: Response = deserialize(&serialized).unwrap();
1265        assert!(matches!(deserialized, Response::DeviceProfiles { .. }));
1266    }
1267
1268    #[test]
1269    fn test_analog_deadzone_ipc_serialization() {
1270        // Test SetAnalogDeadzone request
1271        let request = Request::SetAnalogDeadzone {
1272            device_id: "1532:0220".to_string(),
1273            percentage: 50,
1274        };
1275        let serialized = serialize(&request);
1276        let deserialized: Request = deserialize(&serialized).unwrap();
1277        assert!(matches!(deserialized, Request::SetAnalogDeadzone { .. }));
1278
1279        // Test GetAnalogDeadzone request
1280        let request = Request::GetAnalogDeadzone {
1281            device_id: "1532:0220".to_string(),
1282        };
1283        let serialized = serialize(&request);
1284        let deserialized: Request = deserialize(&serialized).unwrap();
1285        assert!(matches!(deserialized, Request::GetAnalogDeadzone { .. }));
1286
1287        // Test AnalogDeadzoneSet response
1288        let response = Response::AnalogDeadzoneSet {
1289            device_id: "1532:0220".to_string(),
1290            percentage: 50,
1291        };
1292        let serialized = serialize(&response);
1293        let deserialized: Response = deserialize(&serialized).unwrap();
1294        assert_eq!(deserialized, response);
1295
1296        // Test AnalogDeadzone response
1297        let response = Response::AnalogDeadzone {
1298            device_id: "1532:0220".to_string(),
1299            percentage: 43,
1300        };
1301        let serialized = serialize(&response);
1302        let deserialized: Response = deserialize(&serialized).unwrap();
1303        assert_eq!(deserialized, response);
1304    }
1305
1306    #[test]
1307    fn test_mouse_action_serialization() {
1308        // Test individual mouse action variants serialize correctly
1309        let actions = vec![
1310            Action::MousePress(0x110), // BTN_LEFT
1311            Action::MouseRelease(0x110),
1312            Action::MouseMove(10, 20),
1313            Action::MouseScroll(5),
1314        ];
1315
1316        for action in &actions {
1317            let serialized = serialize(action);
1318            let deserialized: Action = deserialize(&serialized).unwrap();
1319            assert_eq!(action, &deserialized);
1320        }
1321
1322        // Test macro entry with mixed keyboard and mouse actions
1323        let macro_entry = MacroEntry {
1324            name: "Mixed Macro".to_string(),
1325            trigger: KeyCombo {
1326                keys: vec![30],
1327                modifiers: vec![],
1328            },
1329            actions: vec![
1330                Action::KeyPress(30),
1331                Action::MousePress(0x110),
1332                Action::Delay(50),
1333                Action::MouseRelease(0x110),
1334                Action::MouseMove(100, 200),
1335                Action::MouseScroll(3),
1336                Action::KeyRelease(30),
1337            ],
1338            device_id: Some("1532:0220".to_string()),
1339            enabled: true,
1340            humanize: false,
1341            capture_mouse: false,
1342        };
1343
1344        let serialized = serialize(&macro_entry);
1345        let deserialized: MacroEntry = deserialize(&serialized).unwrap();
1346        assert_eq!(deserialized.name, "Mixed Macro");
1347        assert_eq!(deserialized.actions.len(), 7);
1348
1349        // Verify each action type survived round-trip
1350        assert!(matches!(deserialized.actions[0], Action::KeyPress(30)));
1351        assert!(matches!(deserialized.actions[1], Action::MousePress(0x110)));
1352        assert!(matches!(deserialized.actions[2], Action::Delay(50)));
1353        assert!(matches!(deserialized.actions[3], Action::MouseRelease(0x110)));
1354        assert!(matches!(deserialized.actions[4], Action::MouseMove(100, 200)));
1355        assert!(matches!(deserialized.actions[5], Action::MouseScroll(3)));
1356        assert!(matches!(deserialized.actions[6], Action::KeyRelease(30)));
1357    }
1358
1359    #[test]
1360    fn test_device_capabilities_serialization() {
1361        let caps = DeviceCapabilities {
1362            has_analog_stick: true,
1363            has_hat_switch: true,
1364            joystick_button_count: 26,
1365            led_zones: vec!["logo".to_string(), "keys".to_string()],
1366        };
1367        let serialized = serialize(&caps);
1368        let deserialized: DeviceCapabilities = deserialize(&serialized).unwrap();
1369        assert_eq!(deserialized.has_analog_stick, true);
1370        assert_eq!(deserialized.has_hat_switch, true);
1371        assert_eq!(deserialized.joystick_button_count, 26);
1372        assert_eq!(deserialized.led_zones.len(), 2);
1373    }
1374
1375    #[test]
1376    fn test_get_device_capabilities_request() {
1377        let request = Request::GetDeviceCapabilities {
1378            device_path: "/dev/input/event0".to_string(),
1379        };
1380        let serialized = serialize(&request);
1381        let deserialized: Request = deserialize(&serialized).unwrap();
1382        assert!(matches!(deserialized, Request::GetDeviceCapabilities { .. }));
1383        if let Request::GetDeviceCapabilities { device_path } = deserialized {
1384            assert_eq!(device_path, "/dev/input/event0");
1385        }
1386    }
1387
1388    #[test]
1389    fn test_device_capabilities_response() {
1390        let response = Response::DeviceCapabilities {
1391            device_path: "/dev/input/event0".to_string(),
1392            capabilities: DeviceCapabilities {
1393                has_analog_stick: true,
1394                has_hat_switch: true,
1395                joystick_button_count: 26,
1396                led_zones: vec![],
1397            },
1398        };
1399        let serialized = serialize(&response);
1400        let deserialized: Response = deserialize(&serialized).unwrap();
1401        assert!(matches!(deserialized, Response::DeviceCapabilities { .. }));
1402    }
1403
1404    #[test]
1405    fn test_layer_mode_serialization() {
1406        // Test Hold variant
1407        let hold_mode = LayerMode::Hold;
1408        let serialized = serialize(&hold_mode);
1409        let deserialized: LayerMode = deserialize(&serialized).unwrap();
1410        assert_eq!(deserialized, LayerMode::Hold);
1411
1412        // Test Toggle variant
1413        let toggle_mode = LayerMode::Toggle;
1414        let serialized = serialize(&toggle_mode);
1415        let deserialized: LayerMode = deserialize(&serialized).unwrap();
1416        assert_eq!(deserialized, LayerMode::Toggle);
1417    }
1418
1419    #[test]
1420    fn test_layer_config_info_serialization() {
1421        let config = LayerConfigInfo {
1422            layer_id: 1,
1423            name: "Gaming".to_string(),
1424            mode: LayerMode::Toggle,
1425            remap_count: 5,
1426            led_color: (0, 0, 255),  // Default blue
1427            led_zone: None,
1428        };
1429
1430        let serialized = serialize(&config);
1431        let deserialized: LayerConfigInfo = deserialize(&serialized).unwrap();
1432
1433        assert_eq!(deserialized.layer_id, 1);
1434        assert_eq!(deserialized.name, "Gaming");
1435        assert_eq!(deserialized.mode, LayerMode::Toggle);
1436        assert_eq!(deserialized.remap_count, 5);
1437    }
1438
1439    #[test]
1440    fn test_get_active_layer_request() {
1441        let request = Request::GetActiveLayer {
1442            device_id: "1532:0220".to_string(),
1443        };
1444
1445        let serialized = serialize(&request);
1446        let deserialized: Request = deserialize(&serialized).unwrap();
1447
1448        assert!(matches!(deserialized, Request::GetActiveLayer { .. }));
1449        if let Request::GetActiveLayer { device_id } = deserialized {
1450            assert_eq!(device_id, "1532:0220");
1451        }
1452    }
1453
1454    #[test]
1455    fn test_active_layer_response() {
1456        let response = Response::ActiveLayer {
1457            device_id: "1532:0220".to_string(),
1458            layer_id: 2,
1459            layer_name: "Gaming".to_string(),
1460        };
1461
1462        let serialized = serialize(&response);
1463        let deserialized: Response = deserialize(&serialized).unwrap();
1464
1465        assert!(matches!(deserialized, Response::ActiveLayer { .. }));
1466        if let Response::ActiveLayer { device_id, layer_id, layer_name } = deserialized {
1467            assert_eq!(device_id, "1532:0220");
1468            assert_eq!(layer_id, 2);
1469            assert_eq!(layer_name, "Gaming");
1470        }
1471    }
1472
1473    #[test]
1474    fn test_set_layer_config_request() {
1475        let request = Request::SetLayerConfig {
1476            device_id: "1532:0220".to_string(),
1477            layer_id: 1,
1478            config: LayerConfigInfo {
1479                layer_id: 1,
1480                name: "Work".to_string(),
1481                mode: LayerMode::Hold,
1482                remap_count: 0,
1483                led_color: (0, 0, 255),
1484                led_zone: None,
1485            },
1486        };
1487
1488        let serialized = serialize(&request);
1489        let deserialized: Request = deserialize(&serialized).unwrap();
1490
1491        assert!(matches!(deserialized, Request::SetLayerConfig { .. }));
1492        if let Request::SetLayerConfig { device_id, layer_id, config } = deserialized {
1493            assert_eq!(device_id, "1532:0220");
1494            assert_eq!(layer_id, 1);
1495            assert_eq!(config.name, "Work");
1496            assert_eq!(config.mode, LayerMode::Hold);
1497        }
1498    }
1499
1500    #[test]
1501    fn test_activate_layer_request() {
1502        let request = Request::ActivateLayer {
1503            device_id: "1532:0220".to_string(),
1504            layer_id: 2,
1505            mode: LayerMode::Toggle,
1506        };
1507
1508        let serialized = serialize(&request);
1509        let deserialized: Request = deserialize(&serialized).unwrap();
1510
1511        assert!(matches!(deserialized, Request::ActivateLayer { .. }));
1512        if let Request::ActivateLayer { device_id, layer_id, mode } = deserialized {
1513            assert_eq!(device_id, "1532:0220");
1514            assert_eq!(layer_id, 2);
1515            assert_eq!(mode, LayerMode::Toggle);
1516        }
1517    }
1518
1519    #[test]
1520    fn test_list_layers_request() {
1521        let request = Request::ListLayers {
1522            device_id: "1532:0220".to_string(),
1523        };
1524
1525        let serialized = serialize(&request);
1526        let deserialized: Request = deserialize(&serialized).unwrap();
1527
1528        assert!(matches!(deserialized, Request::ListLayers { .. }));
1529        if let Request::ListLayers { device_id } = deserialized {
1530            assert_eq!(device_id, "1532:0220");
1531        }
1532    }
1533
1534    #[test]
1535    fn test_layer_list_response() {
1536        let response = Response::LayerList {
1537            device_id: "1532:0220".to_string(),
1538            layers: vec![
1539                LayerConfigInfo {
1540                    layer_id: 0,
1541                    name: "Base".to_string(),
1542                    mode: LayerMode::Hold,
1543                    remap_count: 10,
1544                    led_color: (0, 255, 0),
1545                    led_zone: None,
1546                },
1547                LayerConfigInfo {
1548                    layer_id: 1,
1549                    name: "Gaming".to_string(),
1550                    mode: LayerMode::Toggle,
1551                    remap_count: 5,
1552                    led_color: (255, 0, 0),
1553                    led_zone: Some(LedZone::Side),
1554                },
1555            ],
1556        };
1557
1558        let serialized = serialize(&response);
1559        let deserialized: Response = deserialize(&serialized).unwrap();
1560
1561        assert!(matches!(deserialized, Response::LayerList { .. }));
1562        if let Response::LayerList { device_id, layers } = deserialized {
1563            assert_eq!(device_id, "1532:0220");
1564            assert_eq!(layers.len(), 2);
1565            assert_eq!(layers[0].name, "Base");
1566            assert_eq!(layers[1].name, "Gaming");
1567        }
1568    }
1569
1570    #[test]
1571    fn test_layer_configured_response() {
1572        let response = Response::LayerConfigured {
1573            device_id: "1532:0220".to_string(),
1574            layer_id: 1,
1575        };
1576
1577        let serialized = serialize(&response);
1578        let deserialized: Response = deserialize(&serialized).unwrap();
1579
1580        assert!(matches!(deserialized, Response::LayerConfigured { .. }));
1581        if let Response::LayerConfigured { device_id, layer_id } = deserialized {
1582            assert_eq!(device_id, "1532:0220");
1583            assert_eq!(layer_id, 1);
1584        }
1585    }
1586
1587    #[test]
1588    fn test_focus_changed_request_serialization() {
1589        // Test with full app_id and window title
1590        let request = Request::FocusChanged {
1591            app_id: "org.alacritty".to_string(),
1592            window_title: Some("Alacritty: ~/Projects".to_string()),
1593        };
1594
1595        let serialized = serialize(&request);
1596        let deserialized: Request = deserialize(&serialized).unwrap();
1597
1598        assert!(matches!(deserialized, Request::FocusChanged { .. }));
1599        if let Request::FocusChanged { app_id, window_title } = deserialized {
1600            assert_eq!(app_id, "org.alacritty");
1601            assert_eq!(window_title, Some("Alacritty: ~/Projects".to_string()));
1602        }
1603
1604        // Test with flatpak-style app_id and no title
1605        let request = Request::FocusChanged {
1606            app_id: "org.mozilla.firefox".to_string(),
1607            window_title: None,
1608        };
1609
1610        let serialized = serialize(&request);
1611        let deserialized: Request = deserialize(&serialized).unwrap();
1612
1613        assert!(matches!(deserialized, Request::FocusChanged { .. }));
1614        if let Request::FocusChanged { app_id, window_title } = deserialized {
1615            assert_eq!(app_id, "org.mozilla.firefox");
1616            assert_eq!(window_title, None);
1617        }
1618
1619        // Test with simple app_id (suffix format)
1620        let request = Request::FocusChanged {
1621            app_id: "firefox".to_string(),
1622            window_title: Some("Mozilla Firefox".to_string()),
1623        };
1624
1625        let serialized = serialize(&request);
1626        let deserialized: Request = deserialize(&serialized).unwrap();
1627
1628        assert!(matches!(deserialized, Request::FocusChanged { .. }));
1629        if let Request::FocusChanged { app_id, window_title } = deserialized {
1630            assert_eq!(app_id, "firefox");
1631            assert_eq!(window_title, Some("Mozilla Firefox".to_string()));
1632        }
1633    }
1634
1635    #[test]
1636    fn test_focus_changed_ack_response_serialization() {
1637        let response = Response::FocusChangedAck {
1638            app_id: "org.alacritty".to_string(),
1639        };
1640
1641        let serialized = serialize(&response);
1642        let deserialized: Response = deserialize(&serialized).unwrap();
1643
1644        assert!(matches!(deserialized, Response::FocusChangedAck { .. }));
1645        if let Response::FocusChangedAck { ref app_id } = deserialized {
1646            assert_eq!(app_id, "org.alacritty");
1647        }
1648
1649        // Test round-trip equality
1650        assert_eq!(deserialized, response);
1651    }
1652
1653    #[test]
1654    fn test_analog_calibration_config_with_mode() {
1655        let config = AnalogCalibrationConfig {
1656            deadzone: 0.2,
1657            deadzone_shape: "circular".to_string(),
1658            sensitivity: "linear".to_string(),
1659            sensitivity_multiplier: 1.5,
1660            range_min: -32768,
1661            range_max: 32767,
1662            invert_x: false,
1663            invert_y: true,
1664            exponent: 2.0,
1665            analog_mode: AnalogMode::Wasd,
1666            camera_output_mode: None,
1667        };
1668
1669        let serialized = serialize(&config);
1670        let deserialized: AnalogCalibrationConfig = deserialize(&serialized).unwrap();
1671
1672        assert_eq!(deserialized.analog_mode, AnalogMode::Wasd);
1673        assert_eq!(deserialized.camera_output_mode, None);
1674
1675        // Test Camera mode with output mode
1676        let camera_config = AnalogCalibrationConfig {
1677            analog_mode: AnalogMode::Camera,
1678            camera_output_mode: Some(CameraOutputMode::Keys),
1679            ..config
1680        };
1681
1682        let serialized = serialize(&camera_config);
1683        let deserialized: AnalogCalibrationConfig = deserialize(&serialized).unwrap();
1684
1685        assert_eq!(deserialized.analog_mode, AnalogMode::Camera);
1686        assert_eq!(deserialized.camera_output_mode, Some(CameraOutputMode::Keys));
1687    }
1688}