Restructure config for per-mode style/sound and fix pulse dismiss

Major changes:
- Add StyleConfig dataclass with independent color, width, opacity,
  duration, pulse_speed, sound, and volume per mode (running/completed)
- Replace flat flash_*/sound_*/play_on config with running: and
  completed: YAML sections
- Replace CGEventTap (silently fails in forked daemon) with
  CGEventSourceSecondsSinceLastEventType polling for reliable
  input-based pulse dismissal when Cursor is already frontmost
- Update overlay, sound, and daemon to pass StyleConfig per call
- Rewrite tests for new config shape and dismiss mechanism

Made-with: Cursor
This commit is contained in:
cottongin
2026-03-10 07:01:52 -04:00
parent c0477d2f40
commit 5b71b2275b
24 changed files with 1504 additions and 1034 deletions

40
hooks/notify.py Executable file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""Cursor hook script that notifies the cursor-flasher daemon via Unix socket.
Installed as a Cursor hook (preToolUse, stop) to trigger a window flash
when the agent needs user attention. Reads hook JSON from stdin, extracts
workspace and event info, and sends it to the daemon's socket.
"""
import json
import os
import socket
import sys
SOCKET_PATH = os.path.expanduser("~/.cursor-flasher/flasher.sock")
def main() -> None:
try:
data = json.load(sys.stdin)
except (json.JSONDecodeError, ValueError):
return
workspace_roots = data.get("workspace_roots") or []
workspace = workspace_roots[0] if workspace_roots else ""
event = data.get("hook_event_name", "")
tool = data.get("tool_name", "")
msg = json.dumps({"workspace": workspace, "event": event, "tool": tool})
try:
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.settimeout(1)
s.connect(SOCKET_PATH)
s.sendall(msg.encode())
s.close()
except (ConnectionRefusedError, FileNotFoundError, OSError):
pass
if __name__ == "__main__":
main()