# 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 ''; # Absolute store paths for hypridle (thin PATH). Wake reuses the shared # helper; a mini transition avoids a circular import on hyprland.nix. displayWake = let miniTransition = pkgs.writeShellScriptBin "nomarchy-display-transition" '' set -u case "''${1:-}" in undock|enable) internal="''${2:-}" [ -n "$internal" ] || exit 64 hyprctl keyword monitor "$internal,preferred,auto,1" >/dev/null 2>&1 || true if ! hyprctl monitors -j 2>/dev/null \ | ${pkgs.jq}/bin/jq -e --arg m "$internal" 'any(.[]; .name == $m)' \ >/dev/null 2>&1; then hyprctl reload >/dev/null 2>&1 || true sleep 0.5 fi hyprctl keyword monitor "$internal,preferred,auto,1" >/dev/null 2>&1 || true hyprctl dispatch dpms on >/dev/null 2>&1 || true hyprctl monitors -j 2>/dev/null \ | ${pkgs.jq}/bin/jq -e --arg m "$internal" 'any(.[]; .name == $m)' \ >/dev/null 2>&1 ;; *) exit 64 ;; esac ''; in (import ./display-tools.nix { inherit pkgs; displayTransition = miniTransition; }).displayWakeTool; in { config = lib.mkIf cfg.idle.enable { # Smart suspend on PATH for manual use; hypridle uses the store path. home.packages = [ pkgs.nomarchy-suspend ]; 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. # #127: not only dpms on — if dock mode left zero enabled # outputs (eDP disabled + external gone/black), re-enable the # internal. Absolute store path: hypridle's PATH is thin. after_sleep_cmd = "${lib.getExe displayWake}"; }; 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; # Blanks in every dock/lid state — the #127 clamshell skip is # gone: it was a cure for a cause that does not exist (proven on # hardware 2026-07-16, see ROADMAP). The wake no longer depends # on this daemon surviving — misc:{key_press,mouse_move}_enables_dpms # in hyprland.nix means input wakes the screen compositor-side. on-timeout = "hyprctl dispatch dpms off"; # Same rescue path as after_sleep (#127): re-enables a disabled # internal, which plain `dpms on` cannot do. on-resume = "${lib.getExe displayWake}"; } # 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. # nomarchy-suspend (#115): on battery + hibernate wired + toggle # on → suspend-then-hibernate (1h → disk); else plain suspend. # Lid close is logind's job (not hypridle): undocked lid still # suspends / s2h (HandleLidSwitch); docked/clamshell lid is ignore # (HandleLidSwitchDocked — modules/nixos/power.nix, #86). { timeout = 900; on-timeout = "${onAc} || ${lib.getExe pkgs.nomarchy-suspend}"; } ]; }; }; }; }