refactor(#107): theme-state.json → state.json, theme-sync → state-sync
Some checks failed
Check / eval (push) Has been cancelled

The machine flake's git-tracked settings file is system state, not
"theme" only — rename it to state.json. CLI becomes nomarchy-state-sync
with a nomarchy-theme-sync symlink for scripts and muscle memory.

Eval (mkFlake, doctor, lifecycle) still accepts theme-state.json; the
next write migrates to state.json and removes the legacy file.
Documented in MIGRATION.md; drop the CLI alias after release notes.
This commit is contained in:
2026-07-15 11:26:59 +01:00
parent 013403deb4
commit d8e1a13d50
62 changed files with 463 additions and 369 deletions

View File

@@ -7,11 +7,11 @@ partial.
```
┌──────────────────────────────────────────────────────────────────────────┐
theme-state.json (single source of truth) │
│ state.json (single source of truth) │
│ lives INSIDE your flake checkout, git-tracked │
└───────────────────────────────────┬──────────────────────────────────────┘
nomarchy-theme-sync apply gruvbox
nomarchy-state-sync apply gruvbox
1. merges the preset into the JSON (atomic write)
2. runs `home-manager switch` (no sudo, no system rebuild)
@@ -35,7 +35,7 @@ Flat on purpose. Two module trees, one options file each, no hidden layers.
.
├── flake.nix # inputs + the downstream API (exports below)
├── lib.nix # nomarchy.lib.mkFlake — one-call downstream wrapper
├── theme-state.json # ★ THE single source of truth (git-tracked!)
├── state.json # ★ THE single source of truth (git-tracked!)
├── themes/ # 24 presets: <slug>.json + optional <slug>/ assets
│ ├── nord.json # palette (required, works alone)
│ └── nord/ # assets (optional, fixed filenames)
@@ -72,10 +72,10 @@ Flat on purpose. Two module trees, one options file each, no hidden layers.
│ ├── default/ # reference machine (thin: boot, user, hostname)
│ └── live.nix # bootable live ISO (try the distro, no install)
├── pkgs/
│ ├── nomarchy-theme-sync/ # state writer + rebuild dispatcher (Python)
│ ├── nomarchy-state-sync/ # state writer + rebuild dispatcher (Python)
│ ├── nomarchy-install/ # live-ISO installer (gum + disko + mkFlake)
│ ├── nomarchy-doctor/ # read-only health sheet, one command per failure
│ ├── nomarchy-control-center/ # TUI settings front-end over nomarchy-theme-sync (gum)
│ ├── nomarchy-control-center/ # TUI settings front-end over nomarchy-state-sync (gum)
│ └── nomarchy-battery-notify/ # low-battery toast watcher backing battery-notify.nix
├── templates/downstream/ # machine flake SoT (`flake init` + installer copy/patch)
├── docs/ # human docs — map: docs/README.md
@@ -161,7 +161,7 @@ Nomarchy in place, reusing your existing `hardware-configuration.nix`:
**[docs/MIGRATION.md](docs/MIGRATION.md)**.
You own two files day-to-day: `system.nix` and `home.nix` (plus
`theme-state.json`, written by the CLI). Your `flake.nix` is set up once —
`state.json`, written by the CLI). Your `flake.nix` is set up once —
later by the installer — and never hand-edited; it's a single call:
```nix
@@ -229,7 +229,7 @@ Legacy aliases still work: `sys-update` → pull+rebuild, `sys-rebuild` →
`nomarchy-rebuild`, `home-update``nomarchy-home`.
Override anything via the `nomarchy.*` surface or plain NixOS/HM options:
appearance (gaps/colors/fonts) changes through `nomarchy-theme-sync`,
appearance (gaps/colors/fonts) changes through `nomarchy-state-sync`,
behaviour (input/misc/monitor/chrome) is `mkDefault` so a plain `home.nix`
assignment wins, and bind/exec-once lists concatenate. Full guide with
examples: **[docs/OVERRIDES.md](docs/OVERRIDES.md)**.
@@ -243,7 +243,7 @@ two tables below are split along exactly that line.
| Option | Default | Purpose |
|---|---|---|
| `nomarchy.stateFile` | — (required) | Path to your theme-state.json |
| `nomarchy.stateFile` | — (required) | Path to your state.json |
| `nomarchy.terminal` | `"kitty"` | Terminal for keybinds and `$TERMINAL` (Kitty is the only shipped/themed emulator) |
| `nomarchy.kitty.enable` | `true` | Nomarchy's Kitty config (palette/font/opacity from theme-state) |
| `nomarchy.keyboard.layout` | `"us"` | XKB layout for the Hyprland session (installer writes the matching `services.xserver.xkb` in system.nix; the console + LUKS prompt follow via the distro default) |
@@ -275,7 +275,7 @@ two tables below are split along exactly that line.
| `nomarchy.displayProfiles` | `{}` | Named layouts for the same outputs (docked/undocked/…), switched from System Display Profiles: instant via hyprctl, persisted in-flake (`settings.displayProfile`), baked over `nomarchy.monitors` at the next rebuild. Profiles that disable the laptop panel use the same safe ordering as Dock mode (external on → workspace handoff → internal off). The menu's Auto-switch row (`settings.displayProfileAuto`) applies the matching profile from Hyprland hotplug events. Workspace pins (`workspaces = { "1" = "DP-3"; }`) are moved instantly and baked as rules |
| `nomarchy.launchOrFocus` | `[]` | Launch-or-focus binds: `SUPER+<key>` focuses the app's window (case-insensitive class match) or launches it; entries land in the SUPER+? cheatsheet, and a bind whose app was removed notifies instead of failing silently |
| `nomarchy.themesDir` | Nomarchy's `themes/` | Where per-theme app overrides are probed |
| `nomarchy.package` | overlay's `nomarchy-theme-sync` | The theme/state tool package, overridable if you fork it |
| `nomarchy.package` | overlay's `nomarchy-state-sync` | The theme/state tool package, overridable if you fork it |
**Always-on, no toggle by design:** `services.cliphist`, `services.udiskie`
(automount + safe-removal toasts) and `services.easyeffects` (mic noise
@@ -290,7 +290,7 @@ option, e.g. `services.easyeffects.enable = lib.mkForce false;` — see
| Option | Default | Purpose |
|---|---|---|
| `nomarchy.system.plymouth.enable` | `true` | Branded boot splash, background from the theme JSON (recolors on system rebuilds) |
| `nomarchy.system.stateFile` | `null` | theme-state.json for the system-side consumers (the Plymouth tint); `lib.mkFlake` wires it for you — set it only when composing the modules by hand |
| `nomarchy.system.stateFile` | `null` | state.json for the system-side consumers (the Plymouth tint); `lib.mkFlake` wires it for you — set it only when composing the modules by hand |
| `nomarchy.system.fileManager.enable` | `true` | Thunar GUI + gvfs/tumbler/udisks2 (the "open folder" handler) |
| `nomarchy.system.greeter.enable` | `true` | greetd/tuigreet |
| `nomarchy.system.greeter.autoLogin` | state | Auto-login this user into Hyprland. Owned by System Auto-login (`settings.greeter.autoLogin`; installer seeds it on LUKS machines) — set it here only to pin it against the menu |
@@ -367,13 +367,13 @@ The trap with "read a mutable file from Nix" is pure evaluation: flakes
cannot read arbitrary `$HOME` paths without `--impure` (the old prototype
required it — never again). Nomarchy's convention: **the state file lives
inside the consuming flake** and is wired via
`nomarchy.stateFile = ./theme-state.json;`. Reading it is pure — it's flake
source. It must be git-tracked (`nomarchy-theme-sync` runs
`nomarchy.stateFile = ./state.json;`. Reading it is pure — it's flake
source. It must be git-tracked (`nomarchy-state-sync` runs
`git add --intent-to-add` after every write as a safety net).
### One change = one generation
`nomarchy-theme-sync apply <theme>` merges the preset into the JSON and runs
`nomarchy-state-sync apply <theme>` merges the preset into the JSON and runs
`home-manager switch` (override the command with `$NOMARCHY_REBUILD`, or pass
`--no-switch` to only write). Everything is baked: Hyprland, Waybar, Kitty,
btop, and — via Stylix, mapped onto base16 roles — GTK, Qt, cursors and
@@ -404,7 +404,7 @@ gates the unit, the first enable rebuilds) and `settings.nightlight.on` (the
instant on/off). Expect more `nomarchy.*` toggles to migrate to this pattern.
**Auto-commit (opt-in):** System Auto-commit makes every `apply`/`set`
mutation also `git commit` theme-state.json in your flake — *only* that
mutation also `git commit` state.json in your flake — *only* that
file, so unrelated dirty work is never swept up — turning your settings
history into `git log`. Off by default; the toggle is instant (nothing in
Nix consumes the flag), the off-write is itself committed so history stays
@@ -432,12 +432,12 @@ asset probe).
## 5. Day-to-day
```sh
nomarchy-theme-sync list # 24 presets (nord, gruvbox, rose-pine, …)
nomarchy-theme-sync apply kanagawa # whole desktop, one generation (~a switch)
nomarchy-theme-sync set ui.gapsOut 16 # tweak one knob (also a switch)
nomarchy-theme-sync bg next # cycle wallpapers — instant, no rebuild
nomarchy-theme-sync bg auto # back to the theme's default wallpaper
nomarchy-theme-sync get colors.accent
nomarchy-state-sync list # 24 presets (nord, gruvbox, rose-pine, …)
nomarchy-state-sync apply kanagawa # whole desktop, one generation (~a switch)
nomarchy-state-sync set ui.gapsOut 16 # tweak one knob (also a switch)
nomarchy-state-sync bg next # cycle wallpapers — instant, no rebuild
nomarchy-state-sync bg auto # back to the theme's default wallpaper
nomarchy-state-sync get colors.accent
nomarchy-pull # flake input update (no rebuild)
nomarchy-rebuild # rebuild the system, current lock
nomarchy-home # rebuild just the desktop layer
@@ -484,7 +484,7 @@ reload # exec zsh (reload the shell)
- **New theme:** drop a JSON into `themes/` (schema = any existing preset),
plus an optional `themes/<slug>/` assets directory.
- **New themed value:** add the key to `theme-state.json` and consume it in
- **New themed value:** add the key to `state.json` and consume it in
the Nix modules. One place — there is no second renderer to keep in sync.
- **Importing more old-distro palettes:**
`tools/import-palettes.py <palettes-dir> themes/` (roles are first-class:

View File

@@ -226,7 +226,7 @@ always — the same on-AC question `modules/home/idle.nix`'s suspend listener
already answers with `${onAc} ||`.
In-flake state like the rest (`settings.*`), menu-writable, and read via
`theme-state-read.nix` on the NixOS side — NOT `config.nomarchy.settings`,
`state-read.nix` on the NixOS side — NOT `config.nomarchy.settings`,
which does not exist there (ROADMAP § *NixOS-side state bridges (#116)*; copy
the `bluetooth.enable` shape in `modules/nixos/default.nix` and add a case to
`checks.state-bridges`). Pass = the toggle survives a rebuild,
@@ -234,22 +234,6 @@ a suspended machine hibernates after the configured delay and resumes, and a
machine without hibernate support hides the row instead of offering a
suspend that never wakes.
### 107. Rename `theme.json` to reflect that it is the system state
The state file long ago stopped being about themes: it carries night-light,
keyboard memory, display resolution and profiles, auto-commit, services and
more, so `theme.json` now misnames its own contents and misleads anyone
reading the flake. Rename it to `state.json` (Bernardo's call, 2026-07-14 —
settled, do not revisit), and carry the `nomarchy-theme-sync` tool name along
with it.
Standalone task — do not fold it into a feature. Ships with a compatibility
shim that keeps reading an existing `theme.json` so downstream checkouts do
not break on a pull, plus a migration note. Pass = a fresh install and an
existing downstream checkout both work, every in-repo reference (modules,
tools, template, docs) uses the new name, and the shim is documented with the
release it can be dropped in.
### 104. Runtime Airplane mode
Add Airplane mode under System connectivity controls. It must disable Wi-Fi
@@ -280,7 +264,7 @@ read, so *these two toggles have never actually worked for any user*. That
cuts both ways: "preserve existing behavior" is not a reason to keep them
(there is no behavior to preserve), but the state keys and their bridges are
real now and outlive the TUI — dropping the rows must not drop the keys, or a
machine whose theme-state.json already says `bluetooth.enable = false` will
machine whose state.json already says `bluetooth.enable = false` will
silently flip back on at the next rebuild. `checks.state-bridges` covers those
two keys and will fail loudly if the bridges go with the tool; keep it green,
and if a key is deliberately retired, remove its case in the same commit.

View File

@@ -24,7 +24,7 @@ those, it probably shouldn't exist.
## Feature design
- **In-flake state:** any user-settable config gets a menu writer that
lands it in `theme-state.json` (`settings.*`), git-tracked. No
lands it in `state.json` (`settings.*`), git-tracked. No
`~/.local/state`, no side files. Instant-effect where possible
(`--no-switch` + flip the service; the service reads the *live* working
tree at start — the night-light `ExecCondition` pattern). Rebuild-baked

View File

@@ -14,7 +14,7 @@ confidence, default identity, release bar) live in
on-hardware QA.
2. **Reproducible, with zero hidden state.** The downstream flake checkout
*is* the machine. All user-settable config is menu-writable into the
git-tracked state file (`theme-state.json` `settings.*`) — never
git-tracked state file (`state.json` `settings.*`) — never
`~/.local/state`, never `~/.config` side files. Re-cloning your flake
reproduces the machine, settings and all.
3. **Effortless to configure.** The user never has to learn Nix. Every

View File

@@ -237,7 +237,7 @@ the **T14s** (webcam case).
open Ghostty, so the hand `btop.theme` assets were only guest-file
asserted (main_bg / inactive_fg keys) + desktop/bar rendered for
rose-pine, everforest, summer-night, vantablack, catppuccin. On a
real session: `nomarchy-theme-sync apply <slug>` then `btop` and
real session: `nomarchy-state-sync apply <slug>` then `btop` and
confirm backgrounds/text match the theme (esp. rose-pine Dawn light
bg `#faf4ed`, vantablack near-black `#0d0d0d`, catppuccin Mocha
`#1E1E2E`). One pass cycling those five is enough.

View File

