lcsa-core 0.1.0

Local context substrate for AI-native software - typed signals for clipboard, selection, and focus
Documentation
use std::thread;
use std::time::Duration;

use arboard::Clipboard;

use crate::error::Error;
use crate::event_bus::EventBus;
use crate::signals::{ClipboardContent, ClipboardPayload, ClipboardSignal, StructuralSignal};

const POLL_INTERVAL: Duration = Duration::from_millis(250);

pub(crate) fn spawn_clipboard_monitor(bus: EventBus) -> Result<(), Error> {
    Clipboard::new().map_err(|error| Error::PlatformError(error.to_string()))?;

    thread::spawn(move || {
        let mut clipboard = match Clipboard::new() {
            Ok(clipboard) => clipboard,
            Err(_) => return,
        };

        let mut last_fingerprint: Option<String> = None;

        loop {
            if let Some((fingerprint, signal)) = read_signal(&mut clipboard) {
                let has_changed = last_fingerprint
                    .as_ref()
                    .map(|previous| previous != &fingerprint)
                    .unwrap_or(true);

                if has_changed {
                    last_fingerprint = Some(fingerprint);
                    bus.emit(StructuralSignal::Clipboard(signal));
                }
            }

            thread::sleep(POLL_INTERVAL);
        }
    });

    Ok(())
}

pub(crate) fn read_clipboard_content() -> Result<ClipboardContent, Error> {
    let mut clipboard =
        Clipboard::new().map_err(|error| Error::PlatformError(error.to_string()))?;

    if let Ok(text) = clipboard.get_text() {
        return Ok(ClipboardContent {
            payload: ClipboardPayload::Text(text),
            source_app: "unknown".to_string(),
            captured_at: std::time::SystemTime::now(),
        });
    }

    if let Ok(image) = clipboard.get_image() {
        return Ok(ClipboardContent {
            payload: ClipboardPayload::Image {
                width: image.width,
                height: image.height,
                size_bytes: image.bytes.len(),
            },
            source_app: "unknown".to_string(),
            captured_at: std::time::SystemTime::now(),
        });
    }

    Err(Error::PlatformError(
        "clipboard did not contain readable text or image data".to_string(),
    ))
}

fn read_signal(clipboard: &mut Clipboard) -> Option<(String, ClipboardSignal)> {
    if let Ok(text) = clipboard.get_text() {
        if text.trim().is_empty() {
            return None;
        }

        let fingerprint = format!("text:{}", hash_string(&text));
        let signal = ClipboardSignal::text(&text, "unknown".to_string());
        return Some((fingerprint, signal));
    }

    if let Ok(image) = clipboard.get_image() {
        let size_bytes = image.bytes.len();
        let fingerprint = format!("image:{}:{}:{}", image.width, image.height, size_bytes);
        let signal = ClipboardSignal::image(size_bytes, "unknown".to_string());
        return Some((fingerprint, signal));
    }

    None
}

fn hash_string(input: &str) -> u64 {
    use std::collections::hash_map::DefaultHasher;
    use std::hash::{Hash, Hasher};

    let mut hasher = DefaultHasher::new();
    input.hash(&mut hasher);
    hasher.finish()
}