100 lines
3.2 KiB
Python
100 lines
3.2 KiB
Python
|
|
"""Main daemon loop that ties detection, state machine, overlay, and sound together."""
|
||
|
|
import logging
|
||
|
|
import signal
|
||
|
|
import time
|
||
|
|
|
||
|
|
from Cocoa import NSApplication, NSRunLoop, NSDate
|
||
|
|
|
||
|
|
from cursor_flasher.config import Config, load_config
|
||
|
|
from cursor_flasher.detector import CursorDetector
|
||
|
|
from cursor_flasher.overlay import OverlayManager
|
||
|
|
from cursor_flasher.sound import play_alert
|
||
|
|
from cursor_flasher.state import FlasherState, StateMachine
|
||
|
|
|
||
|
|
logger = logging.getLogger("cursor_flasher")
|
||
|
|
|
||
|
|
|
||
|
|
class FlasherDaemon:
|
||
|
|
def __init__(self, config: Config):
|
||
|
|
self.config = config
|
||
|
|
self.state_machine = StateMachine(cooldown=config.cooldown)
|
||
|
|
self.detector = CursorDetector()
|
||
|
|
self.overlay = OverlayManager(config)
|
||
|
|
self._running = False
|
||
|
|
self._waiting_since: float | None = None
|
||
|
|
|
||
|
|
def run(self) -> None:
|
||
|
|
"""Run the main loop. Blocks until stopped."""
|
||
|
|
NSApplication.sharedApplication()
|
||
|
|
self._running = True
|
||
|
|
|
||
|
|
signal.signal(signal.SIGTERM, self._handle_signal)
|
||
|
|
signal.signal(signal.SIGINT, self._handle_signal)
|
||
|
|
|
||
|
|
logger.info("Cursor Flasher daemon started")
|
||
|
|
|
||
|
|
while self._running:
|
||
|
|
self._tick()
|
||
|
|
NSRunLoop.currentRunLoop().runUntilDate_(
|
||
|
|
NSDate.dateWithTimeIntervalSinceNow_(self.config.poll_interval)
|
||
|
|
)
|
||
|
|
|
||
|
|
self.overlay.hide()
|
||
|
|
logger.info("Cursor Flasher daemon stopped")
|
||
|
|
|
||
|
|
def stop(self) -> None:
|
||
|
|
self._running = False
|
||
|
|
|
||
|
|
def _tick(self) -> None:
|
||
|
|
signals = self.detector.poll()
|
||
|
|
|
||
|
|
if signals is None:
|
||
|
|
if self.state_machine.state == FlasherState.WAITING_FOR_USER:
|
||
|
|
self.state_machine.dismiss()
|
||
|
|
self.overlay.hide()
|
||
|
|
self._waiting_since = None
|
||
|
|
return
|
||
|
|
|
||
|
|
changed = self.state_machine.update(
|
||
|
|
agent_working=signals.agent_working,
|
||
|
|
approval_needed=signals.approval_needed,
|
||
|
|
)
|
||
|
|
|
||
|
|
if not changed:
|
||
|
|
if (
|
||
|
|
self.state_machine.state == FlasherState.WAITING_FOR_USER
|
||
|
|
and self._waiting_since is not None
|
||
|
|
):
|
||
|
|
elapsed = time.monotonic() - self._waiting_since
|
||
|
|
if elapsed > self.config.auto_dismiss:
|
||
|
|
self.state_machine.dismiss()
|
||
|
|
self.overlay.hide()
|
||
|
|
self._waiting_since = None
|
||
|
|
return
|
||
|
|
|
||
|
|
match self.state_machine.state:
|
||
|
|
case FlasherState.WAITING_FOR_USER:
|
||
|
|
pid = self.detector._pid
|
||
|
|
if pid is not None:
|
||
|
|
self.overlay.show(pid)
|
||
|
|
play_alert(self.config)
|
||
|
|
self._waiting_since = time.monotonic()
|
||
|
|
case FlasherState.AGENT_WORKING | FlasherState.IDLE:
|
||
|
|
self.overlay.hide()
|
||
|
|
self._waiting_since = None
|
||
|
|
|
||
|
|
def _handle_signal(self, signum, frame):
|
||
|
|
logger.info(f"Received signal {signum}, shutting down")
|
||
|
|
self.stop()
|
||
|
|
|
||
|
|
|
||
|
|
def run_daemon() -> None:
|
||
|
|
"""Entry point for the daemon."""
|
||
|
|
logging.basicConfig(
|
||
|
|
level=logging.INFO,
|
||
|
|
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
|
||
|
|
)
|
||
|
|
config = load_config()
|
||
|
|
daemon = FlasherDaemon(config)
|
||
|
|
daemon.run()
|