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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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