Docs

ADR-0011: A first-class Tranquil settings tab

Status: Active · Date: 2026-07-19

References

Context

Tranquil had no home for user-adjustable settings. The immediate trigger was a papercut: corner notification toasts vanished too fast (~1s). The obvious suspect was a hard-coded TOAST_TIMEOUT = 1000 in Tranquil’s own notify() helper (duplicated in tranquil-automations/lib/notify.js and tranquil-browser/lib/notify.js). But the actual cap turned out to be a stray global handler in tranquil-browser that force-dismissed every info toast after a hard-coded 1000ms (registered once per browser tab) — it overrode any per-toast timer. Both had to go; the constant needed to become a user setting, and the global 1s dismiss had to be removed.

Rather than just bump a constant, we want a durable place for this and future preferences — and it should live where users already look for settings: the Pulsar settings-view pane, as a proper top-level tab next to Core / Editor / Keybindings, not buried under Packages.

Two constraints shape the approach. First, settings-view exposes no service to register a top-level panel — the built-in tabs are added by an internal addCorePanel(name, icon, factory) method on the live SettingsView instance. Second, the third-party-package rule: we do not modify settings-view (or notifications) source. What we do have: settings-view is a workspace pane item (so atom.workspace.observePaneItems can reach each instance), its SettingsPanel class auto-renders config-bound controls for a whole namespace, and atom.config.setSchema lets us register an arbitrary namespace.

Decision

Tranquil owns a top-level “Tranquil” tab in settings-view, built entirely from owned code. On activation, tranquil-config (a) registers a tranquil.* config namespace via atom.config.setSchema, and (b) observes workspace pane items; for each SettingsView instance it calls the instance’s own addCorePanel('Tranquil', 'gear', factory). The factory wraps core’s SettingsPanel({ namespace: 'tranquil' }) in a minimal .panels-item shell satisfying the element/show/focus/destroy panel contract (mirroring core’s general-panel.js). Reusing SettingsPanel gives two-way atom.config binding for free, and — because it emits the same markup as Core/Editor — the business themes style the whole pane once and the Tranquil tab inherits it.

  • First setting — tranquil.toastDuration (integer milliseconds, default 3000). It is stored in ms (what the toasts read) but edited in whole seconds through a friendlier UI (see the sub-decision below). The two notify() helpers read this key instead of the hard-coded constant; the browser helper keeps a longer floor for errors/warnings so they stay readable.
  • Removed the global 1s force-dismiss. The onDidAddNotification handler in tranquil-browser that killed all info toasts at 1000ms is deleted — this is the real fix for the papercut; without it no toast setting could take effect.
  • No schema minimum/maximum. settings-view clamps integer fields to the schema bounds on every keystroke and rewrites the input, making a value impossible to type. We omit the bounds and enforce a sane floor where the value is consumed (the bridge clamps to the notifications minimum).
  • A native number input, not the auto-generated mini-editor. For this key the factory replaces SettingsPanel’s atom-text-editor[mini] with a native <input type="number"> (whole seconds, up/down steppers) bound to config, plus a “Test” button that previews a toast at the current value.
  • One-way bridge: tranquil-config observes tranquil.toastDuration and writes it to notifications.defaultTimeout (clamped to that key’s minimum), so the handful of direct atom.notifications.add* callers (tranquil-drag-drop, tranquil-window-color) honor the same setting — without editing the notifications package.
  • Settings-pane theming: styles/settings.less in both business themes is expanded to cover the full surface and widgets (inputs, checkboxes, radios, selects, buttons/.btn-primary, badges, breadcrumb, alerts, cards), matching the main-view.html design-system specs, so the pane — and the Tranquil tab — reads as Tranquil, not stock Pulsar.

Options considered

  • Option A: addCorePanel on the live instance + reuse SettingsPanel + tranquil.* namespace (chosen) — a genuine top-level tab, free config binding, auto-themed, extensible (future settings just extend the schema). Cost: leans on settings-view internals (addCorePanel, panelCreateCallbacks, the SettingsPanel module path). Mitigated by lazy-requiring SettingsPanel by resolved package path (only when the tab first opens), duck-typing the SettingsView instance, and guarding idempotency — a missing/moved internal degrades to “no Tranquil tab”, never a crash.
  • Option B: declare a configSchema and rely on the automatic per-package page under Packages — zero UI code, reuses SettingsPanel too. Rejected: it is not a top-level tab; the settings are buried under atom://config/packages/tranquil-config and read as a package’s internals, not a first-class Tranquil surface.
  • Option C: fork/modify settings-view to add a native Tranquil tab — cleanest integration. Rejected: violates the no-third-party-edits rule and forks a large upstream package for one tab.
  • Option D: build a bespoke settings pane/UI from scratch — full control. Rejected: reinvents SettingsPanel’s config binding and would need its own theming, all for no gain over reuse.
  • Setting sub-decision — own tranquil.toastDuration + bridge (chosen) vs. binding the UI directly to notifications.defaultTimeout — a clean tranquil.* namespace keeps future settings coherent and lets one control drive both the helper toasts and the core path; the one-line bridge avoids editing the notifications package.
  • Control sub-decision — native <input type="number"> in seconds (chosen) vs. the core atom-text-editor[mini] in raw ms — the mini-editor showed raw milliseconds (unfriendly for a stepper), rendered in the monospace editor font, collapsed in flex layouts, and was subject to the keystroke-clamp above. A native number input gives a whole-seconds value with real up/down steppers, stores ms behind the scenes, and matches the design system. Cost: it is bespoke (not auto-rendered), so it carries a little glue code and the native-input caveats in Consequences.

Consequences

  • Extensible for free: future simple Tranquil settings are just new properties on the tranquil schema — they auto-render in the tab and auto-theme, no per-setting UI or CSS. The number input + Test button are the one bespoke exception, kept in the panel factory.
  • Native-input caveats (documented): a native <input> in the workspace needs the native-key-bindings class (or Backspace/Cmd-A are swallowed), needs color-scheme set (or its spin buttons are invisible on a dark surface), and cannot render ::before/::after (so themed checkboxes draw their tick via a background-image SVG). These are collected in the Settings UI & Form Widgets guide.
  • Live, no restart (for the value): notify() reads the config per-toast, so changing Toast Duration takes effect immediately. But schema/JS/DOM-factory changes need a full window reload — a stylesheet reload updates CSS only.
  • Internal-coupling risk: if upstream settings-view renames addCorePanel, changes panelCreateCallbacks, or moves lib/settings-panel, the tab silently disappears (guarded, not fatal). Re-evaluate if we pull a major settings-view update.
  • Theme duplication: the expanded settings.less must stay identical in both business themes (they differ only in colors.less) — the standing theme-sync obligation.
  • Re-evaluation triggers: settings-view exposing a real panel-registration service (adopt it); a settings-view major upgrade; or the tranquil.* schema growing enough to warrant grouping/sections.

Validation

Verified live: a Tranquil tab (gear) appears in the settings nav at atom://config/tranquil with a themed Toast Duration control (whole-seconds number field + steppers + Test button); editing it persists ms to ~/.tranquil/config.cson; the Test button and real notify() toasts both honor the value now that the browser 1s force-dismiss is gone; error/warning browser toasts still linger longer; the pane reads correctly in both business light and dark. (See the delivery notes and widgets guide linked at the top for implementation notes and the reusable UI gotchas.)