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

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

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

View File

@@ -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 = [