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.
| Piece | State |
|---|---|
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.themesis 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 viaatom.getLoadSettings().windowSessionId; injects:root { --tq-window-accent: <hex> }under a stablesourcePath(re-inject replaces in place); one command per preset (set-<key>) plusclear-color; and observers ontranquil-window-color.assignments(re-apply) andcore.themes(re-pick light/dark). No external dependencies.lib/presets.js— 8 presets, each with adark/lighthex.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/-light—status-bar.less(identical; the two packages differ only incolors.less).tranquil-theme-dark—status-bar.less;tranquil-theme-light— the.status-barblock inbase.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.themeSourceis 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-listfor a modal picker,required at the top ofmain.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’snode_modulesfor 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 onlyatom, its ownlib, or a dependency it actually vendors in its ownnode_modules. addStyleSheetre-inject: disposing the previous disposable before re-adding under the samesourcePathkeepsstyleElementsBySourcePathclean (StyleManager.removeStyleElementdeletes the map entry on dispose), so updates replace rather than stack.- A new owned package needs its
node_modulessymlink, not just the dev/packages one.yarn startloads owned packages asbundledPackage: truefromtranquil-client/node_modules/<pkg>(a../../<pkg>symlinkyarn installcreates from thelink:../dep). The~/.tranquil/dev/packages/<pkg>symlink alone does not load it. Symptom:getActivePackagereturns a package withpathundernode_modules/butmetadata: {name}only,mainModule: null,mainActivated: false— Atom stubbed it because the node_modules path didn’t exist, soactivate()never ran. Fix without a full reinstall:ln -s ../../<pkg> node_modules/<pkg>, then restart.
Verify
nvm use && yarn start; open two windows.- 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.
- Window Color: Clear Color → status bar reverts to stock theme.
- Quit and relaunch → restored windows keep their colors.
- Switch
tranquil-business-dark↔-light→ the accent re-picks its variant.