From bedb11930f6e5b36c411248a1661efb7e74c8911 Mon Sep 17 00:00:00 2001 From: rope Date: Wed, 8 Jul 2026 11:04:27 +0100 Subject: [PATCH] lockscreen: replace hyprlock with quickshell WlSessionLock Co-Authored-By: Claude Fable 5 --- settings/hyprland.nix | 58 ++------------- settings/quickshell.nix | 158 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 162 insertions(+), 54 deletions(-) diff --git a/settings/hyprland.nix b/settings/hyprland.nix index 5230d97..76bb993 100644 --- a/settings/hyprland.nix +++ b/settings/hyprland.nix @@ -52,7 +52,6 @@ in brightnessctl swayosd playerctl - hyprlock hypridle hyprshot hyprpicker @@ -161,6 +160,9 @@ in focus_on_activate = false; vrr = 2; background_color = rgb c.base00; + # If quickshell dies while holding the session lock, let its + # restarted instance re-attach instead of a dead red screen. + allow_session_lock_restore = true; }; # vfr moved from misc: to debug: in 0.55.0 @@ -340,56 +342,8 @@ in ''; }; - programs.hyprlock = { - enable = true; - settings = { - general = { - grace = 5; - hide_cursor = true; - }; - background = { - blur_passes = 3; - blur_size = 8; - brightness = 0.6; - }; - input-field = { - size = "280, 44"; - outline_thickness = 2; - dots_size = 0.25; - dots_spacing = 0.2; - dots_center = true; - fade_on_empty = true; - fade_timeout = 3000; - placeholder_text = ""; - fail_text = ""; - position = "0, -40"; - halign = "center"; - valign = "center"; - rounding = 8; - }; - label = [ - { - text = "$TIME"; - color = "rgb(${c.base05})"; - font_size = 72; - font_family = "Inter"; - position = "0, 120"; - halign = "center"; - valign = "center"; - } - { - text = "cmd[update:60000] date +'%A, %d %B'"; - color = "rgb(${c.base04})"; - font_size = 16; - font_family = "Inter"; - position = "0, 60"; - halign = "center"; - valign = "center"; - } - ]; - }; - }; - + # Lock screen is quickshell's WlSessionLock (settings/quickshell.nix), + # triggered via `qs ipc call lock lock`. services.hypridle = lib.mkIf isMacbook ( let # Skip the action if any MPRIS player is playing — covers windowed @@ -400,7 +354,7 @@ in enable = true; settings = { general = { - lock_cmd = "pidof hyprlock || hyprlock"; + lock_cmd = "qs ipc call lock lock"; before_sleep_cmd = "loginctl lock-session"; after_sleep_cmd = "hyprctl dispatch dpms on"; }; diff --git a/settings/quickshell.nix b/settings/quickshell.nix index 4658420..45b08e2 100644 --- a/settings/quickshell.nix +++ b/settings/quickshell.nix @@ -137,6 +137,7 @@ in singleton Theme 1.0 Theme.qml singleton Commands 1.0 Commands.qml Bar 1.0 Bar.qml + Lock 1.0 Lock.qml ''; }; @@ -191,6 +192,150 @@ in ''; }; + "quickshell/Lock.qml" = { + onChange = qsRestart; + text = '' + import Quickshell + import Quickshell.Wayland + import Quickshell.Services.Pam + import QtQuick + import Qt5Compat.GraphicalEffects + + // ext-session-lock screen, replacing hyprlock. Lives in the shell + // process: set `locked: true` and the compositor hands every + // screen to a LockSurface below until PAM says otherwise. + WlSessionLock { + id: lock + + property string password: "" + property bool failed: false + + function submit(pw) { + if (pam.active || pw === "") return; + failed = false; + password = pw; + pam.start(); + } + + PamContext { + id: pam + // NixOS ships a "login" pam service; pam_unix verifies the + // user's own password via the unix_chkpwd setuid wrapper. + config: "login" + onPamMessage: { + if (this.responseRequired) this.respond(lock.password); + } + onCompleted: result => { + lock.password = ""; + if (result === PamResult.Success) lock.locked = false; + else lock.failed = true; + } + } + + WlSessionLockSurface { + id: surface + color: "black" + + Image { + id: wall + anchors.fill: parent + source: "file://" + Quickshell.env("HOME") + "/.local/share/backgrounds/wallpaper.png" + fillMode: Image.PreserveAspectCrop + visible: false + } + FastBlur { + anchors.fill: wall + source: wall + radius: 48 + } + Rectangle { anchors.fill: parent; color: "black"; opacity: 0.4 } + + SystemClock { + id: lockClock + precision: SystemClock.Minutes + } + + Column { + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: -60 + spacing: 8 + + Text { + anchors.horizontalCenter: parent.horizontalCenter + text: lockClock.date.toLocaleTimeString(Qt.locale(), "HH:mm") + color: Theme.base05 + font.family: Theme.fontFamily + font.pixelSize: 72 + font.weight: Font.Medium + } + Text { + anchors.horizontalCenter: parent.horizontalCenter + text: lockClock.date.toLocaleDateString(Qt.locale(), "dddd, d MMMM") + color: Theme.base04 + font.family: Theme.fontFamily + font.pixelSize: 16 + } + + Item { width: 1; height: 32 } + + // Password pill: dots for typed chars; border tracks + // state (accent while checking, red on failure). + Rectangle { + anchors.horizontalCenter: parent.horizontalCenter + width: 280 + height: 44 + radius: Theme.radius + color: Theme.cardBg + border.width: Theme.borderWidth + border.color: pam.active ? Theme.base0D + : lock.failed ? Theme.base08 + : Theme.base03 + Behavior on border.color { ColorAnimation { duration: Theme.animFade } } + + Row { + anchors.centerIn: parent + spacing: 8 + Repeater { + model: Math.min(pwInput.text.length, 24) + Rectangle { + width: 8; height: 8; radius: 4 + color: Theme.base05 + } + } + } + + Text { + anchors.centerIn: parent + visible: pwInput.text === "" && lock.failed + text: "wrong password" + color: Theme.base08 + font.family: Theme.fontFamily + font.pixelSize: 13 + } + + TextInput { + id: pwInput + anchors.fill: parent + opacity: 0 + enabled: !pam.active + echoMode: TextInput.Password + focus: true + onAccepted: { + lock.submit(text); + text = ""; + } + Keys.onEscapePressed: text = "" + } + } + } + + Component.onCompleted: pwInput.forceActiveFocus() + } + } + ''; + }; + "quickshell/Commands.qml" = { onChange = qsRestart; text = '' @@ -201,7 +346,6 @@ in readonly property string nmcli: "${nmcli}" readonly property string wifiConnect: "${wifiConnectScript}" readonly property string notifSound: "${pkgs.libcanberra-gtk3}/bin/canberra-gtk-play" - readonly property string hyprlock: "${pkgs.hyprlock}/bin/hyprlock" readonly property string systemctl: "${pkgs.systemd}/bin/systemctl" readonly property string weatherFetch: "${weatherFetchScript}" } @@ -239,6 +383,16 @@ in function reload(): void { Quickshell.reload(false); } } + // Session lock (replaces hyprlock). `qs ipc call lock lock` + // from hypridle / loginctl lock-session; the session menu + // calls root.lock() directly. Idempotent while locked. + Lock { id: lockScreen } + function lock() { lockScreen.locked = true; } + IpcHandler { + target: "lock" + function lock(): void { root.lock(); } + } + // Screenshot pin: the Shift+Super+S keybind brackets hyprshot // with pin/unpin so the focus grab is suspended while slurp // grabs input — otherwise the open menu would close like any @@ -639,7 +793,7 @@ in function activate(act) { open = false; - if (act === "lock") Quickshell.execDetached([Commands.hyprlock]); + if (act === "lock") bar.shellRoot.lock(); else if (act === "logout") Hyprland.dispatch("hl.dsp.exit()"); else if (act === "reboot") Quickshell.execDetached([Commands.systemctl, "reboot"]); else Quickshell.execDetached([Commands.systemctl, "poweroff"]);