Files
skill-template/tools/ui_inspector/recorder/base.py
chendelian 633ecff4e2 chore: archive tools/ui_inspector snapshot before migration removal.
Keep the last in-template copy on main so history can restore it; product continues in jiangchang-ui-recorder.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 11:19:14 +08:00

80 lines
2.1 KiB
Python

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