visual-cortex 0.5.0

Watch screen regions and receive typed events over async streams when patterns match: OCR, template matching, and pixel conditions on live captures.
use std::collections::VecDeque;
use std::sync::Arc;
use std::time::Duration;

use visual_cortex::{
    Brightness, CaptureError, EventKind, Frame, FrameSource, Rate, Region, Session,
};

/// A `FrameSource` that replays a scripted sequence of results, then reports
/// `Exhausted` forever. Lets us drive the target-lost/reattach state machine
/// deterministically with no real capture backend.
struct ScriptedSource {
    steps: VecDeque<Result<Arc<Frame>, CaptureError>>,
}

impl ScriptedSource {
    fn new(steps: impl IntoIterator<Item = Result<Arc<Frame>, CaptureError>>) -> Self {
        Self {
            steps: steps.into_iter().collect(),
        }
    }
}

#[async_trait::async_trait]
impl FrameSource for ScriptedSource {
    async fn capture_frame(&mut self) -> Result<Arc<Frame>, CaptureError> {
        self.steps
            .pop_front()
            .unwrap_or(Err(CaptureError::Exhausted))
    }
}

async fn next_event(stream: &mut visual_cortex::EventStream) -> visual_cortex::Event {
    tokio::time::timeout(Duration::from_secs(30), stream.next())
        .await
        .expect("timed out waiting for event")
        .expect("stream closed unexpectedly")
}

#[tokio::test(start_paused = true)]
async fn target_loss_and_reattach_emit_session_events() {
    let bg = || Ok(Arc::new(Frame::solid(8, 8, [10, 10, 10, 255])));
    let steps = vec![
        bg(),                          // present
        Err(CaptureError::TargetLost), // -> TargetLost
        Err(CaptureError::TargetLost), // still gone, no repeat event
        bg(),                          // -> TargetReattached
        bg(),
    ];

    let session = Session::builder()
        .source(ScriptedSource::new(steps))
        .build()
        .await
        .unwrap();

    // A watcher that never emits its own events (predicate never true), so the
    // stream carries only the session-level events we are asserting on.
    let mut stream = session
        .watch("probe")
        .region(Region::Full)
        .rate(Rate::hz(20.0))
        .detector(Brightness)
        .on_threshold(|_| false)
        .subscribe()
        .unwrap();

    let first = next_event(&mut stream).await;
    assert!(
        matches!(first.kind, EventKind::TargetLost),
        "expected TargetLost, got {:?}",
        first.kind
    );

    let second = next_event(&mut stream).await;
    assert!(
        matches!(second.kind, EventKind::TargetReattached),
        "expected TargetReattached, got {:?}",
        second.kind
    );
}