visual-cortex 0.8.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::sync::Arc;
use std::time::SystemTime;

use visual_cortex_vision::DetectorOutput;

/// An event emitted by a watcher.
#[derive(Debug, Clone)]
pub struct Event {
    pub watcher: Arc<str>,
    pub timestamp: SystemTime,
    pub kind: EventKind,
}

#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum EventKind {
    /// Every-match mode: the detector matched this tick.
    Matched { output: DetectorOutput },
    /// Change mode (default): the detector's output transitioned.
    /// `old` is `None` on the first evaluation.
    Changed {
        old: Option<DetectorOutput>,
        new: DetectorOutput,
    },
    /// Threshold mode: the predicate flipped.
    ThresholdCrossed {
        value: f64,
        direction: ThresholdDirection,
    },
    /// Template mode: the best match score rose to or above the threshold.
    TemplateAppeared { score: f32 },
    /// Template mode: the best match score fell below the threshold.
    TemplateVanished,
    /// The detector failed repeatedly; the watcher keeps running.
    WatcherDegraded { consecutive_errors: u32 },
    /// The capture target disappeared (e.g. the watched window closed).
    /// Session-wide: delivered to every subscriber. Watchers stay registered;
    /// a `TargetReattached` follows if the target comes back.
    TargetLost,
    /// A previously lost capture target is available again. Session-wide.
    TargetReattached,
    /// This stream's receiver fell behind and `skipped` events were dropped.
    ///
    /// Synthesized locally by [`EventStream::next`](crate::EventStream::next)
    /// — it is about one receiver, not the session, and is delivered only to
    /// the lagged stream (so it is NOT `is_session_level`). Consumers that
    /// derive state from transitions (e.g. Change-mode `old`/`new` tracking)
    /// should treat it as "resynchronize"; others can ignore it.
    Lagged { skipped: u64 },
    /// Terminal: this watcher's task has ended after a fatal internal failure
    /// and will emit no further events. The session and its other watchers
    /// keep running. React by restarting the watcher (a fresh
    /// `Session::watch` after `Session::unsubscribe`) or shutting down,
    /// rather than awaiting a stream that will never yield again.
    WatcherStopped,
}

impl EventKind {
    /// Session-wide events that every subscriber should see, regardless of the
    /// watcher names their stream filters on.
    pub fn is_session_level(&self) -> bool {
        matches!(self, EventKind::TargetLost | EventKind::TargetReattached)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ThresholdDirection {
    /// The predicate became true.
    Entered,
    /// The predicate became false.
    Exited,
}