367 lines
18 KiB
Markdown
367 lines
18 KiB
Markdown
# PRODUCT.md — Per-tab theme overrides driven by directory and launch configurations
|
|
|
|
Issue: https://github.com/warpdotdev/warp/issues/478
|
|
Related: https://github.com/warpdotdev/warp/issues/2618 (set warp theme in launch configuration)
|
|
|
|
## Summary
|
|
|
|
The Warp theme is a single global value today (`appearance.themes.theme` in
|
|
`settings.toml`); switching it affects every open tab at once. Users have asked
|
|
for years (`#478`, 55+ upvotes; `#2618`) for tabs to render with different
|
|
themes when they represent different contexts — different projects, local vs.
|
|
remote machines, production vs. development.
|
|
|
|
This spec covers a focused first cut of per-tab theme overrides driven by
|
|
**three** sources, in priority order:
|
|
|
|
1. A user-visible **manual** override on a tab (set via the launch
|
|
configuration YAML or via a right-click menu).
|
|
2. A **directory-pattern** auto-match: the user maps directory paths to
|
|
themes in `settings.toml`, and tabs whose active pane's cwd matches a
|
|
pattern render with the mapped theme. This is the path most users in the
|
|
issue thread describe (`pyronaur`, `janderegg`, `milopersic`): "I `cd`
|
|
into project A, my theme should change."
|
|
3. A **launch-configuration window-level default** that themes every tab a
|
|
given launch configuration opens unless the tab itself has another
|
|
override.
|
|
|
|
The global theme remains the fallback when none of the three sources apply.
|
|
Window chrome (title bar, sidebar, settings views, the tab strip) continues
|
|
to follow the global theme so windows holding mixed-theme tabs remain
|
|
visually coherent at the window level.
|
|
|
|
This spec deliberately scopes out automatic theming triggered by SSH host,
|
|
hostname, runtime escape codes, or shell hooks. Those appear in `#478`
|
|
discussion and are listed as follow-ups that consume the override field this
|
|
spec introduces.
|
|
|
|
Figma: none provided.
|
|
|
|
## Goals / Non-goals
|
|
|
|
In-scope surfaces:
|
|
|
|
- A new settings map `appearance.themes.directory_overrides` whose keys are
|
|
directory paths (tilde-expanded) and whose values are theme identifiers.
|
|
Matching uses longest-prefix-wins.
|
|
- The launch-configuration YAML schema gains an optional `theme:` field at
|
|
the tab level and at the window level.
|
|
- A persisted per-tab override that survives session restore.
|
|
- A right-click tab menu entry for **Pin theme** (manually override the
|
|
active tab's theme to a chosen theme) and **Reset theme** (clear a
|
|
manual override; cwd-pattern matching may then reapply).
|
|
|
|
Out of scope:
|
|
|
|
- SSH-host-driven, hostname-driven, or `whoami`-driven theming
|
|
(`stevenchanin`, `pyronaur`, `zethon`, `janderegg`).
|
|
- Escape-code or shell-hook protocols for runtime theme switching
|
|
(`yatharth`, for Claude-Code session signaling).
|
|
- Per-pane theming. Panes inside a tab continue to share one theme.
|
|
- Per-tab wallpaper or graphics (`scottaw66`, `SheepDomination`).
|
|
- Changes to the global theme storage path
|
|
(`appearance.themes.theme`), the theme picker UI, or custom-theme loading.
|
|
Overrides reuse the existing theme identifier type.
|
|
|
|
## Resolution order
|
|
|
|
A tab's effective theme is determined by walking these layers and returning
|
|
the first hit:
|
|
|
|
1. **Menu pin**, if any. Set by the right-click "Pin theme" menu and
|
|
cleared by the right-click "Reset theme" menu. Persists across
|
|
sessions.
|
|
2. **Launch-configuration manual pin**, if any. Set by a tab-level
|
|
`theme:` in the launch configuration that opened (or restored) the
|
|
tab. Cleared only by an explicit "Forget launch config theme" menu
|
|
entry — `Reset theme` does **not** clear this layer (per Zach's v4
|
|
review). Persists across sessions.
|
|
3. **Directory match**, if any. The active pane's current working
|
|
directory is matched against `appearance.themes.directory_overrides`;
|
|
if a key is a prefix of the cwd, the longest such key wins.
|
|
4. **Launch-configuration window-level default**, if the tab was opened
|
|
from a launch configuration with a window-level `theme:` and no
|
|
closer override applies. Persists across sessions.
|
|
5. **Global theme** as derived from `ThemeSettings` and the system theme,
|
|
exactly as today.
|
|
|
|
If none of the override sources resolve to a known theme, behavior is
|
|
bit-for-bit identical to today.
|
|
|
|
## Behavior
|
|
|
|
### Directory-pattern overrides
|
|
|
|
1. `settings.toml` accepts a new section
|
|
`[appearance.themes.directory_overrides]` whose entries map a directory
|
|
path to a theme identifier. Example:
|
|
|
|
```toml
|
|
[appearance.themes.directory_overrides]
|
|
"~/Work/medone" = "Dark City"
|
|
"~/Work/bondwise" = "Solarized Dark"
|
|
"~/Work/checkpt" = "Dracula"
|
|
```
|
|
|
|
Theme values are the same string form accepted by the global
|
|
`appearance.themes.theme` (the parser tolerates both display names like
|
|
`"Dark City"` and snake-case like `"dark_city"`; see #14 below).
|
|
|
|
2. Keys are tilde-expanded to absolute paths at match time. Trailing
|
|
slashes are normalized away. Symlinks are not resolved — the cwd as
|
|
the shell reports it is what's matched. Path normalization rules,
|
|
per platform:
|
|
|
|
- **Linux:** matching is case-sensitive (matches the filesystem
|
|
semantics on standard ext4/xfs). Path separators are `/`.
|
|
- **macOS:** matching is case-insensitive (matches the default
|
|
HFS+/APFS case-insensitive setting). Path separators are `/`.
|
|
- **Windows:** matching is case-insensitive. Both `/` and `\` are
|
|
accepted as separators in `directory_overrides` keys and are
|
|
normalized internally to a single canonical form before
|
|
comparison. Drive letters are normalized to uppercase (`c:\Work`
|
|
and `C:\Work` are equivalent keys; the second-written one wins
|
|
per TOML duplicate-key semantics). Tilde expands to
|
|
`%USERPROFILE%`.
|
|
|
|
Component-boundary matching (the rule in the previous paragraph
|
|
that prevents `~/Work/medone` from matching `~/Work/medone-archive`)
|
|
uses the platform's path-component definition — on Windows that
|
|
means the boundary follows either `\` or `/` after normalization.
|
|
|
|
3. Match resolution: a key matches if it is a prefix of the active pane's
|
|
cwd at a path-component boundary. `~/Work/medone` matches both
|
|
`~/Work/medone` and `~/Work/medone/apps/admin-api`, but does **not**
|
|
match `~/Work/medone-archive` (no component boundary). When multiple
|
|
keys match, the longest one wins (most specific).
|
|
|
|
4. The cwd evaluated for a tab is the cwd of the **focused pane** in that
|
|
tab. A tab whose focused pane is in a non-shell context (notebook,
|
|
settings view, etc.) has no cwd and falls through directory matching.
|
|
|
|
5. When a tab's active pane changes cwd (because the user ran `cd`, opened
|
|
a subdirectory, or moved focus to a pane in a different cwd), directory
|
|
matching re-runs. If the new cwd matches a different key, the tab
|
|
immediately re-renders with the new theme. If it matches no key, the
|
|
tab falls through to the next layer in the resolution order.
|
|
|
|
6. Adding, editing, or removing entries in `directory_overrides` while
|
|
Warp is running re-evaluates every open tab. Tabs whose effective theme
|
|
changes redraw; tabs whose effective theme is unchanged do not.
|
|
|
|
7. A theme name in `directory_overrides` that does not resolve to a known
|
|
theme is treated the same way an unknown launch-configuration theme is
|
|
(#11): a warning is logged identifying the offending key, the entry is
|
|
skipped for matching purposes, and the rest of the map continues to
|
|
work.
|
|
|
|
7a. **`directory_overrides` is stored locally and never synced to Warp's
|
|
cloud.** Directory paths can encode employer, customer, and project
|
|
names (`~/Work/<client>/<engagement>/...`); cloud-syncing the keys
|
|
would push that organizational context off-machine. Users on
|
|
multiple machines who want shared themes today set them per-machine.
|
|
An opt-in cloud-sync mode is a candidate follow-up. The global
|
|
theme setting (`appearance.themes.theme`) and per-tab pins set via
|
|
the right-click menu remain user-controllable surfaces; only this
|
|
map is local-only.
|
|
|
|
7b. **Diagnostic output never contains raw `directory_overrides` keys.**
|
|
Local logs are routinely shared with Warp support and copied into
|
|
bug reports, so even local diagnostics can leak path keys. The
|
|
invariant is: any warning or telemetry emitted by the
|
|
`directory_overrides` machinery refers to an offending entry by a
|
|
short non-cryptographic hash of its key plus the offending value
|
|
(the theme name, which is non-sensitive). For example, a warning
|
|
that today might say `directory_overrides: "~/Work/AcmeCorp/2026":
|
|
unknown theme "Drakula"` instead reads
|
|
`directory_overrides[hash=8a3f9c]: unknown theme "Drakula"`. The
|
|
user can grep their `settings.toml` for the bad theme name to find
|
|
the offending row. Hashes are stable for the same key but contain
|
|
no path information.
|
|
|
|
### Launch-configuration overrides
|
|
|
|
8. A launch configuration YAML may include `theme:` on any tab entry.
|
|
Accepted values are theme identifiers in the form documented in #14.
|
|
Omitting the field leaves the tab to fall through the resolution order.
|
|
|
|
9. A launch configuration YAML may include `theme:` on any window entry.
|
|
Tabs in that window with no tab-level `theme:` and no directory match
|
|
inherit this window-level value (per the resolution order: #3 sits
|
|
below #2).
|
|
|
|
10. Saving a window's current state to a launch configuration preserves
|
|
every theme override that was *not* derived from directory matching,
|
|
so that reopening the saved configuration produces the same effective
|
|
themes (modulo cwd matching, which re-runs against the current
|
|
`directory_overrides`). Specifically, for each tab the *preserved
|
|
override* is the first non-empty value of, in priority order: the
|
|
tab's menu pin (layer #1), the tab's launch-configuration manual
|
|
pin (layer #2), or the tab's window-level default (layer #4). Tabs
|
|
whose effective theme came only from directory matching (layer #3)
|
|
have no preserved override. The save rule is then:
|
|
|
|
- If every tab in the window has the same preserved override
|
|
`Some(X)` and no tab has a manual pin different from the others,
|
|
the saved YAML emits a single window-level `theme: X` and no
|
|
per-tab `theme:` fields.
|
|
- Otherwise, each tab whose preserved override is `Some(Y)` emits a
|
|
per-tab `theme: Y`; tabs with no preserved override emit no
|
|
`theme:` field; window-level `theme:` is omitted.
|
|
- Directory-matched themes never appear in saved YAML — directory
|
|
matching is settings-level and re-applies on next open.
|
|
|
|
This means a launch configuration that originally opened with a
|
|
window-level `theme:` round-trips correctly: every tab inherited
|
|
the value into its `window_default` slot, the save rule sees a
|
|
shared preserved override, and emits the same window-level
|
|
`theme:` again.
|
|
|
|
### YAML / settings format
|
|
|
|
11. An unknown theme identifier — anywhere it appears — never causes a
|
|
file-level load failure. The deserializer accepts any string; the
|
|
resolver runs at apply time and falls back to the next resolution
|
|
layer for the affected entry only. Other tabs in the same launch
|
|
configuration, and other entries in the same `directory_overrides`
|
|
map, are unaffected. Each unknown name produces exactly one logged
|
|
warning per load.
|
|
|
|
12. Custom themes referenced by an override behave identically to custom
|
|
themes used as the global theme — loaded from the user's themes
|
|
directory, fail-soft to the next layer if the file is missing, same
|
|
trust/validation rules as the global theme loader.
|
|
|
|
13. Overrides persist per-tab through session restore, by source:
|
|
- **Menu pin** (layer #1) — kept on relaunch.
|
|
- **Launch-configuration manual pin** (layer #2) — kept on
|
|
relaunch. The launch configuration that set it is not necessarily
|
|
reopened on restore, so the value travels with the tab.
|
|
- **Launch-configuration window-level default** (layer #4) — kept
|
|
on relaunch, same reason as #2.
|
|
- **Directory match** (layer #3) — not stored; recomputed on
|
|
relaunch from the current `directory_overrides` and the restored
|
|
cwd. Editing `directory_overrides` between sessions therefore
|
|
takes effect on the next launch.
|
|
|
|
14. The accepted form for any theme reference (in
|
|
`directory_overrides`, in launch-config tab `theme:`, in launch-config
|
|
window `theme:`) is a single string. Both the human-readable display
|
|
form (`"Dark City"`, `"Solarized Dark"`, `"Dracula"`) and the
|
|
snake-case form (`"dark_city"`, `"solarized_dark"`, `"dracula"`) are
|
|
accepted. Matching is case-insensitive on whitespace-stripped input.
|
|
Custom themes are referenced by their custom-theme name, same as
|
|
today's global setting.
|
|
|
|
### Rendering scope
|
|
|
|
15. When a tab has an effective override (from any layer), the override
|
|
applies to: the terminal cell foreground/background, the ANSI 16-color
|
|
palette used by the terminal grid, and any in-tab UI surfaces whose
|
|
colors are derived from the active theme (block backgrounds, command
|
|
output styling, accent colors). The window chrome (title bar, sidebar,
|
|
settings views, the tab strip itself) continues to follow the global
|
|
theme.
|
|
|
|
16. Switching tabs is instant — no flash, no progressive paint. Only the
|
|
rendering of the newly-active tab reflects its (possibly different)
|
|
theme; inactive tabs do not redraw on switch.
|
|
|
|
17. Changing the global theme updates every tab whose effective theme
|
|
falls through to the global layer. Tabs with overrides at any
|
|
higher-priority layer are unaffected.
|
|
|
|
### User affordances
|
|
|
|
18. The right-click tab context menu gains three entries, alongside the
|
|
existing per-tab attributes:
|
|
- **Pin theme...** — opens a submenu listing available themes.
|
|
Choosing one sets a *menu pin* on the tab (resolution layer #1),
|
|
which wins over every other layer including a launch-config
|
|
manual pin. Visible at all times.
|
|
- **Reset theme** — clears only the menu pin (layer #1). The tab
|
|
falls through to the launch-config manual pin / directory match /
|
|
window default / global layers in that order. Visible only when
|
|
the tab has a menu pin.
|
|
- **Forget launch config theme** — clears only the launch-config
|
|
manual pin (layer #2). The tab falls through to the directory
|
|
match / window default / global layers. Visible only when the
|
|
tab has a launch-config manual pin.
|
|
|
|
Splitting "Reset theme" from "Forget launch config theme" means a
|
|
user who pinned a different theme via the menu can clear that pin
|
|
without unintentionally also discarding what their launch
|
|
configuration originally set. (Per Zach's v4 review.)
|
|
|
|
19. The existing per-tab `color:` field on a tab template (the small
|
|
colored indicator next to the tab title) is independent of the new
|
|
theme override. Both can be set; both are honored.
|
|
|
|
20. The feature applies on every supported platform (macOS, Linux,
|
|
Windows). It is **gated behind a feature flag** named
|
|
`appearance.themes.per_tab_overrides` (per Zach's v4 review):
|
|
- Initial release ships with the flag **off** in stable and **on**
|
|
in dev/preview, so the rollout can soak with internal users
|
|
before reaching the broader install base.
|
|
- When the flag is off the feature is invisible: the right-click
|
|
menu entries are hidden, `directory_overrides` matching does not
|
|
run (the settings group is still parseable so users who set up
|
|
the map under preview do not lose data), and launch-configuration
|
|
`theme:` fields are deserialized but ignored. The user-visible
|
|
behavior is bit-for-bit identical to today.
|
|
- The default flips to on in stable in a follow-up release, after
|
|
telemetry on the preview/dev cohort confirms no regressions in
|
|
render performance, settings parsing, or session restore.
|
|
- An empty `directory_overrides` map (the default) plus no
|
|
launch-config theme fields plus no pinned themes — even with the
|
|
flag on — is bit-for-bit identical to current behavior.
|
|
|
|
### Accessibility
|
|
|
|
21. The override does not change any text content, accessible labels,
|
|
or focus order. Screen readers continue to report tab titles and
|
|
contents identically. The three new right-click menu entries have
|
|
accessible labels "Pin theme" (with a submenu of theme names),
|
|
"Reset theme", and "Forget launch config theme"; each is announced
|
|
as a menu item, and each follows the menu's existing visibility
|
|
rules (Reset theme appears only when the tab has a menu pin;
|
|
Forget launch config theme appears only when the tab has a
|
|
launch-config manual pin).
|
|
|
|
## User-visible failure modes
|
|
|
|
- **Unknown theme name** — anywhere it appears, the entry is skipped at
|
|
apply time, a one-line warning is written to the Warp log identifying
|
|
the source (launch configuration filename + tab title or index for
|
|
launch-config sources; **redacted entry identifier** for
|
|
`directory_overrides` sources, never the raw path key — see #7b),
|
|
and the rest of the configuration loads normally.
|
|
- **Custom theme file missing** — same fallback the global theme uses
|
|
today: tab opens with the next-layer theme, warning logged.
|
|
- **Two `directory_overrides` keys are equivalent after tilde expansion**
|
|
— last-write-wins per TOML semantics; a warning is logged identifying
|
|
the duplicate.
|
|
- **A pane's cwd is unavailable** (non-shell pane content) — that pane
|
|
contributes no cwd to directory matching; if it is the focused pane the
|
|
tab falls through to the window/global layers.
|
|
|
|
## Open questions
|
|
|
|
(Resolved in v4 review and folded into the spec — kept here as a record
|
|
of the design choices made.)
|
|
|
|
- ~~`directory_overrides` keys: glob vs. prefix matching.~~ Resolved:
|
|
prefix matching only (Zach v4: "i think prefix-match is fine to
|
|
start"). Globs are a follow-up.
|
|
- ~~"Reset theme" scope: clear all manual layers, or only menu pins.~~
|
|
Resolved: only menu pins (Zach v4: "i don't think it should" clear
|
|
launch-config pins). A separate "Forget launch config theme" entry
|
|
exists for the launch-config layer. Reflected in behaviors #18 and
|
|
in the resolution order.
|
|
- Should saving a launch configuration emit `directory_overrides`
|
|
entries so themes travel with the launch config? Spec keeps the two
|
|
surfaces independent — launch configs carry only manual pins;
|
|
directory matching is settings-level and does not round-trip through
|
|
saved launch configs. A future "shareable theme bundle" could
|
|
compose them. Open for product-team input.
|