refactor(#107): theme-state.json → state.json, theme-sync → state-sync
Some checks failed
Check / eval (push) Has been cancelled
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:
42
README.md
42
README.md
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:00–00: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).
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
power‑profile icons open the power menu. Your ryzenadj scripts are gone;
|
||||
if you miss a specific TDP behaviour, that's a follow‑up, 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 |
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -72,7 +72,7 @@ Reference: [HARDWARE.md](HARDWARE.md) §4–§5, §11 (doctor).
|
||||
| Idea | Intent | Notes |
|
||||
|------|--------|-------|
|
||||
| **Silent first-week card** ✓ | One dismissible “you’re 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) |
|
||||
|
||||
|
||||
92
flake.nix
92
flake.nix
@@ -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 = [
|
||||
|
||||
@@ -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
21
lib.nix
@@ -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 = {
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
Environment = [
|
||||
"PATH=${lib.makeBinPath [
|
||||
pkgs.libnotify
|
||||
pkgs.nomarchy-theme-sync
|
||||
pkgs.nomarchy-state-sync
|
||||
pkgs.coreutils
|
||||
]}"
|
||||
"NOMARCHY_PATH=%h/.nomarchy"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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, ... }:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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, ... }:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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`.
|
||||
#
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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.
|
||||
#
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ''
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}'';
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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,30 +126,46 @@ 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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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).
|
||||
};
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user