# hyprlock + hypridle — screen locking and idle management, themed from # 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, ... }: let cfg = config.nomarchy; t = cfg.theme; c = t.colors; inherit (config.nomarchy.lib) rgb; # Exits 0 when running on AC: a mains adapter reports `online` 1 (battery # supplies have no `online` node, so they never match). Used as # `${onAc} || ` so the action is skipped while plugged in. onAc = pkgs.writeShellScript "nomarchy-on-ac" '' for f in /sys/class/power_supply/*/online; do [ -r "$f" ] && [ "$(cat "$f")" = "1" ] && exit 0 done exit 1 ''; in { config = lib.mkIf cfg.idle.enable { programs.hyprlock = { enable = true; settings = { general.hide_cursor = true; background = [{ monitor = ""; color = rgb c.base; }]; input-field = [{ monitor = ""; size = "300, 50"; outline_thickness = t.ui.borderSize; dots_size = 0.25; outer_color = rgb c.accent; inner_color = rgb c.surface; font_color = rgb c.text; check_color = rgb c.warn; fail_color = rgb c.bad; rounding = t.ui.rounding; # Sentence case, matching the $FPRINTPROMPT label below: both can be # on screen at once, so they must not read as two different voices. placeholder_text = "Password…"; }]; # The lock screen is deliberately just the clock: the input field # fades out while empty (hyprlock's fade_on_empty default) and only # appears once you type. So the fingerprint hint CANNOT live in the # field's placeholder — $FPRINTPROMPT renders faithfully into a widget # nobody sees until they have already given up on the reader and # started typing. It needs a surface that is visible at rest, and a # label is the one that keeps the clock-only look. (Confirmed on # hardware 2026-07-14: labels do expand $FPRINTPROMPT, so the line is # live — the ready message, then the present message on touch — and # not static text. Both live in auth.fingerprint below.) label = [{ monitor = ""; text = "$TIME"; color = rgb c.text; font_size = 64; font_family = t.fonts.ui; position = "0, 120"; halign = "center"; valign = "center"; }] ++ lib.optional cfg.idle.fingerprint { monitor = ""; text = "$FPRINTPROMPT"; # subtext-on-base is the palette's secondary-text role and is held # to a 3.0 contrast floor on every theme by checks.theme-contrast — # the same guard that exists because two themes once shipped # subtext == base and made hint text invisible. color = rgb c.subtext; font_size = 16; font_family = t.fonts.ui; position = "0, -160"; halign = "center"; valign = "center"; }; } // lib.optionalAttrs cfg.idle.fingerprint { # hyprlock does NOT take a fingerprint through PAM: its PAM stack runs # only on submit, so a parallel module never gets to poll the reader. # This is a separate backend of its own, talking to fprintd over # D-Bus, and `nomarchy.hardware.fingerprint.pam` does not reach it — # which is why finger-unlock at the lock screen did nothing at all # until this option existed, while sudo took a finger happily. auth.fingerprint = { enabled = true; ready_message = "Enter password or scan your finger"; present_message = "Scanning your finger…"; }; }; }; services.hypridle = { enable = true; settings = { general = { lock_cmd = "pidof hyprlock || hyprlock"; # No before_sleep_cmd here: locking before sleep is driven # system-side so it can fire for suspend but skip an encrypted # hibernate — whose LUKS resume already gates the machine, so a # hyprlock on top is a second password (and a Wayland session-lock # can't be safely dropped after the fact — killing the locker trips # its "go to a tty" crash failsafe). See nomarchy-lock-before-sleep # in modules/nixos/default.nix. after_sleep_cmd = "hyprctl dispatch dpms on"; }; listener = [ # Lock and screen-off are the same on either power source — # they're about privacy and the panel, not battery. { timeout = 300; on-timeout = "loginctl lock-session"; } { timeout = 600; on-timeout = "hyprctl dispatch dpms off"; on-resume = "hyprctl dispatch dpms on"; } # Suspend only on battery, and sooner than the old fixed 30 min # (it now only fires unplugged). Plugged in, the machine stays # up — long builds, media, presentations aren't killed mid-idle. # Lid close is logind's job (not hypridle): undocked lid still # suspends (HandleLidSwitch); docked/clamshell lid is ignore # (HandleLidSwitchDocked — modules/nixos/power.nix, #86). { timeout = 900; on-timeout = "${onAc} || systemctl suspend"; } ]; }; }; }; }