fix(nixos): two toggles that reported success and did nothing — wire the state bridges
All checks were successful
Check / eval (push) Successful in 3m16s

BACKLOG #116. `nomarchy.settings` is declared in exactly one place —
modules/home/options.nix:412, the Home Manager side. On NixOS the attribute
does not exist, and `or <fallback>` swallows the missing-attribute error, so
four options that "defaulted from the state" had silently been their fallback
on every machine ever built.

The item said three options, and called them benign. Both halves were wrong,
and re-grepping rather than trusting the account is what found it:

  * There were four. The original enumeration read options.nix instead of
    modules/nixos/ and missed services.nix's printing.enable — the same
    mistake in miniature as the bug it was filing.
  * Two were live user-facing bugs. Control Center is shipped
    (default.nix:337) and reachable from the menu; its Bluetooth and Printing
    toggles wrote settings.{bluetooth,printing}.enable and printed "requires
    rebuild", and the rebuild changed nothing. They had never worked.

The fix is one shape, now uniform: the option declares a STATIC default, and
the implementing module reads the state via theme-state-read.nix (fails closed
on bad JSON, unlike greeter.nix's raw fromJSON — also moved onto the reader
here) and mkDefaults it behind `mkIf (state != null)`. An absent key leaves the
option default as the single source of the fallback; a hand-set system.nix
value still pins it. batteryChargeLimit gets no bridge and loses its dead read:
power.nix's oneshot already reads that key with jq at RUNTIME and prefers it
over the baked value, which is why that menu worked all along.

V2. The bug is proved real before/after on the same flipped state: BEFORE,
bluetooth stays true and printing stays false; AFTER, both flip, and a hand-set
value still outranks the state. Nothing in a build fails when a bridge dies, so
the guards are the point — checks.state-bridges asserts 11 eval cases, and
checks.printing-from-state boots a VM whose only input is the state file and
waits for a running cups.service. The guard was itself proved to fail:
re-breaking the bluetooth bridge makes it throw, naming both assertions. A
check that passes whether or not the property holds is worse than no check
(625b7e3). flake check, option-docs, template-sot, downstream-template-*,
installer-safety, hardware-toggles and battery-charge-limit all pass.

