update
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-07-13
|
||||
@@ -0,0 +1,160 @@
|
||||
## Context
|
||||
|
||||
The user runs jpporta-nixos (Hyprland on Wayland) and jpporta-deck (a OrangePi running cage + foot as a "writer deck"). They maintain 11 hand-curated palettes under `~/.config/colorschemes/<theme>/` with per-app config files (hyprland, waybar, swaync, wlogout, kitty, ghostty, nvim, rofi, gtk-4.0, plus a `wallpapers/` directory). Currently:
|
||||
|
||||
- **Hyprland, awww, waybar, kitty, ghostty** are reachable from the themed dirs (with manual edits).
|
||||
- **swaync, wlogout, alacritty** have their colors hardcoded as Nix strings in `modules/home-manager/*/default.nix`, so swapping themes requires `home-manager switch`.
|
||||
- **darkman** runs a dark/light switch (geolocation-based) that flips GTK theme and hyprsunset temperature — its `gtk-theme` step overlaps with what theme-switch will own.
|
||||
- **No canonical "current theme" pointer** exists. The `~/.config/colorschemes/wallpapers` symlink is a one-off pointing at gruvbox-dark.
|
||||
- **No live propagation contract** — every themed app needs its own reload mechanism (file watch, IPC, signal, or restart-on-next-launch).
|
||||
|
||||
The user wants: one command to swap themes, no rebuild, every themed app reflects the new palette, dark/light mode is owned by the theme (not by darkman), darkman keeps doing what it's good at (time-of-day temperature), and the writer deck can adopt the same workflow.
|
||||
|
||||
Constraints discovered during exploration:
|
||||
|
||||
- All per-theme assets must remain editable in `~/.config/colorschemes/<theme>/` without rebuild — this is the precedent set by the `awww` module (reads wallpaper dir live, never copies to store).
|
||||
- Theme colors live in dotfiles, not Nix. Anything baked into Nix is a second source of truth and must be removed.
|
||||
- The `.active` symlink pattern is preferred over a sentinel file because it's atomic (`ln -sfn`) and serves as the directory apps follow directly.
|
||||
- Darkman should keep its sunset/sunrise logic for `hyprsunset`, but stop touching GTK (theme owns that).
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Atomic theme swap with one shell command (`theme-switch <name>`) — no Nix rebuild, no per-app scripting by the user.
|
||||
- Single canonical pointer (`~/.config/colorschemes/.active` symlink) that every themed app can follow.
|
||||
- Per-app live reload: each app gets exactly the reload signal it natively supports, scripted in one place.
|
||||
- gsettings `color-scheme` flip driven by `meta` (theme declares its mode).
|
||||
- Hook point for user-owned wallpaper logic (`~/.config/colorschemes/.hook <theme-name>`), exec'd at the end of every switch.
|
||||
- Boot-time application: a systemd user oneshot re-applies `.current` on graphical-session start so login is consistent.
|
||||
- Rofi-driven picker (`theme-picker`) wired to a Hyprland keybind (`SUPER+T`).
|
||||
- Same module works on the writer deck; foot has no live reload but a relaunch in cage is fine.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Generating palettes from images (this is what matugen/pywal do — user wants named curated palettes).
|
||||
- Per-app templating engines. Each app already has its own config format; we hand-write the swap for each.
|
||||
- Supporting themes that live outside `~/.config/colorschemes/`.
|
||||
- Backwards compatibility for the existing `~/.config/colorschemes/wallpapers` symlink (it becomes redundant once `.active/wallpapers/` exists; the script handles the migration).
|
||||
- Replacing darkman. darkman stays for `hyprsunset` time-of-day control.
|
||||
- Auto-detecting new themes. The `meta` file must exist per theme; the user adds them by hand.
|
||||
- Adding new external dependencies (no new packages).
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1. `.active` symlink as the single source of truth, not a sentinel file
|
||||
|
||||
**Choice:** `~/.config/colorschemes/.active` is a symlink pointing at the current theme's directory. A `.current` text file mirrors the name for tools that can't follow symlinks (some app loaders, the boot-time applier).
|
||||
|
||||
**Why:** Atomic swap with one syscall (`ln -sfn`). Apps that support symlink-following read `~/.config/colorschemes/.active/<app>/` directly. Apps that need a name read `.current`. Both can coexist; the cost is two writes per switch.
|
||||
|
||||
**Alternatives considered:**
|
||||
- *Plain `.current` text file only*: requires every app to maintain its own per-app link/symlink. Doesn't scale.
|
||||
- *Environment variable*: lost across reboots, hard to introspect from running processes, fights with systemd user services.
|
||||
- *dconf / gsettings schema*: forces every app into a GTK-ish world; waybar/kitty/ghostty/hyprland don't read it.
|
||||
|
||||
### D2. Per-app reload handled inside `theme-switch`, not via inotify watchers
|
||||
|
||||
**Choice:** `theme-switch` is the orchestrator. It runs each per-app reload step explicitly. No daemon watches `~/.config/colorschemes/.active` for changes.
|
||||
|
||||
**Why:** Hyprland auto-watches its config (free). Other apps don't, and inventing an inotify daemon to *poke* apps adds a new failure surface. The user's stance: "if the app watches its own config, fine; otherwise, send the signal." That's exactly what `theme-switch` does.
|
||||
|
||||
**Alternatives considered:**
|
||||
- *inotifywait daemon*: one more systemd service, one more race condition, no benefit over the explicit script.
|
||||
- *DBus signal + per-app subscribers*: significant scaffolding for what amounts to ~10 commands.
|
||||
|
||||
### D3. Theme metadata via per-theme `meta` file, not Nix
|
||||
|
||||
**Choice:** Each theme gets a one-line `~/.config/colorschemes/<theme>/meta` file with `name=Display Name` and `mode=dark|light` (KEY=value, simple parser).
|
||||
|
||||
**Why:** Keeps the colorscheme dir fully self-describing. The `theme-picker` script reads these for its rofi menu. `theme-switch` reads the mode field to flip gsettings. No Nix expression, no rebuild.
|
||||
|
||||
**Alternatives considered:**
|
||||
- *Filename convention (`gruvbox-dark/`, `e-ink-light/`) — infer mode from name*: fragile, requires a registry.
|
||||
- *First-line comment in `colors.conf`*: mixes concerns (colors.conf is for hyprland, not for metadata).
|
||||
|
||||
### D4. Cleanup pass on swaync/wlogout/alacritty is required, not optional
|
||||
|
||||
**Choice:** Remove the hardcoded gruvbox colors from the three Nix modules. They become thin wrappers that `xdg.configFile` the active theme dir via out-of-store symlinks (same pattern as the existing `nvim` module).
|
||||
|
||||
**Why:** Without this, three of the user's themed apps simply won't switch (their colors are baked at build time). The cleanup is a one-time `home-manager switch`; theme switches thereafter are runtime only.
|
||||
|
||||
**Alternatives considered:**
|
||||
- *Leave Nix modules alone, accept that swaync/wlogout/alacritty don't switch*: violates the user's goal of "every app picks up the new theme."
|
||||
- *Template them at activation time*: requires `home-manager switch` on every theme change — explicitly rejected.
|
||||
|
||||
### D5. Darkman narrowed to hyprsunset only
|
||||
|
||||
**Choice:** `darkman` stays enabled. Its scripts lose the `gtk-theme` step (theme-switch owns that now) and keep the `hyprsunset` step (time-of-day temperature). `theme-switch` does not poke darkman.
|
||||
|
||||
**Why:** Theme owns *which palette* (dark vs light). Darkman owns *when to warm the screen* (sunset/sunrise). They're orthogonal and shouldn't fight. Conflating them — e.g., theme-switch telling darkman what mode to be in — creates two writers for the same gsettings keys.
|
||||
|
||||
**Alternatives considered:**
|
||||
- *Theme-switch sets the darkman mode file*: simple but couples two systems that don't otherwise need to know about each other.
|
||||
- *Kill darkman*: loses the automatic sunset temperature shift, which the user said they want to keep.
|
||||
|
||||
### D6. Wallpaper hook is user-owned, executed at end of switch
|
||||
|
||||
**Choice:** If `~/.config/colorschemes/.hook` exists and is executable, `theme-switch` runs it as `~/.config/colorschemes/.hook <theme-name>` at the very end of the switch. If it doesn't exist, the switch still completes.
|
||||
|
||||
**Why:** Wallpaper policy is personal (random pick? curated list? match a subdirectory?). The change shouldn't ship a wallpaper-rotation script the user didn't ask for. The hook is a single, documented extension point.
|
||||
|
||||
**Alternatives considered:**
|
||||
- *Bundle a default random-wallpaper script in the Nix module*: scope creep, and the user explicitly said "you can leave that part to me."
|
||||
- *awww integration baked into theme-switch*: same as above; also, awww already has its own `awww-cycle` systemd timer that picks from `~/Wallpapers`, and the `wallpapers` symlink today points at the theme's dir — so all theme-switch needs to do is repoint the symlink.
|
||||
|
||||
### D7. Boot-time re-apply via systemd user oneshot
|
||||
|
||||
**Choice:** `theme-switch` (no args) is idempotent: it reads `.current` and re-applies. A `theme-apply.service` systemd user oneshot is `WantedBy=graphical-session.target` and runs it on session start.
|
||||
|
||||
**Why:** Ensures the desktop matches `.current` after login even if a previous session crashed mid-switch. Cheap (the script no-ops if the symlink already points there).
|
||||
|
||||
**Alternatives considered:**
|
||||
- *Home Manager activation hook*: runs only on `home-manager switch`, not on plain reboots.
|
||||
- *Hyprland autostart command*: works but mixes init logic into compositor config; systemd target is cleaner.
|
||||
|
||||
### D8. Rofi picker is a thin wrapper, not a replacement for the CLI
|
||||
|
||||
**Choice:** `theme-picker` is a small script that reads each theme's `meta`, pipes them through `rofi -dmenu`, and `exec`s `theme-switch` on the chosen name. The CLI is the source of truth; the picker is sugar.
|
||||
|
||||
**Why:** Scriptability (cron, hyprland keybind action, shell aliases) belongs in the CLI. The picker is one Hyprland keybind away.
|
||||
|
||||
**Alternatives considered:**
|
||||
- *Waybar module*: cute but adds a waybar dependency to a feature that doesn't need it.
|
||||
- *TUI (fzf, gum)*: extra dependency, no gain.
|
||||
|
||||
### D9. Writer deck adopts the same script, with foot handled as "relaunch on switch"
|
||||
|
||||
**Choice:** Same Nix module imports into `hosts/writter-deck/home.nix`. The `theme-switch` script on the deck has a foot-specific step: `pkill foot && foot --server &` (foot's server-mode makes this near-instant for a writer deck).
|
||||
|
||||
**Why:** Zero new concepts on the deck. The user's deck is already gruvbox; this just lets them pick another.
|
||||
|
||||
**Alternatives considered:**
|
||||
- *Separate `theme-switch-foot` script*: duplication for one app.
|
||||
- *Skip the deck*: explicit non-goal of the user — they want it if cheap.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **Atomicity of multi-app reload** → Mitigation: order the steps so data lands before signals (`hyprland` config swap is just a file rewrite; signals come after). If one app fails mid-sequence, the user can re-run `theme-switch <name>` — it's idempotent.
|
||||
|
||||
- **Hyprland's `source =` directive points at a path that must exist before hyprland reads it** → Mitigation: on first install, the activation script creates `.active` from `custom.theme.current` *before* hyprland starts. Hyprland only loads on graphical-session start (after `theme-apply.service`).
|
||||
|
||||
- **Waybar `@import` and symlink interplay** → Mitigation: waybar's `style.css` will `@import` from `themes/current.css` (a symlink the script updates); SIGUSR2 forces re-eval.
|
||||
|
||||
- **nvim running sessions won't recolor** → Mitigation: the script tries `nvim --remote-expr "colorscheme <name>"` if `$NVIM` is set; if not, only new nvim windows pick up the new theme. Documented as expected.
|
||||
|
||||
- **alacritty has no live color reload** → Mitigation: next-launched alacritty instances pick up the new config; existing windows keep their colors until restarted. Documented. (This matches alacritty's own behavior; not a regression.)
|
||||
|
||||
- **ghostty live reload is `SIGHUP`** → Mitigation: send `SIGHUP` to the running ghostty; on some versions this re-reads the config. If ghostty is unresponsive, the user restarts the terminal. Acceptable.
|
||||
|
||||
- **First-time switch after the cleanup rebuild may visually flash old-then-new** → Mitigation: the boot-time oneshot re-applies `.current` *before* the user sees the desktop; the flash only happens if the user runs `theme-switch` interactively, which is the point.
|
||||
|
||||
- **Adding a new theme requires writing a `meta` file** → Mitigation: documented in the theme module's option description. The `meta` file is two lines.
|
||||
|
||||
- **`darkman`'s gsettings writes could fight `theme-switch`'s** → Mitigation: the cleanup pass removes darkman's `gtk-theme` script entirely. After this change, darkman writes only to `hyprsunset`. No overlap.
|
||||
|
||||
- **Nix modules becoming "thin" can feel like over-abstraction to a reader who expects colors there** → Mitigation: the modules keep their `enable` options and any non-color config (font, opacity, layout). Only color strings leave. Comment in each module notes "colors come from `~/.config/colorschemes/.active/<app>/`."
|
||||
|
||||
- **Hook script runs as the user with no sandboxing** → Mitigation: it lives in the user's home dir, owned by the user. This is the same trust level as a shell alias. Documented in `theme-switch`'s usage output.
|
||||
|
||||
- **`ln -sfn` over an existing `.active` symlink is correct, but if `.active` exists as a *directory* (corruption), the symlink will be created inside it** → Mitigation: the script first checks `[ -L .active ] || [ ! -e .active ]` and bails with a clear error if `.active` is a directory. Documented in the script header.
|
||||
@@ -0,0 +1,50 @@
|
||||
## Why
|
||||
|
||||
Today, switching color themes on jpporta-nixos requires manual editing of files scattered across `~/.config/`, plus a `nixos-rebuild switch` whenever a themed Nix module (swaync, wlogout) is involved. The user already maintains 11 curated palettes under `~/.config/colorschemes/<theme>/` (catppuccin, gruvbox-dark, tokyo-night, nord-darker, rose-pine, kanagawa, nightfox, everforest-dark, e-ink, noir, and one more), but there is no canonical "current theme" pointer, no atomic swap, no live propagation to running apps, and no keybind-driven picker. The user wants to switch themes in one command — fast, with no rebuild — and have every themed app update.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **New module `modules/home-manager/theme/`** that installs `theme-switch` (CLI) and `theme-picker` (rofi wrapper) into `~/.local/bin`, adds a `SUPER+T` Hyprland keybind for the picker, declares `custom.theme.current` as the boot-time default, and registers a systemd user oneshot that re-applies the current theme on graphical-session start.
|
||||
- **New script `theme-switch <name>`** that performs an atomic theme swap by rewriting a single `~/.config/colorschemes/.active` symlink, then emits per-app reload signals (waybar SIGUSR2, swaync-client `--reload-css`, kitty `@ set-colors`, ghostty SIGUSR1, gsettings color-scheme flip, etc.) so live apps reflect the new palette without restart. Idempotent: invoked with no args, it re-applies whatever `.current` says (used on boot).
|
||||
- **New script `theme-picker`** — a rofi-driven wrapper around `theme-switch` that lists available themes from `~/.config/colorschemes/*/meta` and invokes the switcher on selection.
|
||||
- **New per-theme `meta` file** (one per theme): `name=...`, `mode=dark|light`. Drives gsettings and the darkman integration.
|
||||
- **Hook point `~/.config/colorschemes/.hook <theme-name>`**: `theme-switch` execs this at the end of every switch. This is where the user drops their wallpaper-rotation script. The change does NOT write the wallpaper hook — that is owned by the user.
|
||||
- **Cleanup pass** on `swaync`, `wlogout`, `alacritty`, and `darkman` Nix modules: drop the hardcoded gruvbox color strings (Nix store), point their configs at the active theme dir via out-of-store symlinks, and reduce `darkman` to only drive `hyprsunset` (time-of-day temperature). After this one-time rebuild, theme switching never touches Nix again.
|
||||
- **Bonus (writer-deck)**: same Nix module imported by `hosts/writter-deck/home.nix` so the foot terminal on the Steam Deck can switch themes the same way (foot has no live reload, but a relaunch in cage is acceptable for a writer deck).
|
||||
|
||||
No app configuration is being deleted from `dotfiles/colorschemes/`; the cleanup pass only removes the *duplicate* hardcoded gruvbox strings that currently live in Nix modules while leaving the themed files in `~/.config/colorschemes/<theme>/` untouched.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `theme-switch`: the runtime CLI + rofi picker + boot-time applier; defines the contract for what "switching themes" means in this system (atomic `.active` symlink, per-app reload signals, hook execution, gsettings flip driven by `meta` mode).
|
||||
- `theme-colors-sources`: the convention that every themed app on this system reads its colors from `~/.config/colorschemes/.active/<app>/...` rather than from Nix-store-baked strings; covers the cleanup pass on swaync/wlogout/alacritty so future theme additions don't require module edits.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
<!-- No existing specs in openspec/specs/ yet, so nothing to list. The cleanup pass on swaync/wlogout/alacritty/darkman is implementation-level, not a requirement change for any existing spec. -->
|
||||
|
||||
## Impact
|
||||
|
||||
- **New files**:
|
||||
- `modules/home-manager/theme/default.nix`
|
||||
- `modules/home-manager/theme/theme-switch.sh` (script template embedded in Nix)
|
||||
- `modules/home-manager/theme/theme-picker.sh` (script template embedded in Nix)
|
||||
- `~/.config/colorschemes/.current` (runtime, owned by script)
|
||||
- `~/.config/colorschemes/.active` (symlink, runtime)
|
||||
- `~/.config/colorschemes/.hook` (user-owned, optional)
|
||||
- `~/.config/colorschemes/<theme>/meta` (one per theme)
|
||||
- **Modified files**:
|
||||
- `modules/home-manager/swaync/default.nix` — drop inline gruvbox, point at out-of-store config
|
||||
- `modules/home-manager/wlogout/default.nix` — same
|
||||
- `modules/home-manager/alacritty/default.nix` — add `import` for external colors file (font/opacity stay Nix)
|
||||
- `modules/home-manager/darkman/default.nix` — remove `gtk-theme` scripts, keep `hyprsunset`
|
||||
- `modules/home-manager/hyprland/default.nix` — add `SUPER+T` keybind for `theme-picker` (or document the user adding it)
|
||||
- `hosts/jpporta-nixos/home.nix` — import new theme module
|
||||
- `hosts/writter-deck/home.nix` — import new theme module (bonus)
|
||||
- `~/.config/hypr/hyprland.conf` (or the lua-generated version) — point hyprland at `colorschemes/.active/hypr/colors.conf` via `source =`
|
||||
- `~/.config/waybar/style.css` — switch from `@import "./themes/gruvbox-dark.css"` to `@import` from a symlinked `themes/current.css` (or equivalent)
|
||||
- **Affected apps** (live-reload behavior changes): swaync (CSS reload instead of restart), wlogout (next-invocation reload, no live signal), waybar (SIGUSR2), kitty (IPC), ghostty (SIGUSR1), alacritty (next-launch), nvim (new windows; optional remote-expr for running session), gtk-4 apps (gsettings flip).
|
||||
- **No new external dependencies**: rofi, swaync-client, kitty, ghostty, waybar, gsettings, notify-send (libnotify) are already installed.
|
||||
- **No breaking changes** for users not using the new command — existing gruvbox-dark setup continues to work; the first `theme-switch` after rebuild is a no-op if `.current` already matches.
|
||||
@@ -0,0 +1,85 @@
|
||||
# Spec: theme-colors-sources
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Swaync reads colors from the active theme dir, not from Nix
|
||||
|
||||
The swaync Home Manager module SHALL NOT inline color strings. Instead, the module SHALL ensure that `~/.config/swaync/style.css` (and any related color file) is sourced from the active theme dir via an out-of-store symlink to `~/.config/colorschemes/.active/swaync/`.
|
||||
|
||||
#### Scenario: swaync config is symlinked to active theme
|
||||
- **WHEN** the user has applied this change
|
||||
- **THEN** `~/.config/swaync/colors.css` resolves through `~/.config/colorschemes/.active/swaync/colors.css`
|
||||
- **AND** the swaync Nix module contains no color hex strings
|
||||
|
||||
#### Scenario: Theme switch reflects in swaync without rebuild
|
||||
- **WHEN** the user runs `theme-switch catppuccin`
|
||||
- **THEN** swaync restyles via `swaync-client --reload-css` after the symlink swap
|
||||
- **AND** no `home-manager switch` is required
|
||||
|
||||
### Requirement: Wlogout reads colors from the active theme dir, not from Nix
|
||||
|
||||
The wlogout Home Manager module SHALL NOT inline color strings. Instead, the module SHALL ensure that `~/.config/wlogout/style.css` is sourced from the active theme dir via an out-of-store symlink to `~/.config/colorschemes/.active/wlogout/`.
|
||||
|
||||
#### Scenario: wlogout config is symlinked to active theme
|
||||
- **WHEN** the user has applied this change
|
||||
- **THEN** `~/.config/wlogout/style.css` resolves through `~/.config/colorschemes/.active/wlogout/style.css`
|
||||
- **AND** the wlogout Nix module contains no color hex strings
|
||||
|
||||
#### Scenario: Next wlogout invocation uses new colors
|
||||
- **WHEN** the user runs `theme-switch` and then invokes wlogout
|
||||
- **THEN** wlogout uses the new palette (no live reload required; wlogout reads its config at launch)
|
||||
|
||||
### Requirement: Alacritty imports colors from an external file
|
||||
|
||||
The alacritty Home Manager module SHALL configure alacritty to import colors from `~/.config/alacritty/colors.toml` (or equivalent), which is symlinked from the active theme dir.
|
||||
|
||||
#### Scenario: alacritty config imports external colors file
|
||||
- **WHEN** the user has applied this change
|
||||
- **THEN** `~/.config/alacritty/alacritty.toml` contains an `import = [...]` directive that includes the colors file
|
||||
- **AND** `~/.config/alacritty/colors.toml` resolves through `~/.config/colorschemes/.active/alacritty/colors.toml`
|
||||
- **AND** the alacritty Nix module contains no color hex strings (font, opacity, padding remain in Nix)
|
||||
|
||||
#### Scenario: Next alacritty launch uses new colors
|
||||
- **WHEN** the user runs `theme-switch` and then launches a new alacritty window
|
||||
- **THEN** the new window uses the new palette
|
||||
|
||||
### Requirement: Darkman is narrowed to hyprsunset only
|
||||
|
||||
The darkman Home Manager module SHALL retain its `hyprsunset` script (sunset/sunrise temperature) and SHALL NOT contain any `gtk-theme` script. Theme-driven dark/light preference is owned by `theme-switch` via gsettings, not by darkman.
|
||||
|
||||
#### Scenario: darkman scripts contain no gtk-theme step
|
||||
- **WHEN** the user has applied this change
|
||||
- **THEN** `services.darkman.darkModeScripts` does not include a `gtk-theme` key
|
||||
- **AND** `services.darkman.lightModeScripts` does not include a `gtk-theme` key
|
||||
|
||||
#### Scenario: darkman still drives hyprsunset
|
||||
- **WHEN** the time crosses the darkman sunset/sunrise boundary
|
||||
- **THEN** the `hyprsunset` script runs and adjusts temperature/gamma
|
||||
- **AND** gsettings `color-scheme` is NOT modified by darkman
|
||||
|
||||
### Requirement: Hyprland sources its colors from the active theme dir
|
||||
|
||||
The hyprland Home Manager module (or its user-facing config file) SHALL be configured so that hyprland's color directives come from a `source =` directive pointing at `~/.config/colorschemes/.active/hypr/colors.conf`. The Nix module SHALL NOT inline color hex strings for theme-affected values (active/inactive borders, etc.).
|
||||
|
||||
#### Scenario: Hyprland config sources from active theme
|
||||
- **WHEN** the user has applied this change
|
||||
- **THEN** `~/.config/hypr/hyprland.conf` (or its lua-generated form) contains `source = ~/.config/colorschemes/.active/hypr/colors.conf`
|
||||
- **AND** the Hyprland module contains no inline color hex strings for theme-affected directives
|
||||
|
||||
#### Scenario: Theme switch reflects in hyprland without rebuild
|
||||
- **WHEN** the user runs `theme-switch`
|
||||
- **THEN** hyprland automatically reloads its config (it watches the file)
|
||||
- **AND** no `home-manager switch` is required
|
||||
|
||||
### Requirement: Every themed app reads from a single source of truth
|
||||
|
||||
Every app whose colors are themed on this host SHALL read its palette from `~/.config/colorschemes/.active/<app>/...` (directly or via symlink). No themed app SHALL have its colors baked into a Nix-managed file.
|
||||
|
||||
#### Scenario: Audit: no color hex strings in themed-app Nix modules
|
||||
- **WHEN** the user runs `rg -i '#[0-9a-f]{6}' modules/home-manager/{swaync,wlogout,alacritty,hyprland,darkman}` after applying this change
|
||||
- **THEN** the only matches are in non-theme-affected contexts (e.g., alacritty font color is fine; swaync/wlogout/hyprland theme colors are gone)
|
||||
|
||||
#### Scenario: Adding a new theme requires no Nix change
|
||||
- **WHEN** the user creates a new `~/.config/colorschemes/<new-theme>/` directory with the same per-app subdirs as existing themes, plus a `meta` file
|
||||
- **THEN** `theme-switch <new-theme>` works without any `home-manager switch`
|
||||
- **AND** the new theme appears in the rofi picker on next invocation
|
||||
@@ -0,0 +1,155 @@
|
||||
# Spec: theme-switch
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Atomic theme swap via a single canonical pointer
|
||||
|
||||
The system SHALL provide a single shell command, `theme-switch <theme-name>`, that switches the active theme by atomically rewriting `~/.config/colorschemes/.active` (a symlink) to point at `~/.config/colorschemes/<theme-name>/`.
|
||||
|
||||
#### Scenario: Successful switch to a valid theme
|
||||
- **WHEN** the user runs `theme-switch catppuccin` and `~/.config/colorschemes/catppuccin/` exists
|
||||
- **THEN** `~/.config/colorschemes/.active` is a symlink whose target resolves to `~/.config/colorschemes/catppuccin`
|
||||
- **AND** `~/.config/colorschemes/.current` contains the text `catppuccin`
|
||||
|
||||
#### Scenario: Switch with an unknown theme name
|
||||
- **WHEN** the user runs `theme-switch does-not-exist` and the directory does not exist
|
||||
- **THEN** the command exits non-zero
|
||||
- **AND** prints a list of valid theme names to stderr
|
||||
- **AND** does not modify `.active` or `.current`
|
||||
|
||||
#### Scenario: Re-applying the current theme is a no-op
|
||||
- **WHEN** the user runs `theme-switch` with no arguments
|
||||
- **THEN** the command reads `.current`
|
||||
- **AND** re-applies every per-app reload step (idempotent)
|
||||
|
||||
### Requirement: Per-theme metadata drives picker and gsettings
|
||||
|
||||
The system SHALL read each theme's mode (dark or light) and display name from a `meta` file at `~/.config/colorschemes/<theme>/meta`.
|
||||
|
||||
#### Scenario: meta file format
|
||||
- **WHEN** the user runs `theme-switch` against any theme
|
||||
- **THEN** the script reads `<theme>/meta`
|
||||
- **AND** parses lines of the form `key=value`
|
||||
- **AND** uses `mode=dark` or `mode=light` to drive gsettings
|
||||
- **AND** uses `name=...` for the rofi picker label
|
||||
|
||||
#### Scenario: Missing meta file defaults to dark
|
||||
- **WHEN** a theme directory has no `meta` file
|
||||
- **THEN** `theme-switch` treats the theme as `mode=dark`
|
||||
- **AND** uses the directory name as the display name
|
||||
- **AND** emits a warning to stderr
|
||||
|
||||
### Requirement: Per-app live reload covers every themed app on jpporta-nixos
|
||||
|
||||
`theme-switch` SHALL emit the appropriate reload signal for each themed app on the host, in an order that places file/data changes before signals.
|
||||
|
||||
#### Scenario: Hyprland picks up new colors via file watch
|
||||
- **WHEN** `theme-switch` rewrites `~/.config/hypr/hyprland.conf` so that its `source =` directive points at `~/.config/colorschemes/.active/hypr/colors.conf`
|
||||
- **THEN** the running Hyprland instance reloads automatically (it watches its config file)
|
||||
|
||||
#### Scenario: waybar picks up new colors via SIGUSR2
|
||||
- **WHEN** `theme-switch` swaps the waybar CSS symlink and the colors.css file under it
|
||||
- **THEN** the running waybar instance receives SIGUSR2
|
||||
- **AND** restyles with the new palette
|
||||
|
||||
#### Scenario: swaync picks up new CSS via swaync-client
|
||||
- **WHEN** `theme-switch` swaps `~/.config/swaync/colors.css`
|
||||
- **THEN** the script invokes `swaync-client --reload-css`
|
||||
- **AND** swaync restyles without restart
|
||||
|
||||
#### Scenario: kitty picks up new colors via IPC
|
||||
- **WHEN** `theme-switch` invokes `kitty @ set-colors --all ~/.config/colorschemes/.active/kitty/colors.conf`
|
||||
- **THEN** every running kitty window restyles without restart
|
||||
|
||||
#### Scenario: ghostty picks up new colors via SIGHUP
|
||||
- **WHEN** `theme-switch` sends SIGHUP to the running ghostty process
|
||||
- **THEN** ghostty re-reads its config (including the colors block)
|
||||
|
||||
#### Scenario: alacritty uses the new palette on next launch
|
||||
- **WHEN** `theme-switch` swaps the alacritty colors file that `alacritty.toml` imports
|
||||
- **THEN** running alacritty windows keep their current colors until restarted
|
||||
- **AND** any newly launched alacritty instance uses the new palette
|
||||
|
||||
#### Scenario: nvim recolors in-session when $NVIM is set
|
||||
- **WHEN** `theme-switch` runs and the user has `$NVIM` set (a running nvim server)
|
||||
- **THEN** the script invokes `nvim --remote-expr 'lua vim.cmd.colorscheme("<name>")'`
|
||||
- **AND** the running nvim recolors immediately
|
||||
- **WHEN** `$NVIM` is not set
|
||||
- **THEN** the script takes no action for nvim
|
||||
- **AND** newly launched nvim instances pick up the theme from the symlinked config
|
||||
|
||||
#### Scenario: GTK dark/light preference follows theme meta
|
||||
- **WHEN** `theme-switch` reads `mode=dark` from the new theme's meta
|
||||
- **THEN** the script invokes `gsettings set org.gnome.desktop.interface color-scheme prefer-dark`
|
||||
- **WHEN** `mode=light`
|
||||
- **THEN** the script invokes `gsettings set org.gnome.desktop.interface color-scheme prefer-light`
|
||||
|
||||
### Requirement: Rofi-driven theme picker is a thin wrapper over theme-switch
|
||||
|
||||
The system SHALL provide a `theme-picker` script that lists available themes (display name + mode) via `rofi -dmenu` and invokes `theme-switch` on the user's selection.
|
||||
|
||||
#### Scenario: Picker shows all available themes
|
||||
- **WHEN** the user runs `theme-picker`
|
||||
- **THEN** rofi displays one entry per theme directory under `~/.config/colorschemes/`
|
||||
- **AND** each entry shows the display name from `meta` and a `(dark)` or `(light)` suffix
|
||||
|
||||
#### Scenario: Picker invokes theme-switch on selection
|
||||
- **WHEN** the user selects an entry in rofi
|
||||
- **THEN** `theme-picker` invokes `theme-switch <selected-theme-name>`
|
||||
- **AND** exits with the same status code
|
||||
|
||||
### Requirement: Boot-time re-apply guarantees a consistent desktop on login
|
||||
|
||||
The system SHALL provide a systemd user oneshot service that runs `theme-switch` (no args) on graphical-session start, ensuring `.current` is applied before the user sees the desktop.
|
||||
|
||||
#### Scenario: Service runs on graphical-session start
|
||||
- **WHEN** the user's graphical session (Hyprland on jpporta-nixos; cage on jpporta-deck) starts
|
||||
- **THEN** `theme-apply.service` runs `theme-switch`
|
||||
- **AND** the service has no effect if `.current` already matches the running state
|
||||
|
||||
#### Scenario: Service does not run before graphical session
|
||||
- **WHEN** the user's session is not yet at graphical-session.target
|
||||
- **THEN** `theme-apply.service` is not started
|
||||
- **AND** waits for the target
|
||||
|
||||
### Requirement: Wallpaper hook is user-owned and exec'd at end of switch
|
||||
|
||||
`theme-switch` SHALL execute `~/.config/colorschemes/.hook <theme-name>` at the end of every successful switch if that file exists and is executable.
|
||||
|
||||
#### Scenario: Hook is invoked after all app reloads
|
||||
- **WHEN** `theme-switch` finishes the per-app reload block
|
||||
- **AND** `~/.config/colorschemes/.hook` exists and is executable
|
||||
- **THEN** the script executes `~/.config/colorschemes/.hook <theme-name>`
|
||||
- **AND** any non-zero exit status is logged but does not fail the switch
|
||||
|
||||
#### Scenario: Missing hook does not fail the switch
|
||||
- **WHEN** `~/.config/colorschemes/.hook` does not exist or is not executable
|
||||
- **THEN** the switch still completes successfully
|
||||
|
||||
### Requirement: Writer-deck adopts the same mechanism with foot handled at switch time
|
||||
|
||||
The same `theme-switch` script SHALL work on jpporta-deck (cage + foot). Foot has no live color reload, so the deck profile SHALL include a foot-specific step that sends `SIGUSR1` (footserver reload) or, as a fallback, restarts the foot server.
|
||||
|
||||
#### Scenario: Foot recolors via SIGUSR1 to footserver
|
||||
- **WHEN** `theme-switch` runs on jpporta-deck
|
||||
- **THEN** the script sends `SIGUSR1` to the footserver process if running
|
||||
- **AND** foot reloads its config without losing the running instance
|
||||
|
||||
#### Scenario: footserver not running
|
||||
- **WHEN** `theme-switch` runs on jpporta-deck and no footserver is running
|
||||
- **THEN** the script takes no action for foot
|
||||
- **AND** the next foot launch picks up the new theme
|
||||
|
||||
### Requirement: First-install migration creates .active from custom.theme.current
|
||||
|
||||
On a fresh `home-manager switch` after this change is applied, the theme module SHALL ensure `~/.config/colorschemes/.active` exists and points at the theme named in `custom.theme.current`.
|
||||
|
||||
#### Scenario: First install with default theme
|
||||
- **WHEN** the user applies this change for the first time and `custom.theme.current = "gruvbox-dark"`
|
||||
- **THEN** the activation creates `~/.config/colorschemes/.active` as a symlink to `~/.config/colorschemes/gruvbox-dark/`
|
||||
- **AND** writes `gruvbox-dark` to `~/.config/colorschemes/.current`
|
||||
|
||||
#### Scenario: Existing wallpapers symlink is left in place during migration
|
||||
- **WHEN** `~/.config/colorschemes/wallpapers` is already a symlink (legacy)
|
||||
- **THEN** the migration does not remove it
|
||||
- **AND** documents in a comment that it is now redundant
|
||||
@@ -0,0 +1,91 @@
|
||||
# Tasks: theme-switch
|
||||
|
||||
## 1. Per-theme metadata
|
||||
|
||||
- [ ] 1.1 Create `~/.config/colorschemes/<theme>/meta` for each of the 11 existing themes with `name=<Display Name>` and `mode=dark` (or `mode=light` for `e-ink` and `noir`).
|
||||
|
||||
## 2. Theme module scaffold
|
||||
|
||||
- [ ] 2.1 Create `modules/home-manager/theme/default.nix` declaring `options.custom.theme.current` (string, default `"gruvbox-dark"`) and `options.custom.theme.enable` (bool).
|
||||
- [ ] 2.2 Embed the `theme-switch` shell script in the module via `pkgs.writeShellScriptBin` so it's installed to `~/.local/bin/theme-switch`.
|
||||
- [ ] 2.3 Embed the `theme-picker` shell script in the module via `pkgs.writeShellScriptBin` so it's installed to `~/.local/bin/theme-picker`.
|
||||
- [ ] 2.4 Add a systemd user oneshot `theme-apply.service` that runs `theme-switch` (no args) on `graphical-session.target`.
|
||||
- [ ] 2.5 Add a `home.activation` block that creates `~/.config/colorschemes/.active` (symlink to `custom.theme.current`) and `.current` (text file with the name) if missing.
|
||||
|
||||
## 3. theme-switch script
|
||||
|
||||
- [ ] 3.1 Implement argument parsing: `<theme-name>` switches; no args reads `.current` and re-applies.
|
||||
- [ ] 3.2 Validate `<theme-name>` exists under `~/.config/colorschemes/`; print list and exit non-zero otherwise.
|
||||
- [ ] 3.3 Refuse to proceed if `~/.config/colorschemes/.active` is a non-symlink directory (defensive guard).
|
||||
- [ ] 3.4 Atomically swap `.active` with `ln -sfn`.
|
||||
- [ ] 3.5 Write `<theme-name>` to `~/.config/colorschemes/.current`.
|
||||
- [ ] 3.6 Parse the new theme's `meta` for `name=` and `mode=`.
|
||||
- [ ] 3.7 Emit `gsettings set org.gnome.desktop.interface color-scheme prefer-{dark,light}` based on `mode=`.
|
||||
- [ ] 3.8 Update hyprland config: rewrite `~/.config/hypr/hyprland.conf` so its `source =` directive points at `~/.config/colorschemes/.active/hypr/colors.conf`. No signal (hyprland watches).
|
||||
- [ ] 3.9 Update waybar: atomic symlink swap for `~/.config/waybar/themes/current.css` → `<theme>/waybar/colors.css`, then `pkill -USR2 waybar`.
|
||||
- [ ] 3.10 Update swaync: atomic swap for `~/.config/swaync/colors.css` → `<theme>/swaync/colors.css`, then `swaync-client --reload-css`.
|
||||
- [ ] 3.11 Update wlogout: atomic swap for `~/.config/wlogout/style.css` → `<theme>/wlogout/style.css`. No live signal (next-launch reload).
|
||||
- [ ] 3.12 Update kitty: `kitty @ set-colors --all ~/.config/colorschemes/.active/kitty/colors.conf`.
|
||||
- [ ] 3.13 Update ghostty: `pkill -SIGUSR1 ghostty`.
|
||||
- [ ] 3.14 Update alacritty: atomic swap for `~/.config/alacritty/colors.toml` → `<theme>/alacritty/colors.toml`. No live signal (next-launch reload).
|
||||
- [ ] 3.15 Update nvim: if `$NVIM` is set, `nvim --remote-expr 'lua vim.cmd.colorscheme("<name>")'`. Else no-op.
|
||||
- [ ] 3.16 If `~/.config/colorschemes/.hook` exists and is executable, run `~/.config/colorschemes/.hook <theme-name>` at the end. Tolerate non-zero exit.
|
||||
- [ ] 3.17 Notify via `notify-send "Theme: <name>"` if libnotify is available.
|
||||
|
||||
## 4. theme-picker script
|
||||
|
||||
- [ ] 4.1 List `~/.config/colorschemes/*/meta` files.
|
||||
- [ ] 4.2 Parse each `meta` for `name=` and `mode=`.
|
||||
- [ ] 4.3 Pipe the formatted entries (`<name> (<mode>)`) into `rofi -dmenu -p "Theme"`.
|
||||
- [ ] 4.4 On selection, invoke `theme-switch <selected-theme-name>` and exit with its status.
|
||||
|
||||
## 5. Cleanup pass: swaync
|
||||
|
||||
- [ ] 5.1 Strip the inline gruvbox color strings from `modules/home-manager/swaync/default.nix` (the `let` block at the top with `background`, `text`, etc.).
|
||||
- [ ] 5.2 Replace the `style = ''...''` block with `xdg.configFile."swaync/style.css".source = config.lib.file.mkOutOfStoreSymlink "${config.home.homeDirectory}/.config/swaync/style.css"` (so the file lives in dotfiles and is theme-driven).
|
||||
- [ ] 5.3 Verify `~/.config/colorschemes/<theme>/swaync/colors.css` exists for all 11 themes; if any are missing, copy from `gruvbox-dark/swaync/colors.css` as a placeholder.
|
||||
|
||||
## 6. Cleanup pass: wlogout
|
||||
|
||||
- [ ] 6.1 Strip the inline gruvbox color strings from `modules/home-manager/wlogout/default.nix`.
|
||||
- [ ] 6.2 Replace the `style = ''...''` block with `xdg.configFile."wlogout/style.css".source = config.lib.file.mkOutOfStoreSymlink ...`.
|
||||
- [ ] 6.3 Verify `~/.config/colorschemes/<theme>/wlogout/colors.css` exists for all 11 themes.
|
||||
|
||||
## 7. Cleanup pass: alacritty
|
||||
|
||||
- [ ] 7.1 Keep font, opacity, padding, blur in `modules/home-manager/alacritty/default.nix`.
|
||||
- [ ] 7.2 Add `import = [ "~/.config/alacritty/colors.toml" ]` to `programs.alacritty.settings`.
|
||||
- [ ] 7.3 Ensure `~/.config/colorschemes/<theme>/alacritty/colors.toml` exists for all 11 themes (or document a one-time port if the existing files are in a different format).
|
||||
- [ ] 7.4 Add `xdg.configFile."alacritty/colors.toml".source = out-of-store symlink to ~/.config/colorschemes/.active/alacritty/colors.toml` (or document leaving it as a mutable file the script swaps).
|
||||
|
||||
## 8. Cleanup pass: darkman
|
||||
|
||||
- [ ] 8.1 Remove the `gtk-theme` key from both `services.darkman.darkModeScripts` and `services.darkman.lightModeScripts`.
|
||||
- [ ] 8.2 Leave the `hyprsunset` key in both modes (still time-of-day driven).
|
||||
- [ ] 8.3 Add a comment in the module noting that gsettings `color-scheme` is now owned by `theme-switch`.
|
||||
|
||||
## 9. Hyprland integration
|
||||
|
||||
- [ ] 9.1 In `modules/home-manager/hyprland/default.nix` (or the lua config it writes), ensure `~/.config/hypr/hyprland.conf` `source`s `~/.config/colorschemes/.active/hypr/colors.conf`.
|
||||
- [ ] 9.2 Strip inline color hex strings (active/inactive border) from the lua config.
|
||||
- [ ] 9.3 Add the `SUPER+T` keybind to launch `theme-picker` (in the `hl.bind` block).
|
||||
|
||||
## 10. Wire into hosts
|
||||
|
||||
- [ ] 10.1 Add `../../modules/home-manager/theme` to `imports` in `hosts/jpporta-nixos/home.nix`.
|
||||
- [ ] 10.2 Set `custom.theme.current = "gruvbox-dark";` (or another default) in the same file.
|
||||
- [ ] 10.3 (Bonus, deck) Add `../../modules/home-manager/theme` to `imports` in `hosts/writter-deck/home.nix` and set `custom.theme.current`.
|
||||
- [ ] 10.4 (Bonus, deck) Add a footserver-specific step to `theme-switch` that sends SIGUSR1 to the running footserver, or skips if none.
|
||||
|
||||
## 11. Verification
|
||||
|
||||
- [ ] 11.1 `home-manager switch` succeeds with no errors after all module changes.
|
||||
- [ ] 11.2 `~/.config/colorschemes/.active` exists and resolves to the configured theme on first boot.
|
||||
- [ ] 11.3 `theme-switch catppuccin` (or any other valid theme) swaps `.active`, updates `.current`, and emits each per-app reload signal.
|
||||
- [ ] 11.4 Each themed app reflects the new palette within ~2 seconds (manual eyeball test).
|
||||
- [ ] 11.5 `theme-switch does-not-exist` prints the list of valid themes and exits non-zero without modifying `.active`.
|
||||
- [ ] 11.6 `theme-picker` opens rofi, lists all 11 themes with their mode suffix, and switches on selection.
|
||||
- [ ] 11.7 `SUPER+T` in Hyprland opens the rofi picker.
|
||||
- [ ] 11.8 After a logout/login, the desktop matches `.current` (the systemd oneshot ran).
|
||||
- [ ] 11.9 Drop a placeholder `~/.config/colorschemes/.hook` that logs the theme name; verify it runs after a switch.
|
||||
- [ ] 11.10 (Deck, if wired) `theme-switch` works on the deck and footserver reloads colors (or relaunches).
|
||||
@@ -0,0 +1,4 @@
|
||||
schema: spec-driven
|
||||
|
||||
context: |
|
||||
Tech stack: Unix System, NixOS, Flakes, Home-manager
|
||||
Reference in New Issue
Block a user