# 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). # # 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/state.json or regenerate from a preset: 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-state-sync validate Eval-time field checks live in modules/home/theme.nix. • Still on theme-state.json? Easiest is any menu write / `nomarchy-state-sync set` — it migrates and stages the rename for you. Renaming by hand needs `git add state.json` too. • File IS on disk but eval still says missing? On a git flake only *tracked* files exist — an untracked state.json is invisible: git add state.json''; missingMsg = '' Nomarchy: state file is missing: ${pathStr} This file is required (appearance + menu settings). Add state.json to your flake checkout — git-tracked — so evaluation stays pure. ${tip}''; emptyMsg = '' Nomarchy: state file is empty: ${pathStr} ${tip}''; notObjectMsg = '' Nomarchy: state file must be a JSON object `{ ... }`: ${pathStr} ${tip}''; # lib.trim / lib.strings.trim — strip leading/trailing whitespace so an # all-whitespace file counts as empty and a BOM-less `{` is recognized. strip = s: let # Prefer lib.trim when present (nixpkgs ≥ 23.11); fall back so a # very old pin still gates missing/non-object. trim = if lib ? trim then lib.trim else if lib.strings ? trim then lib.strings.trim else (x: x); in trim s; in if !(builtins.pathExists path) then throw missingMsg else let raw = builtins.readFile path; stripped = strip raw; in if stripped == "" then throw emptyMsg # Reject non-objects before fromJSON where we can (null / array / string # literals). Subtle syntax errors still surface from fromJSON itself — # those messages already include line/column; the path tip above is the # part a raw stack used to bury. else if builtins.match "[[:space:]]*\\{.*" stripped == null then throw notObjectMsg else let value = builtins.fromJSON raw; in if !(builtins.isAttrs value) then throw notObjectMsg else value