diff --git a/README.md b/README.md index 16df672..16a3b7b 100644 --- a/README.md +++ b/README.md @@ -282,7 +282,7 @@ option, e.g. `services.easyeffects.enable = lib.mkForce false;` — see | `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.fileManager.enable` | `true` | Thunar GUI + gvfs/tumbler/udisks2 (the "open folder" handler) | | `nomarchy.system.greeter.enable` | `true` | greetd/tuigreet | -| `nomarchy.system.greeter.autoLogin` | `null` | Auto-login this user into Hyprland (installer sets it on LUKS machines) | +| `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 | | `nomarchy.system.audio.enable` | `true` | PipeWire stack; WirePlumber priority rules provide HDMI/USB preference/fallback, while the Home Manager dock watcher performs the fresh-display reprobe and explicit default selection | | `nomarchy.system.bluetooth.enable` | `true` | Bluetooth + blueman | | `nomarchy.system.autoTimezone.enable` | `false` | Automatic timezone (geoclue + automatic-timezoned) — the clock follows your location; toggle from System › Auto timezone (a menu enable rebuilds: it has to unset the static `time.timeZone`) | diff --git a/agent/BACKLOG.md b/agent/BACKLOG.md index 884a465..44320c7 100644 --- a/agent/BACKLOG.md +++ b/agent/BACKLOG.md @@ -95,6 +95,59 @@ pre-existing ISO-pinning gap. ## NEXT +### 115. Suspend-then-hibernate, with a way to set it up + +Bernardo, 2026-07-14: a suspended laptop should be able to fall through to +hibernate after a while, so a bag-carried machine stops draining, and setting +that up must be easy — not a systemd-sleep man-page trip. + +The mechanism is `systemd`'s `suspend-then-hibernate` plus +`HibernateDelaySec`; the work is the surfacing, and the constraints are +already in the tree. Hibernate needs the resume offset + swap the installer +detects (`resumeOffset`/`rootUuid` in patch-template.py) — on a machine +without them this must self-gate, not fail at suspend time, the way Battery +limit gates on a battery. `modules/nixos/default.nix` already declines to +hyprlock before an encrypted hibernate (the LUKS resume gates it); check that +reasoning still holds when the suspend leads to hibernate. Menu placement: it +belongs with the power settings, not the root (§ menu placement convention). + +Settle first (`[human]`): whether the delay is a preset list (30 min / 1 h / +2 h / never) or a free-form entry, and whether it applies on battery only or +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`, +which does not exist there (see #116). Pass = the toggle survives a rebuild, +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. + +### 116. Three NixOS options read a `config.nomarchy.settings` that does not exist + +Found while wiring System › Auto-login (2026-07-14). `nomarchy.settings` is +declared and populated **only on the Home Manager side** (`modules/home/ +options.nix`, filled by `theme.nix`). On the NixOS side the attribute is +absent, and `or ` swallows the missing-attribute error — so these +defaults have silently always been their fallback on every machine: + +- `nomarchy.system.greeter.autoLogin` — **fixed in this commit** (now reads + the state via `theme-state-read.nix` in `greeter.nix`, the working pattern + from `hardware.nix`/`timezone.nix`). Left here as the worked example. +- `modules/nixos/options.nix` `bluetooth.enable` — `settings.bluetooth.enable` + never read; benign today (fallback `true` = the intended default) but the + state key is dead. +- `modules/nixos/options.nix` `power.batteryChargeLimit` — `settings.power. + batteryChargeLimit` never read; the Battery limit menu works because it + patches the baked option in `system.nix` instead, which is the older model. + +Decide per option whether the state key should work (wire it like the +greeter) or the phantom read should just go. The trap is that `or` makes both +a live bridge and a dead one look identical — a grep for +`config.nomarchy.settings` under `modules/nixos/` should return nothing when +this is done. Pass = no NixOS option claims a state default it cannot read, +and any bridge kept is proved by an eval that flips with the state file. + ### 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, diff --git a/agent/HARDWARE-QUEUE.md b/agent/HARDWARE-QUEUE.md index d920347..effaf53 100644 --- a/agent/HARDWARE-QUEUE.md +++ b/agent/HARDWARE-QUEUE.md @@ -181,9 +181,24 @@ the **T14s** (webcam case). authentication daemon to re-prove an evaluated invariant is a bad trade. - [ ] **#55 fingerprint enroll on real reader** — with `nomarchy.hardware.fingerprint.enable` and a physical reader: System › - Fingerprint › Enroll a finger; List shows it; Verify succeeds; optional - Use for login (on) → `nomarchy-rebuild` → login/sudo accept fingerprint. - Menu surface is V1/V2 without a reader (self-gate + dry paths). + Fingerprint › Enroll a finger; List shows it; Verify succeeds. + Enroll/List/Verify verified on hardware 2026-07-14 (T14s); the + remaining unknown is a reader-less machine's self-gate. +- [ ] **fingerprint + auto-login toggles on hardware** (this commit) — on the + T14s, after removing the pinning lines from `~/.nomarchy/system.nix` + (`greeter.autoLogin`, `hardware.fingerprint.pam`): + 1. System › Fingerprint shows **Fingerprint (on)** (state already true). + Toggle it off → terminal opens, sudo system rebuild + home switch → + toast "Fingerprint off". Expect: sudo now refuses the finger and asks + only for a password; the lock screen (SUPER+CTRL+L) shows no + "or scan your finger" line. Toggle back on → both return. + 2. With fingerprint OFF and no finger enrolled, the toggle must REFUSE + with "Enroll a finger first" and write nothing. + 3. System › Auto-login shows **(on)**. Toggle off → sudo rebuild → + reboot → tuigreet asks, and accepts password OR finger (fingerprint + on). Toggle back on → reboot → straight to the session, no prompt. + Expected throughout: the two are independent, and auto-login on means + no boot prompt regardless of the fingerprint switch. - [ ] **#60 non-BAT* battery name (if available)** — on a machine whose system battery is **not** named `BAT*` (e.g. `CMB0`): confirm charge-limit oneshot writes the threshold, System › Battery limit diff --git a/docs/HARDWARE.md b/docs/HARDWARE.md index 928bbfb..76f97e6 100644 --- a/docs/HARDWARE.md +++ b/docs/HARDWARE.md @@ -173,9 +173,18 @@ nomarchy.hardware.fingerprint.enable = true; # services.fprintd ### Enroll (menu or CLI) **Shipped #55:** System › Fingerprint (self-gated when `fprintd-list` is -on PATH) — Enroll / List / Verify / Delete all, plus **Use for login** -which writes `settings.fingerprint.pam` and applies on the next -`sys-rebuild` (option default follows theme-state.json). +on PATH) — a single **Fingerprint (on/off)** switch, plus Enroll / List / +Verify / Delete all (all usable while it's off — turning it on needs an +enrolled finger). The switch writes `settings.fingerprint.pam`, the one +state key behind *both* halves of "use my finger": login/sudo PAM here, +and the lock-screen unlock in `modules/home/idle.nix`. It runs +`nomarchy-fingerprint`, which sudos a system rebuild and then a home +switch, because the two halves live in different configurations. + +It does **not** decide whether login prompts at all: auto-login skips the +greeter entirely, so this adds the finger to the prompts that actually +happen — sudo, the lock screen, and the greeter only when +[auto-login](#auto-login) is off. **Hints (#73):** MOTD line when `fingerprint.enable` is on; first-boot tip when `fprintd-list` is on PATH (`SUPER+M → System › Fingerprint` / @@ -185,12 +194,34 @@ tip when `fprintd-list` is on PATH (`SUPER+M → System › Fingerprint` / # CLI still works: fprintd-enroll fprintd-list "$USER" -# or: System › Fingerprint › Use for login (on) → sys-rebuild +nomarchy-fingerprint toggle # or on | off | status — what the menu row runs ``` PAM stays opt-in on purpose: password-only remains the cautious default until a finger is enrolled. Full enroll on a real reader is V3/hardware. +### Auto-login + +`settings.greeter.autoLogin` (System › Auto-login, or `nomarchy-autologin +[toggle|on|off|status]`) decides whether boot goes straight to the desktop +or stops at the greeter. The installer seeds it ON for LUKS-encrypted +machines — the disk passphrase already gates the machine, so a greeter +password is a second prompt for the same thing — and leaves it off +without LUKS, where the greeter is the only thing between power-on and the +desktop. It's baked into greetd at system rebuild, so the change shows on +the next boot. + +Auto-login and fingerprint are independent, and auto-login wins at boot: +with it on you are never asked for anything at startup, whatever the +fingerprint switch says. Turn auto-login off and the greeter asks — for a +password, or a password *or* finger when fingerprint is on. + +Both are in-flake state rather than lines in `system.nix` on purpose: a +hand-set `nomarchy.system.greeter.autoLogin` (or +`nomarchy.hardware.fingerprint.pam`) outranks the state and pins the +setting, leaving the menu toggle unable to move it. That's the escape +hatch, not the default — leave those lines commented to use the menu. + ### Parallel prompt (password *or* finger, whichever first) With `fingerprint.pam` on, sudo/login/hyprlock show **one** prompt that diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index 1b9c802..d173eba 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -169,10 +169,11 @@ 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) - # LUKS machines: the disk passphrase already gates access, so boot - # straight into the session — this is what nomarchy-install sets on - # encrypted installs. Omit it on unencrypted machines. - nomarchy.system.greeter.autoLogin = username; + # Auto-login is NOT set here: it lives in theme-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 + # installs: the LUKS passphrase already gates access). # CRITICAL: keep your ORIGINAL install's value — never Nomarchy's 26.05. system.stateVersion = "24.11"; diff --git a/modules/home/options.nix b/modules/home/options.nix index 01ed2e3..d2f9367 100644 --- a/modules/home/options.nix +++ b/modules/home/options.nix @@ -367,23 +367,29 @@ in dockAudio.enable = lib.mkEnableOption "settled PipeWire/WirePlumber reprobe and automatic default-output switch to an available dock/monitor sink (HDMI/DisplayPort/USB) on fresh display hotplug, with a toast and journal result; a later manual choice sticks until the next plug" // { default = true; }; firstBootWelcome.enable = lib.mkEnableOption "one dismissible \"you're set\" toast on the first session (menu/themes/keys + network pointer); marker is settings.firstBootShown in the flake checkout" // { default = true; }; idle.enable = lib.mkEnableOption "hyprlock + hypridle (idle lock, display off, suspend)" // { default = true; }; - idle.fingerprint = lib.mkEnableOption '' - unlocking the lock screen with a fingerprint as well as the password, - and saying so on the input field. + idle.fingerprint = lib.mkOption { + type = lib.types.bool; + default = config.nomarchy.settings.fingerprint.pam or false; + defaultText = lib.literalExpression + "(settings.fingerprint.pam from theme-state.json) or false"; + description = '' + Unlock the lock screen with a fingerprint as well as the password, and + say so on the input field. - Mirrors `nomarchy.hardware.fingerprint.pam` in system.nix — keep the two - matched, the same way `nomarchy.keyboard.layout` matches - `services.xserver.xkb.layout`. It cannot be derived: hyprlock is - configured here, in standalone Home Manager, which has no `osConfig` to - read the NixOS side from. + Reads the SAME `settings.fingerprint.pam` state key that + `nomarchy.hardware.fingerprint.pam` defaults from, so the one + System › Fingerprint toggle moves the lock screen and login/sudo + together — "fingerprint on" is one decision, not two that can drift. + (It has to arrive by state, not by reading the NixOS side: hyprlock is + configured here, in standalone Home Manager, which has no `osConfig`.) - Deliberately separate from the PAM flag rather than implied by it: - hyprlock does NOT unlock by fingerprint through PAM. Its PAM stack only - runs on submit, so a parallel module never gets to poll — hyprlock has - its own fprintd-over-D-Bus backend instead, and this is the switch for - it. With no reader present it advertises a scan that cannot happen, so - it stays opt-in - ''; + The two remain separate *mechanisms*, which is why this option still + exists to be set by hand: hyprlock does NOT unlock by fingerprint + through PAM. Its PAM stack only runs on submit, so a parallel module + never gets to poll — hyprlock has its own fprintd-over-D-Bus backend + instead, and this is the switch for it. + ''; + }; yazi.enable = lib.mkEnableOption "the yazi TUI file manager, themed with a curated plugin set" // { default = true; }; osd.enable = lib.mkEnableOption "swayosd on-screen display for volume/brightness/mute" // { default = true; }; shell.enable = lib.mkEnableOption "the zsh shell experience (starship prompt, bat/eza/zoxide)" // { default = true; }; diff --git a/modules/home/rofi.nix b/modules/home/rofi.nix index da87248..8a9a678 100644 --- a/modules/home/rofi.nix +++ b/modules/home/rofi.nix @@ -901,6 +901,16 @@ ${themeRows} || { notify-send "Auto timezone" "Unavailable on this machine."; exit 0; } exec ${cfg.terminal} -e nomarchy-autotimezone toggle ;; + autologin) + # Toggle booting straight into the session (greetd initial_session). + # A SYSTEM setting baked at rebuild, so this sudos a system switch in + # a terminal, like Auto timezone — and the change shows on the NEXT + # boot, not now. Off is what makes the greeter ask for a password (or + # a finger, when System › Fingerprint is on). + command -v nomarchy-autologin >/dev/null 2>&1 \ + || { notify-send "Auto-login" "Unavailable on this machine."; exit 0; } + exec ${cfg.terminal} -e nomarchy-autologin toggle ;; + autocommit) # Toggle opt-in auto-commit: every menu/theme mutation also commits # theme-state.json (that file only) in the downstream flake, so @@ -977,13 +987,17 @@ ${themeRows} 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) - case "$pam" in true|True) pam_label="Use for login (on)" ;; *) pam_label="Use for login (off)" ;; esac + 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 + # the rest (enroll/list/verify/delete) is the plumbing behind it — + # all still usable while the switch is off, since turning it on + # requires an enrolled finger. + row "$pam_label" system-lock-screen row "Enroll finger" preferences-desktop-user-password row "List enrolled" view-list row "Verify" dialog-password row "Delete all" edit-delete - row "$pam_label" system-lock-screen back } | rofi_menu -show-icons -p Fingerprint) || exit 0 case "$choice" in @@ -1013,17 +1027,15 @@ ${themeRows} *) echo "Cancelled." ;; esac printf "\nEnter to close."; read -r _' ;; - *"Use for login"*) - case "$pam" in true|True) new=false ;; *) new=true ;; esac - if [ "$new" = true ]; then - if fprintd-list "$USER" 2>/dev/null | grep -qiE 'no fingers enrolled|No devices available'; then - notify-send "Fingerprint" "Enroll a finger first, then enable login." - exit 0 - fi - fi - nomarchy-theme-sync --quiet set settings.fingerprint.pam "$new" --no-switch - notify-send "Fingerprint" "Use for login: $new — applies on next nomarchy-rebuild." - ;; + *"Fingerprint ("*) + # Drives BOTH sides off one state key (sudo/login PAM + the + # hyprlock unlock), so it sudos a system rebuild and runs a home + # switch — in a terminal, like Auto timezone. The enrolled-finger + # guard and the state write live in the tool, so the CLI and this + # row can't disagree. + command -v nomarchy-fingerprint >/dev/null 2>&1 \ + || { notify-send "Fingerprint" "nomarchy-fingerprint not on PATH (hardware.fingerprint.enable?)."; exit 0; } + exec ${cfg.terminal} -e nomarchy-fingerprint toggle ;; esac ;; controlcenter) @@ -1139,6 +1151,17 @@ ${themeRows} then row "Auto timezone (on)" preferences-system-time else row "Auto timezone (off)" preferences-system-time fi + # Auto-login — self-gated on the tool (greeter.enable). Off = the + # 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 + # from a newer generation can't misreport against an older tool. + case "$(nomarchy-theme-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 ] then row "Auto-commit (on)" git @@ -1178,6 +1201,7 @@ ${themeRows} *Printers*) exec "$0" printers ;; *"Do Not Disturb"*) exec "$0" dnd ;; *"Auto timezone"*) exec "$0" autotimezone ;; + *"Auto-login"*) exec "$0" autologin ;; *"Auto-commit"*) exec "$0" autocommit ;; *Snapshots*) exec "$0" snapshot ;; *Rollback*) exec "$0" rollback ;; @@ -1294,7 +1318,7 @@ ${themeRows} esac ;; *) - echo "usage: nomarchy-menu [lookfeel|tools|system|power|power-profile|powermgmt|batterylimit|theme|clipboard|calc|files|emoji|web|network|bluetooth|audio|display|display-profile|keyboard|printers|capture|colorpicker|keybinds|ask|dnd|nightlight|autotimezone|autocommit|vpn|snapshot|doctor|firmware|fingerprint|controlcenter|rollback|whatchanged]" >&2 + echo "usage: nomarchy-menu [lookfeel|tools|system|power|power-profile|powermgmt|batterylimit|theme|clipboard|calc|files|emoji|web|network|bluetooth|audio|display|display-profile|keyboard|printers|capture|colorpicker|keybinds|ask|dnd|nightlight|autotimezone|autologin|autocommit|vpn|snapshot|doctor|firmware|fingerprint|controlcenter|rollback|whatchanged]" >&2 exit 64 ;; esac ''; diff --git a/modules/nixos/greeter.nix b/modules/nixos/greeter.nix index 54ea9ad..28a488c 100644 --- a/modules/nixos/greeter.nix +++ b/modules/nixos/greeter.nix @@ -14,16 +14,67 @@ # theme's colors). ANSI "black" stays dark even in light themes — # the greeter reads terminal-dark there, the same convention every # terminal applies to ANSI colors. +# +# Auto-login is in-flake state like the rest (settings.greeter.autoLogin, +# written by System › Auto-login via nomarchy-autologin below), NOT a baked +# line in system.nix: a hand-set `nomarchy.system.greeter.autoLogin` outranks +# the state default, which would leave the menu toggle flipping JSON that +# nothing reads. The installer therefore seeds the STATE on LUKS machines and +# the template keeps its example commented (templates/downstream/system.nix). { config, lib, pkgs, ... }: let cfg = config.nomarchy.system; distroName = config.system.nixos.distroName; + sync = lib.getExe pkgs.nomarchy-theme-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 + # system switch. greetd's initial_session is baked at system rebuild, so + # there is nothing to apply live — the next boot is the observable change. + nomarchy-autologin = pkgs.writeShellScriptBin "nomarchy-autologin" '' + set -e + if [ "$(id -u)" -eq 0 ]; then + echo "nomarchy-autologin: run as your normal user (it sudos the rebuild itself)" >&2 + exit 1 + fi + flake="''${NOMARCHY_PATH:-$HOME/.nomarchy}" + + cur=$(${sync} get settings.greeter.autoLogin 2>/dev/null) || cur=null + case "''${1:-toggle}" in + on) new="\"$USER\"" ;; + off) new=null ;; + toggle) case "$cur" in null|""|None) new="\"$USER\"" ;; *) new=null ;; esac ;; + status) echo "$cur"; exit 0 ;; + *) echo "usage: nomarchy-autologin [toggle|on|off|status]" >&2; exit 64 ;; + esac + + ${sync} --quiet set settings.greeter.autoLogin "$new" --no-switch + + notify-send "Auto-login" "Rebuilding the system…" 2>/dev/null || true + sudo nixos-rebuild switch --flake "$flake#default" + + if [ "$new" = null ]; then + notify-send "Auto-login off" "The greeter asks who you are on the next boot." 2>/dev/null || true + else + notify-send "Auto-login on" "Next boot goes straight to the desktop." 2>/dev/null || true + fi + ''; + state = if cfg.stateFile != null then builtins.fromJSON (builtins.readFile cfg.stateFile) else { }; + + # The auto-login user from the state, or null. Read here via the state file + # — NOT `config.nomarchy.settings`, which exists only on the Home Manager + # side: on NixOS that attribute is missing, and `or null` swallows the + # error, so the old default silently evaluated to null on every machine. + stateAutoLogin = + let v = (state.settings or { }).greeter.autoLogin or null; + in if builtins.isString v && v != "" then v else null; + # A sparse/hand-rolled state without a proper ansi block just skips the # theming (stock tuigreet grey) — never an eval error. ansi = state.ansi or [ ]; @@ -44,6 +95,15 @@ let in { config = { + # Shipped unconditionally so the menu can turn auto-login back ON while + # it's off — the same reason nomarchy-autotimezone is unconditional. + environment.systemPackages = [ nomarchy-autologin ]; + + # Track the in-flake flag; mkDefault so a hand-set + # nomarchy.system.greeter.autoLogin in system.nix still wins (the + # autoTimezone pattern). + nomarchy.system.greeter.autoLogin = lib.mkDefault stateAutoLogin; + # VT palette from the theme (RRGGBB, no #; lands as vt.default_* kernel # params). mkDefault so a downstream console.colors wins. console.colors = lib.mkIf themed (lib.mkDefault (map (lib.removePrefix "#") ansi)); diff --git a/modules/nixos/hardware.nix b/modules/nixos/hardware.nix index eaeb996..f745a13 100644 --- a/modules/nixos/hardware.nix +++ b/modules/nixos/hardware.nix @@ -23,6 +23,58 @@ let then import ../theme-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; + + # The single fingerprint on/off switch (System › Fingerprint). One state key + # for one user-facing decision — it drives login/sudo PAM here AND the + # hyprlock unlock in modules/home/idle.nix, which reads the same + # settings.fingerprint.pam. Two rebuilds, because the two live in different + # configurations: sudo the system switch (PAM), then a home switch + # (hyprlock). Same user-owns-the-flake shape as nomarchy-autotimezone. + # + # This does NOT decide whether login prompts at all — auto-login skips the + # greeter entirely, so "fingerprint on" adds the finger to whatever prompts + # actually happen (sudo, lock screen, and the greeter only when auto-login + # is off). See nomarchy-autologin in ./greeter.nix. + nomarchy-fingerprint = pkgs.writeShellScriptBin "nomarchy-fingerprint" '' + set -e + if [ "$(id -u)" -eq 0 ]; then + echo "nomarchy-fingerprint: run as your normal user (it sudos the rebuild itself)" >&2 + exit 1 + fi + flake="''${NOMARCHY_PATH:-$HOME/.nomarchy}" + + cur=$(${sync} get settings.fingerprint.pam 2>/dev/null) || cur=false + case "''${1:-toggle}" in + on) new=true ;; + off) new=false ;; + toggle) case "$cur" in true|True) new=false ;; *) new=true ;; esac ;; + status) echo "$cur"; exit 0 ;; + *) echo "usage: nomarchy-fingerprint [toggle|on|off|status]" >&2; exit 64 ;; + esac + + # Turning it ON with no enrolled finger would advertise a scan that cannot + # succeed on every prompt — refuse instead, and say where to go. + if [ "$new" = true ] \ + && fprintd-list "$USER" 2>/dev/null | grep -qiE 'no fingers enrolled|No devices available'; then + notify-send "Fingerprint" "Enroll a finger first (System › Fingerprint › Enroll)." 2>/dev/null || true + echo "nomarchy-fingerprint: no finger enrolled — run fprintd-enroll first" >&2 + exit 1 + fi + + ${sync} --quiet set settings.fingerprint.pam "$new" --no-switch + + notify-send "Fingerprint" "Rebuilding…" 2>/dev/null || true + sudo nixos-rebuild switch --flake "$flake#default" + home-manager switch --flake "$flake" + + if [ "$new" = true ]; then + notify-send "Fingerprint on" "Password or finger — at sudo, the lock screen, and the greeter." 2>/dev/null || true + else + notify-send "Fingerprint off" "Password only. Enrolled fingers are kept." 2>/dev/null || true + fi + ''; in { options.nomarchy.hardware = { @@ -214,6 +266,9 @@ in # every interactive service we care about to follow our opt-in flag. (lib.mkIf cfg.fingerprint.enable { services.fprintd.enable = true; + # Ships whenever a reader exists, regardless of the pam flag: the + # toggle's whole job is to turn the flag back on while it's off. + environment.systemPackages = [ nomarchy-fingerprint ]; security.pam.services = lib.genAttrs [ "login" "sudo" "su" "greetd" "hyprlock" "sshd" "passwd" "chsh" "chfn" "chpasswd" diff --git a/modules/nixos/options.nix b/modules/nixos/options.nix index 1bc84ca..82f7fb7 100644 --- a/modules/nixos/options.nix +++ b/modules/nixos/options.nix @@ -11,13 +11,20 @@ greeter.autoLogin = lib.mkOption { type = lib.types.nullOr lib.types.str; - default = config.nomarchy.settings.greeter.autoLogin or null; + default = null; + defaultText = lib.literalExpression + "(settings.greeter.autoLogin from theme-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. - The installer sets it on LUKS-encrypted machines — the disk - passphrase already gates access, a second prompt is ceremony. + + Normally you leave this alone and use System › Auto-login, which + writes `settings.greeter.autoLogin` in theme-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 + hand pins the choice and the menu toggle can no longer move it. ''; }; diff --git a/pkgs/nomarchy-install/patch-template.py b/pkgs/nomarchy-install/patch-template.py index f2c2e2d..d589c85 100644 --- a/pkgs/nomarchy-install/patch-template.py +++ b/pkgs/nomarchy-install/patch-template.py @@ -104,12 +104,10 @@ def build_installer_region(v: dict) -> str: " # active; heavier opt-ins stay in the commented catalog below.", ] - if v.get("autoLogin"): - user = nix_str(v["username"]) - lines += [ - " # LUKS passphrase already gates this machine — skip the greeter password.", - f' nomarchy.system.greeter.autoLogin = "{user}";', - ] + # Auto-login is deliberately NOT emitted here — it is seeded into + # theme-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. if v.get("laptop"): lines += [ @@ -250,6 +248,23 @@ def patch_system(text: str, v: dict) -> str: return text +def patch_state(text: str, v: dict) -> str: + """Seed menu-owned settings into theme-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 + and strand the toggle. Auto-login is on when the disk is encrypted — the + LUKS passphrase already gates the machine, so a greeter password is a + second prompt for the same thing; without LUKS it stays off, where the + greeter is the only thing standing between power-on and the desktop. + """ + state = json.loads(text) + settings = state.setdefault("settings", {}) + if v.get("autoLogin"): + settings.setdefault("greeter", {})["autoLogin"] = v["username"] + return json.dumps(state, indent=2) + "\n" + + def main() -> None: if len(sys.argv) != 2: sys.exit("usage: patch-template.py ") @@ -260,6 +275,7 @@ def main() -> None: "flake.nix": patch_flake, "home.nix": patch_home, "system.nix": patch_system, + "theme-state.json": patch_state, } for name, fn in mapping.items(): path = flake_dir / name diff --git a/pkgs/nomarchy-theme-sync/nomarchy-theme-sync.py b/pkgs/nomarchy-theme-sync/nomarchy-theme-sync.py index affae8e..21a73ca 100644 --- a/pkgs/nomarchy-theme-sync/nomarchy-theme-sync.py +++ b/pkgs/nomarchy-theme-sync/nomarchy-theme-sync.py @@ -589,10 +589,13 @@ def cmd_get(args) -> None: node = node[key] except (KeyError, TypeError): die(f"no such key: {args.path}") - # Booleans print JSON-style (true/false, not Python's True/False) so - # shell consumers can compare against the same literal they `set`. + # Booleans and null print JSON-style (true/false/null, not Python's + # True/False/None) so shell consumers can compare against the same + # literal they `set`. null matters for genuinely-nullable keys like + # settings.greeter.autoLogin, where "None" would silently miss every + # `case ... null)` a caller writes. print(json.dumps(node, indent=2) - if isinstance(node, (dict, list, bool)) else node) + if node is None or isinstance(node, (dict, list, bool)) else node) else: print(json.dumps(state, indent=2)) diff --git a/templates/downstream/home.nix b/templates/downstream/home.nix index c15434f..3aad767 100644 --- a/templates/downstream/home.nix +++ b/templates/downstream/home.nix @@ -18,12 +18,13 @@ # nomarchy.waybar.enable = false; # bring your own bar # nomarchy.stylix.enable = false; # opt out of GTK/Qt theming # nomarchy.dockAudio.enable = false; # keep audio put when docking - # nomarchy.idle.fingerprint = true; # unlock the lock screen by finger too, and - # # say so on the field. Set it alongside - # # nomarchy.hardware.fingerprint.pam in - # # system.nix — hyprlock is configured here - # # and cannot read the NixOS side, and it - # # does not take a finger through PAM at all. + # nomarchy.idle.fingerprint = true; # pin lock-screen finger unlock on/off. Normally + # # you want System › Fingerprint instead: it moves + # # this AND login/sudo together off one state key, + # # and this line would override it. Separate options + # # only because hyprlock is configured here (it can't + # # read the NixOS side) and doesn't take a finger + # # through PAM at all — it has its own fprintd backend. # # 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` diff --git a/templates/downstream/system.nix b/templates/downstream/system.nix index eff511e..b4843b3 100644 --- a/templates/downstream/system.nix +++ b/templates/downstream/system.nix @@ -62,7 +62,9 @@ # amd.rocm.enable = true; # ROCm GPU compute (multi-GB, opt-in) # amd.rocm.gfxOverride = "11.0.0"; # HSA override for an unlisted iGPU # fingerprint.enable = true; # fprintd; installer-set when a reader is detected - # fingerprint.pam = true; # use the fingerprint for login + sudo + # fingerprint.pam = true; # pin the fingerprint on/off instead of letting the + # # System › Fingerprint toggle own it (this line WINS + # # over the menu — leave it commented to use the menu) # fingerprint.parallel = false; # sequential prompt instead of password-or-finger # npu.enable = true; # on-die NPU driver (experimental; userspace runtime BYO) # latestKernel = true; # newest kernel for very-new hardware (drivers not yet in the default)