All checks were successful
Check / eval (push) Successful in 3m44s
Recover from DPMS/lock blackout without a second machine: undock/enable always issue dpms on; hypridle on-resume and after_sleep run nomarchy-display-wake (zero-output undock rescue + auto-dump). nomarchy-display-dump writes ~/nomarchy-display-dump-*.txt (SUPER+SHIFT+D or Ctrl+Alt+F3 TTY). HARDWARE-QUEUE #127 no longer requires SSH. V1: nix flake check --no-build. V3: docked idle on AMD box still open.
192 lines
8.1 KiB
Nix
192 lines
8.1 KiB
Nix
# 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} || <action>` 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
|
|
'';
|
|
|
|
# Same transition binary hyprland.nix puts on PATH — re-derived here so
|
|
# hypridle can pin absolute store paths (thin PATH in the user unit).
|
|
# Must stay in lockstep with modules/home/hyprland.nix's transition.
|
|
# We only need undock/enable + dpms; call via PATH-installed name is
|
|
# fragile, so wake embeds the transition from a minimal import chain:
|
|
# dump+wake live in display-tools.nix; transition is the public name
|
|
# on PATH after HM activation — wake script uses absolute path from
|
|
# a local reimplementation of just the enable/dpms rescue when the
|
|
# full transition package isn't injectable without circular imports.
|
|
#
|
|
# Practical approach: invoke `nomarchy-display-wake` from PATH after
|
|
# activation; for the unit Exec lines use a wrapper that prefers the
|
|
# store path of a wake script built with transition = null fallback
|
|
# (hyprctl keyword + reload + dpms) matching undock's public contract.
|
|
displayWake =
|
|
let
|
|
# Minimal undock-equivalent for idle's store path (no circular dep on
|
|
# hyprland.nix). Prefer the real transition when on PATH at runtime.
|
|
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 = "<i>Password…</i>";
|
|
}];
|
|
|
|
# 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;
|
|
on-timeout = "hyprctl dispatch dpms off";
|
|
# Same rescue path as after_sleep (#127).
|
|
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}";
|
|
}
|
|
];
|
|
};
|
|
};
|
|
};
|
|
}
|