# Dark/Light Theme Support — Design ## Summary Add dark/light mode theme support to cursor-flasher. Users can define separate border styles for each OS appearance mode via new `dark` and `light` config sections. A `theme` option controls which styles are active: `"dark"`, `"light"`, or `"auto"` (follows macOS appearance in real-time). ## Config Format The old top-level `running`/`completed` format is replaced (breaking change). Modes are now nested under theme sections: ```yaml theme: auto # "dark" | "light" | "auto" dark: running: color: "#FF9500" width: 4 opacity: 0.85 pulse_speed: 1.5 sound: "Glass" volume: 0.5 completed: color: "#00FF00" width: 4 opacity: 0.85 duration: 1.5 sound: "" volume: 0.0 light: running: color: "#3B82F6" width: 4 opacity: 0.9 pulse_speed: 1.5 sound: "Glass" volume: 0.5 completed: color: "#22C55E" width: 4 opacity: 0.9 duration: 1.5 sound: "" volume: 0.0 flash: mode: "screen" approval_tools: - Shell - Write - Delete general: approval_delay: 2.5 cooldown: 2.0 ``` ## Data Model - `StyleConfig` — unchanged (color, width, opacity, duration, pulse_speed, sound, volume). - New `ThemeStyles` dataclass — groups `running: StyleConfig` and `completed: StyleConfig` for one theme. - `Config` — replaces `running`/`completed` with `dark: ThemeStyles` and `light: ThemeStyles`. Adds `theme: str` field. Exposes `active_styles(system_appearance: str) -> ThemeStyles` method that resolves the correct theme based on the `theme` setting and the passed-in system appearance string. ## Appearance Detection The daemon detects macOS appearance via `NSApplication.sharedApplication().effectiveAppearance().name()`. If the name contains "Dark", the appearance is `"dark"`; otherwise `"light"`. This check happens at flash/pulse trigger time (not polled), so it picks up OS appearance changes between flashes with zero overhead. ## Daemon Integration Two call sites change: `_check_pending()` and `_handle_stop()`. Each resolves the active theme styles at trigger time: ```python styles = self.config.active_styles(_get_system_appearance()) self.overlay.pulse(frames, styles.running) play_alert(styles.running) ``` ## Decisions - **Modes under themes** (not themes under modes) — `dark.running` rather than `running.dark`. - **Old format not supported** — top-level `running`/`completed` keys are ignored. - **Real-time detection** — appearance checked at each flash trigger, not just at startup. - **Config stays pure** — no Cocoa imports in config.py; appearance detection lives in daemon.py.