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::time::Duration;

use visual_cortex::{Error, EventKind, FakeSource, Frame, Rate, Region, Session};

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")
}

/// 8x8 black/white checkerboard PNG.
fn checker_png() -> Vec<u8> {
    let img = image::ImageBuffer::from_fn(8, 8, |x, y| {
        if (x + y) % 2 == 0 {
            image::Luma([255u8])
        } else {
            image::Luma([0u8])
        }
    });
    let mut out = Vec::new();
    img.write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png)
        .expect("png encode");
    out
}

fn bg_frame() -> Frame {
    Frame::from_fn(40, 30, |_, _| [30, 30, 30, 255])
}

/// Background with the checkerboard icon stamped at (10, 6).
fn icon_frame() -> Frame {
    Frame::from_fn(40, 30, |x, y| {
        if (10..18).contains(&x) && (6..14).contains(&y) {
            if ((x - 10) + (y - 6)) % 2 == 0 {
                [255, 255, 255, 255]
            } else {
                [0, 0, 0, 255]
            }
        } else {
            [30, 30, 30, 255]
        }
    })
}

#[tokio::test(start_paused = true)]
async fn template_appears_and_vanishes() {
    // bg x3 (no event: first observation is "absent"), icon x4 -> Appeared,
    // then bg repeats forever -> Vanished, then silence.
    let mut frames = vec![bg_frame(), bg_frame(), bg_frame()];
    frames.extend([icon_frame(), icon_frame(), icon_frame(), icon_frame()]);
    frames.push(bg_frame());

    let session = Session::builder()
        .source(FakeSource::new(frames))
        .build()
        .await
        .unwrap();

    let mut stream = session
        .watch("icon")
        .region(Region::Full)
        .rate(Rate::hz(20.0))
        .template(&checker_png(), 0.9)
        .subscribe()
        .unwrap();

    let appeared = next_event(&mut stream).await;
    assert!(
        matches!(appeared.kind, EventKind::TemplateAppeared { score } if score > 0.9),
        "expected TemplateAppeared with high score, got {:?}",
        appeared.kind
    );

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

    let more = tokio::time::timeout(Duration::from_millis(500), stream.next()).await;
    assert!(
        more.is_err(),
        "no further template events without a transition"
    );
}

#[tokio::test]
async fn invalid_template_errors_at_subscribe() {
    let session = Session::builder()
        .source(FakeSource::new([Frame::solid(2, 2, [0, 0, 0, 255])]))
        .build()
        .await
        .unwrap();

    let err = session
        .watch("bad")
        .template(b"definitely not a png", 0.9)
        .subscribe()
        .unwrap_err();
    assert!(matches!(err, Error::InvalidTemplate(name, _) if name == "bad"));
}

#[test]
fn facade_reexports_all_detectors() {
    // Compile-time check: consumers need only the visual_cortex crate.
    fn assert_detector<D: visual_cortex::Detector>() {}
    assert_detector::<visual_cortex::Brightness>();
    assert_detector::<visual_cortex::ColorMatch>();
    assert_detector::<visual_cortex::ColorDominance>();
    assert_detector::<visual_cortex::TemplateMatcher>();
    assert_detector::<visual_cortex::FrameDiff>();
    let _channel = visual_cortex::Channel::Red;
}