lcsa-core 0.1.0

Local context substrate for AI-native software - typed signals for clipboard, selection, and focus
Documentation
use std::env;
use std::fmt;
use std::time::{SystemTime, UNIX_EPOCH};

use serde::{Deserialize, Serialize};

use crate::signals::StructuralSignal;

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Platform {
    Linux,
    MacOs,
    Windows,
    Ios,
    Android,
    Browser,
    Unknown,
}

impl Platform {
    pub fn as_str(self) -> &'static str {
        match self {
            Platform::Linux => "linux",
            Platform::MacOs => "macos",
            Platform::Windows => "windows",
            Platform::Ios => "ios",
            Platform::Android => "android",
            Platform::Browser => "browser",
            Platform::Unknown => "unknown",
        }
    }
}

impl fmt::Display for Platform {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DeviceClass {
    Desktop,
    Laptop,
    Tablet,
    Phone,
    Server,
    Browser,
    Unknown,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeviceContext {
    pub device_id: String,
    pub device_name: String,
    pub platform: Platform,
    pub device_class: DeviceClass,
    pub os_version: Option<String>,
}

impl Default for DeviceContext {
    fn default() -> Self {
        let platform = current_platform();
        let device_name = env::var("HOSTNAME")
            .or_else(|_| env::var("COMPUTERNAME"))
            .unwrap_or_else(|_| "local-device".to_string());
        let platform_label = platform.as_str();

        Self {
            device_id: format!("{platform_label}:{device_name}"),
            device_name,
            platform,
            device_class: DeviceClass::Desktop,
            os_version: None,
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ApplicationContext {
    pub app_id: String,
    pub app_name: String,
    pub app_version: Option<String>,
}

impl Default for ApplicationContext {
    fn default() -> Self {
        Self {
            app_id: "lcsa-client".to_string(),
            app_name: "lcsa-client".to_string(),
            app_version: None,
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SignalSource {
    Clipboard,
    Filesystem,
    Selection,
    Focus,
    Terminal,
    Browser,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SignalEnvelope {
    pub signal_id: String,
    pub emitted_at: SystemTime,
    pub source: SignalSource,
    pub device: DeviceContext,
    pub application: ApplicationContext,
    pub payload: StructuralSignal,
}

impl SignalEnvelope {
    pub fn new(
        source: SignalSource,
        device: DeviceContext,
        application: ApplicationContext,
        payload: StructuralSignal,
    ) -> Self {
        let micros = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map(|duration| duration.as_micros())
            .unwrap_or_default();

        Self {
            signal_id: format!("{source:?}-{micros}").to_ascii_lowercase(),
            emitted_at: SystemTime::now(),
            source,
            device,
            application,
            payload,
        }
    }

    pub fn from_signal(
        device: DeviceContext,
        application: ApplicationContext,
        payload: StructuralSignal,
    ) -> Self {
        let source = payload.source();
        Self::new(source, device, application, payload)
    }
}

fn current_platform() -> Platform {
    #[cfg(target_os = "linux")]
    {
        Platform::Linux
    }
    #[cfg(target_os = "macos")]
    {
        Platform::MacOs
    }
    #[cfg(target_os = "windows")]
    {
        Platform::Windows
    }
    #[cfg(target_os = "ios")]
    {
        Platform::Ios
    }
    #[cfg(target_os = "android")]
    {
        Platform::Android
    }
    #[cfg(not(any(
        target_os = "linux",
        target_os = "macos",
        target_os = "windows",
        target_os = "ios",
        target_os = "android"
    )))]
    {
        Platform::Unknown
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::signals::{ClipboardSignal, StructuralSignal};

    #[test]
    fn default_device_context_has_platform() {
        let device = DeviceContext::default();
        assert_ne!(device.platform, Platform::Unknown);
    }

    #[test]
    fn envelope_wraps_payload_with_identity() {
        let envelope = SignalEnvelope::from_signal(
            DeviceContext::default(),
            ApplicationContext::default(),
            StructuralSignal::Clipboard(ClipboardSignal::text(
                "cargo test",
                "terminal".to_string(),
            )),
        );

        assert_eq!(envelope.source, SignalSource::Clipboard);
        assert!(matches!(envelope.payload, StructuralSignal::Clipboard(_)));
        assert!(envelope.signal_id.starts_with("clipboard-"));
    }

    #[test]
    fn envelope_infers_selection_source() {
        let envelope = SignalEnvelope::from_signal(
            DeviceContext::default(),
            ApplicationContext::default(),
            StructuralSignal::Selection(crate::signals::SelectionSignal::text(
                "hello",
                "editor".to_string(),
                true,
            )),
        );

        assert_eq!(envelope.source, SignalSource::Selection);
        assert!(envelope.signal_id.starts_with("selection-"));
    }
}