@@ -19,6 +19,17 @@ Template:
---
## 2026-07-15 — #107 state.json + nomarchy-state-sync
- **Task:** BACKLOG #107 — rename misnamed system state file and CLI.
- **Did:** `theme-state.json``state.json` (template, root, docs, modules);
package/CLI `nomarchy-theme-sync``nomarchy-state-sync` with symlink
alias; `modules/state-read.nix`; mkFlake/doctor/lifecycle read legacy
name; writes always land on `state.json` and remove the old file.
- **Verified:** py_compile; legacy-file unit migrate (write removes
theme-state.json); V0 flake check pending in same push.
- **Pending:** drop `nomarchy-theme-sync` alias after release notes.
- **Next suggestion:** #104 airplane, or #98 with a screenshot.
## 2026-07-15 — Close #94 installed chromium + kill Hyprland update-news
- **Task:** Residual #94 (chromium on installed path) + first-boot polish
seen while probing.
@@ -373,7 +384,7 @@ Template:
(`modules/home/options.nix:412`), so `or <fallback>` had been swallowing a
missing-attribute error on the NixOS side forever. Options now declare a
*static* default and the implementing module reads the state via
`theme-state-read.nix` + `lib.mkIf (state != null) (lib.mkDefault state)`
`state-read.nix` + `lib.mkIf (state != null) (lib.mkDefault state)`
done for `bluetooth.enable` (default.nix) and `services.printing.enable`
(services.nix). `batteryChargeLimit`'s read is simply deleted: power.nix's
oneshot reads the key with `jq` at *runtime*, which is why that menu worked.
@@ -421,7 +432,7 @@ Template:
Making both state-owned surfaced two real bugs: `greeter.autoLogin`
defaulted from `config.nomarchy.settings…`, an attribute that exists ONLY
on the HM side, and `or null` swallowed it — the state path never worked on
any machine ever built; and `theme-sync get` printed Python's `None` for a
any machine ever built; and `state-sync get` printed Python's `None` for a
JSON null, so any `case … null)` would miss.
- **Verified:** **V1** — flake check, installer-safety, option-docs pass.
Proved by eval/build: a state carrying autoLogin yields greetd
@@ -580,14 +591,14 @@ Template:
- **Next suggestion:** wait on round 7 before touching the dock path again.
## 2026-07-13 — #99 live-seed git-init; theme-switch failure filed as #113 (this commit)
- **Task:** BACKLOG #99 — false "theme-state.json NOT git-tracked" doctor/
- **Task:** BACKLOG #99 — false "state.json NOT git-tracked" doctor/
Waybar warning in the live session; resolve the held question from Fable.
- **Did:** hosts/live.nix `nomarchy-seed-flake` now `git init`+`add`+`commit`s
the seeded `~/.nomarchy` (like nomarchy-install.sh:654), so the live
checkout is a committed repo. Also committed #94's live-posture comment in
flake.nix (separate commit).
- **Verified:** V2 — focused serial-log VM run (`/tmp/nomarchy-v2-theme-99/`):
doctor prints `theme-state.json is git-tracked` + `✔ flake checkout is
doctor prints `✔ state.json is git-tracked` + `✔ flake checkout is
clean`, `GIT_REPO_OK`/`TRACKED_OK` markers. Held question RESOLVED: the
same run's offline gruvbox switch failed with a 1176-drv source rebuild,
but a local drv-equivalence test proved the failing
@@ -885,14 +896,14 @@ Template:
## 2026-07-12 — auto-commit sweep before pull/rebuild/home (interactive)
- **Task:** Bernardo's request (no BACKLOG item): with autoCommit on,
hand edits to `~/.nomarchy` (his had 4) stayed forever-dirty —
theme-sync's commit is pathspec-limited to theme-state.json by design.
state-sync's commit is pathspec-limited to state.json by design.
- **Did:** New internal `nomarchy-autocommit` in nomarchy-lifecycle:
live-reads `settings.autoCommit`, commits *everything* dirty with
message `nomarchy: auto-commit before <label>` + file list in the
body; called (`|| true`, never blocks) at the top of nomarchy-pull /
-rebuild / -home. Exposed as `passthru.autocommit`; package exported.
Docs/comments swept: README, RECOVERY, ROADMAP ✓ entry, rofi toggle
text, control-center prompt, theme-sync docstring.
text, control-center prompt, state-sync docstring.
- **Verified:** V1+ — `nix flake check --no-build` green; new permanent
`checks.lifecycle-autocommit` builds (real binary, sandbox repos:
sweep/clean/off/non-repo); end-to-end on a *copy* of the real
@@ -1301,7 +1312,7 @@ Template:
- **Task:** Bernardo challenged the "V3 real-session" framing — a simulated
sunset IS VM-testable. He's right; I'd conflated "needs a real desktop"
with "needs the generic home-manager switch."
- **Did:** Added `checks.auto-theme` runNixOSTest: installs nomarchy-theme-sync,
- **Did:** Added `checks.auto-theme` runNixOSTest: installs nomarchy-state-sync,
points `NOMARCHY_DEFAULT_THEMES` at presets, stubs the switch via
`NOMARCHY_REBUILD` (a marker), configures the pair+schedule, then **walks
the VM clock** 19:55→20:05→20:06→next-day 07:30 asserting slug goes
@@ -1320,7 +1331,7 @@ Template:
- **Task:** BACKLOG #79 slice 3 — the menu; completes the feature.
- **Did:** `rofi.nix` — "Auto theme (on/off)" row in Look & Feel + an
`autotheme)` submenu: toggle, pick day/night (from
`nomarchy-theme-sync list`), set sunrise/sunset (HH:MM input, validated).
`nomarchy-state-sync list`), set sunrise/sunset (HH:MM input, validated).
Enable writes the flag + defaults the summer pair `--no-switch`, then
`exec auto --force` — one rebuild installs the timer AND applies the right
theme. Disable is instant `--no-switch` (`auto` self-gates; the lingering
@@ -1342,14 +1353,14 @@ Template:
- **Did:** `modules/home/autotheme.nix` (imported in home default.nix):
`systemd.user.{service,timer}.nomarchy-auto-theme`, install gated on
`config.nomarchy.settings.autoTheme.enable or false` (state-driven, like
updates.nix gates on its enable). Service runs `nomarchy-theme-sync auto`
updates.nix gates on its enable). Service runs `nomarchy-state-sync auto`
with a PATH for `home-manager`; timer `OnStartupSec=1min` +
`OnCalendar=*:0/15` + `Persistent`. Chose periodic over exact
sunrise/sunset timers — robust to suspend/DST, and `auto` is idempotent so
ticks are cheap (rebuild only at the transition).
- **Verified:** **V1** — real-config eval (state temp-enabled, restored):
`settings.autoTheme.enable`→true, timer `OnCalendar`=`*:0/15`, service
ExecStart=`… nomarchy-theme-sync auto`; disabled→no units (isolation eval
ExecStart=`… nomarchy-state-sync auto`; disabled→no units (isolation eval
condition=false). `systemd-analyze calendar '*:0/15'` valid. **V0** flake
check green. (Gotcha: a new .nix file is invisible to the flake until
`git add` — flake eval only sees tracked files.)
@@ -1360,7 +1371,7 @@ Template:
## 2026-07-10 — #79 slice 1: auto time-of-day theme CLI primitive
- **Task:** Split BACKLOG #79 (`VISION § D`, `[big]`) into 3 slices (CLI /
timer / menu); took slice 1.
- **Did:** `nomarchy-theme-sync auto` — reads
- **Did:** `nomarchy-state-sync auto` — reads
`settings.autoTheme.{enable,day,night,sunrise,sunset}`, computes day/night
by the local clock, and applies the matching preset **only if different**
(idempotent → the slice-2 timer won't rebuild needlessly). `--which`
@@ -1371,7 +1382,7 @@ Template:
against scratch state: wide-day→day slug, 00:0000:01 window→night,
disabled→no-op, missing day/night→die, `auto --no-switch` writes state,
re-run→"already on" no-op, plain `apply` still writes (refactor intact).
**V0** py_compile + flake check + `checks.theme-sync-validate` green.
**V0** py_compile + flake check + `checks.state-sync-validate` green.
- **Pending:** slices 2 (systemd sunrise/sunset timers + login apply, V2)
and 3 (Look & Feel menu) remain under #79.
- **Next suggestion:** #79 slice 2 — the timer/hook (V2 runNixOSTest).

View File

@@ -100,10 +100,10 @@ iteration would otherwise rediscover.
- **NixOS release bump is a trap:** the discarded attempt
(branch deleted 2026-06-22) hit a Hyprland OOM blocker; a redo is a
deliberate `v2`, never part of routine lock bumps.
- `theme-state.json` is git-tracked inside an 86 MB flake tree, so every
- `state.json` is git-tracked inside an 86 MB flake tree, so every
state write re-copies the source before eval — the wallpapers-artifact
split (BACKLOG LATER) is the decided fix (§ Faster switches).
- **Friendly theme-state load** (`modules/theme-state-read.nix`, #66):
- **Friendly theme-state load** (`modules/state-read.nix`, #66):
`builtins.tryEval` does **not** catch `readFile`/`fromJSON` failures —
gate with `pathExists` + empty/non-object checks before `fromJSON`.
Subtle JSON syntax errors still surface from nlohmann (line/col);

View File

@@ -169,7 +169,7 @@ ryzenadj)**, **no Secure Boot**, and the **stateVersion override**.
nomarchy.hardware.amd.enable = true; # amd-pstate + radeonsi VA-API
# nomarchy.hardware.amd.rocm.enable = true; # opt-in GPU compute (multi-GB)
# Auto-login is NOT set here: it lives in theme-state.json
# Auto-login is NOT set here: it lives in state.json
# (settings.greeter.autoLogin) so System Auto-login can move it — a line
# here would outrank the state and pin it. Turn it on after the first boot
# with `nomarchy-autologin on` (what nomarchy-install seeds on encrypted
@@ -206,9 +206,9 @@ emacs setup).
}
```
### `theme-state.json`
### `state.json`
Copy the template's `theme-state.json` (or let `nomarchy-menu theme` write
Copy the template's `state.json` (or let `nomarchy-menu theme` write
it after the switch). Your old `nomarchy-state.nix` prototype (schema
`nomarchy.theme = "nord"` …) is **retired** — the current distro uses this
JSON. `nord` is a shipped Nomarchy theme, so you lose nothing.
@@ -247,7 +247,7 @@ radeonsi), display brightness.
You should also get a one-shot **You're set** toast (menu / themes /
keys). If it never appears: `systemctl --user status nomarchy-first-boot`
and re-try with
`nomarchy-theme-sync set settings.firstBootShown false --no-switch`
`nomarchy-state-sync set settings.firstBootShown false --no-switch`
then log out/in.
If anything is wrong: **reboot → old generation.** Nothing is committed as
@@ -261,7 +261,7 @@ default yet.
powerprofile icons open the power menu. Your ryzenadj scripts are gone;
if you miss a specific TDP behaviour, that's a followup, not a blocker.
- **Theme:** `nomarchy-menu theme` → pick a preset (writes
`theme-state.json`).
`state.json`).
- **Snapshots:** `nomarchy-menu` → System → Snapshots should see your
existing `@snapshots` subvolume.
- **Fingerprint:** `fingerprint.enable = true` only starts **fprintd**
@@ -446,6 +446,25 @@ just reports that no swap is configured.
---
## State file rename (`theme-state.json` → `state.json`, #107)
The machine flake's git-tracked state file is **`state.json`** (appearance +
menu settings). Older checkouts may still have `theme-state.json`.
- **Eval:** `lib.mkFlake` and the reader accept either name (prefer
`state.json`).
- **Write:** `nomarchy-state-sync` (and the menu) always write `state.json`
and remove a leftover `theme-state.json` so you never have two sources.
- **CLI:** `nomarchy-state-sync` is the real name; `nomarchy-theme-sync`
remains a symlink for scripts and muscle memory. Drop the alias after the
next stable release notes call it out.
No action required on pull: the next theme apply or menu write migrates you.
To migrate by hand: `mv theme-state.json state.json && git add state.json`
(and `git rm theme-state.json` if it was tracked).
---
## TuringMachine — the decisions, at a glance
| Item | Choice |

View File