No V3: the mechanism is fully proved headlessly. Design record in ROADMAP §
NixOS-side state bridges (#116); new #117 (PROPOSED) for the control-center
toggles still leaving the rebuild to the user.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 16:03:12 +01:00
parent 7353568115
commit a9f3a642ee
8 changed files with 268 additions and 46 deletions

View File

@@ -20,6 +20,19 @@ let
nomarchyLogoFont = pkgs.runCommandLocal "nomarchy-logo-font" { } ''
install -Dm444 ${./branding/Nomarchy.ttf} $out/share/fonts/truetype/Nomarchy.ttf
'';
# 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
# leaves the option's own default (true) alone.
sysState =
if cfg.stateFile != null
then import ../theme-state-read.nix { inherit lib; } cfg.stateFile
else { };
stateBluetooth =
let v = (sysState.settings or { }).bluetooth.enable or null;
in if builtins.isBool v then v else null;
in
{
imports = [ ./options.nix ./plymouth.nix ./greeter.nix ./file-manager.nix ./power.nix ./services.nix ./hardware.nix ./timezone.nix ./oom.nix ];
@@ -217,6 +230,13 @@ in
# overrides this regardless.
users.defaultUserShell = lib.mkOverride 500 pkgs.zsh;
# The in-flake state drives the toggle; mkDefault so a hand-set
# nomarchy.system.bluetooth.enable in system.nix still pins it (the
# greeter.autoLogin shape). mkIf, not a fallback expression, so an absent
# key leaves the option default as the single source of `true`.
nomarchy.system.bluetooth.enable =
lib.mkIf (stateBluetooth != null) (lib.mkDefault stateBluetooth);
hardware.bluetooth.enable = lib.mkDefault cfg.bluetooth.enable;
services.blueman.enable = lib.mkDefault cfg.bluetooth.enable;

View File

@@ -62,9 +62,12 @@ let
fi
'';
# Fails closed with an actionable message via theme-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 builtins.fromJSON (builtins.readFile cfg.stateFile)
then import ../theme-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

@@ -51,7 +51,16 @@
option'' // { default = true; };
audio.enable = lib.mkEnableOption "the Pipewire audio stack" // { default = true; };
bluetooth.enable = lib.mkEnableOption "Bluetooth support with blueman" // { default = config.nomarchy.settings.bluetooth.enable or true; };
# default stays a plain `true` here; ./default.nix mkDefaults it from
# settings.bluetooth.enable (Control Center's Bluetooth toggle). Reading
# the state in the option default is the trap ROADMAP § "NixOS-side state
# bridges (#116)" documents: `config.nomarchy.settings` does not exist on
# the NixOS side, and `or true` silently swallowed that for years.
bluetooth.enable = lib.mkEnableOption "Bluetooth support with blueman" // {
default = true;
defaultText = lib.literalExpression
"(settings.bluetooth.enable from theme-state.json) or true";
};
autoTimezone.enable = lib.mkEnableOption ''
automatic timezone detection (geoclue + automatic-timezoned): the
@@ -109,7 +118,14 @@
batteryChargeLimit = lib.mkOption {
type = lib.types.nullOr (lib.types.ints.between 50 100);
default = config.nomarchy.settings.power.batteryChargeLimit or null;
# No state bridge at eval time, by design: ./power.nix's oneshot reads
# settings.power.batteryChargeLimit out of the live theme-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
# and so was always null — dead, but harmless precisely because the
# runtime path never depended on it (ROADMAP § state bridges, #116).
default = null;
# Dell Adaptive charge mode ignores the end threshold unless we
# also select Custom (power.nix oneshot); see Latitude 5310 QA.
example = 80;

View File

@@ -7,6 +7,19 @@
let
cfg = config.nomarchy.services;
# 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
# 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
else { };
statePrinting =
let v = (svcState.settings or { }).printing.enable or null;
in if builtins.isBool v then v else null;
in
{
options.nomarchy.services = {
@@ -74,10 +87,18 @@ in
with the lmstudio/alpaca GUIs). CPU by default set
`services.ollama.acceleration` natively for GPU offload'';
# default stays a plain `false` here; the state bridge is a mkDefault
# below, from settings.printing.enable (Control Center's Printing
# toggle) — see ROADMAP § state bridges (#116) for why the old read of
# `config.nomarchy.settings` never worked.
printing.enable = lib.mkEnableOption ''
CUPS printing with Avahi/mDNS, so network printers are auto-discovered
(add vendor drivers via `services.printing.drivers`); the menu's
System Printers entry opens the system-config-printer GUI'' // { default = config.nomarchy.settings.printing.enable or false; };
System Printers entry opens the system-config-printer GUI'' // {
default = false;
defaultText = lib.literalExpression
"(settings.printing.enable from theme-state.json) or false";
};
openrgb.enable = lib.mkEnableOption ''
the OpenRGB daemon and GUI for controlling RGB lighting on peripherals
@@ -124,6 +145,14 @@ in
};
config = lib.mkMerge [
# The in-flake state drives the toggle; mkDefault so a hand-set
# nomarchy.services.printing.enable in system.nix still pins it (the
# greeter.autoLogin shape). mkIf, not a fallback expression, so an absent
# key leaves the option default as the single source of `false`.
(lib.mkIf (statePrinting != null) {
nomarchy.services.printing.enable = lib.mkDefault statePrinting;
})
(lib.mkIf cfg.tailscale.enable {
services.tailscale.enable = true;
# Let the login user drive tailscale (up/down/set — and so the VPN menu's