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>
80 lines
2.1 KiB
Python
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 {}
|