feat: add main daemon loop wiring detection, overlay, and sound

Made-with: Cursor
This commit is contained in:
cottongin
2026-03-10 02:45:17 -04:00
parent 3cbe529b7a
commit bcd8d4da1a

View File

@@ -0,0 +1,99 @@
"""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()