@@ -81,11 +81,11 @@ Both distros reskin everything from one place. In Nomarchy:
- **`SUPER+T`** (or `nomarchy-menu theme`) — pick from 24 presets with live
previews; it rebuilds and applies to Hyprland, Waybar, the terminal,
btop and GTK/Qt at once.
- Or from a shell: `nomarchy-theme-sync apply <slug>` (e.g. `gruvbox`,
- Or from a shell: `nomarchy-state-sync apply <slug>` (e.g. `gruvbox`,
`rose-pine`, `nord`).
- Tweak gaps / rounding / fonts / opacity / icon pack without leaving the
theme system — see [OVERRIDES.md](OVERRIDES.md). One git-tracked JSON
(`theme-state.json`) is the single source of truth.
(`state.json`) is the single source of truth.
---

View File

@@ -9,16 +9,16 @@ rule of thumb:
## 1. Appearance (gaps, colors, rounding, fonts, opacity) — use the CLI
Everything that defines the *look* flows from `theme-state.json`, the single
source of truth. Change it with `nomarchy-theme-sync`, which writes the JSON
Everything that defines the *look* flows from `state.json`, the single
source of truth. Change it with `nomarchy-state-sync`, which writes the JSON
and rebuilds — one generation, applied to Hyprland, Waybar, Kitty, btop and
Stylix at once:
```sh
nomarchy-theme-sync set ui.gapsOut 16 # gaps, borders, rounding, opacity
nomarchy-theme-sync set ui.rounding 0
nomarchy-theme-sync set fonts.mono "FiraCode Nerd Font"
nomarchy-theme-sync apply gruvbox # whole palette
nomarchy-state-sync set ui.gapsOut 16 # gaps, borders, rounding, opacity
nomarchy-state-sync set ui.rounding 0
nomarchy-state-sync set fonts.mono "FiraCode Nerd Font"
nomarchy-state-sync apply gruvbox # whole palette
```
### Icon pack
@@ -28,7 +28,7 @@ icon pack shipped by default (Papirus alone is ~1 GiB, so extra packs are
opt-in rather than a cost every install pays). To switch to another pack:
```sh
nomarchy-theme-sync set icons "Tela-dark" # or "" to return to Papirus-by-mode
nomarchy-state-sync set icons "Tela-dark" # or "" to return to Papirus-by-mode
```
Only the pack you name is pulled into your system (the first switch downloads
@@ -55,11 +55,11 @@ Turn it on from **Look & Feel Auto theme** (`SUPER+M`): toggle it, pick the
day and night themes, and set the sunrise/sunset times. The same from a shell:
```sh
nomarchy-theme-sync set settings.autoTheme.day summer-day --no-switch
nomarchy-theme-sync set settings.autoTheme.night summer-night --no-switch
nomarchy-theme-sync set settings.autoTheme.sunset 20:00 --no-switch
nomarchy-theme-sync set settings.autoTheme.enable true --no-switch
nomarchy-theme-sync auto --force # one rebuild: installs the timer + applies now
nomarchy-state-sync set settings.autoTheme.day summer-day --no-switch
nomarchy-state-sync set settings.autoTheme.night summer-night --no-switch
nomarchy-state-sync set settings.autoTheme.sunset 20:00 --no-switch
nomarchy-state-sync set settings.autoTheme.enable true --no-switch
nomarchy-state-sync auto --force # one rebuild: installs the timer + applies now
```
State lives in `settings.autoTheme`:
@@ -67,14 +67,14 @@ State lives in `settings.autoTheme`:
| Field | Meaning |
|---|---|
| `enable` | on / off |
| `day` / `night` | theme slugs — e.g. `summer-day` / `summer-night` (see `nomarchy-theme-sync list`) |
| `day` / `night` | theme slugs — e.g. `summer-day` / `summer-night` (see `nomarchy-state-sync list`) |
| `sunrise` / `sunset` | switch times, `"HH:MM"` (24-hour) |
A timer re-checks every ~15 minutes: the day theme applies around sunrise, the
night theme around sunset — but it only rebuilds when the active theme
actually needs to change, so most checks do nothing. Enabling rebuilds once
(to install the timer); disabling is instant. Preview the current decision
without switching with `nomarchy-theme-sync auto --which`.
without switching with `nomarchy-state-sync auto --which`.
These values are deliberately kept at normal priority in the modules, so they
stay owned by the theme system. If you *insist* on pinning one in `home.nix`
@@ -197,7 +197,7 @@ value is theme-owned at normal priority — either change it via the CLI (§1) o
| You want to… | Do this |
|---|---|
| Change gaps / colors / rounding / fonts | `nomarchy-theme-sync set …` or `apply` |
| Change gaps / colors / rounding / fonts | `nomarchy-state-sync set …` or `apply` |
| Change input / misc / monitor / animations / terminal chrome | plain assignment in `home.nix` |
| Arrange monitors declaratively | `nomarchy.monitors` (values via `nwg-displays`) |
| Add keybinds / autostarts | add to the `bind` / `exec-once` list (concatenates) |

View File

@@ -24,7 +24,7 @@ home-manager generations # newest first, one per theme/HM change
The same picker lives in the menu: **System Rollback** lists the
recent desktop generations — pick one and it activates.
Or simply apply a theme you know is good: `nomarchy-theme-sync apply
Or simply apply a theme you know is good: `nomarchy-state-sync apply
boreal` (or any preset). If a switch failed halfway, the state file is written
*before* the rebuild — fix the cause and re-run
`home-manager switch --flake ~/.nomarchy` (or `nomarchy-home`).

View File

@@ -19,7 +19,7 @@ Design/decision records and a running log of shipped work (items marked
## Roadmap
- **Menu system** (apps launcher + theme switching + system actions), built
on rofi 2.0 (native Wayland on 26.05) — its `.rasi` theme is baked from
theme-state.json like every other app, with rich per-element styling:
state.json like every other app, with rich per-element styling:
- ✓ shipped: `modules/home/rofi.nix` (per-element theme generated from
the palette, `themes/<slug>/rofi.rasi` whole-swap) and the
`nomarchy-menu` dispatcher: root picker (no args) · `power`
@@ -197,18 +197,18 @@ Design/decision records and a running log of shipped work (items marked
(confirmed): `themes/` is 86 MB and `backgrounds/` is **all** of it — the
palette JSONs + per-theme overrides are only ~208 KB, and the wallpapers are
**never read at Nix eval** (only the Python `swww` path uses them). But
`theme-state.json` is git-tracked, so every `apply` rewrites it → the flake
`state.json` is git-tracked, so every `apply` rewrites it → the flake
tree changes → Nix re-copies the whole 86 MB source before `home-manager
switch` can evaluate. You pay an 86 MB copy to change a 1 KB file.
**Decided approach (deferred — don't want the extra moving part yet):**
Option 1, a **separate pinned wallpapers artifact** — a `Nomarchy-wallpapers`
repo or release tarball, pulled once via a flake input / `fetchurl` (pinned
by hash → content-addressed, never re-copied on a state write), with
`nomarchy-theme-sync` reading wallpapers from that stable store path (the
`nomarchy-state-sync` reading wallpapers from that stable store path (the
`NOMARCHY_DEFAULT_THEMES` env hook already anticipates external theme
assets). Keeps eval **pure**; the live ISO still bakes them in (fetched at
build). A state write then re-copies only ~208 KB. Rejected alternative:
moving `theme-state.json` out to `~/.config` + `--impure` eval (one repo, no
moving `state.json` out to `~/.config` + `--impure` eval (one repo, no
second artifact, but trades away the in-tree-pinned-state reproducibility).
Follow-on if `home-manager switch` itself is still the bottleneck after the
copy is gone: **pre-built theme variants** (build each theme's generation
@@ -302,7 +302,7 @@ Design/decision records and a running log of shipped work (items marked
- ✓ **Auto time-of-day theme (#79, `VISION § D`):** an automatic light/dark
theme switch through the **same** engine as a manual apply — no second
pipeline. `settings.autoTheme = {enable, day, night, sunrise, sunset}`
drives `nomarchy-theme-sync auto`, which picks day vs night by the local
drives `nomarchy-state-sync auto`, which picks day vs night by the local
clock and applies the preset **only if it differs** (idempotent).
`modules/home/autotheme.nix` installs a user timer
(`OnStartupSec` + `OnCalendar=*:0/15` + `Persistent`, gated on the state
@@ -329,7 +329,7 @@ Design/decision records and a running log of shipped work (items marked
`settings.{bluetooth,printing}.enable` and printed "requires rebuild", and
the rebuild changed nothing, for as long as those toggles had existed.
**The pattern, now uniform:** the option declares a *static* default and
the implementing module reads the state via `modules/theme-state-read.nix`
the implementing module reads the state via `modules/state-read.nix`
(fails closed on bad JSON) and `mkDefault`s it —
`lib.mkIf (stateX != null) (lib.mkDefault stateX)`, so an absent key leaves
the option default as the single source of the fallback and a hand-set
@@ -427,6 +427,11 @@ Design/decision records and a running log of shipped work (items marked
default `$TERMINAL` / SUPER+Return, doctor + calendar + what-changed use
`kitty --class=com.nomarchy.*`. Ghostty module and install-time GL probe
removed. V3: Acer SUPER+Return + doctor float.
- ✓ **State file rename (#107):** `theme-state.json` → `state.json`; CLI
`nomarchy-theme-sync` → `nomarchy-state-sync` (old binary name kept as a
symlink). Eval/read shims accept the legacy file name; the next write
migrates. Documented in MIGRATION.md. Drop the CLI alias after the next
stable release notes call it out.
- ✓ **Installed default browser path (#94 residual):** live half closed by
#103 + `checks.live-baseline-apps`. Installed path V2 (2026-07-15): re-boot
of offline `test-install` disk — `chromium --version` on PATH (150.x),
@@ -1112,7 +1117,7 @@ Design/decision records and a running log of shipped work (items marked
`settings.nightlight.on` (runtime on/off). **Off by default.** The *first*
enable from the menu writes `installed` and rebuilds to create the unit (the
one accepted rebuild); **every toggle after is instant** —
`nomarchy-theme-sync set settings.nightlight.on … --no-switch` (atomic +
`nomarchy-state-sync set settings.nightlight.on … --no-switch` (atomic +
`git add -N`, no rebuild) plus a `systemctl` start/stop. Splitting the sticky
flag from the on/off is what avoids a **decay** bug: an instant-off writes
only `on`, so an unrelated later rebuild (e.g. `sys-update`) never drops the
@@ -1143,7 +1148,7 @@ Design/decision records and a running log of shipped work (items marked
(opt-in, off by default) — geoclue + `services.automatic-timezoned` drive
`/etc/localtime` from your location, so travelling to another zone updates the
Waybar clock on its own. **Menu-driven, in-flake state** (the night-light /
keyboard philosophy): the flag is `settings.autoTimezone` in theme-state.json,
keyboard philosophy): the flag is `settings.autoTimezone` in state.json,
git-tracked; the System-menu entry (`nomarchy-menu autotimezone` →
`nomarchy-autotimezone`) writes it and rebuilds. **Not instant like
night-light** — it's a *system* service (not a user unit you can start/stop),
@@ -1318,10 +1323,10 @@ Design/decision records and a running log of shipped work (items marked
- ✓ **Opt-in auto-commit of state mutations (in-flake state, Phase 4):**
`settings.autoCommit` (menu: **System Auto-commit**, self-gated on the
flake being a git repo) makes every `apply`/`set` also `git commit`
theme-state.json in the downstream flake. Design decisions: the flag is
state.json in the downstream flake. Design decisions: the flag is
**live-read by the tool** on each write — nothing in Nix consumes it, so
the toggle is instant, no rebuild, no option-surface addition; the commit
is **pathspec-limited** (`git commit -- theme-state.json`) so unrelated
is **pathspec-limited** (`git commit -- state.json`) so unrelated
dirty files are never swept up; it fires when the flag is on before *or*
after the write, so the disable-toggle itself lands in history instead of
staying forever-dirty; a same-value `set` no-ops (diff against HEAD); a
@@ -1347,7 +1352,7 @@ Design/decision records and a running log of shipped work (items marked
the real binary: sweep, no-op on clean/off/non-repo).
- **"nomarchy" control center:** a single TUI/GUI front-end over the common
toggles — theme, power profile, opt-in services, display, DND — built on
the same `nomarchy-theme-sync` / `nomarchy.*` surface the menu already
the same `nomarchy-state-sync` / `nomarchy.*` surface the menu already
uses. Plus a first-boot welcome that lands new installs in a guided "pick
your theme / essentials" flow (ties into the branding work).
@@ -1390,11 +1395,11 @@ Design/decision records and a running log of shipped work (items marked
hardware QA, 2026-07-04): exec-once has no supervisor, so any crash
orphaned the session until relogin — and the switch path itself was
crash-prone: `reload_style_on_change` (inotify on style.css) and
theme-sync's SIGUSR2 both fired while the HM symlinks flipped, a
state-sync's SIGUSR2 both fired while the HM symlinks flipped, a
double-reload race in waybar's in-place reload. Fix: (1) the bar runs
under a `nomarchy-waybar` supervisor (waybar.nix) — ANY exit respawns
it, with a crash-loop guard (5 fast exits → critical notification,
stop); (2) theme-sync now prefers a clean `pkill -x waybar` when it
stop); (2) state-sync now prefers a clean `pkill -x waybar` when it
sees the supervisor (a restart with fresh config+style — no reload
code path at all), keeping SIGUSR2 only as the fallback for
unsupervised/custom bars; `reload_style_on_change` stays for manual

View File

@@ -72,10 +72,10 @@ Work through these in order; each one exercises a different layer.
| 2 | Waybar shows at top, themed (blue accent on dark) | HM waybar module, palette baking |
| 3 | `SUPER+Return` opens Kitty with the default theme colors (Boreal) | terminal default, ANSI palette |
| 4 | `btop` in the terminal is themed | per-theme asset baking |
| 5 | `nomarchy-theme-sync list` prints 24 presets | package, baked themes dir |
| 6 | `nomarchy-theme-sync apply boreal` (or re-apply the **current** theme) offline → switch succeeds. Applying a *different* preset may need a network — the ISO pins the default theme's generation, not every preset (#113) | state write + offline rebuild contract |
| 5 | `nomarchy-state-sync list` prints 24 presets | package, baked themes dir |
| 6 | `nomarchy-state-sync apply boreal` (or re-apply the **current** theme) offline → switch succeeds. Applying a *different* preset may need a network — the ISO pins the default theme's generation, not every preset (#113) | state write + offline rebuild contract |
| 7 | `SUPER+SHIFT+T` cycles wallpapers instantly (try `tokyo-night` for 4, or `boreal` for its set) | the runtime wallpaper path |
| 8 | `nomarchy-theme-sync apply summer-night` → after the switch the bar has its own identity (light bar, different styling) | whole-swap waybar.css assets |
| 8 | `nomarchy-state-sync apply summer-night` → after the switch the bar has its own identity (light bar, different styling) | whole-swap waybar.css assets |
| 9 | Open a GTK app — dark theme matching the palette | Stylix layer |
| 10 | `home-manager generations` lists one generation per theme change; activating an older one rolls the theme back | atomicity / rollback story |
@@ -137,7 +137,7 @@ Hyprland's headless backend otherwise aborts in a VM-only zero-output state.
- Session/login problems: `journalctl -b -u greetd`, then `journalctl
--user -b`.
- Theme switch failures: run `nomarchy-theme-sync apply <x>` from a
- Theme switch failures: run `nomarchy-state-sync apply <x>` from a
terminal — the home-manager output streams there. The state file is
written *before* the rebuild, so after fixing you can just re-run
`home-manager switch --flake ~/.nomarchy`.

View File

@@ -72,7 +72,7 @@ Reference: [HARDWARE.md](HARDWARE.md) §4§5, §11 (doctor).
| Idea | Intent | Notes |
|------|--------|-------|
| **Silent first-week card** ✓ | One dismissible “youre set” (menu, themes, keys, network) — not a wizard | ✓ shipped #81`settings.firstBootShown` + notify; live ISO keeps its own toast |
| **Boreal as default** ✓ | Identity on first boot | ✓ seed `theme-state.json` + install path use Boreal (2026-07-09) |
| **Boreal as default** ✓ | Identity on first boot | ✓ seed `state.json` + install path use Boreal (2026-07-09) |
| **Generation readability** ✓ | “What changed last rebuild” in plain language | ✓ shipped #82 — nvd toast + System What changed? |
| **Post-install hints** | One-shot MOTD/notify for fwupd / fprintd when relevant | Partial: MOTD + control-center tips; still no self-gated *first-session* toast when hardware is present (see BACKLOG PROPOSED) |

View File

@@ -75,12 +75,12 @@
# ─── Downstream API ────────────────────────────────────────────
# A user's machine flake imports these and layers its own
# system.nix / home.nix on top (see templates/downstream).
# Their theme-state.json lives in THEIR flake and is wired up via
# Their state.json lives in THEIR flake and is wired up via
# the `nomarchy.stateFile` option — reading it stays pure.
overlays.default = final: prev: {
nomarchy-theme-sync = final.callPackage ./pkgs/nomarchy-theme-sync {
# Presets baked into the package, so `nomarchy-theme-sync list`
nomarchy-state-sync = final.callPackage ./pkgs/nomarchy-state-sync {
# Presets baked into the package, so `nomarchy-state-sync list`
# works on machines that don't check out this repo.
themesDir = ./themes;
};
@@ -127,7 +127,9 @@
};
packages.${system} = {
nomarchy-theme-sync = pkgs.nomarchy-theme-sync;
nomarchy-state-sync = pkgs.nomarchy-state-sync;
# #107 compat alias — same derivation; drop after release notes say so.
nomarchy-theme-sync = pkgs.nomarchy-state-sync;
nomarchy-install = nomarchyInstall;
# Overlay tools exported for `nix build .#nomarchy-doctor` etc.
# (maintainer/CI convenience — modules install them via the overlay).
@@ -138,7 +140,7 @@
nomarchy-first-boot = pkgs.nomarchy-first-boot;
nomarchy-what-changed = pkgs.nomarchy-what-changed;
nomarchy-detect-hw = pkgs.nomarchy-detect-hw;
default = pkgs.nomarchy-theme-sync;
default = pkgs.nomarchy-state-sync;
};
# ─── Checks ────────────────────────────────────────────────────
@@ -155,11 +157,11 @@
hardwareProfile = "framework-13-7040-amd"; # exercises the mapping
};
# theme-state.json fixtures for the state-bridge checks below,
# state.json fixtures for the state-bridge checks below,
# built off the real shipped template so they stay honest about
# what a machine actually carries.
baseState = builtins.fromJSON
(builtins.readFile ./templates/downstream/theme-state.json);
(builtins.readFile ./templates/downstream/state.json);
stateFileOf = name: overrides: pkgs.writeText name
(builtins.toJSON (nixpkgs.lib.recursiveUpdate baseState overrides));
# The shipped template carries none of these keys — the
@@ -236,7 +238,7 @@
throw ''
State bridge broken a settings.* key no longer reaches the config:
${nixpkgs.lib.concatMapStringsSep "\n" (c: " ${c.n}") failed}
Read the state via modules/theme-state-read.nix and mkDefault it
Read the state via modules/state-read.nix and mkDefault it
in the module `config.nomarchy.settings` does NOT exist on the
NixOS side, and `or <fallback>` swallows that silently.
See ROADMAP § "NixOS-side state bridges (#116)".''
@@ -330,53 +332,53 @@
# reports broken fixtures by field with a fix, good state
# passes, and an invalid `set` is refused BEFORE the write —
# the on-disk file must be byte-identical after the attempt.
theme-sync-validate = pkgs.runCommand "nomarchy-theme-sync-validate"
state-sync-validate = pkgs.runCommand "nomarchy-state-sync-validate"
{ nativeBuildInputs = [ pkgs.python3 pkgs.jq ]; }
''
set -eu
tool=${./pkgs/nomarchy-theme-sync/nomarchy-theme-sync.py}
tool=${./pkgs/nomarchy-state-sync/nomarchy-state-sync.py}
export NOMARCHY_DEFAULT_THEMES=${./themes}
mkdir good && cp ${./templates/downstream/theme-state.json} good/theme-state.json
chmod +w good/theme-state.json
mkdir good && cp ${./templates/downstream/state.json} good/state.json
chmod +w good/state.json
NOMARCHY_PATH=$PWD/good python3 "$tool" validate
mkdir syntax && printf '{ "slug": "x", }' > syntax/theme-state.json
mkdir syntax && printf '{ "slug": "x", }' > syntax/state.json
if NOMARCHY_PATH=$PWD/syntax python3 "$tool" validate 2>err; then
echo "FAIL: syntax fixture accepted"; exit 1; fi
grep -q "syntax error at line" err
mkdir badhex && jq '.colors.accent = "7aa2f7"' \
good/theme-state.json > badhex/theme-state.json
good/state.json > badhex/state.json
if NOMARCHY_PATH=$PWD/badhex python3 "$tool" validate 2>err; then
echo "FAIL: bad hex accepted"; exit 1; fi
grep -q "colors.accent" err && grep -q "fix:" err
mkdir badtype && jq '.ui.gapsOut = "12"' \
good/theme-state.json > badtype/theme-state.json
good/state.json > badtype/state.json
if NOMARCHY_PATH=$PWD/badtype python3 "$tool" validate 2>err; then
echo "FAIL: bad type accepted"; exit 1; fi
grep -q "ui.gapsOut" err
mkdir unknown && jq '.extraKey = 1' \
good/theme-state.json > unknown/theme-state.json
good/state.json > unknown/state.json
NOMARCHY_PATH=$PWD/unknown python3 "$tool" validate \
| grep -q "unknown top-level key"
cp good/theme-state.json before.json
cp good/state.json before.json
if NOMARCHY_PATH=$PWD/good python3 "$tool" --quiet \
set ui.gapsOut '"12"' --no-switch 2>err; then
echo "FAIL: invalid set was written"; exit 1; fi
grep -q "refusing to write" err
cmp good/theme-state.json before.json
cmp good/state.json before.json
touch $out
'';
# The lifecycle "sweep before switch": with settings.autoCommit
# on, nomarchy-pull/-rebuild/-home first commit every dirty file
# in the machine flake (theme-sync's own auto_commit stays
# pathspec-limited to theme-state.json). Exercises the real
# in the machine flake (state-sync's own auto_commit stays
# pathspec-limited to state.json). Exercises the real
# helper binary against sandbox repos: flag on → one commit,
# clean tree, fallback identity, file list in the body; clean
# tree / flag off / no repo → untouched, never an error.
@@ -388,7 +390,7 @@
export HOME=$PWD # no global git config fallback identity
mkdir flake && git -C flake init -q
printf '{ "settings": { "autoCommit": true } }' > flake/theme-state.json
printf '{ "settings": { "autoCommit": true } }' > flake/state.json
echo base > flake/system.nix
git -C flake add -A
git -C flake -c user.name=T -c user.email=t@t commit -qm init
@@ -408,7 +410,7 @@
[ "$(git -C flake rev-list --count HEAD)" = 2 ]
# Flag off dirty stays dirty, no commit
printf '{ "settings": { "autoCommit": false } }' > flake/theme-state.json
printf '{ "settings": { "autoCommit": false } }' > flake/state.json
echo more >> flake/system.nix
NOMARCHY_PATH=$PWD/flake $tool rebuild
[ "$(git -C flake rev-list --count HEAD)" = 2 ]
@@ -416,7 +418,7 @@
# Not a git repo silent no-op, exit 0
mkdir plain
printf '{ "settings": { "autoCommit": true } }' > plain/theme-state.json
printf '{ "settings": { "autoCommit": true } }' > plain/state.json
NOMARCHY_PATH=$PWD/plain $tool rebuild
touch $out
@@ -1391,7 +1393,7 @@
modules = [
self.homeModules.nomarchy
{
nomarchy.stateFile = ./templates/downstream/theme-state.json;
nomarchy.stateFile = ./templates/downstream/state.json;
nomarchy.nightlight = {
enable = true;
latitude = "52.52";
@@ -1482,14 +1484,14 @@
# First-session welcome toast gate (#81): fires once when
# settings.firstBootShown is unset, writes the marker via
# theme-sync, silent on re-run; skips hostname nomarchy-live.
# notify-send + theme-sync shimmed on PATH (same pattern as
# notify-send + state-sync shimmed on PATH (same pattern as
# battery-notify). Real toast rendering stays session/V3.
first-boot = pkgs.testers.runNixOSTest {
name = "nomarchy-first-boot";
nodes.machine = { ... }: {
environment.systemPackages = [
pkgs.nomarchy-first-boot
pkgs.nomarchy-theme-sync
pkgs.nomarchy-state-sync
pkgs.jq
];
};
@@ -1499,8 +1501,8 @@
# Writable flake checkout with a valid theme-state (template).
machine.succeed(
"mkdir -p /tmp/fake-nomarchy && "
"cp ${./templates/downstream/theme-state.json} "
"/tmp/fake-nomarchy/theme-state.json"
"cp ${./templates/downstream/state.json} "
"/tmp/fake-nomarchy/state.json"
)
machine.succeed(
@@ -1521,7 +1523,7 @@
assert "SUPER+M" in out and "SUPER+T" in out, f"key pointers missing:\n{out}"
assert "Network" in out or "Wi" in out, f"network pointer missing:\n{out}"
shown = machine.succeed(
f"{env} nomarchy-theme-sync get settings.firstBootShown"
f"{env} nomarchy-state-sync get settings.firstBootShown"
).strip()
assert shown == "true", f"marker not written: {shown!r}"
@@ -1532,7 +1534,7 @@
# Live ISO hostname: skip even with marker cleared.
machine.succeed(
f"{env} nomarchy-theme-sync --quiet set settings.firstBootShown false --no-switch"
f"{env} nomarchy-state-sync --quiet set settings.firstBootShown false --no-switch"
)
machine.succeed("rm -f /tmp/notifications")
machine.succeed(f"{env} hostname nomarchy-live || true")
@@ -1810,7 +1812,7 @@
# Auto time-of-day theme (#79, VISION § D): simulate crossing
# sunset and sunrise by moving the VM clock, and assert
# `nomarchy-theme-sync auto` switches the active theme accordingly
# `nomarchy-state-sync auto` switches the active theme accordingly
# — day stays day (no needless rebuild), sunset flips to night,
# a later tick is idempotent, sunrise flips back. The actual
# `home-manager switch` is stubbed via NOMARCHY_REBUILD (that
@@ -1819,7 +1821,7 @@
auto-theme = pkgs.testers.runNixOSTest {
name = "nomarchy-auto-theme";
nodes.machine = { pkgs, ... }: {
environment.systemPackages = [ pkgs.nomarchy-theme-sync ];
environment.systemPackages = [ pkgs.nomarchy-state-sync ];
time.timeZone = "UTC"; # naive HH:MM == the clock we set
};
testScript = ''
@@ -1830,7 +1832,7 @@
# that records each invocation instead of running home-manager.
machine.succeed("mkdir -p /root/.nomarchy")
machine.succeed(
"echo '{\"slug\":\"summer-day\"}' > /root/.nomarchy/theme-state.json"
"echo '{\"slug\":\"summer-day\"}' > /root/.nomarchy/state.json"
)
machine.succeed(
"printf '#!/bin/sh\\necho switch >> /root/rebuilds\\n' > /root/rebuild"
@@ -1847,34 +1849,34 @@
"settings.autoTheme.night summer-night",
"settings.autoTheme.sunrise 07:00",
"settings.autoTheme.sunset 20:00"]:
machine.succeed(env + "nomarchy-theme-sync --quiet set " + kv + " --no-switch")
machine.succeed(env + "nomarchy-state-sync --quiet set " + kv + " --no-switch")
def slug():
return machine.succeed(env + "nomarchy-theme-sync get slug").strip()
return machine.succeed(env + "nomarchy-state-sync get slug").strip()
def rebuilds():
return int(machine.succeed("wc -l < /root/rebuilds 2>/dev/null || echo 0").strip())
# Before sunset: already the day theme no switch, no rebuild.
machine.succeed("date -s '2026-01-02 19:55:00'")
machine.succeed(env + "nomarchy-theme-sync auto")
machine.succeed(env + "nomarchy-state-sync auto")
assert slug() == "summer-day", f"pre-sunset should stay day, got {slug()}"
assert rebuilds() == 0, f"no needless rebuild, got {rebuilds()}"
# Cross sunset night theme, exactly one rebuild triggered.
machine.succeed("date -s '2026-01-02 20:05:00'")
machine.succeed(env + "nomarchy-theme-sync auto")
machine.succeed(env + "nomarchy-state-sync auto")
assert slug() == "summer-night", f"post-sunset should be night, got {slug()}"
assert rebuilds() == 1, f"sunset should trigger one rebuild, got {rebuilds()}"
# Another tick still after sunset idempotent, no extra rebuild.
machine.succeed("date -s '2026-01-02 20:06:00'")
machine.succeed(env + "nomarchy-theme-sync auto")
machine.succeed(env + "nomarchy-state-sync auto")
assert slug() == "summer-night"
assert rebuilds() == 1, f"idempotent tick must not rebuild, got {rebuilds()}"
# Cross sunrise the next morning back to day, second rebuild.
machine.succeed("date -s '2026-01-03 07:30:00'")
machine.succeed(env + "nomarchy-theme-sync auto")
machine.succeed(env + "nomarchy-state-sync auto")
assert slug() == "summer-day", f"post-sunrise should be day, got {slug()}"
assert rebuilds() == 2, f"sunrise should trigger a rebuild, got {rebuilds()}"
'';
@@ -1946,12 +1948,12 @@
# ─── Reference host ────────────────────────────────────────────
# Manually wired rather than via lib.mkFlake: the repo-root
# theme-state.json is load-bearing (live ISO + in-repo CLI dev),
# state.json is load-bearing (live ISO + in-repo CLI dev),
# which doesn't match mkFlake's src-layout convention.
# Deliberately split rebuild paths:
# system → sudo nixos-rebuild switch --flake .#nomarchy (rare)
# desktop → home-manager switch --flake .#nomarchy (no sudo;
# this is what `nomarchy-theme-sync apply` runs)
# this is what `nomarchy-state-sync apply` runs)
# Theme changes never touch the system layer, so they never need
# root and never rebuild the world.
nixosConfigurations.nomarchy = nixpkgs.lib.nixosSystem {
@@ -1959,7 +1961,7 @@
specialArgs = { inherit username; };
modules = [
self.nixosModules.nomarchy
{ nomarchy.system.stateFile = ./theme-state.json; }
{ nomarchy.system.stateFile = ./state.json; }
./hosts/default/configuration.nix
];
};
@@ -1971,7 +1973,7 @@
{
# The single source of truth — pure read: the file is part
# of this flake's source.
nomarchy.stateFile = ./theme-state.json;
nomarchy.stateFile = ./state.json;
home = {
inherit username;
homeDirectory = "/home/${username}";
@@ -2004,7 +2006,7 @@
useUserPackages = true;
users.${username} = { pkgs, ... }: {
imports = [ self.homeModules.nomarchy ];
nomarchy.stateFile = ./theme-state.json;
nomarchy.stateFile = ./state.json;
# Live-session baseline apps (ROADMAP § live-ISO baseline
# apps, #103). Until now the
@@ -2045,7 +2047,7 @@
};
};
# The standalone CLI that `nomarchy-theme-sync apply` runs for
# The standalone CLI that `nomarchy-state-sync apply` runs for
# theme switching, from the same pinned input — plus the
# guided installer.
environment.systemPackages = [

View File

@@ -7,10 +7,10 @@
let
# ISO boot branding: the Nomarchy monogram recolored to the palette accent,
# centred on the theme base. Built from the vendored vector logo and the
# live theme-state.json (Boreal by default). The same composed image
# live state.json (Boreal by default). The same composed image
# backs both the isolinux (BIOS) splash and the GRUB (UEFI) theme below, so
# the two boot paths match.
state = builtins.fromJSON (builtins.readFile ../theme-state.json);
state = builtins.fromJSON (builtins.readFile ../state.json);
isoColor = key: fallback: lib.removePrefix "#" ((state.colors or { }).${key} or fallback);
accent = isoColor "accent" "B79BE8";
base = isoColor "base" "21272F";
@@ -156,7 +156,7 @@ in
chmod -R u+w "$home/.nomarchy"
# Committed git repo, exactly like nomarchy-install produces
# otherwise nomarchy-doctor (and its Waybar badge) false-alarms
# "theme-state.json is NOT git-tracked" in the live session.
# "state.json is NOT git-tracked" in the live session.
(
cd "$home/.nomarchy"
${pkgs.git}/bin/git init -q
@@ -174,8 +174,8 @@ in
Welcome to the Nomarchy live environment.
The graphical session autologins as '${username}' (no password).
Theme switching: nomarchy-theme-sync apply <name> (or SUPER+T)
Wallpapers: nomarchy-theme-sync bg next (or SUPER+SHIFT+T)
Theme switching: nomarchy-state-sync apply <name> (or SUPER+T)
Wallpapers: nomarchy-state-sync bg next (or SUPER+SHIFT+T)
Install to disk: nomarchy-install
The flake lives at ~/.nomarchy.
'';

21
lib.nix
View File

@@ -1,5 +1,5 @@
# Downstream sugar: one call wires a whole machine flake.
# Users own ONLY system.nix, home.nix and theme-state.json; their flake.nix
# Users own ONLY system.nix, home.nix and state.json; their flake.nix
# is generated once (by `nix flake init -t` or the future installer) and
# never hand-edited. The raw exports (nixosModules/homeModules/overlays)
# in flake.nix remain the escape hatch for power users.
@@ -8,9 +8,9 @@
let
inherit (nixpkgs) lib;
# Shared pure reader (modules/theme-state-read.nix) — re-exported so
# Shared pure reader (modules/state-read.nix) — re-exported so
# power users composing without mkFlake can call it too.
readThemeState = import ./modules/theme-state-read.nix { inherit lib; };
readThemeState = import ./modules/state-read.nix { inherit lib; };
in
{
inherit readThemeState;
@@ -59,13 +59,18 @@ in
config.allowUnfree = true;
};
# Early fail-closed gate: missing/empty/non-object theme-state.json
# Early fail-closed gate: missing/empty/non-object state.json
# throws here with a template + validate pointer, before module
# evaluation buries a raw readFile/fromJSON stack. Field-level
# schema still runs in modules/home/theme.nix after defaults merge.
# Forced via seq on the whole return set — attrNames alone must not
# skip the check (Nix is lazy on unused let bindings and attr values).
statePath = src + "/theme-state.json";
# #107: prefer state.json; accept legacy theme-state.json until
# the user's next menu write migrates them.
statePath =
if builtins.pathExists (src + "/state.json") then src + "/state.json"
else if builtins.pathExists (src + "/theme-state.json") then src + "/theme-state.json"
else src + "/state.json";
_themeState = readThemeState statePath;
in
builtins.seq _themeState {
@@ -78,7 +83,7 @@ in
[
nomarchy.nixosModules.nomarchy
# The standalone HM CLI ships with the system so theme switching
# (`nomarchy-theme-sync apply` → `home-manager switch`) works
# (`nomarchy-state-sync apply` → `home-manager switch`) works
# out of the box — same pinned input as the desktop modules.
{ environment.systemPackages = [ home-manager.packages.${system}.home-manager ]; }
# System-side theme consumers (Plymouth splash background)
@@ -94,14 +99,14 @@ in
# Desktop layer — every theme change, no sudo:
# home-manager switch --flake .#<username>
# (`nomarchy-theme-sync apply` runs this for you.)
# (`nomarchy-state-sync apply` runs this for you.)
homeConfigurations.${username} = home-manager.lib.homeManagerConfiguration {
inherit pkgs;
modules = [
nomarchy.homeModules.nomarchy
(src + "/home.nix")
{
# Written by nomarchy-theme-sync; reading it is pure — the
# Written by nomarchy-state-sync; reading it is pure — the
# file is part of the downstream flake's source.
nomarchy.stateFile = statePath;
home = {

View File

@@ -1,5 +1,5 @@
# Auto time-of-day theme switch (BACKLOG #79, VISION § D). A user timer
# periodically runs `nomarchy-theme-sync auto`, which reads
# periodically runs `nomarchy-state-sync auto`, which reads
# settings.autoTheme = { enable, day, night, sunrise, sunset } from the
# state file and applies the day or night preset for the current clock —
# through the SAME one engine as a manual `apply` (no second pipeline).
@@ -20,7 +20,7 @@ lib.mkIf (config.nomarchy.settings.autoTheme.enable or false) {
# The rebuild (`home-manager switch`) and its tools resolve from the
# system + per-user profiles — same PATH shape nomarchy-updates uses.
Environment = "PATH=/run/current-system/sw/bin:/etc/profiles/per-user/${config.home.username}/bin";
ExecStart = "${pkgs.nomarchy-theme-sync}/bin/nomarchy-theme-sync auto";
ExecStart = "${pkgs.nomarchy-state-sync}/bin/nomarchy-state-sync auto";
};
};

View File

@@ -6,7 +6,7 @@
{
imports = [
./options.nix # the nomarchy.* option surface
./theme.nix # ingests theme-state.json, owns the live-sync hooks
./theme.nix # ingests state.json, owns the live-sync hooks
./stylix.nix # GTK/Qt/cursor/fonts from the same JSON
./hyprland.nix
./waybar.nix
@@ -106,8 +106,8 @@
# (system nixpkgs.config alone does not cover those entry points).
NIXPKGS_ALLOW_UNFREE = "1";
# Where the Nomarchy flake (and therefore theme-state.json) lives on
# disk. nomarchy-theme-sync writes its state here; rebuilds read from
# Where the Nomarchy flake (and therefore state.json) lives on
# disk. nomarchy-state-sync writes its state here; rebuilds read from
# here. Clone/symlink your flake to this path.
NOMARCHY_PATH = "$HOME/.nomarchy";
};

View File

@@ -32,7 +32,7 @@
Environment = [
"PATH=${lib.makeBinPath [
pkgs.libnotify
pkgs.nomarchy-theme-sync
pkgs.nomarchy-state-sync
pkgs.coreutils
]}"
"NOMARCHY_PATH=%h/.nomarchy"

View File

@@ -1,4 +1,4 @@
# Hyprland — fully driven by config.nomarchy.theme (theme-state.json).
# Hyprland — fully driven by config.nomarchy.theme (state.json).
# Gaps, borders and colors are baked from the JSON at eval time; theme
# changes arrive via `home-manager switch`.
{ config, lib, pkgs, ... }:
@@ -8,7 +8,7 @@ let
c = t.colors;
inherit (config.nomarchy.lib) rgb rgba;
# nomarchy-theme-sync — the in-flake state writer the keyboard watcher uses
# nomarchy-state-sync — the in-flake state writer the keyboard watcher uses
# to remember per-device layouts (same path night-light's toggle takes).
sync = lib.getExe config.nomarchy.package;
@@ -883,7 +883,7 @@ in
"awww-daemon"
# Paint the wallpaper as soon as the session is up (waits for
# the daemon internally).
"nomarchy-theme-sync wallpaper"
"nomarchy-state-sync wallpaper"
# Polkit authentication agent — without one, EVERY pkexec/polkit
# prompt in the session fails silently (btrfs-assistant-launcher
# was the discovery case). hyprpolkitagent is Hyprland's own Qt
@@ -903,8 +903,8 @@ in
];
# ── Theme-driven look ──────────────────────────────────────────
# These flow from theme-state.json and stay at NORMAL priority:
# change them with `nomarchy-theme-sync set ui.<key>` (the intended
# These flow from state.json and stay at NORMAL priority:
# change them with `nomarchy-state-sync set ui.<key>` (the intended
# path), or lib.mkForce in home.nix to hardcode against the theme.
# The non-theme knobs around them are lib.mkDefault — override
# those with a plain home.nix assignment. See docs/OVERRIDES.md.

View File

@@ -1,5 +1,5 @@
# hyprlock + hypridle — screen locking and idle management, themed from
# theme-state.json. One concern, one file: hypridle drives WHEN (idle
# state.json. One concern, one file: hypridle drives WHEN (idle
# lock, display off, suspend, lock-before-sleep), hyprlock is the
# themed lock screen itself (also behind the power menu's Lock entry).
{ config, lib, pkgs, ... }:

View File

@@ -26,7 +26,7 @@
# home-manager switch (progress via notify-send).
{ mods = "$mod"; key = "T"; action = "exec, nomarchy-menu theme"; desc = "Theme picker"; group = "Menu"; }
# Cycle the current theme's wallpapers (instant, no rebuild).
{ mods = "$mod SHIFT"; key = "T"; action = "exec, nomarchy-theme-sync bg next"; desc = "Next wallpaper"; group = "Menu"; }
{ mods = "$mod SHIFT"; key = "T"; action = "exec, nomarchy-state-sync bg next"; desc = "Next wallpaper"; group = "Menu"; }
# Power menu via the dispatcher. Not Escape: Super+Escape gets
# swallowed before reaching the dispatcher on some setups.

View File

@@ -1,4 +1,4 @@
# Kitty — Nomarchy's only terminal, themed from theme-state.json.
# Kitty — Nomarchy's only terminal, themed from state.json.
# Colors, fonts and the full 16-color ANSI palette are baked from the
# JSON at eval time (same contract Ghostty used to have). Always
# installed: SUPER+Return, SUPER+E, doctor, calendar, and $TERMINAL

View File

@@ -88,11 +88,11 @@ in
# ── Required ───────────────────────────────────────────────────
stateFile = lib.mkOption {
type = lib.types.path;
example = lib.literalExpression "./theme-state.json";
example = lib.literalExpression "./state.json";
description = ''
Path to theme-state.json, the single source of truth for all UI
Path to state.json, the single source of truth for all UI
configuration. Must live inside your flake (so evaluation stays
pure) and be git-tracked. nomarchy-theme-sync writes to the
pure) and be git-tracked. nomarchy-state-sync writes to the
on-disk copy; rebuilds bake it into the generation.
'';
};
@@ -182,9 +182,9 @@ in
package = lib.mkOption {
type = lib.types.package;
default = pkgs.nomarchy-theme-sync;
defaultText = lib.literalExpression "pkgs.nomarchy-theme-sync";
description = "The nomarchy-theme-sync package (provided by overlays.default).";
default = pkgs.nomarchy-state-sync;
defaultText = lib.literalExpression "pkgs.nomarchy-state-sync";
description = "The nomarchy-state-sync package (provided by overlays.default).";
};
themesDir = lib.mkOption {
@@ -379,7 +379,7 @@ in
type = lib.types.bool;
default = config.nomarchy.settings.fingerprint.pam or false;
defaultText = lib.literalExpression
"(settings.fingerprint.pam from theme-state.json) or false";
"(settings.fingerprint.pam from state.json) or false";
description = ''
Unlock the lock screen with a fingerprint as well as the password, and
say so on the input field.

View File

@@ -2,7 +2,7 @@
# lock). The media keys drive it via swayosd-client (it performs the
# action AND shows the OSD), so changing volume or brightness gives the
# visual feedback that bare wpctl/brightnessctl didn't. Themed from
# theme-state.json. Brightness needs the backlight udev rule shipped
# state.json. Brightness needs the backlight udev rule shipped
# system-side (modules/nixos/default.nix → services.udev.packages).
{ config, lib, pkgs, ... }:

View File

@@ -1,5 +1,5 @@
# rofi (2.0, native Wayland on 26.05) — launcher + dmenu renderer for
# the menu system, themed from theme-state.json. rofi's .rasi is far more
# the menu system, themed from state.json. rofi's .rasi is far more
# expressive than a flat scheme, so the generated theme styles each
# element (accent border, highlighted selection, rounded inputbar); a
# themes/<slug>/rofi.rasi whole-swap replaces it entirely.
@@ -71,7 +71,7 @@ let
# One grid row per theme (square thumb + name, ✓ on the active one), and a
# Name→slug map so the picked label resolves back to the preset that
# nomarchy-theme-sync applies (pretty names ≠ slugs, hence the map). Themes
# nomarchy-state-sync applies (pretty names ≠ slugs, hence the map). Themes
# with no preview degrade to a plain-name row.
themeRows = lib.concatMapStringsSep "\n" (th:
let label = th.name + lib.optionalString (th.slug == t.slug) " ";
@@ -190,7 +190,7 @@ let
nomarchy-menu = pkgs.writeShellScriptBin "nomarchy-menu" ''
# Nomarchy menu dispatcher thin presentation layer over
# `rofi -dmenu` (via rofi_menu helper); actions delegate to
# nomarchy-theme-sync, systemctl, hyprctl and friends. Menu entries
# nomarchy-state-sync, systemctl, hyprctl and friends. Menu entries
# carry real icons from the theme's icon set (Papirus) via rofi's
# per-row icon protocol see row() below. `nomarchy-menu` with no
# argument shows the module picker.
@@ -350,7 +350,7 @@ let
# power-profile icons (the two granular System Power profile /
# Battery limit rows stay for direct access). No new backend: profile
# rows drive powerprofilesctl like the power-profile picker, charge
# rows drive the same theme-sync writer as batterylimit. Each half
# rows drive the same state-sync writer as batterylimit. Each half
# self-gates on its hardware, so a desktop with neither just toasts.
haveppd=false; command -v powerprofilesctl >/dev/null 2>&1 && haveppd=true
havecharge=false; has_charge_threshold && havecharge=true
@@ -359,7 +359,7 @@ let
exit 0
fi
curp=""; $haveppd && curp=$(powerprofilesctl get 2>/dev/null)
curc=$(nomarchy-theme-sync get settings.power.batteryChargeLimit 2>/dev/null)
curc=$(nomarchy-state-sync get settings.power.batteryChargeLimit 2>/dev/null)
choice=$( {
if $haveppd; then
powerprofilesctl list 2>/dev/null | sed -nE 's/^[* ] ([a-z-]+):$/\1/p' \
@@ -384,7 +384,7 @@ let
# allows users to restart it) so a non-writable sysfs node still
# updates. Fallback: direct echo if the node is group-writable.
set_limit() {
nomarchy-theme-sync --quiet set settings.power.batteryChargeLimit "$1" --no-switch
nomarchy-state-sync --quiet set settings.power.batteryChargeLimit "$1" --no-switch
n="$1"; [ "$n" = null ] && n=100
# Root oneshot sets Custom charge type (Dell) + thresholds.
applied=
@@ -452,7 +452,7 @@ ${themeRows}
[ "$choice" = "$BACK" ] && exec "$0"
choice="''${choice% }" # drop the active marker if present
slug="''${THEME_SLUG[$choice]:-}"
[ -n "$slug" ] && exec nomarchy-theme-sync apply "$slug" ;;
[ -n "$slug" ] && exec nomarchy-state-sync apply "$slug" ;;
clipboard)
sel=$( { cliphist list; printf '%s\n' "$BACK"; } | rofi_menu -p clip) || exit 0
@@ -691,7 +691,7 @@ ${themeRows}
pos=$(printf '%s' "$mons" | jq -r --arg n "$name" '.[]|select(.name==$n)|"\(.x)x\(.y)"')
scale=$(printf '%s' "$mons" | jq -r --arg n "$name" '.[]|select(.name==$n)|.scale')
if hyprctl keyword monitor "$name,$mode,$pos,$scale" >/dev/null 2>&1; then
nomarchy-theme-sync --quiet set "settings.monitors.$name" "$mode" --no-switch
nomarchy-state-sync --quiet set "settings.monitors.$name" "$mode" --no-switch
got=$(hyprctl monitors -j | jq -r --arg n "$name" \
'.[]|select(.name==$n)|"\(.width)x\(.height)@\(.refreshRate|round)"')
notify-send "Display" "$name $mode (now ${got:-?})"
@@ -759,7 +759,7 @@ ${themeRows}
# Power / Keybindings). SUPER+T / SUPER+SHIFT+T still hit leaves.
choice=$( {
row "Theme${menuHint "theme"}" preferences-desktop-theme
row "Next wallpaper${hintForAction "exec, nomarchy-theme-sync bg next"}" preferences-desktop-wallpaper
row "Next wallpaper${hintForAction "exec, nomarchy-state-sync bg next"}" preferences-desktop-wallpaper
row "Reset wallpaper (auto)" preferences-desktop-wallpaper
# Use the toggle script (knows hyprsunset vs wlsunset); a hard-coded
# hyprsunset-only check always showed (off) in geo mode and when
@@ -768,7 +768,7 @@ ${themeRows}
then row "Night light (on)" weather-clear-night
else row "Night light (off)" weather-clear-night
fi
if [ "$(nomarchy-theme-sync get settings.autoTheme.enable 2>/dev/null)" = true ]
if [ "$(nomarchy-state-sync get settings.autoTheme.enable 2>/dev/null)" = true ]
then row "Auto theme (on)" preferences-desktop-theme
else row "Auto theme (off)" preferences-desktop-theme
fi
@@ -778,8 +778,8 @@ ${themeRows}
"$BACK") exec "$0" ;;
*"Auto theme"*) exec "$0" autotheme ;;
*Theme*) exec "$0" theme ;;
*"Next wallpaper"*) exec nomarchy-theme-sync bg next ;;
*"Reset wallpaper"*) exec nomarchy-theme-sync bg auto ;;
*"Next wallpaper"*) exec nomarchy-state-sync bg next ;;
*"Reset wallpaper"*) exec nomarchy-state-sync bg auto ;;
*"Night light"*) exec "$0" nightlight ;;
esac ;;
@@ -796,7 +796,7 @@ ${themeRows}
if [ "$p" = "$active" ]; then printf ' %s\n' "$p"; else printf ' %s\n' "$p"; fi
done
printf 'Base layout\n'
if [ "$(nomarchy-theme-sync get settings.displayProfileAuto 2>/dev/null)" = true ]
if [ "$(nomarchy-state-sync get settings.displayProfileAuto 2>/dev/null)" = true ]
then printf 'Auto-switch (on)\n'
else printf 'Auto-switch (off)\n'
fi
@@ -808,11 +808,11 @@ ${themeRows}
*"Auto-switch"*)
# Instant in-flake flag; the watcher reads it live on the next
# output change no rebuild, no unit juggling.
if [ "$(nomarchy-theme-sync get settings.displayProfileAuto 2>/dev/null)" = true ]; then
nomarchy-theme-sync --quiet set settings.displayProfileAuto false --no-switch
if [ "$(nomarchy-state-sync get settings.displayProfileAuto 2>/dev/null)" = true ]; then
nomarchy-state-sync --quiet set settings.displayProfileAuto false --no-switch
notify-send "Display profiles" "Auto-switch off profiles change only from this menu."
else
nomarchy-theme-sync --quiet set settings.displayProfileAuto true --no-switch
nomarchy-state-sync --quiet set settings.displayProfileAuto true --no-switch
notify-send "Display profiles" "Auto-switch on plugging/unplugging outputs picks the matching profile."
fi
exec "$0" display-profile ;;
@@ -869,12 +869,12 @@ ${themeRows}
# (settings.autoTheme, applied by the nomarchy-auto-theme timer from
# autotheme.nix). The enable flag GATES the timer's install, so
# turning it *on* must rebuild; day/night/times are read live by
# `nomarchy-theme-sync auto`, so those writes are instant
# `nomarchy-state-sync auto`, so those writes are instant
# (--no-switch). Enabling writes the flag then `auto --force`, so a
# single rebuild both installs the timer and applies the right theme
# now. Disabling is instant: the flag flips and `auto` self-gates to
# a no-op, so the lingering timer does nothing until the next rebuild.
at_get() { nomarchy-theme-sync get "settings.autoTheme.$1" 2>/dev/null; }
at_get() { nomarchy-state-sync get "settings.autoTheme.$1" 2>/dev/null; }
en=$(at_get enable); day=$(at_get day); night=$(at_get night)
sunrise=$(at_get sunrise); sunset=$(at_get sunset)
choice=$( {
@@ -892,25 +892,25 @@ ${themeRows}
"$BACK") exec "$0" lookfeel ;;
*"Auto theme"*)
if [ "$en" = true ]; then
nomarchy-theme-sync --quiet set settings.autoTheme.enable false --no-switch
nomarchy-state-sync --quiet set settings.autoTheme.enable false --no-switch
notify-send "Auto theme" "Off current theme stays."
exec "$0" autotheme
fi
# Need a day + night pair before enabling; default the summer pair.
[ -n "$day" ] || nomarchy-theme-sync --quiet set settings.autoTheme.day summer-day --no-switch
[ -n "$night" ] || nomarchy-theme-sync --quiet set settings.autoTheme.night summer-night --no-switch
nomarchy-theme-sync --quiet set settings.autoTheme.enable true --no-switch
[ -n "$day" ] || nomarchy-state-sync --quiet set settings.autoTheme.day summer-day --no-switch
[ -n "$night" ] || nomarchy-state-sync --quiet set settings.autoTheme.night summer-night --no-switch
nomarchy-state-sync --quiet set settings.autoTheme.enable true --no-switch
notify-send "Auto theme" "On applying the theme for now"
exec nomarchy-theme-sync auto --force ;; # one rebuild: install timer + apply
exec nomarchy-state-sync auto --force ;; # one rebuild: install timer + apply
"Day theme"*)
slug=$( { nomarchy-theme-sync list; printf '%s\n' "$BACK"; } | rofi_menu -p "Day theme") || exec "$0" autotheme
slug=$( { nomarchy-state-sync list; printf '%s\n' "$BACK"; } | rofi_menu -p "Day theme") || exec "$0" autotheme
[ "$slug" != "$BACK" ] && [ -n "$slug" ] \
&& nomarchy-theme-sync --quiet set settings.autoTheme.day "$slug" --no-switch
&& nomarchy-state-sync --quiet set settings.autoTheme.day "$slug" --no-switch
exec "$0" autotheme ;;
"Night theme"*)
slug=$( { nomarchy-theme-sync list; printf '%s\n' "$BACK"; } | rofi_menu -p "Night theme") || exec "$0" autotheme
slug=$( { nomarchy-state-sync list; printf '%s\n' "$BACK"; } | rofi_menu -p "Night theme") || exec "$0" autotheme
[ "$slug" != "$BACK" ] && [ -n "$slug" ] \
&& nomarchy-theme-sync --quiet set settings.autoTheme.night "$slug" --no-switch
&& nomarchy-state-sync --quiet set settings.autoTheme.night "$slug" --no-switch
exec "$0" autotheme ;;
"Sunrise"*|"Sunset"*)
key=sunrise; label=Sunrise; cur="''${sunrise:-07:00}"
@@ -919,7 +919,7 @@ ${themeRows}
case "$t" in
""|"$BACK") : ;;
[01][0-9]:[0-5][0-9]|2[0-3]:[0-5][0-9])
nomarchy-theme-sync --quiet set "settings.autoTheme.$key" "$t" --no-switch ;;
nomarchy-state-sync --quiet set "settings.autoTheme.$key" "$t" --no-switch ;;
*) notify-send "Auto theme" "Ignored '$t' use HH:MM (24-hour)." ;;
esac
exec "$0" autotheme ;;
@@ -945,18 +945,18 @@ ${themeRows}
autocommit)
# Toggle opt-in auto-commit: every menu/theme mutation also commits
# theme-state.json (that file only) in the downstream flake, so
# state.json (that file only) in the downstream flake, so
# settings history is `git log`; nomarchy-pull/-rebuild/-home
# additionally sweep everything else dirty (hand edits, lock bumps)
# into a commit first. Nothing in Nix consumes the flag
# the tool reads the live state on each write so the toggle is
# instant, no rebuild. The off-write commits too (the tool fires
# when the flag was on before OR after), keeping history consistent.
if [ "$(nomarchy-theme-sync get settings.autoCommit 2>/dev/null)" = true ]; then
nomarchy-theme-sync --quiet set settings.autoCommit false --no-switch
if [ "$(nomarchy-state-sync get settings.autoCommit 2>/dev/null)" = true ]; then
nomarchy-state-sync --quiet set settings.autoCommit false --no-switch
notify-send "Auto-commit" "Off menu changes stay uncommitted in your flake."
else
nomarchy-theme-sync --quiet set settings.autoCommit true --no-switch
nomarchy-state-sync --quiet set settings.autoCommit true --no-switch
notify-send "Auto-commit" "On menu changes commit; rebuilds sweep pending edits in too."
fi
exit 0 ;;
@@ -1017,7 +1017,7 @@ ${themeRows}
# CLI (services.fprintd). Enroll is interactive terminal.
command -v fprintd-list >/dev/null 2>&1 \
|| { notify-send "Fingerprint" "fprintd not available (nomarchy.hardware.fingerprint.enable?)."; exit 0; }
pam=$(nomarchy-theme-sync get settings.fingerprint.pam 2>/dev/null || echo false)
pam=$(nomarchy-state-sync get settings.fingerprint.pam 2>/dev/null || echo false)
case "$pam" in true|True) pam_label="Fingerprint (on)" ;; *) pam_label="Fingerprint (off)" ;; esac
choice=$( {
# The switch leads: it is the decision this menu exists for, and
@@ -1178,7 +1178,7 @@ ${themeRows}
&& row "Keyboard${menuHint "keyboard"}" preferences-desktop-keyboard
command -v system-config-printer >/dev/null 2>&1 && row "Printers" printer
row "Do Not Disturb${menuHint "dnd"}" notification-disabled
if [ "$(nomarchy-theme-sync get settings.autoTimezone 2>/dev/null)" = true ]
if [ "$(nomarchy-state-sync get settings.autoTimezone 2>/dev/null)" = true ]
then row "Auto timezone (on)" preferences-system-time
else row "Auto timezone (off)" preferences-system-time
fi
@@ -1186,15 +1186,15 @@ ${themeRows}
# greeter asks; that's the prompt the Fingerprint switch feeds.
if command -v nomarchy-autologin >/dev/null 2>&1; then
# Unset (get exits 1, empty) and null both read as off. "None" is
# what an older theme-sync printed for null accepted so a menu
# what an older state-sync printed for null accepted so a menu
# from a newer generation can't misreport against an older tool.
case "$(nomarchy-theme-sync get settings.greeter.autoLogin 2>/dev/null)" in
case "$(nomarchy-state-sync get settings.greeter.autoLogin 2>/dev/null)" in
null|""|None) row "Auto-login (off)" system-users ;;
*) row "Auto-login (on)" system-users ;;
esac
fi
if [ -e "''${NOMARCHY_PATH:-$HOME/.nomarchy}/.git" ]; then
if [ "$(nomarchy-theme-sync get settings.autoCommit 2>/dev/null)" = true ]
if [ "$(nomarchy-state-sync get settings.autoCommit 2>/dev/null)" = true ]
then row "Auto-commit (on)" git
else row "Auto-commit (off)" git
fi
@@ -1255,7 +1255,7 @@ ${themeRows}
|| { notify-send "Battery limit" \
"This machine's firmware does not expose a charge-stop control (no charge_control_end_threshold in /sys), so a charge limit cannot be set here. The battery still works normally this is a hardware capability, not a Nomarchy problem."
exit 0; }
cur=$(nomarchy-theme-sync get settings.power.batteryChargeLimit 2>/dev/null)
cur=$(nomarchy-state-sync get settings.power.batteryChargeLimit 2>/dev/null)
sel=$( {
row "80% (recommended)" battery-080
row "90%" battery-090
@@ -1265,7 +1265,7 @@ ${themeRows}
back
} | rofi_menu -show-icons -p "Battery limit (now: ''${cur:-default})") || exit 0
set_limit() {
nomarchy-theme-sync --quiet set settings.power.batteryChargeLimit "$1" --no-switch
nomarchy-state-sync --quiet set settings.power.batteryChargeLimit "$1" --no-switch
n="$1"; [ "$n" = null ] && n=100
applied=
if systemctl restart nomarchy-battery-charge-limit.service 2>/dev/null; then

View File

@@ -1,5 +1,5 @@
# Stylix — themes the long tail of applications (GTK, Qt, cursors,
# fonts) from the same theme-state.json that drives the live engine.
# fonts) from the same state.json that drives the live engine.
#
# Division of labour: the hot-reload trio (Hyprland, Waybar, Kitty)
# is owned by the Nomarchy engine and updates instantly; everything

View File

@@ -1,5 +1,5 @@
# swaync — notification daemon + control centre, themed from
# theme-state.json. Until this shipped, nothing rendered notify-send at
# state.json. Until this shipped, nothing rendered notify-send at
# all: the theme-switch progress toasts, the CLI's font warnings and the
# live ISO's welcome message were all invisible.
{ config, lib, ... }:
@@ -29,7 +29,7 @@ in
};
style = ''
/* Palette baked from theme-state.json. Only roles guaranteed to
/* Palette baked from state.json. Only roles guaranteed to
contrast @base in EVERY palette are used: subtext/surface mean
"on-surface" in some light themes (summer-day: subtext==base,
surface==text body text was invisible on hardware, item 25).

View File

@@ -1,10 +1,10 @@
# Nomarchy theming engine.
#
# nomarchy.stateFile (theme-state.json, inside the consuming flake) is
# nomarchy.stateFile (state.json, inside the consuming flake) is
# ingested at evaluation time — pure, because flake files are store
# paths — and exposed to every other module as `config.nomarchy.theme`.
#
# Theme changes are fully Home Manager managed: `nomarchy-theme-sync
# Theme changes are fully Home Manager managed: `nomarchy-state-sync
# apply <theme>` writes the JSON and runs `home-manager switch`, baking
# everything (Hyprland, Waybar, Kitty, btop, Stylix) into one
# read-only generation. No runtime patching, no partial states; theme
@@ -13,17 +13,17 @@
# The one runtime exception is the wallpaper (swww is imperative by
# nature): applied at session start, after every switch (hook below), and
# after output hotplug (hyprland.nix); cycled instantly with
# `nomarchy-theme-sync bg next`.
# `nomarchy-state-sync bg next`.
{ config, lib, pkgs, ... }:
let
cfg = config.nomarchy;
# Fail-closed load: missing / empty / non-object get a short pointer at
# the template + `nomarchy-theme-sync validate`, not a raw readFile stack
# the template + `nomarchy-state-sync validate`, not a raw readFile stack
# from deep inside a consumer (nightlight, hyprland, …). Field-level
# checks below still run on the merged result.
themeState = import ../theme-state-read.nix { inherit lib; } cfg.stateFile;
themeState = import ../state-read.nix { inherit lib; } cfg.stateFile;
# Defaults guarantee evaluation succeeds on a sparse or older state
# file (e.g. one written before a schema field was added). The shipped
@@ -92,7 +92,7 @@ let
parsed = lib.recursiveUpdate defaults themeState;
# ── Friendly eval-time validation ───────────────────────────────────
# The same schema nomarchy-theme-sync enforces before every write. A
# The same schema nomarchy-state-sync enforces before every write. A
# HAND-edited state file (the one path that bypasses the tool) must
# fail with the field, the problem, and the fix — not a Nix stack
# trace deep in some consumer. Checks run on `parsed` (after the
@@ -137,14 +137,14 @@ let
if problems == [ ] then parsed
else throw ''
Nomarchy: your theme-state.json is invalid:
Nomarchy: your state.json is invalid:
${lib.concatMapStrings (p: " ${p}\n") problems}
Fix the named field(s) in the theme-state.json of your flake
checkout (usually ~/.nomarchy/theme-state.json the store copy at
Fix the named field(s) in the state.json of your flake
checkout (usually ~/.nomarchy/state.json the store copy at
${toString cfg.stateFile} is a snapshot of it). The tool prints
the same report with per-field fixes: `nomarchy-theme-sync
the same report with per-field fixes: `nomarchy-state-sync
validate`. Re-applying any preset resets all appearance fields:
`nomarchy-theme-sync apply boreal`.'';
`nomarchy-state-sync apply boreal`.'';
# A border value is a palette key (look it up in colors) unless it's
# already a literal hex. Unknown roles are rejected by the field checks
@@ -155,7 +155,7 @@ let
Nomarchy: border role "${v}" is not in the palette (colors.*).
Use one of: ${lib.concatStringsSep ", " colorRoles}
or a literal "#RRGGBB". Validate with: nomarchy-theme-sync validate'');
or a literal "#RRGGBB". Validate with: nomarchy-state-sync validate'');
border = {
active = resolveColor checked.border.active;
inactive = resolveColor checked.border.inactive;

View File

@@ -2,7 +2,7 @@
# timezone. Waybar's clock module captures the zone once at construction, so a
# runtime timezone change (automatic-timezoned, nomarchy.system.autoTimezone)
# would NOT show until a relogin. A tiny watcher subscribes to timedate1's
# change signal and reloads Waybar (SIGUSR2 = the same reload theme-sync uses),
# change signal and reloads Waybar (SIGUSR2 = the same reload state-sync uses),
# so the clock follows your location live. Also catches a manual
# `timedatectl set-timezone`.
#

View File

@@ -1,7 +1,7 @@
# Waybar — two-tier theming:
#
# 1. Default: structure, fonts, geometry AND palette baked from
# theme-state.json (colors as GTK named colors, @define-color).
# state.json (colors as GTK named colors, @define-color).
#
# 2. Whole-swap: themes with their own visual identity ship
# <themesDir>/<slug>/waybar.css (and optionally waybar.jsonc) which
@@ -35,7 +35,7 @@ let
# Waybar supervisor — exec-once has no restart, so a crashed bar used to
# leave the session bar-less until relogin (seen on hardware: a theme
# switch crashed waybar mid-reload). Respawns on ANY exit — a plain
# `pkill -x waybar` is now a clean restart, which nomarchy-theme-sync
# `pkill -x waybar` is now a clean restart, which nomarchy-state-sync
# uses instead of the crash-prone in-place SIGUSR2 reload when it sees
# this supervisor running. Crash-loop guard: 5 exits within 10s of
# their start → give up with a critical notification instead of
@@ -377,7 +377,7 @@ let
};
generatedStyle = ''
/* Palette baked from theme-state.json */
/* Palette baked from state.json */
${colorDefs}
/* NB: this `*` reset reaches the SNI tray menus Waybar hosts too
@@ -522,7 +522,7 @@ in
# on a warm relogin — it started before the socket was up, exited, landed
# in `failed`, and was never retried, so the bar vanished. exec-once only
# fires once Hyprland is up, dodging the race; theme switches reload the
# running bar via SIGUSR2 (nomarchy-theme-sync). No uwsm here to manage the
# running bar via SIGUSR2 (nomarchy-state-sync). No uwsm here to manage the
# session target, so we don't depend on its lifecycle.
systemd.enable = false;

View File

@@ -1,6 +1,6 @@
# yazi — the flagship file manager: a fast, keyboard-driven TUI that
# fits the distro's identity (Kitty graphics previews, rofi
# menus, everything from one JSON). Themed from theme-state.json and
# menus, everything from one JSON). Themed from state.json and
# shipped with a curated plugin set. The GUI half (Thunar, "open folder"
# handler) is nomarchy.system.fileManager on the system side.
#

View File

@@ -24,11 +24,11 @@ let
# Control Center's Bluetooth toggle writes settings.bluetooth.enable; read
# it from the state file, the only place it exists on the NixOS side (the
# hardware.nix/timezone.nix bridge). Missing/invalid JSON fails closed via
# theme-state-read.nix rather than a raw stack. null = key absent, which
# state-read.nix rather than a raw stack. null = key absent, which
# leaves the option's own default (true) alone.
sysState =
if cfg.stateFile != null
then import ../theme-state-read.nix { inherit lib; } cfg.stateFile
then import ../state-read.nix { inherit lib; } cfg.stateFile
else { };
stateBluetooth =
let v = (sysState.settings or { }).bluetooth.enable or null;
@@ -70,7 +70,7 @@ in
nomarchy-pull update flake inputs (nomarchy, nixpkgs, )
nomarchy-rebuild rebuild the system (current lock)
nomarchy-home rebuild the desktop / Home Manager
nomarchy-theme-sync apply <theme> switch the whole palette
nomarchy-state-sync apply <theme> switch the whole palette
nomarchy-doctor read-only health check
SUPER+? keybindings cheatsheet
SUPER+M System Firmware check LVFS firmware updates (fwupd)
@@ -336,7 +336,7 @@ in
# ── Fonts ────────────────────────────────────────────────────────
# The ten most popular Nerd Fonts ship by default, so any of them
# can be named in the theme state's fonts.mono and actually resolve
# (nomarchy-theme-sync warns when a configured font is missing).
# (nomarchy-state-sync warns when a configured font is missing).
fonts = {
packages = with pkgs; [
nerd-fonts.jetbrains-mono
@@ -373,7 +373,7 @@ in
# ── Essential packages ───────────────────────────────────────────
environment.systemPackages = with pkgs; [
nomarchy-theme-sync # provided by overlays.default
nomarchy-state-sync # provided by overlays.default
nomarchy-doctor # read-only health check (System Doctor)
nomarchy-control-center # TUI control center
nomarchy-detect-hw # post-install hardware re-probe (HARDWARE.md §8)
@@ -508,7 +508,7 @@ in
experimental-features = [ "nix-command" "flakes" ];
auto-optimise-store = lib.mkDefault true;
# The downstream flake (~/.nomarchy) is meant to be a live working
# tree: nomarchy-theme-sync rewrites theme-state.json on every
# tree: nomarchy-state-sync rewrites state.json on every
# switch (and you needn't commit each tweak), so the "Git tree is
# dirty" warning fires on every rebuild and is pure noise here.
warn-dirty = lib.mkDefault false;

View File

@@ -1,4 +1,4 @@
# Greeter — greetd/tuigreet, themed from the same theme-state.json that
# Greeter — greetd/tuigreet, themed from the same state.json that
# drives the desktop (nomarchy.system.stateFile; the Plymouth model:
# baked at SYSTEM rebuild, so it follows the theme as of the last
# sys-update, not the last instant apply).
@@ -27,7 +27,7 @@ let
cfg = config.nomarchy.system;
distroName = config.system.nixos.distroName;
sync = lib.getExe pkgs.nomarchy-theme-sync;
sync = lib.getExe pkgs.nomarchy-state-sync;
# Menu/CLI toggle, same shape as nomarchy-autotimezone: runs as the normal
# user (it owns the flake checkout + writes the state), sudos only the
@@ -62,12 +62,12 @@ let
fi
'';
# Fails closed with an actionable message via theme-state-read.nix, like
# Fails closed with an actionable message via state-read.nix, like
# every other stateFile consumer — a raw fromJSON here would bury a bad
# state file under a Nix stack pointing at greeter.nix.
state =
if cfg.stateFile != null
then import ../theme-state-read.nix { inherit lib; } cfg.stateFile
then import ../state-read.nix { inherit lib; } cfg.stateFile
else { };
# The auto-login user from the state, or null. Read here via the state file

View File

@@ -15,16 +15,16 @@
let
cfg = config.nomarchy.hardware;
# Fingerprint PAM can follow theme-state.json (menu toggle → next
# Fingerprint PAM can follow state.json (menu toggle → next
# sys-rebuild), same bridge as autoTimezone (BACKLOG #55). Missing or
# invalid JSON fails closed (theme-state-read.nix) instead of a raw stack.
# invalid JSON fails closed (state-read.nix) instead of a raw stack.
hwState =
if config.nomarchy.system.stateFile != null
then import ../theme-state-read.nix { inherit lib; } config.nomarchy.system.stateFile
then import ../state-read.nix { inherit lib; } config.nomarchy.system.stateFile
else { };
pamFromState = (hwState.settings or { }).fingerprint.pam or false;
sync = lib.getExe pkgs.nomarchy-theme-sync;
sync = lib.getExe pkgs.nomarchy-state-sync;
# The single fingerprint on/off switch (System Fingerprint). One state key
# for one user-facing decision — it drives login/sudo PAM here AND the
@@ -156,11 +156,11 @@ in
type = lib.types.bool;
default = pamFromState;
defaultText = lib.literalExpression
"(settings.fingerprint.pam from theme-state.json) or false";
"(settings.fingerprint.pam from state.json) or false";
description = ''
Use the fingerprint for login and sudo (PAM). Opt-in password-only
stays the default for the cautious; enroll a finger first. Defaults
from theme-state.json `settings.fingerprint.pam` (System Fingerprint
from state.json `settings.fingerprint.pam` (System Fingerprint
menu) when set; otherwise false.
'';
};

View File

@@ -13,14 +13,14 @@
type = lib.types.nullOr lib.types.str;
default = null;
defaultText = lib.literalExpression
"(settings.greeter.autoLogin from theme-state.json) or null";
"(settings.greeter.autoLogin from state.json) or null";
example = "ada";
description = ''
Log this user straight into Hyprland on boot (greetd
initial_session); logging out lands on the normal greeter.
Normally you leave this alone and use System Auto-login, which
writes `settings.greeter.autoLogin` in theme-state.json
writes `settings.greeter.autoLogin` in state.json
./greeter.nix mkDefaults this option from it. The installer seeds
that state on LUKS-encrypted machines: the disk passphrase already
gates access, so a second prompt is ceremony. Setting this option by
@@ -30,15 +30,15 @@
plymouth.enable = lib.mkEnableOption ''
the Nomarchy Plymouth boot splash (logo + progress + LUKS prompt),
background-tinted from theme-state.json via nomarchy.system.stateFile.
background-tinted from state.json via nomarchy.system.stateFile.
Recolors on system rebuilds theme switches don't touch the initrd'' // { default = true; };
stateFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = lib.literalExpression "./theme-state.json";
example = lib.literalExpression "./state.json";
description = ''
theme-state.json for the system-side consumers (currently the
state.json for the system-side consumers (currently the
Plymouth splash background). lib.mkFlake wires it automatically
from your flake; null falls back to the Boreal base color.
'';
@@ -59,7 +59,7 @@
bluetooth.enable = lib.mkEnableOption "Bluetooth support with blueman" // {
default = true;
defaultText = lib.literalExpression
"(settings.bluetooth.enable from theme-state.json) or true";
"(settings.bluetooth.enable from state.json) or true";
};
autoTimezone.enable = lib.mkEnableOption ''
@@ -119,7 +119,7 @@
batteryChargeLimit = lib.mkOption {
type = lib.types.nullOr (lib.types.ints.between 50 100);
# No state bridge at eval time, by design: ./power.nix's oneshot reads
# settings.power.batteryChargeLimit out of the live theme-state.json
# settings.power.batteryChargeLimit out of the live state.json
# with jq at *runtime* and prefers it over this baked value, so the
# menu applies before (and without) a rebuild. This used to read
# `config.nomarchy.settings…`, which does not exist on the NixOS side

View File

@@ -1,10 +1,10 @@
# Plymouth boot splash — Nomarchy-branded (ported from the legacy
# iteration), background tinted from the same theme-state.json that
# iteration), background tinted from the same state.json that
# drives the desktop (nomarchy.system.stateFile, wired automatically by
# lib.mkFlake). One caveat by design: theme switches are Home
# Manager-only and never touch the initrd, so the splash follows the
# theme as of the last SYSTEM rebuild (`sys-update`), not the last
# `nomarchy-theme-sync apply`.
# `nomarchy-state-sync apply`.
{ config, lib, pkgs, ... }:
let
@@ -12,7 +12,7 @@ let
state =
if cfg.stateFile != null
then import ../theme-state-read.nix { inherit lib; } cfg.stateFile
then import ../state-read.nix { inherit lib; } cfg.stateFile
else { };
colorOf = key: fallback: lib.removePrefix "#" ((state.colors or { }).${key} or fallback);
# Fallbacks match the distro default theme (Boreal) when stateFile is null.

View File

@@ -58,7 +58,7 @@ in
# Instant menu path (user decision 2026-07-10): the threshold node is
# group-writable for `users` so nomarchy-menu can echo live without
# rebuild; this oneshot still owns boot + AC-replug re-apply. Prefer
# live theme-state.json under /home/*/.nomarchy so a menu change
# live state.json under /home/*/.nomarchy so a menu change
# survives reboot before the next sys-rebuild bakes the Nix option.
systemd.services.nomarchy-battery-charge-limit = lib.mkIf cfg.laptop {
description = "Apply battery charge end threshold from state or config";
@@ -80,7 +80,7 @@ in
# Baked generation default (empty = full charge / no cap).
limit=${if cfg.batteryChargeLimit != null then toString cfg.batteryChargeLimit else ""}
# Prefer the newest live state write (menu path, no rebuild yet).
for st in /home/*/.nomarchy/theme-state.json; do
for st in /home/*/.nomarchy/state.json; do
[ -r "$st" ] || continue
v=$(jq -r '.settings.power.batteryChargeLimit // empty' "$st" 2>/dev/null || true)
case "$v" in

View File

@@ -11,11 +11,11 @@ let
# Control Center's Printing toggle writes settings.printing.enable; read it
# from the state file, the only place it exists on the NixOS side (the
# hardware.nix/timezone.nix bridge). Missing/invalid JSON fails closed via
# theme-state-read.nix rather than a raw stack. null = key absent, which
# state-read.nix rather than a raw stack. null = key absent, which
# leaves the option's own default alone.
svcState =
if config.nomarchy.system.stateFile != null
then import ../theme-state-read.nix { inherit lib; } config.nomarchy.system.stateFile
then import ../state-read.nix { inherit lib; } config.nomarchy.system.stateFile
else { };
statePrinting =
let v = (svcState.settings or { }).printing.enable or null;
@@ -97,7 +97,7 @@ in
System Printers entry opens the system-config-printer GUI'' // {
default = false;
defaultText = lib.literalExpression
"(settings.printing.enable from theme-state.json) or false";
"(settings.printing.enable from state.json) or false";
};
openrgb.enable = lib.mkEnableOption ''

