lcsa-core 0.1.0

Local context substrate for AI-native software - typed signals for clipboard, selection, and focus
Documentation
use std::collections::VecDeque;
use std::io::{self, Write};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, UNIX_EPOCH};

use lcsa_core::{ContextApi, SignalEnvelope, SignalSupport, SignalType, StructuralSignal};

const MAX_EVENTS: usize = 18;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut api = ContextApi::new()?;
    let events = Arc::new(Mutex::new(VecDeque::<String>::new()));

    for signal_type in [
        SignalType::Clipboard,
        SignalType::Selection,
        SignalType::Focus,
    ] {
        if !matches!(api.signal_support(signal_type), SignalSupport::Supported) {
            continue;
        }

        let events = Arc::clone(&events);
        api.subscribe_enveloped(signal_type, move |envelope| {
            let line = summarize_envelope(&envelope);
            let mut queue = events.lock().expect("event queue mutex poisoned");
            queue.push_front(line);
            while queue.len() > MAX_EVENTS {
                let _ = queue.pop_back();
            }
        })?;
    }

    loop {
        draw_dashboard(&api, &events)?;
        thread::sleep(Duration::from_millis(350));
    }
}

fn draw_dashboard(
    api: &ContextApi,
    events: &Arc<Mutex<VecDeque<String>>>,
) -> Result<(), Box<dyn std::error::Error>> {
    print!("\x1B[2J\x1B[H");
    println!("LCSA Live View");
    println!(
        "device={} platform={:?}",
        api.device_context().device_name,
        api.device_context().platform
    );
    println!();
    println!("Signal Capabilities");
    for signal_type in [
        SignalType::Clipboard,
        SignalType::Selection,
        SignalType::Focus,
    ] {
        println!(
            "  {:<10} {:?}",
            format!("{signal_type:?}"),
            api.signal_support(signal_type)
        );
    }
    println!();
    println!("Recent Context Events");
    println!("  (copy text, select text, or switch windows to see updates)");

    let queue = events.lock().expect("event queue mutex poisoned");
    if queue.is_empty() {
        println!("  - waiting for signals...");
    } else {
        for line in queue.iter() {
            println!("  - {line}");
        }
    }

    io::stdout().flush()?;
    Ok(())
}

fn summarize_envelope(envelope: &SignalEnvelope) -> String {
    let secs = envelope
        .emitted_at
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_secs())
        .unwrap_or_default();

    let summary = match &envelope.payload {
        StructuralSignal::Clipboard(signal) => format!(
            "clipboard type={:?} bytes={} source={}",
            signal.content_type, signal.size_bytes, signal.source_app
        ),
        StructuralSignal::Selection(signal) => format!(
            "selection type={:?} bytes={} source={}",
            signal.content_type, signal.size_bytes, signal.source_app
        ),
        StructuralSignal::Focus(signal) => {
            format!(
                "focus target={:?} source={}",
                signal.target, signal.source_app
            )
        }
        StructuralSignal::Filesystem(signal) => format!(
            "filesystem event={} action={:?}",
            signal.event_name(),
            signal.action
        ),
    };

    format!("{secs} {summary}")
}