greentic-redbutton 0.4.2

Cross-platform Greentic red-button CLI scaffold with embedded i18n and release automation
use chrono::{DateTime, Utc};
use serde::Serialize;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeviceMatcher {
    pub vendor_id: u16,
    pub product_id: u16,
    pub key: ButtonKey,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ButtonKey {
    Enter,
    Other(String),
}

impl ButtonKey {
    pub fn parse(raw: &str) -> Self {
        let normalized = raw.trim().to_ascii_lowercase();
        match normalized.as_str() {
            "enter" | "return" => Self::Enter,
            other => Self::Other(other.to_string()),
        }
    }

    pub fn as_config_value(&self) -> &str {
        match self {
            Self::Enter => "enter",
            Self::Other(value) => value.as_str(),
        }
    }

    pub fn usage_id(&self) -> Option<u8> {
        match self {
            Self::Enter => Some(0x28),
            Self::Other(value) => keyboard_usage_for_name(value),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeviceInfo {
    pub name: Option<String>,
    pub vendor_id: u16,
    pub product_id: u16,
    pub backend: &'static str,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ButtonEventKind {
    Down,
    Up,
}

#[derive(Debug, Clone)]
pub struct ButtonEvent {
    pub kind: ButtonEventKind,
    pub timestamp: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize)]
pub struct WebhookEvent {
    pub source: &'static str,
    pub event_type: &'static str,
    pub vendor_id: u16,
    pub product_id: u16,
    pub key: String,
    pub timestamp: DateTime<Utc>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub device_name: Option<String>,
    pub os: &'static str,
    pub arch: &'static str,
}

fn keyboard_usage_for_name(name: &str) -> Option<u8> {
    match name {
        "enter" | "return" => Some(0x28),
        "escape" | "esc" => Some(0x29),
        "space" => Some(0x2c),
        "tab" => Some(0x2b),
        "backspace" => Some(0x2a),
        "up" => Some(0x52),
        "down" => Some(0x51),
        "left" => Some(0x50),
        "right" => Some(0x4f),
        "f1" => Some(0x3a),
        "f2" => Some(0x3b),
        "f3" => Some(0x3c),
        "f4" => Some(0x3d),
        "f5" => Some(0x3e),
        "f6" => Some(0x3f),
        "f7" => Some(0x40),
        "f8" => Some(0x41),
        "f9" => Some(0x42),
        "f10" => Some(0x43),
        "f11" => Some(0x44),
        "f12" => Some(0x45),
        _ => {
            let bytes = name.as_bytes();
            if bytes.len() == 1 {
                let ch = bytes[0];
                match ch {
                    b'a'..=b'z' => Some(0x04 + (ch - b'a')),
                    b'1'..=b'9' => Some(0x1e + (ch - b'1')),
                    b'0' => Some(0x27),
                    _ => None,
                }
            } else {
                name.strip_prefix("0x")
                    .and_then(|hex| u8::from_str_radix(hex, 16).ok())
            }
        }
    }
}

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

    #[test]
    fn parses_common_keys() {
        assert_eq!(ButtonKey::parse("Enter"), ButtonKey::Enter);
        assert_eq!(ButtonKey::parse("A"), ButtonKey::Other("a".to_string()));
    }

    #[test]
    fn maps_keyboard_usage_codes() {
        assert_eq!(ButtonKey::Enter.usage_id(), Some(0x28));
        assert_eq!(ButtonKey::Other("a".to_string()).usage_id(), Some(0x04));
        assert_eq!(ButtonKey::Other("0x28".to_string()).usage_id(), Some(0x28));
        assert_eq!(ButtonKey::Other("unknown".to_string()).usage_id(), None);
    }
}