Files
cursor-flasher/src/cursor_flasher/daemon.py

100 lines
3.2 KiB
Python
Raw Normal View History

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