"""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()