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