View File

@@ -4,7 +4,7 @@
# /etc/localtime at runtime.
#
# In-flake state, menu-driven (the keyboard/night-light philosophy): the on/off
# flag lives in the same theme-state.json under `settings.autoTimezone`
# flag lives in the same state.json under `settings.autoTimezone`
# (git-tracked, reproducible), written by the System-menu toggle
# (nomarchy-autotimezone). Because this is a SYSTEM service — not a user unit it
# can start/stop instantly like night-light — the toggle drives a system rebuild
@@ -16,14 +16,14 @@ let
# Read the same state file the rest of the system side uses (Plymouth too),
# wired by lib.mkFlake. The flag defaults off when stateFile is null;
# a set-but-missing/invalid path fails closed via theme-state-read.nix.
# a set-but-missing/invalid path fails closed via state-read.nix.
state =
if cfg.stateFile != null
then import ../theme-state-read.nix { inherit lib; } cfg.stateFile
then import ../state-read.nix { inherit lib; } cfg.stateFile
else { };
stateEnabled = (state.settings or { }).autoTimezone or false;
sync = lib.getExe pkgs.nomarchy-theme-sync;
sync = lib.getExe pkgs.nomarchy-state-sync;
# Menu/CLI toggle. Runs as the normal user (it owns the flake checkout +
# writes the state); sudos only the system switch, like sys-update. Writes

