lockscreen: replace hyprlock with quickshell WlSessionLock

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-07-08 11:04:27 +01:00
parent 24123270b2
commit bedb11930f
2 changed files with 162 additions and 54 deletions

View file

@ -52,7 +52,6 @@ in
brightnessctl brightnessctl
swayosd swayosd
playerctl playerctl
hyprlock
hypridle hypridle
hyprshot hyprshot
hyprpicker hyprpicker
@ -161,6 +160,9 @@ in
focus_on_activate = false; focus_on_activate = false;
vrr = 2; vrr = 2;
background_color = rgb c.base00; 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 # vfr moved from misc: to debug: in 0.55.0
@ -340,56 +342,8 @@ in
''; '';
}; };
programs.hyprlock = { # Lock screen is quickshell's WlSessionLock (settings/quickshell.nix),
enable = true; # triggered via `qs ipc call lock lock`.
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";
}
];
};
};
services.hypridle = lib.mkIf isMacbook ( services.hypridle = lib.mkIf isMacbook (
let let
# Skip the action if any MPRIS player is playing — covers windowed # Skip the action if any MPRIS player is playing — covers windowed
@ -400,7 +354,7 @@ in
enable = true; enable = true;
settings = { settings = {
general = { general = {
lock_cmd = "pidof hyprlock || hyprlock"; lock_cmd = "qs ipc call lock lock";
before_sleep_cmd = "loginctl lock-session"; before_sleep_cmd = "loginctl lock-session";
after_sleep_cmd = "hyprctl dispatch dpms on"; after_sleep_cmd = "hyprctl dispatch dpms on";
}; };

View file

@ -137,6 +137,7 @@ in
singleton Theme 1.0 Theme.qml singleton Theme 1.0 Theme.qml
singleton Commands 1.0 Commands.qml singleton Commands 1.0 Commands.qml
Bar 1.0 Bar.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" = { "quickshell/Commands.qml" = {
onChange = qsRestart; onChange = qsRestart;
text = '' text = ''
@ -201,7 +346,6 @@ in
readonly property string nmcli: "${nmcli}" readonly property string nmcli: "${nmcli}"
readonly property string wifiConnect: "${wifiConnectScript}" readonly property string wifiConnect: "${wifiConnectScript}"
readonly property string notifSound: "${pkgs.libcanberra-gtk3}/bin/canberra-gtk-play" 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 systemctl: "${pkgs.systemd}/bin/systemctl"
readonly property string weatherFetch: "${weatherFetchScript}" readonly property string weatherFetch: "${weatherFetchScript}"
} }
@ -239,6 +383,16 @@ in
function reload(): void { Quickshell.reload(false); } 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 // Screenshot pin: the Shift+Super+S keybind brackets hyprshot
// with pin/unpin so the focus grab is suspended while slurp // with pin/unpin so the focus grab is suspended while slurp
// grabs input otherwise the open menu would close like any // grabs input otherwise the open menu would close like any
@ -639,7 +793,7 @@ in
function activate(act) { function activate(act) {
open = false; 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 === "logout") Hyprland.dispatch("hl.dsp.exit()");
else if (act === "reboot") Quickshell.execDetached([Commands.systemctl, "reboot"]); else if (act === "reboot") Quickshell.execDetached([Commands.systemctl, "reboot"]);
else Quickshell.execDetached([Commands.systemctl, "poweroff"]); else Quickshell.execDetached([Commands.systemctl, "poweroff"]);