"""Recorder abstractions.""" from __future__ import annotations import threading from abc import ABC, abstractmethod from dataclasses import dataclass, field from datetime import datetime from typing import Any, Callable, Optional def _now_iso() -> str: return datetime.now().astimezone().isoformat() @dataclass class RawUserEvent: kind: str # mouse_down | mouse_up | key | input | navigation | focus | checkpoint surface: str timestamp: str = field(default_factory=_now_iso) button: str = "" key: str = "" text: str = "" x: Optional[int] = None y: Optional[int] = None target: dict[str, Any] = field(default_factory=dict) frame: dict[str, Any] = field(default_factory=dict) url: str = "" meta: dict[str, Any] = field(default_factory=dict) from_inspector: bool = False class RecorderState: IDLE = "idle" RECORDING = "recording" PAUSED = "paused" STOPPED = "stopped" class RecorderBase(ABC): """Process-external observer. No injection / hooks into target.""" def __init__( self, *, on_event: Optional[Callable[[RawUserEvent], None]] = None, exclude_hwnds: Optional[Callable[[], set[int]]] = None, ) -> None: self.on_event = on_event self.exclude_hwnds = exclude_hwnds or (lambda: set()) self.status = RecorderState.IDLE self._thread: Optional[threading.Thread] = None self._stop = threading.Event() def emit(self, event: RawUserEvent) -> None: if self.status != RecorderState.RECORDING: return if event.from_inspector: return if self.on_event: self.on_event(event) @abstractmethod def start(self) -> None: ... @abstractmethod def stop(self) -> None: ... def pause(self) -> None: if self.status == RecorderState.RECORDING: self.status = RecorderState.PAUSED def resume(self) -> None: if self.status == RecorderState.PAUSED: self.status = RecorderState.RECORDING @property def capability(self) -> dict[str, str]: return {}