View File

@@ -1,4 +1,4 @@
# Pure theme-state.json loader — fail closed with a short, actionable
# Pure state.json loader — fail closed with a short, actionable
# message instead of a raw `readFile` / `fromJSON` stack buried in a
# consumer. Used by modules/home/theme.nix (required path), the NixOS
# stateFile consumers, and lib.mkFlake (early gate).
@@ -6,43 +6,49 @@
# Field-level schema checks stay in theme.nix (post-defaults). This file
# only gates *existence* and *JSON-shape* so the first failure the user
# sees points at the state file, not at nightlight.nix.
#
# Callers may still pass a legacy path (state.json); messages name
# whatever path they gave so a half-migrated checkout is still debuggable.
{ lib }:
path:
let
pathStr = toString path;
baseName = baseNameOf pathStr;
tip = ''
Fix:
Missing file copy the template next to your flake.nix:
templates/downstream/theme-state.json
templates/downstream/state.json
or regenerate from a preset:
nomarchy-theme-sync apply boreal
Bad syntax / wrong shape edit theme-state.json (trailing commas
nomarchy-state-sync apply boreal
Bad syntax / wrong shape edit ${baseName} (trailing commas
are the usual culprit) or reset with `apply` as above.
Field-level schema (colors, ui, border, ):
nomarchy-theme-sync validate
Eval-time field checks live in modules/home/theme.nix.'';
nomarchy-state-sync validate
Eval-time field checks live in modules/home/theme.nix.
Still on theme-state.json? Rename it to state.json (or just run
any menu write / `nomarchy-state-sync set` it migrates).'';
missingMsg = ''
Nomarchy: theme-state.json is missing:
Nomarchy: state file is missing:
${pathStr}
This file is required (appearance + menu settings). Add it to your
flake checkout so evaluation stays pure.
This file is required (appearance + menu settings). Add state.json to
your flake checkout so evaluation stays pure.
${tip}'';
emptyMsg = ''
Nomarchy: theme-state.json is empty:
Nomarchy: state file is empty:
${pathStr}
${tip}'';
notObjectMsg = ''
Nomarchy: theme-state.json must be a JSON object `{ ... }`:
Nomarchy: state file must be a JSON object `{ ... }`:
${pathStr}
${tip}'';

View File

@@ -1,28 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail
# Nomarchy Control Center — TUI frontend over nomarchy-theme-sync and tools
# Nomarchy Control Center — TUI frontend over nomarchy-state-sync and tools
# Uses charmbracelet/gum for rendering.
trap 'exit 0' SIGINT
function get_state() {
nomarchy-theme-sync get "$1" 2>/dev/null || echo ""
nomarchy-state-sync get "$1" 2>/dev/null || echo ""
}
function set_state() {
nomarchy-theme-sync --quiet set "$1" "$2" --no-switch
nomarchy-state-sync --quiet set "$1" "$2" --no-switch
}
function first_boot() {
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "Welcome to Nomarchy!"
echo "Pick a Theme Preset:"
themes=$(nomarchy-theme-sync list)
themes=$(nomarchy-state-sync list)
chosen_theme=$(printf "%s" "$themes" | gum choose)
if [ -n "$chosen_theme" ]; then
echo "Applying theme $chosen_theme..."
nomarchy-theme-sync apply "$chosen_theme"
nomarchy-state-sync apply "$chosen_theme"
fi
if gum confirm "Enable auto-commit for your config? (settings changes + a sweep of pending edits before each rebuild)"; then
@@ -113,10 +113,10 @@ function appearance_menu() {
choice=$(gum choose "Pick Theme" "Toggle Blur" "Set Gaps" "Back")
case "$choice" in
"Pick Theme")
themes=$(nomarchy-theme-sync list)
themes=$(nomarchy-state-sync list)
chosen=$(printf "%s" "$themes" | gum choose)
if [ -n "$chosen" ]; then
nomarchy-theme-sync apply "$chosen"
nomarchy-state-sync apply "$chosen"
fi
;;
"Toggle Blur")

View File

@@ -67,21 +67,33 @@ for mp in / /boot /nix; do
done
# ── the flake + state file ───────────────────────────────────────────
if [ -f "$flake/theme-state.json" ]; then
if jq empty "$flake/theme-state.json" 2>/dev/null; then
ok "theme-state.json parses"
# #107: prefer state.json; theme-state.json still counts until migrated.
state_file=
if [ -f "$flake/state.json" ]; then
state_file="$flake/state.json"
elif [ -f "$flake/theme-state.json" ]; then
state_file="$flake/theme-state.json"
fi
if [ -n "$state_file" ]; then
state_base=$(basename "$state_file")
if jq empty "$state_file" 2>/dev/null; then
ok "$state_base parses"
else
bad "theme-state.json is not valid JSON (rebuilds will fail)" \
"nomarchy-theme-sync validate (names the spot; fix it, or re-apply a theme)"
bad "$state_base is not valid JSON (rebuilds will fail)" \
"nomarchy-state-sync validate (names the spot; fix it, or re-apply a theme)"
fi
if git -C "$flake" ls-files --error-unmatch theme-state.json >/dev/null 2>&1; then
ok "theme-state.json is git-tracked"
if git -C "$flake" ls-files --error-unmatch "$state_base" >/dev/null 2>&1; then
ok "$state_base is git-tracked"
else
bad "theme-state.json is NOT git-tracked (flake evaluation can't see it)" \
"git -C $flake add theme-state.json"
bad "$state_base is NOT git-tracked (flake evaluation can't see it)" \
"git -C $flake add $state_base"
fi
if [ "$state_base" = "theme-state.json" ]; then
warn "state file still named theme-state.json — run any menu write or" \
"nomarchy-state-sync set … to migrate to state.json"
fi
else
skip "theme-state.json (no flake checkout at $flake)"
skip "state.json (no flake checkout at $flake)"
fi
if [ -d "$flake/.git" ]; then

View File

@@ -1,6 +1,6 @@
# First-session "you're set" toast (nomarchy.firstBootWelcome). A package
# so checks.first-boot can exercise the gate on a minimal node — same
# pattern as nomarchy-battery-notify. libnotify + theme-sync stay on PATH
# pattern as nomarchy-battery-notify. libnotify + state-sync stay on PATH
# (user unit / VM shim), not runtimeInputs.
{ writeShellApplication, coreutils }:

View File

@@ -1,7 +1,7 @@
# nomarchy-first-boot — one-shot "you're set" toast on the first session.
# Marker is settings.firstBootShown in the flake's theme-state.json
# Marker is settings.firstBootShown in the flake's state.json
# (in-checkout state; never ~/.local/state). notify-send and
# nomarchy-theme-sync come from PATH so the VM check can shim them.
# nomarchy-state-sync come from PATH so the VM check can shim them.
# Live ISO already has its own welcome (hosts/live.nix); skip there so
# users aren't double-toasted and the live seed doesn't get a sticky
@@ -13,8 +13,8 @@ if [ "$hn" = nomarchy-live ]; then
fi
# Already shown → silent. Missing key / no checkout → treat as not shown
# (theme-sync get exits non-zero when the key is absent).
shown=$(nomarchy-theme-sync get settings.firstBootShown 2>/dev/null || true)
# (state-sync get exits non-zero when the key is absent).
shown=$(nomarchy-state-sync get settings.firstBootShown 2>/dev/null || true)
case "$shown" in
true|1|yes) exit 0 ;;
esac
@@ -46,7 +46,7 @@ if [ -z "$ok" ]; then
fi
# Persist in the checkout so re-login is silent. --no-switch: marker only.
if ! nomarchy-theme-sync --quiet set settings.firstBootShown true --no-switch; then
if ! nomarchy-state-sync --quiet set settings.firstBootShown true --no-switch; then
# No writable flake checkout (or tool missing) — still showed the toast;
# without a marker it may reappear next login. Don't fail the unit.
exit 0

