libinput-rs 0.2.1

Fail-open Rust touchpad companion and drop-in libinput ABI replacement
Documentation
use serde::{de, Deserialize, Deserializer};
use std::fs;

pub const DEFAULT_POINTER_ACCELERATION: f32 = 2.2;
const NORMALIZATION_REFERENCE_ACCELERATION: f32 = 2.5;
const DEFAULT_POINTER_MULTIPLIER: f32 =
    DEFAULT_POINTER_ACCELERATION / NORMALIZATION_REFERENCE_ACCELERATION;

fn deserialize_pointer_acceleration<'de, D>(deserializer: D) -> Result<f32, D::Error>
where
    D: Deserializer<'de>,
{
    let configured = f32::deserialize(deserializer)?;
    if !configured.is_finite() || configured <= 0.0 {
        return Err(de::Error::custom(
            "pointer_acceleration must be a finite positive number",
        ));
    }

    Ok(configured / NORMALIZATION_REFERENCE_ACCELERATION)
}

#[derive(Deserialize, Debug, PartialEq)]
pub struct InputConfig {
    pub tap_to_click: bool,
    pub natural_scrolling: bool,
    #[serde(deserialize_with = "deserialize_pointer_acceleration")]
    pub pointer_acceleration: f32,
    pub disable_while_typing: bool,
}

impl Default for InputConfig {
    fn default() -> Self {
        Self {
            tap_to_click: true,
            natural_scrolling: true,
            pointer_acceleration: DEFAULT_POINTER_MULTIPLIER,
            disable_while_typing: true,
        }
    }
}

#[allow(dead_code)]
pub fn load_config() -> Option<InputConfig> {
    let path = "/etc/libinput-rs/config.json";
    if let Ok(data) = fs::read_to_string(path) {
        serde_json::from_str(&data).ok()
    } else {
        None
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn assert_close(actual: f32, expected: f32) {
        assert!(
            (actual - expected).abs() < 0.000_001,
            "expected {expected}, got {actual}"
        );
    }

    #[test]
    fn shipped_config_matches_runtime_defaults() {
        let shipped: InputConfig = serde_json::from_str(include_str!("config.json"))
            .expect("the shipped companion configuration must remain valid JSON");

        assert_eq!(shipped, InputConfig::default());
        assert_close(shipped.pointer_acceleration, 0.88);
    }

    #[test]
    fn configured_speed_preserves_the_legacy_scale() {
        let calibrated: InputConfig = serde_json::from_str(
            r#"{
                "tap_to_click": true,
                "natural_scrolling": true,
                "pointer_acceleration": 2.2,
                "disable_while_typing": true
            }"#,
        )
        .expect("calibrated configuration must parse");
        let reference: InputConfig = serde_json::from_str(
            r#"{
                "tap_to_click": true,
                "natural_scrolling": true,
                "pointer_acceleration": 2.5,
                "disable_while_typing": true
            }"#,
        )
        .expect("reference configuration must parse");

        assert_close(calibrated.pointer_acceleration, 0.88);
        assert_close(reference.pointer_acceleration, 1.0);
        assert_close(0.45 * calibrated.pointer_acceleration, 0.18 * 2.2);
    }

    #[test]
    fn non_positive_speed_is_rejected() {
        let invalid = r#"{
            "tap_to_click": true,
            "natural_scrolling": true,
            "pointer_acceleration": 0.0,
            "disable_while_typing": true
        }"#;

        assert!(serde_json::from_str::<InputConfig>(invalid).is_err());
    }
}