use crate::error::Error;
use crate::event::{ImageFormat, SaveMask};
use crate::frame::Rect;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Target {
#[serde(rename = "hwnd")]
ByHwnd(isize),
#[serde(rename = "title")]
ByTitleRegex(String),
#[serde(rename = "exe")]
ByExe(String),
#[serde(rename = "pid")]
ByPid(u32),
}
impl Default for Target {
fn default() -> Self {
Target::ByTitleRegex(String::new())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RoiKind {
Watch,
Spinner,
Volatile,
Ignore,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RoiHint {
pub kind: RoiKind,
pub label: String,
pub rect_norm: [f32; 4],
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct ImageOpts {
pub format: ImageFormat,
pub scale: f32,
}
impl Default for ImageOpts {
fn default() -> Self {
Self {
format: ImageFormat::Png,
scale: 1.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct Rotation {
pub max_frames: u64,
pub max_bytes: u64,
}
impl Default for Rotation {
fn default() -> Self {
Self {
max_frames: 5000,
max_bytes: 2 * 1024 * 1024 * 1024, }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
#[non_exhaustive]
pub struct Config {
pub target: Target,
pub out_dir: PathBuf,
pub fps_cap: u32,
pub wait_ms: u64,
pub stop_after_ms: u64,
pub stop_after_images: u64,
pub stop_after_settled: bool,
pub min_emit_interval_ms: u64,
pub settle_ms: u64,
pub max_active_ms: u64,
pub tile_grid: (u16, u16),
pub tile_change_threshold: u8,
pub meaningful_area_ratio: f32,
pub dedup_hamming: u32,
pub volatility_window: u16,
pub busy_rate_threshold: f32,
pub value_sample_ms: u64,
pub emit_transition_start: bool,
pub save_image_for: SaveMask,
pub image: ImageOpts,
pub rois: Vec<RoiHint>,
pub crop: Option<Rect>,
pub rotation: Rotation,
}
impl Default for Config {
fn default() -> Self {
Self {
target: Target::default(),
out_dir: PathBuf::from("./.framewatch"),
fps_cap: 30,
wait_ms: 0,
stop_after_ms: 0,
stop_after_images: 0,
stop_after_settled: false,
min_emit_interval_ms: 200,
settle_ms: 350,
max_active_ms: 5000,
tile_grid: (32, 18),
tile_change_threshold: 12,
meaningful_area_ratio: 0.002,
dedup_hamming: 8,
volatility_window: 32,
busy_rate_threshold: 0.5,
value_sample_ms: 1000,
emit_transition_start: false,
save_image_for: SaveMask::default(),
image: ImageOpts::default(),
rois: Vec::new(),
crop: None,
rotation: Rotation::default(),
}
}
}
impl Config {
pub fn builder() -> ConfigBuilder {
ConfigBuilder::new()
}
pub fn from_toml_path(path: impl AsRef<Path>) -> Result<Self, Error> {
let text = std::fs::read_to_string(path.as_ref())?;
Self::from_toml_str(&text)
}
pub fn from_toml_str(text: &str) -> Result<Self, Error> {
toml::from_str(text).map_err(|e| Error::Config(e.to_string()))
}
pub fn to_toml_string(&self) -> Result<String, Error> {
toml::to_string_pretty(self).map_err(|e| Error::Config(e.to_string()))
}
pub fn cols(&self) -> u16 {
self.tile_grid.0
}
pub fn rows(&self) -> u16 {
self.tile_grid.1
}
pub fn validate(&self) -> Result<(), Error> {
match &self.target {
Target::ByTitleRegex(s) | Target::ByExe(s) if s.is_empty() => {
return Err(Error::Config("target is empty".into()));
}
_ => {}
}
if self.tile_grid.0 == 0 || self.tile_grid.1 == 0 {
return Err(Error::Config("tile_grid must be non-zero".into()));
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct ConfigBuilder {
cfg: Config,
}
impl Default for ConfigBuilder {
fn default() -> Self {
Self::new()
}
}
impl ConfigBuilder {
pub fn new() -> Self {
Self {
cfg: Config::default(),
}
}
pub fn from_config(cfg: Config) -> Self {
Self { cfg }
}
pub fn target(mut self, target: Target) -> Self {
self.cfg.target = target;
self
}
pub fn out_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.cfg.out_dir = dir.into();
self
}
pub fn settle_ms(mut self, ms: u64) -> Self {
self.cfg.settle_ms = ms;
self
}
pub fn max_active_ms(mut self, ms: u64) -> Self {
self.cfg.max_active_ms = ms;
self
}
pub fn wait_ms(mut self, ms: u64) -> Self {
self.cfg.wait_ms = ms;
self
}
pub fn stop_after_ms(mut self, ms: u64) -> Self {
self.cfg.stop_after_ms = ms;
self
}
pub fn stop_after_images(mut self, n: u64) -> Self {
self.cfg.stop_after_images = n;
self
}
pub fn stop_after_settled(mut self, on: bool) -> Self {
self.cfg.stop_after_settled = on;
self
}
pub fn value_sample_ms(mut self, ms: u64) -> Self {
self.cfg.value_sample_ms = ms;
self
}
pub fn tile_grid(mut self, cols: u16, rows: u16) -> Self {
self.cfg.tile_grid = (cols, rows);
self
}
pub fn fps_cap(mut self, fps: u32) -> Self {
self.cfg.fps_cap = fps;
self
}
pub fn min_emit_interval_ms(mut self, ms: u64) -> Self {
self.cfg.min_emit_interval_ms = ms;
self
}
pub fn dedup_hamming(mut self, d: u32) -> Self {
self.cfg.dedup_hamming = d;
self
}
pub fn save_image_for(mut self, mask: SaveMask) -> Self {
self.cfg.save_image_for = mask;
self
}
pub fn emit_transition_start(mut self, on: bool) -> Self {
self.cfg.emit_transition_start = on;
self
}
pub fn image_scale(mut self, scale: f32) -> Self {
self.cfg.image.scale = scale;
self
}
pub fn image_format(mut self, format: ImageFormat) -> Self {
self.cfg.image.format = format;
self
}
pub fn crop(mut self, rect: Rect) -> Self {
self.cfg.crop = Some(rect);
self
}
pub fn crop_xywh(self, x: i32, y: i32, w: u32, h: u32) -> Self {
self.crop(Rect::new(x, y, w, h))
}
pub fn roi(mut self, hint: RoiHint) -> Self {
self.cfg.rois.push(hint);
self
}
fn push_roi(mut self, kind: RoiKind, label: impl Into<String>, rect: [f32; 4]) -> Self {
self.cfg.rois.push(RoiHint {
kind,
label: label.into(),
rect_norm: rect,
});
self
}
pub fn spinner_roi(self, label: impl Into<String>, rect: [f32; 4]) -> Self {
self.push_roi(RoiKind::Spinner, label, rect)
}
pub fn volatile_roi(self, label: impl Into<String>, rect: [f32; 4]) -> Self {
self.push_roi(RoiKind::Volatile, label, rect)
}
pub fn watch_roi(self, label: impl Into<String>, rect: [f32; 4]) -> Self {
self.push_roi(RoiKind::Watch, label, rect)
}
pub fn ignore_roi(self, label: impl Into<String>, rect: [f32; 4]) -> Self {
self.push_roi(RoiKind::Ignore, label, rect)
}
pub fn rotation(mut self, rotation: Rotation) -> Self {
self.cfg.rotation = rotation;
self
}
pub fn build(self) -> Result<Config, Error> {
self.cfg.validate()?;
Ok(self.cfg)
}
pub fn build_unchecked(self) -> Config {
self.cfg
}
}
pub use crate::event::EventKind as Kind;