View File

@@ -17,7 +17,7 @@
, btrfs-progs # inspect-internal map-swapfile (hibernation offset)
, xkeyboard_config # human-readable installed layout/variant catalog
# Baked metadata — what this installer installs and where it came from.
, templateDir # templates/downstream (home.nix, theme-state.json)
, templateDir # templates/downstream (home.nix, state.json)
, nomarchyLock # the distro's flake.lock (for offline lock composition)
, hardwareModuleNames # newline-separated nixos-hardware module names
, nixpkgsPath # pinned nixpkgs source (NIX_PATH for disko's eval)
@@ -54,7 +54,7 @@ stdenvNoCC.mkDerivation {
mkdir -p "$share/template"
# Full downstream template is the SoT; install script copies + patches.
cp ${templateDir}/flake.nix ${templateDir}/system.nix \
${templateDir}/home.nix ${templateDir}/theme-state.json \
${templateDir}/home.nix ${templateDir}/state.json \
"$share/template/"
install -Dm644 patch-template.py "$share/patch-template.py"

View File

@@ -585,7 +585,7 @@ rm -rf /mnt/etc/nixos
cp "$SHARE/template/flake.nix" \
"$SHARE/template/system.nix" \
"$SHARE/template/home.nix" \
"$SHARE/template/theme-state.json" \
"$SHARE/template/state.json" \
"$FLAKE_DIR/"
# Detected hardware → flags for the patcher (safe defaults active).
@@ -679,7 +679,7 @@ if (( compose_ok != 0 )); then
(cd "$FLAKE_DIR" && nix --extra-experimental-features "nix-command flakes" flake lock)
fi
# A flake worktree must be git-tracked (theme-state.json especially).
# A flake worktree must be git-tracked (state.json especially).
(
cd "$FLAKE_DIR"
git init -q
@@ -702,7 +702,7 @@ success "Configuration written to ~$USERNAME/.nomarchy"
section "Installing (this takes a while)"
# Seed the target store with the flake source + all inputs so the first
# `nomarchy-theme-sync apply` (and the HM pre-activation below) work
# `nomarchy-state-sync apply` (and the HM pre-activation below) work
# before the machine has ever seen a network. Two steps because
# `flake archive --to` enforces signatures and locally-evaluated source
# paths have none; plain `nix copy` accepts --no-check-sigs.

View File

@@ -116,7 +116,7 @@ def build_installer_region(v: dict) -> str:
]
# Auto-login is deliberately NOT emitted here — it is seeded into
# theme-state.json instead (patch_state). A line in system.nix outranks
# state.json instead (patch_state). A line in system.nix outranks
# the state default, which would make the System Auto-login toggle
# write JSON that nothing reads.
@@ -260,7 +260,7 @@ def patch_system(text: str, v: dict) -> str:
def patch_state(text: str, v: dict) -> str:
"""Seed menu-owned settings into theme-state.json.
"""Seed menu-owned settings into state.json.
These live in the state rather than system.nix precisely so the menu can
change them later: a baked Nix assignment would outrank the state default
@@ -286,7 +286,7 @@ def main() -> None:
"flake.nix": patch_flake,
"home.nix": patch_home,
"system.nix": patch_system,
"theme-state.json": patch_state,
"state.json": patch_state,
}
for name, fn in mapping.items():
path = flake_dir / name

