Skip to main content

context_live_view/
context_live_view.rs

1use std::collections::VecDeque;
2use std::io::{self, Write};
3use std::sync::{Arc, Mutex};
4use std::thread;
5use std::time::{Duration, UNIX_EPOCH};
6
7use lcsa_core::{ContextApi, SignalEnvelope, SignalSupport, SignalType, StructuralSignal};
8
9const MAX_EVENTS: usize = 18;
10
11fn main() -> Result<(), Box<dyn std::error::Error>> {
12    let mut api = ContextApi::new()?;
13    let events = Arc::new(Mutex::new(VecDeque::<String>::new()));
14
15    for signal_type in [
16        SignalType::Clipboard,
17        SignalType::Selection,
18        SignalType::Focus,
19    ] {
20        if !matches!(api.signal_support(signal_type), SignalSupport::Supported) {
21            continue;
22        }
23
24        let events = Arc::clone(&events);
25        api.subscribe_enveloped(signal_type, move |envelope| {
26            let line = summarize_envelope(&envelope);
27            let mut queue = events.lock().expect("event queue mutex poisoned");
28            queue.push_front(line);
29            while queue.len() > MAX_EVENTS {
30                let _ = queue.pop_back();
31            }
32        })?;
33    }
34
35    loop {
36        draw_dashboard(&api, &events)?;
37        thread::sleep(Duration::from_millis(350));
38    }
39}
40
41fn draw_dashboard(
42    api: &ContextApi,
43    events: &Arc<Mutex<VecDeque<String>>>,
44) -> Result<(), Box<dyn std::error::Error>> {
45    print!("\x1B[2J\x1B[H");
46    println!("LCSA Live View");
47    println!(
48        "device={} platform={:?}",
49        api.device_context().device_name,
50        api.device_context().platform
51    );
52    println!();
53    println!("Signal Capabilities");
54    for signal_type in [
55        SignalType::Clipboard,
56        SignalType::Selection,
57        SignalType::Focus,
58    ] {
59        println!(
60            "  {:<10} {:?}",
61            format!("{signal_type:?}"),
62            api.signal_support(signal_type)
63        );
64    }
65    println!();
66    println!("Recent Context Events");
67    println!("  (copy text, select text, or switch windows to see updates)");
68
69    let queue = events.lock().expect("event queue mutex poisoned");
70    if queue.is_empty() {
71        println!("  - waiting for signals...");
72    } else {
73        for line in queue.iter() {
74            println!("  - {line}");
75        }
76    }
77
78    io::stdout().flush()?;
79    Ok(())
80}
81
82fn summarize_envelope(envelope: &SignalEnvelope) -> String {
83    let secs = envelope
84        .emitted_at
85        .duration_since(UNIX_EPOCH)
86        .map(|duration| duration.as_secs())
87        .unwrap_or_default();
88
89    let summary = match &envelope.payload {
90        StructuralSignal::Clipboard(signal) => format!(
91            "clipboard type={:?} bytes={} source={}",
92            signal.content_type, signal.size_bytes, signal.source_app
93        ),
94        StructuralSignal::Selection(signal) => format!(
95            "selection type={:?} bytes={} source={}",
96            signal.content_type, signal.size_bytes, signal.source_app
97        ),
98        StructuralSignal::Focus(signal) => {
99            format!(
100                "focus target={:?} source={}",
101                signal.target, signal.source_app
102            )
103        }
104        StructuralSignal::Filesystem(signal) => format!(
105            "filesystem event={} action={:?}",
106            signal.event_name(),
107            signal.action
108        ),
109    };
110
111    format!("{secs} {summary}")
112}