Skip to content

Preferences subprocess

This page mirrors implementation notes maintained in CLAUDE.md. Update both when changing this subsystem.

The Preferences window is a separate subprocess launched via platform.launch_settings(). It uses customtkinter and writes config back to disk atomically; the parent observes the subprocess exit and reloads its BrowserAuth cache + reconciles streaming.enabled against a pre-launch snapshot.

Why a subprocess

  • The menu-bar app's run loop (rumps on macOS, pystray on Windows) is single-threaded for UI. A long-running customtkinter window on the same loop would deadlock the menu/tray.
  • Crash isolation — if a Preferences widget hits a Tk bug, the menu-bar app keeps running.
  • IPC simplicity — there's none. The subprocess writes config.json + browser_auth.json atomically; the parent reads them on subprocess exit. No pipes, no sockets, no shared-memory.
sequenceDiagram
    participant Tray as Menu bar / tray
    participant Sub as settings_window subprocess
    participant Disk as config.json + browser_auth.json

    Tray->>Sub: spawn (or focus existing window)
    Sub->>Disk: read snapshot on launch
    Sub->>Sub: user edits in-memory draft
    Sub->>Disk: atomic write on Apply
    Tray->>Tray: subprocess exits → reload BrowserAuth, reconcile streaming

Apply / Revert flow

Draft edits stay in memory until the user clicks Apply. The footer Apply/Revert buttons enable only while diff(draft, snapshot) is non-empty and every visible field validates (regex compile, URL shape, etc.). See settings_model for the validation logic.

Destructive actions — Clear Data, Reset Privacy Permissions, per-browser "Ask now" — bypass the draft and commit immediately. Those clicks are intent-to-act, not setting edits.

Multi-instance guard

The subprocess takes a file lock at <config root>/.cue.settings.lock (fcntl on POSIX, msvcrt on Windows). A second launch exits silently on lock contention. The parent process also tracks the live subprocess PID and calls a platform-specific focus_subprocess() helper to bring the existing window to the front instead of spawning a new one.

Module split

Module Purpose
cue.settings_model Pure-logic layer: load_draft() / validate_regex() / validate_api_key() / diff() / commit() / supported_browsers(). stdlib-only, unit-testable. No customtkinter, no Tk imports.
cue.settings_window UI shell. customtkinter widgets, layout, the four tabs (General / Privacy / Data / About). Imports the model layer.

The split exists so the validation rules and diff logic are covered by tests without spinning up a Tk root window.

Tab inventory

The four tabs and what they hold are walked through in Preferences (user-facing). The internals of those widgets live in cue.settings_window.

Top-level tray items are kept short on both platforms: Open Cue / Privacy paused / Enable Streaming / Preferences… / Quit Cue. New stateful settings go into Preferences tabs, not new tray menu items.

See also