Docs

Per-Window Accent Color — Delivery Notes

Delivery summary for assigning each window a preset accent color so isolated windows are visually distinguishable. For the decision record, read ADR-0007: Per-window identity accent color; this note says what shipped and where.

Status

Implemented and verified working. Colors apply per-window and persist; the status bar fills with the accent under the active theme. Not yet committed.

PieceState
tranquil-window-color package (commands, apply, persistence)✅ verified
var(--tq-window-accent, …) status-bar hooks in all four UI themes✅ verified (business themes); ✅ built (plain themes)
Wired into tranquil-client (link dep + node_modules symlink)✅ done

Why this shape

This is the visual companion to ADR-0005 session isolation: windows are now separate login profiles but look identical. Two constraints ruled out the obvious “theme per window” path:

  • core.themes is global — every renderer reads the same array.
  • Theme colors are compile-time LESS hex, not runtime CSS variables.

So the accent is injected as a per-window CSS custom property (--tq-window-accent) — each renderer has its own StyleManager, making atom.styles.addStyleSheet inherently per-window — and the themes consume it with a fallback so unassigned windows are unchanged.

What shipped & where

New owned package — tranquil-window-color (../tranquil-window-color, linked into tranquil-client and symlinked into ~/.tranquil/dev/packages/):

  • lib/main.js — resolves this window’s id via atom.getLoadSettings().windowSessionId; injects :root { --tq-window-accent: <hex> } under a stable sourcePath (re-inject replaces in place); one command per preset (set-<key>) plus clear-color; and observers on tranquil-window-color.assignments (re-apply) and core.themes (re-pick light/dark). No external dependencies.
  • lib/presets.js — 8 presets, each with a dark/light hex.
  • menus/, README.md.

main.js injects two properties: --tq-window-accent (the color) and --tq-window-accent-fg (an auto-contrasted near-black/white foreground, picked by the accent’s luminance so status-bar text stays legible on any preset).

Storage — one global config map tranquil-window-color.assignments = { <windowSessionId>: <presetKey> }. The id is restored on relaunch (ADR-0005), so the accent returns automatically — no main-process/core changes.

Theme hooks — the status bar background fills with the accent, its foreground uses the contrast color. Added to all four UI themes so the accent works whichever is active:

  • tranquil-business-dark / -lightstatus-bar.less (identical; the two packages differ only in colors.less).
  • tranquil-theme-darkstatus-bar.less; tranquil-theme-light — the .status-bar block in base.less.

Each hook uses var(--tq-window-accent, <theme default>) so the no-color state is a no-op. Only the status bar is tinted — an earlier version also filled the tab bar, removed by request. The native OS title bar is not tintable (see non-goals).

Constraints / non-goals (confirmed)

  • Native title bar not tinted — OS-drawn; nativeTheme.themeSource is process-global.
  • Not per-window light/dark — theme activation is global; only the accent varies per window.

Gotchas found while building

  • Dev-package require resolution (the one that bit us): the first cut used atom-select-list for a modal picker, required at the top of main.js. A dev package loads from its sibling repo realpath (~/.tranquil/dev/packages/<name> is a symlink), and Atom’s module cache does not reliably fall back to the app’s node_modules for it — so the require threw at module load, activation aborted, and zero commands registered (no error surfaced — the classic silent-crash pattern). Fix: drop the external dependency entirely and expose one command per preset instead of a modal. Bonus — every color is now directly discoverable in the command palette. Lesson: a dev package should require only atom, its own lib, or a dependency it actually vendors in its own node_modules.
  • addStyleSheet re-inject: disposing the previous disposable before re-adding under the same sourcePath keeps styleElementsBySourcePath clean (StyleManager.removeStyleElement deletes the map entry on dispose), so updates replace rather than stack.
  • A new owned package needs its node_modules symlink, not just the dev/packages one. yarn start loads owned packages as bundledPackage: true from tranquil-client/node_modules/<pkg> (a ../../<pkg> symlink yarn install creates from the link:../ dep). The ~/.tranquil/dev/packages/<pkg> symlink alone does not load it. Symptom: getActivePackage returns a package with path under node_modules/ but metadata: {name} only, mainModule: null, mainActivated: false — Atom stubbed it because the node_modules path didn’t exist, so activate() never ran. Fix without a full reinstall: ln -s ../../<pkg> node_modules/<pkg>, then restart.

Verify

  1. nvm use && yarn start; open two windows.
  2. Window Color: Set Blue in window A, Set Amber in window B → each window’s status bar fills with its color (text auto-contrasted); the other window is unaffected.
  3. Window Color: Clear Color → status bar reverts to stock theme.
  4. Quit and relaunch → restored windows keep their colors.
  5. Switch tranquil-business-dark-light → the accent re-picks its variant.