View File

@@ -15,12 +15,12 @@ let
flake="''${NOMARCHY_PATH:-$HOME/.nomarchy}"
'';
# Opt-in sweep (settings.autoCommit — the same flag theme-sync honours):
# Opt-in sweep (settings.autoCommit — the same flag state-sync honours):
# commit EVERYTHING dirty in the machine flake before a pull/rebuild/
# home switch, so hand edits to system.nix/home.nix and lock bumps land
# in history at the moment they become live — `git log` mirrors the
# generation list. Complements nomarchy-theme-sync's auto_commit, which
# is pathspec-limited to theme-state.json on menu writes precisely so
# generation list. Complements nomarchy-state-sync's auto_commit, which
# is pathspec-limited to state.json on menu writes precisely so
# half-finished hand edits never ride a settings-named commit; here the
# sweep is the point, and the commit body lists what was swept. Never
# fatal — callers `|| true` so a git hiccup can't block a rebuild. Not
@@ -31,8 +31,12 @@ let
label="''${1:-rebuild}"
[ -d "$flake/.git" ] || exit 0
command -v git >/dev/null 2>&1 || exit 0
# #107: prefer state.json; still honour a legacy theme-state.json
# until the next menu write migrates the checkout.
state="$flake/state.json"
[ -r "$state" ] || state="$flake/theme-state.json"
flag=$(${jq}/bin/jq -r '.settings.autoCommit // false' \
"$flake/theme-state.json" 2>/dev/null || true)
"$state" 2>/dev/null || true)
[ "$flag" = "true" ] || exit 0
dirty=$(git -C "$flake" status --porcelain || true)
[ -n "$dirty" ] || exit 0

View File

