use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::PathBuf;
pub use serde;
pub use bincode;
pub use tokio;
pub use tracing;
pub mod ipc_client;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum DeviceType {
Keyboard,
Mouse,
Gamepad,
Keypad,
Other,
}
impl fmt::Display for DeviceType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
DeviceType::Keyboard => write!(f, "Keyboard"),
DeviceType::Mouse => write!(f, "Mouse"),
DeviceType::Gamepad => write!(f, "Gamepad"),
DeviceType::Keypad => write!(f, "Keypad"),
DeviceType::Other => write!(f, "Other"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DeviceInfo {
pub name: String,
pub path: PathBuf,
pub vendor_id: u16,
pub product_id: u16,
pub phys: String,
pub device_type: DeviceType,
}
impl fmt::Display for DeviceInfo {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} (VID: {:04X}, PID: {:04X}, Type: {})",
self.name, self.vendor_id, self.product_id, self.device_type)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct KeyCombo {
pub keys: Vec<u16>, pub modifiers: Vec<u16>, }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct HotkeyBinding {
pub modifiers: Vec<String>,
pub key: String,
pub profile_name: String,
pub device_id: Option<String>,
pub layer_id: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AutoSwitchRule {
pub app_id: String,
pub profile_name: String,
pub device_id: Option<String>,
pub layer_id: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Action {
KeyPress(u16),
KeyRelease(u16),
Delay(u32),
Execute(String),
Type(String),
MousePress(u16),
MouseRelease(u16),
MouseMove(i32, i32),
MouseScroll(i32),
AnalogMove {
axis_code: u16,
normalized: f32,
},
}
impl fmt::Display for Action {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Action::KeyPress(code) => write!(f, "KeyPress({})", code),
Action::KeyRelease(code) => write!(f, "KeyRelease({})", code),
Action::Delay(ms) => write!(f, "Delay({}ms)", ms),
Action::Execute(cmd) => write!(f, "Execute({})", cmd),
Action::Type(text) => write!(f, "Type({})", text),
Action::MousePress(btn) => write!(f, "MousePress({})", btn),
Action::MouseRelease(btn) => write!(f, "MouseRelease({})", btn),
Action::MouseMove(x, y) => write!(f, "MouseMove({}, {})", x, y),
Action::MouseScroll(amount) => write!(f, "MouseScroll({})", amount),
Action::AnalogMove { axis_code, normalized } => {
let axis_name = match axis_code {
61000 => "X",
61001 => "Y",
61002 => "Z",
61003 => "RX",
61004 => "RY",
61005 => "RZ",
_ => "UNKNOWN",
};
write!(f, "Analog({}, {}={:.2})", axis_name, axis_code, normalized)
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MacroSettings {
pub latency_offset_ms: u32,
pub jitter_pct: f32,
pub capture_mouse: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MacroEntry {
pub name: String,
pub trigger: KeyCombo,
pub actions: Vec<Action>,
pub device_id: Option<String>, pub enabled: bool,
#[serde(default)]
pub humanize: bool,
#[serde(default)]
pub capture_mouse: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RemapProfileInfo {
pub name: String,
pub description: Option<String>,
pub remap_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RemapEntry {
pub from_key: String,
pub to_key: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DeviceCapabilities {
pub has_analog_stick: bool,
pub has_hat_switch: bool,
pub joystick_button_count: usize,
pub led_zones: Vec<String>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum LayerMode {
Hold,
Toggle,
}
impl Default for LayerMode {
fn default() -> Self {
LayerMode::Hold
}
}
impl fmt::Display for LayerMode {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
LayerMode::Hold => write!(f, "hold"),
LayerMode::Toggle => write!(f, "toggle"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CommonLayerConfig {
#[serde(default)]
pub layer_id: usize,
#[serde(default)]
pub name: String,
#[serde(default)]
pub mode: LayerMode,
#[serde(default = "default_layer_color")]
pub led_color: (u8, u8, u8),
#[serde(default)]
pub led_zone: Option<LedZone>,
}
fn default_layer_color() -> (u8, u8, u8) {
(0, 0, 255) }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct LayerConfigInfo {
pub layer_id: usize,
pub name: String,
pub mode: LayerMode,
pub remap_count: usize,
#[serde(default = "default_layer_color")]
pub led_color: (u8, u8, u8),
#[serde(default)]
pub led_zone: Option<LedZone>,
}
impl Default for DeviceCapabilities {
fn default() -> Self {
Self {
has_analog_stick: false,
has_hat_switch: false,
joystick_button_count: 0,
led_zones: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AnalogCalibrationConfig {
pub deadzone: f32,
pub deadzone_shape: String,
pub sensitivity: String,
pub sensitivity_multiplier: f32,
pub range_min: i32,
pub range_max: i32,
pub invert_x: bool,
pub invert_y: bool,
#[serde(default = "default_exponent")]
pub exponent: f32,
#[serde(default)]
pub analog_mode: AnalogMode,
#[serde(default)]
pub camera_output_mode: Option<CameraOutputMode>,
}
fn default_exponent() -> f32 {
2.0
}
impl Default for AnalogCalibrationConfig {
fn default() -> Self {
Self {
deadzone: 0.15,
deadzone_shape: "circular".to_string(),
sensitivity: "linear".to_string(),
sensitivity_multiplier: 1.0,
range_min: -32768,
range_max: 32767,
invert_x: false,
invert_y: false,
exponent: 2.0,
analog_mode: AnalogMode::Disabled,
camera_output_mode: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Request {
GetDevices,
SetMacro {
device_path: String,
macro_entry: MacroEntry,
},
ListMacros,
DeleteMacro {
name: String,
},
ReloadConfig,
LedSet {
device_path: String,
color: (u8, u8, u8), },
RecordMacro {
device_path: String,
name: String,
capture_mouse: bool,
},
StopRecording,
TestMacro {
name: String,
},
GetStatus,
SaveProfile {
name: String,
},
LoadProfile {
name: String,
},
ListProfiles,
DeleteProfile {
name: String,
},
GenerateToken {
client_id: String,
},
Authenticate {
token: String,
},
ExecuteMacro {
name: String,
},
GrabDevice {
device_path: String,
},
UngrabDevice {
device_path: String,
},
GetDeviceProfiles {
device_id: String, },
ActivateProfile {
device_id: String, profile_name: String,
},
DeactivateProfile {
device_id: String, },
GetActiveProfile {
device_id: String, },
GetActiveRemaps {
device_path: String,
},
ListRemapProfiles {
device_path: String,
},
ActivateRemapProfile {
device_path: String,
profile_name: String,
},
DeactivateRemapProfile {
device_path: String,
},
GetDeviceCapabilities {
device_path: String,
},
GetActiveLayer {
device_id: String,
},
SetLayerConfig {
device_id: String,
layer_id: usize,
config: LayerConfigInfo,
},
ActivateLayer {
device_id: String,
layer_id: usize,
mode: LayerMode,
},
ListLayers {
device_id: String,
},
SetAnalogSensitivity {
device_id: String,
sensitivity: f32, },
GetAnalogSensitivity {
device_id: String,
},
SetAnalogResponseCurve {
device_id: String,
curve: String, },
GetAnalogResponseCurve {
device_id: String,
},
SetAnalogDeadzone {
device_id: String,
percentage: u8, },
GetAnalogDeadzone {
device_id: String,
},
SetAnalogDeadzoneXY {
device_id: String,
x_percentage: u8, y_percentage: u8, },
GetAnalogDeadzoneXY {
device_id: String,
},
SetAnalogOuterDeadzoneXY {
device_id: String,
x_percentage: u8, y_percentage: u8, },
GetAnalogOuterDeadzoneXY {
device_id: String,
},
SetAnalogDpadMode {
device_id: String,
mode: String, },
GetAnalogDpadMode {
device_id: String,
},
SetLedColor {
device_id: String,
zone: LedZone,
red: u8,
green: u8,
blue: u8,
},
GetLedColor {
device_id: String,
zone: LedZone,
},
GetAllLedColors {
device_id: String,
},
SetLedBrightness {
device_id: String,
zone: Option<LedZone>, brightness: u8, },
GetLedBrightness {
device_id: String,
zone: Option<LedZone>,
},
SetLedPattern {
device_id: String,
pattern: LedPattern,
},
GetLedPattern {
device_id: String,
},
FocusChanged {
app_id: String, window_title: Option<String>, },
RegisterHotkey {
device_id: String,
binding: HotkeyBinding,
},
ListHotkeys {
device_id: String,
},
RemoveHotkey {
device_id: String,
key: String,
modifiers: Vec<String>,
},
SetAutoSwitchRules {
rules: Vec<AutoSwitchRule>,
},
GetAutoSwitchRules,
GetAnalogCalibration {
device_id: String,
layer_id: usize,
},
SetAnalogCalibration {
device_id: String,
layer_id: usize,
calibration: AnalogCalibrationConfig,
},
SubscribeAnalogInput {
device_id: String,
},
UnsubscribeAnalogInput {
device_id: String,
},
SetMacroSettings(MacroSettings),
GetMacroSettings,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AnalogMode {
Disabled,
Dpad,
Gamepad,
Camera,
Mouse,
Wasd,
}
impl fmt::Display for AnalogMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AnalogMode::Disabled => write!(f, "Disabled"),
AnalogMode::Dpad => write!(f, "D-pad (Arrows)"),
AnalogMode::Gamepad => write!(f, "Gamepad"),
AnalogMode::Camera => write!(f, "Camera"),
AnalogMode::Mouse => write!(f, "Mouse"),
AnalogMode::Wasd => write!(f, "WASD"),
}
}
}
impl Default for AnalogMode {
fn default() -> Self {
AnalogMode::Disabled
}
}
impl AnalogMode {
pub const ALL: [AnalogMode; 6] = [
AnalogMode::Disabled,
AnalogMode::Dpad,
AnalogMode::Gamepad,
AnalogMode::Wasd,
AnalogMode::Mouse,
AnalogMode::Camera,
];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CameraOutputMode {
Scroll,
Keys,
}
impl fmt::Display for CameraOutputMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CameraOutputMode::Scroll => write!(f, "Scroll"),
CameraOutputMode::Keys => write!(f, "Key Repeat"),
}
}
}
impl Default for CameraOutputMode {
fn default() -> Self {
CameraOutputMode::Scroll
}
}
impl CameraOutputMode {
pub const ALL: [CameraOutputMode; 2] = [
CameraOutputMode::Scroll,
CameraOutputMode::Keys,
];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum LedPattern {
Static,
Breathing,
Rainbow,
RainbowWave,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum LedZone {
Side,
Logo,
Keys,
Thumbstick,
All,
Global,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatusInfo {
pub version: String,
pub uptime_seconds: u64,
pub devices_count: usize,
pub macros_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Response {
Devices(Vec<DeviceInfo>),
Macros(Vec<MacroEntry>),
Ack,
Status {
version: String,
uptime_seconds: u64,
devices_count: usize,
macros_count: usize,
},
RecordingStarted {
device_path: String,
name: String,
},
RecordingStopped {
macro_entry: MacroEntry,
},
Profiles(Vec<String>),
ProfileLoaded {
name: String,
macros_count: usize,
},
ProfileSaved {
name: String,
macros_count: usize,
},
Error(String),
Token(String),
Authenticated,
DeviceProfiles {
device_id: String,
profiles: Vec<String>,
},
ProfileActivated {
device_id: String,
profile_name: String,
},
ProfileDeactivated {
device_id: String,
},
ActiveProfile {
device_id: String,
profile_name: Option<String>,
},
ActiveRemaps {
device_path: String,
profile_name: Option<String>,
remaps: Vec<RemapEntry>,
},
RemapProfiles {
device_path: String,
profiles: Vec<RemapProfileInfo>,
},
RemapProfileActivated {
device_path: String,
profile_name: String,
},
RemapProfileDeactivated {
device_path: String,
},
DeviceCapabilities {
device_path: String,
capabilities: DeviceCapabilities,
},
ActiveLayer {
device_id: String,
layer_id: usize,
layer_name: String,
},
LayerConfigured {
device_id: String,
layer_id: usize,
},
LayerList {
device_id: String,
layers: Vec<LayerConfigInfo>,
},
AnalogSensitivitySet {
device_id: String,
sensitivity: f32,
},
AnalogSensitivity {
device_id: String,
sensitivity: f32,
},
AnalogResponseCurveSet {
device_id: String,
curve: String,
},
AnalogResponseCurve {
device_id: String,
curve: String,
},
AnalogDeadzoneSet {
device_id: String,
percentage: u8,
},
AnalogDeadzone {
device_id: String,
percentage: u8,
},
AnalogDeadzoneXYSet {
device_id: String,
x_percentage: u8,
y_percentage: u8,
},
AnalogDeadzoneXY {
device_id: String,
x_percentage: u8,
y_percentage: u8,
},
AnalogOuterDeadzoneXYSet {
device_id: String,
x_percentage: u8,
y_percentage: u8,
},
AnalogOuterDeadzoneXY {
device_id: String,
x_percentage: u8,
y_percentage: u8,
},
AnalogDpadModeSet {
device_id: String,
mode: String,
},
AnalogDpadMode {
device_id: String,
mode: String,
},
LedColorSet {
device_id: String,
zone: LedZone,
color: (u8, u8, u8),
},
LedColor {
device_id: String,
zone: LedZone,
color: Option<(u8, u8, u8)>,
},
AllLedColors {
device_id: String,
colors: std::collections::HashMap<LedZone, (u8, u8, u8)>,
},
LedBrightnessSet {
device_id: String,
zone: Option<LedZone>,
brightness: u8,
},
LedBrightness {
device_id: String,
zone: Option<LedZone>,
brightness: u8,
},
LedPatternSet {
device_id: String,
pattern: LedPattern,
},
LedPattern {
device_id: String,
pattern: LedPattern,
},
FocusChangedAck {
app_id: String,
},
HotkeyRegistered {
device_id: String,
key: String,
modifiers: Vec<String>,
},
HotkeyList {
device_id: String,
bindings: Vec<HotkeyBinding>,
},
HotkeyRemoved {
device_id: String,
key: String,
modifiers: Vec<String>,
},
AutoSwitchRulesAck,
AutoSwitchRules {
rules: Vec<AutoSwitchRule>,
},
AnalogCalibration {
device_id: String,
layer_id: usize,
calibration: Option<AnalogCalibrationConfig>,
},
AnalogCalibrationAck,
AnalogInputUpdate {
device_id: String,
axis_x: f32, axis_y: f32, },
AnalogInputSubscribed,
MacroSettings(MacroSettings),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Profile {
pub name: String,
pub macros: std::collections::HashMap<String, MacroEntry>,
}
pub fn serialize<T: Serialize>(msg: &T) -> Vec<u8> {
bincode::serialize(msg).unwrap_or_else(|e| {
tracing::error!("Failed to serialize message: {:?}", e);
Vec::new()
})
}
pub fn deserialize<'a, T: Deserialize<'a>>(bytes: &'a [u8]) -> Result<T, bincode::Error> {
bincode::deserialize(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_macro_settings_serialization() {
let settings = MacroSettings {
latency_offset_ms: 10,
jitter_pct: 0.05,
capture_mouse: false,
};
let serialized = serialize(&settings);
let deserialized: MacroSettings = deserialize(&serialized).unwrap();
assert_eq!(deserialized, settings);
}
#[test]
fn test_ipc_serialization() {
let request = Request::GetDevices;
let serialized = serialize(&request);
let deserialized: Request = deserialize(&serialized).unwrap();
assert!(matches!(deserialized, Request::GetDevices));
}
#[test]
fn test_macro_entry_serialization() {
let macro_entry = MacroEntry {
name: "Test Macro".to_string(),
trigger: KeyCombo {
keys: vec![30, 40], modifiers: vec![29], },
actions: vec![
Action::KeyPress(30),
Action::Delay(100),
Action::KeyRelease(30),
],
device_id: Some("test_device".to_string()),
enabled: true,
humanize: false,
capture_mouse: false,
};
let serialized = serialize(¯o_entry);
let deserialized: MacroEntry = deserialize(&serialized).unwrap();
assert_eq!(deserialized.name, "Test Macro");
assert_eq!(deserialized.trigger.keys, vec![30, 40]);
}
#[test]
fn test_profile_ipc_serialization() {
let request = Request::GetDeviceProfiles {
device_id: "1532:0220".to_string(),
};
let serialized = serialize(&request);
let deserialized: Request = deserialize(&serialized).unwrap();
assert!(matches!(deserialized, Request::GetDeviceProfiles { .. }));
let response = Response::DeviceProfiles {
device_id: "1532:0220".to_string(),
profiles: vec!["gaming".to_string(), "work".to_string()],
};
let serialized = serialize(&response);
let deserialized: Response = deserialize(&serialized).unwrap();
assert!(matches!(deserialized, Response::DeviceProfiles { .. }));
}
#[test]
fn test_analog_deadzone_ipc_serialization() {
let request = Request::SetAnalogDeadzone {
device_id: "1532:0220".to_string(),
percentage: 50,
};
let serialized = serialize(&request);
let deserialized: Request = deserialize(&serialized).unwrap();
assert!(matches!(deserialized, Request::SetAnalogDeadzone { .. }));
let request = Request::GetAnalogDeadzone {
device_id: "1532:0220".to_string(),
};
let serialized = serialize(&request);
let deserialized: Request = deserialize(&serialized).unwrap();
assert!(matches!(deserialized, Request::GetAnalogDeadzone { .. }));
let response = Response::AnalogDeadzoneSet {
device_id: "1532:0220".to_string(),
percentage: 50,
};
let serialized = serialize(&response);
let deserialized: Response = deserialize(&serialized).unwrap();
assert_eq!(deserialized, response);
let response = Response::AnalogDeadzone {
device_id: "1532:0220".to_string(),
percentage: 43,
};
let serialized = serialize(&response);
let deserialized: Response = deserialize(&serialized).unwrap();
assert_eq!(deserialized, response);
}
#[test]
fn test_mouse_action_serialization() {
let actions = vec![
Action::MousePress(0x110), Action::MouseRelease(0x110),
Action::MouseMove(10, 20),
Action::MouseScroll(5),
];
for action in &actions {
let serialized = serialize(action);
let deserialized: Action = deserialize(&serialized).unwrap();
assert_eq!(action, &deserialized);
}
let macro_entry = MacroEntry {
name: "Mixed Macro".to_string(),
trigger: KeyCombo {
keys: vec![30],
modifiers: vec![],
},
actions: vec![
Action::KeyPress(30),
Action::MousePress(0x110),
Action::Delay(50),
Action::MouseRelease(0x110),
Action::MouseMove(100, 200),
Action::MouseScroll(3),
Action::KeyRelease(30),
],
device_id: Some("1532:0220".to_string()),
enabled: true,
humanize: false,
capture_mouse: false,
};
let serialized = serialize(¯o_entry);
let deserialized: MacroEntry = deserialize(&serialized).unwrap();
assert_eq!(deserialized.name, "Mixed Macro");
assert_eq!(deserialized.actions.len(), 7);
assert!(matches!(deserialized.actions[0], Action::KeyPress(30)));
assert!(matches!(deserialized.actions[1], Action::MousePress(0x110)));
assert!(matches!(deserialized.actions[2], Action::Delay(50)));
assert!(matches!(deserialized.actions[3], Action::MouseRelease(0x110)));
assert!(matches!(deserialized.actions[4], Action::MouseMove(100, 200)));
assert!(matches!(deserialized.actions[5], Action::MouseScroll(3)));
assert!(matches!(deserialized.actions[6], Action::KeyRelease(30)));
}
#[test]
fn test_device_capabilities_serialization() {
let caps = DeviceCapabilities {
has_analog_stick: true,
has_hat_switch: true,
joystick_button_count: 26,
led_zones: vec!["logo".to_string(), "keys".to_string()],
};
let serialized = serialize(&caps);
let deserialized: DeviceCapabilities = deserialize(&serialized).unwrap();
assert_eq!(deserialized.has_analog_stick, true);
assert_eq!(deserialized.has_hat_switch, true);
assert_eq!(deserialized.joystick_button_count, 26);
assert_eq!(deserialized.led_zones.len(), 2);
}
#[test]
fn test_get_device_capabilities_request() {
let request = Request::GetDeviceCapabilities {
device_path: "/dev/input/event0".to_string(),
};
let serialized = serialize(&request);
let deserialized: Request = deserialize(&serialized).unwrap();
assert!(matches!(deserialized, Request::GetDeviceCapabilities { .. }));
if let Request::GetDeviceCapabilities { device_path } = deserialized {
assert_eq!(device_path, "/dev/input/event0");
}
}
#[test]
fn test_device_capabilities_response() {
let response = Response::DeviceCapabilities {
device_path: "/dev/input/event0".to_string(),
capabilities: DeviceCapabilities {
has_analog_stick: true,
has_hat_switch: true,
joystick_button_count: 26,
led_zones: vec![],
},
};
let serialized = serialize(&response);
let deserialized: Response = deserialize(&serialized).unwrap();
assert!(matches!(deserialized, Response::DeviceCapabilities { .. }));
}
#[test]
fn test_layer_mode_serialization() {
let hold_mode = LayerMode::Hold;
let serialized = serialize(&hold_mode);
let deserialized: LayerMode = deserialize(&serialized).unwrap();
assert_eq!(deserialized, LayerMode::Hold);
let toggle_mode = LayerMode::Toggle;
let serialized = serialize(&toggle_mode);
let deserialized: LayerMode = deserialize(&serialized).unwrap();
assert_eq!(deserialized, LayerMode::Toggle);
}
#[test]
fn test_layer_config_info_serialization() {
let config = LayerConfigInfo {
layer_id: 1,
name: "Gaming".to_string(),
mode: LayerMode::Toggle,
remap_count: 5,
led_color: (0, 0, 255), led_zone: None,
};
let serialized = serialize(&config);
let deserialized: LayerConfigInfo = deserialize(&serialized).unwrap();
assert_eq!(deserialized.layer_id, 1);
assert_eq!(deserialized.name, "Gaming");
assert_eq!(deserialized.mode, LayerMode::Toggle);
assert_eq!(deserialized.remap_count, 5);
}
#[test]
fn test_get_active_layer_request() {
let request = Request::GetActiveLayer {
device_id: "1532:0220".to_string(),
};
let serialized = serialize(&request);
let deserialized: Request = deserialize(&serialized).unwrap();
assert!(matches!(deserialized, Request::GetActiveLayer { .. }));
if let Request::GetActiveLayer { device_id } = deserialized {
assert_eq!(device_id, "1532:0220");
}
}
#[test]
fn test_active_layer_response() {
let response = Response::ActiveLayer {
device_id: "1532:0220".to_string(),
layer_id: 2,
layer_name: "Gaming".to_string(),
};
let serialized = serialize(&response);
let deserialized: Response = deserialize(&serialized).unwrap();
assert!(matches!(deserialized, Response::ActiveLayer { .. }));
if let Response::ActiveLayer { device_id, layer_id, layer_name } = deserialized {
assert_eq!(device_id, "1532:0220");
assert_eq!(layer_id, 2);
assert_eq!(layer_name, "Gaming");
}
}
#[test]
fn test_set_layer_config_request() {
let request = Request::SetLayerConfig {
device_id: "1532:0220".to_string(),
layer_id: 1,
config: LayerConfigInfo {
layer_id: 1,
name: "Work".to_string(),
mode: LayerMode::Hold,
remap_count: 0,
led_color: (0, 0, 255),
led_zone: None,
},
};
let serialized = serialize(&request);
let deserialized: Request = deserialize(&serialized).unwrap();
assert!(matches!(deserialized, Request::SetLayerConfig { .. }));
if let Request::SetLayerConfig { device_id, layer_id, config } = deserialized {
assert_eq!(device_id, "1532:0220");
assert_eq!(layer_id, 1);
assert_eq!(config.name, "Work");
assert_eq!(config.mode, LayerMode::Hold);
}
}
#[test]
fn test_activate_layer_request() {
let request = Request::ActivateLayer {
device_id: "1532:0220".to_string(),
layer_id: 2,
mode: LayerMode::Toggle,
};
let serialized = serialize(&request);
let deserialized: Request = deserialize(&serialized).unwrap();
assert!(matches!(deserialized, Request::ActivateLayer { .. }));
if let Request::ActivateLayer { device_id, layer_id, mode } = deserialized {
assert_eq!(device_id, "1532:0220");
assert_eq!(layer_id, 2);
assert_eq!(mode, LayerMode::Toggle);
}
}
#[test]
fn test_list_layers_request() {
let request = Request::ListLayers {
device_id: "1532:0220".to_string(),
};
let serialized = serialize(&request);
let deserialized: Request = deserialize(&serialized).unwrap();
assert!(matches!(deserialized, Request::ListLayers { .. }));
if let Request::ListLayers { device_id } = deserialized {
assert_eq!(device_id, "1532:0220");
}
}
#[test]
fn test_layer_list_response() {
let response = Response::LayerList {
device_id: "1532:0220".to_string(),
layers: vec![
LayerConfigInfo {
layer_id: 0,
name: "Base".to_string(),
mode: LayerMode::Hold,
remap_count: 10,
led_color: (0, 255, 0),
led_zone: None,
},
LayerConfigInfo {
layer_id: 1,
name: "Gaming".to_string(),
mode: LayerMode::Toggle,
remap_count: 5,
led_color: (255, 0, 0),
led_zone: Some(LedZone::Side),
},
],
};
let serialized = serialize(&response);
let deserialized: Response = deserialize(&serialized).unwrap();
assert!(matches!(deserialized, Response::LayerList { .. }));
if let Response::LayerList { device_id, layers } = deserialized {
assert_eq!(device_id, "1532:0220");
assert_eq!(layers.len(), 2);
assert_eq!(layers[0].name, "Base");
assert_eq!(layers[1].name, "Gaming");
}
}
#[test]
fn test_layer_configured_response() {
let response = Response::LayerConfigured {
device_id: "1532:0220".to_string(),
layer_id: 1,
};
let serialized = serialize(&response);
let deserialized: Response = deserialize(&serialized).unwrap();
assert!(matches!(deserialized, Response::LayerConfigured { .. }));
if let Response::LayerConfigured { device_id, layer_id } = deserialized {
assert_eq!(device_id, "1532:0220");
assert_eq!(layer_id, 1);
}
}
#[test]
fn test_focus_changed_request_serialization() {
let request = Request::FocusChanged {
app_id: "org.alacritty".to_string(),
window_title: Some("Alacritty: ~/Projects".to_string()),
};
let serialized = serialize(&request);
let deserialized: Request = deserialize(&serialized).unwrap();
assert!(matches!(deserialized, Request::FocusChanged { .. }));
if let Request::FocusChanged { app_id, window_title } = deserialized {
assert_eq!(app_id, "org.alacritty");
assert_eq!(window_title, Some("Alacritty: ~/Projects".to_string()));
}
let request = Request::FocusChanged {
app_id: "org.mozilla.firefox".to_string(),
window_title: None,
};
let serialized = serialize(&request);
let deserialized: Request = deserialize(&serialized).unwrap();
assert!(matches!(deserialized, Request::FocusChanged { .. }));
if let Request::FocusChanged { app_id, window_title } = deserialized {
assert_eq!(app_id, "org.mozilla.firefox");
assert_eq!(window_title, None);
}
let request = Request::FocusChanged {
app_id: "firefox".to_string(),
window_title: Some("Mozilla Firefox".to_string()),
};
let serialized = serialize(&request);
let deserialized: Request = deserialize(&serialized).unwrap();
assert!(matches!(deserialized, Request::FocusChanged { .. }));
if let Request::FocusChanged { app_id, window_title } = deserialized {
assert_eq!(app_id, "firefox");
assert_eq!(window_title, Some("Mozilla Firefox".to_string()));
}
}
#[test]
fn test_focus_changed_ack_response_serialization() {
let response = Response::FocusChangedAck {
app_id: "org.alacritty".to_string(),
};
let serialized = serialize(&response);
let deserialized: Response = deserialize(&serialized).unwrap();
assert!(matches!(deserialized, Response::FocusChangedAck { .. }));
if let Response::FocusChangedAck { ref app_id } = deserialized {
assert_eq!(app_id, "org.alacritty");
}
assert_eq!(deserialized, response);
}
#[test]
fn test_analog_calibration_config_with_mode() {
let config = AnalogCalibrationConfig {
deadzone: 0.2,
deadzone_shape: "circular".to_string(),
sensitivity: "linear".to_string(),
sensitivity_multiplier: 1.5,
range_min: -32768,
range_max: 32767,
invert_x: false,
invert_y: true,
exponent: 2.0,
analog_mode: AnalogMode::Wasd,
camera_output_mode: None,
};
let serialized = serialize(&config);
let deserialized: AnalogCalibrationConfig = deserialize(&serialized).unwrap();
assert_eq!(deserialized.analog_mode, AnalogMode::Wasd);
assert_eq!(deserialized.camera_output_mode, None);
let camera_config = AnalogCalibrationConfig {
analog_mode: AnalogMode::Camera,
camera_output_mode: Some(CameraOutputMode::Keys),
..config
};
let serialized = serialize(&camera_config);
let deserialized: AnalogCalibrationConfig = deserialize(&serialized).unwrap();
assert_eq!(deserialized.analog_mode, AnalogMode::Camera);
assert_eq!(deserialized.camera_output_mode, Some(CameraOutputMode::Keys));
}
}