Files
cursor-flasher/docs/plans/2026-03-10-dark-light-theme-design.md
cottongin 49c03e4b71 Add design doc for dark/light theme support
Defines the config format, data model, and daemon integration
approach for theme-aware styling.

Made-with: Cursor
2026-03-10 08:34:43 -04:00

2.6 KiB

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:

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:

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.