@@ -11,8 +11,8 @@
}:
stdenvNoCC.mkDerivation {
pname = "nomarchy-theme-sync";
version = "0.4.0";
pname = "nomarchy-state-sync";
version = "0.5.0";
src = ./.;
@@ -22,8 +22,8 @@ stdenvNoCC.mkDerivation {
installPhase = ''
runHook preInstall
install -Dm755 nomarchy-theme-sync.py $out/bin/nomarchy-theme-sync
patchShebangs $out/bin/nomarchy-theme-sync
install -Dm755 nomarchy-state-sync.py $out/bin/nomarchy-state-sync
patchShebangs $out/bin/nomarchy-state-sync
${lib.optionalString (themesDir != null) ''
mkdir -p $out/share/nomarchy
@@ -32,18 +32,23 @@ stdenvNoCC.mkDerivation {
# Stdlib-only Python. home-manager is deliberately NOT wrapped in —
# the rebuild must use the user's own home-manager from their PATH.
wrapProgram $out/bin/nomarchy-theme-sync \
wrapProgram $out/bin/nomarchy-state-sync \
--prefix PATH : ${lib.makeBinPath [ awww libnotify git ]} \
${lib.optionalString (themesDir != null)
"--set NOMARCHY_DEFAULT_THEMES $out/share/nomarchy/themes"}
# #107: old CLI name kept as a symlink so muscle memory and scripts
# survive a pull (drop after the next stable release notes say so).
# After wrapProgram so it points at the wrapper, not the raw script.
ln -s nomarchy-state-sync $out/bin/nomarchy-theme-sync
runHook postInstall
'';
meta = {
description = "Nomarchy theming: JSON state writer + Home Manager rebuild dispatcher";
description = "Nomarchy state writer + Home Manager rebuild dispatcher";
license = lib.licenses.mit;
mainProgram = "nomarchy-theme-sync";
mainProgram = "nomarchy-state-sync";
platforms = lib.platforms.linux;
};
}

View File

@@ -1,8 +1,10 @@
#!/usr/bin/env python3
"""nomarchy-theme-sync — state writer for Nomarchy's declarative theming.
"""nomarchy-state-sync — state writer for Nomarchy's declarative theming.
Single source of truth: $NOMARCHY_PATH/theme-state.json (inside the flake,
Single source of truth: $NOMARCHY_PATH/state.json (inside the flake,
read purely by Home Manager via the nomarchy.stateFile option).
Legacy name `theme-state.json` is still read (and migrated on write).
The old binary name `nomarchy-theme-sync` is a symlink to this tool.
Theme changes are applied by Home Manager: this tool writes the new state
and runs `home-manager switch` (override with $NOMARCHY_REBUILD, or skip
@@ -39,7 +41,24 @@ from pathlib import Path
# ─── Paths ────────────────────────────────────────────────────────────────
FLAKE_DIR = Path(os.environ.get("NOMARCHY_PATH", Path.home() / ".nomarchy")).expanduser()
STATE_FILE = FLAKE_DIR / "theme-state.json"
# Canonical name after #107. Legacy theme-state.json (and a brief theme.json
# misnomer some notes used) are still accepted for existing checkouts.
STATE_NAME = "state.json"
LEGACY_STATE_NAMES = ("theme-state.json", "theme.json")
def resolve_state_file(flake_dir: Path = FLAKE_DIR) -> Path:
preferred = flake_dir / STATE_NAME
if preferred.exists():
return preferred
for name in LEGACY_STATE_NAMES:
legacy = flake_dir / name
if legacy.exists():
return legacy
return preferred
STATE_FILE = resolve_state_file()
# Preset search path: the user's flake first (their custom themes win),
# then the presets baked into this package by the Nix build.
@@ -51,21 +70,24 @@ WALLPAPER_EXTS = {".png", ".jpg", ".jpeg", ".webp"}
QUIET = False
# argv[0] may still be the compatibility name nomarchy-theme-sync.
_TOOL = Path(sys.argv[0]).name or "nomarchy-state-sync"
def log(msg: str) -> None:
if not QUIET:
print(f"nomarchy-theme-sync: {msg}")
print(f"{_TOOL}: {msg}")
def die(msg: str) -> "None":
print(f"nomarchy-theme-sync: error: {msg}", file=sys.stderr)
print(f"{_TOOL}: error: {msg}", file=sys.stderr)
sys.exit(1)
# All Nomarchy theme notifications share this synchronous tag, so the
# notification daemon (swaync) REPLACES the previous one in place instead
# of stacking — the "rebuilding…" toast becomes the "applied ✓" toast.
NOTIFY_SYNC_TAG = "nomarchy-theme-sync"
NOTIFY_SYNC_TAG = "nomarchy-state-sync"
def notify(body: str, summary: str = "Nomarchy", *,
@@ -87,13 +109,14 @@ def notify(body: str, summary: str = "Nomarchy", *,
# ─── State management ─────────────────────────────────────────────────────
def load_state(path: Path = STATE_FILE) -> dict:
def load_state(path: Path | None = None) -> dict:
path = path or resolve_state_file()
try:
return json.loads(path.read_text())
except FileNotFoundError:
die(f"state file not found: {path} (set $NOMARCHY_PATH to your flake checkout)")
except json.JSONDecodeError as e:
die(f"theme-state.json has a JSON syntax error at line {e.lineno}, "
die(f"{path.name} has a JSON syntax error at line {e.lineno}, "
f"column {e.colno}: {e.msg}\n"
f" file: {path}\n"
f" fix: correct the syntax by hand (a trailing comma is the "
@@ -103,32 +126,48 @@ def load_state(path: Path = STATE_FILE) -> dict:
def write_state(state: dict) -> None:
"""Atomic write: render to a temp file in the same dir, then rename.
Validates first an invalid state never reaches disk (the old file
stays untouched), so a bad `set` can't brick the next rebuild."""
stays untouched), so a bad `set` can't brick the next rebuild.
Always writes the canonical state.json; removes a legacy-named sibling
so a checkout never has two competing sources of truth."""
errors, warnings = validate_state(state)
for w in warnings:
log(f"warning: {w}")
if errors:
die("refusing to write an invalid state (nothing changed):\n"
+ "\n".join(errors))
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
fd, tmp = tempfile.mkstemp(dir=STATE_FILE.parent, prefix=".theme-state.", suffix=".json")
out = FLAKE_DIR / STATE_NAME
out.parent.mkdir(parents=True, exist_ok=True)
fd, tmp = tempfile.mkstemp(dir=out.parent, prefix=".state.", suffix=".json")
try:
with os.fdopen(fd, "w") as f:
json.dump(state, f, indent=2)
f.write("\n")
os.replace(tmp, STATE_FILE)
os.replace(tmp, out)
except BaseException:
os.unlink(tmp)
raise
log(f"state written to {STATE_FILE}")
for name in LEGACY_STATE_NAMES:
legacy = FLAKE_DIR / name
if legacy.exists() and legacy.resolve() != out.resolve():
try:
legacy.unlink()
log(f"migrated: removed legacy {name} (now {STATE_NAME})")
except OSError as e:
log(f"warning: could not remove legacy {name}: {e}")
log(f"state written to {out}")
# Flakes only see git-tracked files. theme-state.json ships tracked,
# Flakes only see git-tracked files. state.json ships tracked,
# but make sure a fresh checkout / reset can't silently hide it.
if (FLAKE_DIR / ".git").exists() and shutil.which("git"):
subprocess.run(
["git", "-C", str(FLAKE_DIR), "add", "--intent-to-add", "theme-state.json"],
["git", "-C", str(FLAKE_DIR), "add", "--intent-to-add", STATE_NAME],
capture_output=True,
)
for name in LEGACY_STATE_NAMES:
subprocess.run(
["git", "-C", str(FLAKE_DIR), "rm", "-f", "--ignore-unmatch", name],
capture_output=True,
)
def auto_commit_enabled(state: dict) -> bool:
@@ -136,7 +175,7 @@ def auto_commit_enabled(state: dict) -> bool:
def auto_commit(message: str) -> None:
"""Opt-in (settings.autoCommit): commit theme-state.json — and nothing
"""Opt-in (settings.autoCommit): commit state.json — and nothing
else after a mutation, so settings history is `git log`. The pathspec
keeps unrelated dirty files out of the commit; a missing git identity
falls back to a Nomarchy one so a fresh machine never errors. Callers
@@ -150,16 +189,18 @@ def auto_commit(message: str) -> None:
if not (FLAKE_DIR / ".git").exists() or shutil.which("git") is None:
return
git = ["git", "-C", str(FLAKE_DIR)]
# Pathspec: preferred name plus any leftover legacy file still staged.
paths = [STATE_NAME, *LEGACY_STATE_NAMES]
# No-op when the file already matches HEAD (a `set` to the same value).
# On a repo with no commits yet this diff errors — then just commit.
if subprocess.run(git + ["diff", "--quiet", "HEAD", "--", "theme-state.json"],
if subprocess.run(git + ["diff", "--quiet", "HEAD", "--"] + paths,
capture_output=True).returncode == 0:
return
if not subprocess.run(git + ["config", "user.email"],
capture_output=True, text=True).stdout.strip():
git += ["-c", "user.name=Nomarchy", "-c", "user.email=nomarchy@localhost"]
result = subprocess.run(
git + ["commit", "--quiet", "-m", message, "--", "theme-state.json"],
git + ["commit", "--quiet", "-m", message, "--"] + paths,
capture_output=True, text=True)
if result.returncode == 0:
log(f"auto-committed: {message}")
@@ -212,7 +253,7 @@ def validate_state(state) -> tuple:
if not isinstance(state, dict):
err("top level", "must be a JSON object",
"re-apply a preset: nomarchy-theme-sync apply boreal")
"re-apply a preset: nomarchy-state-sync apply boreal")
return errors, warnings
for k in state:
@@ -315,7 +356,7 @@ def cmd_validate(_args) -> None:
for w in warnings:
print(f"{w}")
if errors:
print(f"nomarchy-theme-sync: {STATE_FILE} has "
print(f"nomarchy-state-sync: {STATE_FILE} has "
f"{len(errors)} problem(s):", file=sys.stderr)
for e in errors:
print(f"{e}", file=sys.stderr)
@@ -340,7 +381,7 @@ def check_fonts(state: dict) -> None:
families = [f.strip().lower() for f in result.stdout.split(",")]
if result.returncode != 0 or name.strip().lower() not in families:
fallback = result.stdout.strip() or "unknown"
print(f"nomarchy-theme-sync: warning: fonts.{key} '{name}' is not "
print(f"nomarchy-state-sync: warning: fonts.{key} '{name}' is not "
f"installed — fontconfig will substitute '{fallback}'",
file=sys.stderr)
notify(f"Font '{name}' is not installed — using '{fallback}'")
@@ -490,7 +531,7 @@ def apply_named(theme: str, no_switch: bool = False) -> None:
candidate = Path(theme).expanduser()
preset_path = candidate if candidate.is_file() else find_preset(theme)
if preset_path is None:
die(f"unknown theme '{theme}' (try `nomarchy-theme-sync list`)")
die(f"unknown theme '{theme}' (try `nomarchy-state-sync list`)")
preset = json.loads(preset_path.read_text())
# Merge over current state. Presets carry a full appearance block —
@@ -642,7 +683,7 @@ def main() -> None:
global QUIET
parser = argparse.ArgumentParser(
prog="nomarchy-theme-sync",
prog="nomarchy-state-sync",
description="Nomarchy theming — state writer + Home Manager rebuild dispatcher.",
)
parser.add_argument("--quiet", action="store_true", help="suppress progress output")
@@ -671,7 +712,7 @@ def main() -> None:
p.add_argument("--no-switch", action="store_true", help="write state only, skip the rebuild")
p.set_defaults(func=cmd_auto)
sub.add_parser("validate", help="check theme-state.json against the schema (read-only)").set_defaults(func=cmd_validate)
sub.add_parser("validate", help="check state.json against the schema (read-only)").set_defaults(func=cmd_validate)
sub.add_parser("wallpaper", help="apply the current wallpaper via swww").set_defaults(func=cmd_wallpaper)

View File

@@ -15,19 +15,19 @@ keyboard, detected hardware, password hash, …). Keep commented opt-ins and
services — the login user is created from `username` automatically) and
`home.nix` (your packages).
3. `git init && git add -A` — flakes only see tracked files, including
`theme-state.json`.
`state.json`.
4. System: `sudo nixos-rebuild switch --flake .#default`
5. Desktop: `nix run home-manager -- switch --flake .#me`
(afterwards just `home-manager switch --flake .#me` — it's installed)
6. Clone/symlink this directory to `~/.nomarchy` (or export
`NOMARCHY_PATH`) so `nomarchy-theme-sync` knows where the state lives.
`NOMARCHY_PATH`) so `nomarchy-state-sync` knows where the state lives.
Day-to-day:
```sh
nomarchy-theme-sync list # 24 shipped presets
nomarchy-theme-sync apply gruvbox # writes state + home-manager switch
nomarchy-theme-sync bg next # cycle wallpapers (instant, no rebuild)
nomarchy-state-sync list # 24 shipped presets
nomarchy-state-sync apply gruvbox # writes state + home-manager switch
nomarchy-state-sync bg next # cycle wallpapers (instant, no rebuild)
```
The system layer only needs `nixos-rebuild` when you change `system.nix`.

View File

@@ -29,10 +29,10 @@
#
# Icon pack: Papirus ships by default (auto Dark/Light by theme mode) and
# is the ONLY icon pack in your closure. To use another, set the `icons`
# field in your theme-state.json to a theme name from a known pack — only
# field in your state.json to a theme name from a known pack — only
# that pack is then added (no bloat otherwise). Known packs: Papirus,
# Tela, Qogir, Reversal, Numix (see modules/home/theme.nix `iconPacks`).
# e.g. theme-state.json: "icons": "Tela-dark"
# e.g. state.json: "icons": "Tela-dark"
# ── Opt-in features — uncomment and tweak to enable ─────────────────
# nomarchy.nightlight = { # scheduled blue-light filter (hyprsunset)

View File

@@ -37,10 +37,10 @@ pkgs.testers.runNixOSTest {
# PolicyKit test action so pkexec can raise the agent.
security.polkit.enable = true;
nomarchy.system.stateFile = flake + "/theme-state.json";
nomarchy.system.stateFile = flake + "/state.json";
systemd.tmpfiles.rules = [
"d /home/nomarchy/.nomarchy 0755 nomarchy users -"
"C /home/nomarchy/.nomarchy/theme-state.json 0644 nomarchy users - ${flake + "/theme-state.json"}"
"C /home/nomarchy/.nomarchy/state.json 0644 nomarchy users - ${flake + "/state.json"}"
];
services.greetd.settings.initial_session = {
command = "start-hyprland";
@@ -49,7 +49,7 @@ pkgs.testers.runNixOSTest {
home-manager.useGlobalPkgs = true;
home-manager.users.nomarchy = {
imports = [ flake.homeModules.nomarchy ];
nomarchy.stateFile = flake + "/theme-state.json";
nomarchy.stateFile = flake + "/state.json";
nomarchy.idle.enable = false;
# Ensure pinentry-qt is the active pinentry (keys.nix default).
};

View File

@@ -11,7 +11,7 @@ flake seed files (see pkgs/nomarchy-install/default.nix installPhase).
The package copies a fixed file list (not a recursive tree copy):
flake.nix system.nix home.nix theme-state.json
flake.nix system.nix home.nix state.json
README.md and hardware-configuration.nix stay repo-only (docs /
nixos-generate-config at install time) and are intentionally absent
@@ -31,7 +31,7 @@ import sys
from pathlib import Path
# Must match the cp list in pkgs/nomarchy-install/default.nix installPhase.
SHIPPED = ("flake.nix", "system.nix", "home.nix", "theme-state.json")
SHIPPED = ("flake.nix", "system.nix", "home.nix", "state.json")
def fail(msg: str) -> None:

View File

@@ -42,7 +42,7 @@ pkgs.testers.runNixOSTest {
nomarchy.system.stateFile = flake + "/themes/${slug}.json";
systemd.tmpfiles.rules = [
"d /home/nomarchy/.nomarchy 0755 nomarchy users -"
"C /home/nomarchy/.nomarchy/theme-state.json 0644 nomarchy users - ${flake + "/themes/${slug}.json"}"
"C /home/nomarchy/.nomarchy/state.json 0644 nomarchy users - ${flake + "/themes/${slug}.json"}"
];
services.greetd.settings.initial_session = {
command = "start-hyprland";

View File

@@ -131,7 +131,7 @@ def convert(theme_dir: Path) -> dict:
"name": slug.replace("-", " ").title(),
"slug": slug,
"mode": mode,
# Empty = auto: nomarchy-theme-sync falls back to the first file in
# Empty = auto: nomarchy-state-sync falls back to the first file in
# the theme's backgrounds/ directory.
"wallpaper": "",
"colors": {

View File

@@ -41,7 +41,7 @@ pkgs.testers.runNixOSTest {
nomarchy.system.stateFile = flake + "/themes/boreal.json";
systemd.tmpfiles.rules = [
"d /home/nomarchy/.nomarchy 0755 nomarchy users -"
"C /home/nomarchy/.nomarchy/theme-state.json 0644 nomarchy users - ${flake + "/themes/boreal.json"}"
"C /home/nomarchy/.nomarchy/state.json 0644 nomarchy users - ${flake + "/themes/boreal.json"}"
"f /run/nomarchy-test-lid-state 0644 nomarchy users - state:_closed"
];
services.greetd.settings.initial_session = {

View File

@@ -3,12 +3,12 @@
# screenshots into $out: desktop.png, menu.png (rofi root open), and
# btop.png (Kitty running btop, when the terminal starts under softGL).
# Also asserts nomarchy-doctor-status JSON (class:bad) — the harness
# seeds an untracked theme-state.json, so doctor always reports ✖.
# seeds an untracked state.json, so doctor always reports ✖.
# Maintainer tool, NOT a checks.* gate (a full-desktop VM is far too
# heavy for CI). Impure: THEME env picks the theme slug.
# THEME=<slug> nix build --impure -f tools/theme-shot.nix --no-link --print-out-paths
# The guest gets a minimal ~/.nomarchy (just the theme JSON as
# theme-state.json), seeded via tmpfiles BEFORE greetd starts the
# state.json), seeded via tmpfiles BEFORE greetd starts the
# session, so the session's own exec-once wallpaper paint works —
# assets come from the themesDir baked into the tool. Bar, menus AND
# wallpaper render.
@@ -42,13 +42,13 @@ pkgs.testers.runNixOSTest {
extraGroups = [ "wheel" "video" ];
};
nomarchy.system.stateFile = flake + "/themes/${slug}.json";
# Seed the runtime state (theme-sync reads $NOMARCHY_PATH, default
# Seed the runtime state (state-sync reads $NOMARCHY_PATH, default
# ~/.nomarchy) before greetd auto-starts the session, so the
# exec-once `nomarchy-theme-sync wallpaper` finds it and paints.
# exec-once `nomarchy-state-sync wallpaper` finds it and paints.
# NixOS activation creates /home/nomarchy before systemd runs.
systemd.tmpfiles.rules = [
"d /home/nomarchy/.nomarchy 0755 nomarchy users -"
"C /home/nomarchy/.nomarchy/theme-state.json 0644 nomarchy users - ${flake + "/themes/${slug}.json"}"
"C /home/nomarchy/.nomarchy/state.json 0644 nomarchy users - ${flake + "/themes/${slug}.json"}"
];
services.greetd.settings.initial_session = {
command = "start-hyprland";