Files
DankMaterialShell/quickshell/Modules/Lock/Lock.qml
T
Hatta 3081941592 lock: lock before sleep when the command skips logind (#3587)
* lock: lock before sleep when the command skips logind

Custom hibernate never emits PrepareForSleep, so the session stayed unlocked. Wait for the compositor lock on every sleep path, and release the delay inhibitor if that lock finished first.

* lock: release the sleep inhibitor only after lockerReady

* lock: read the sleep inhibitor under its mutex in the race test
2026-09-29 16:50:13 -04:00

508 lines
15 KiB
QML

pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import qs.Common
import qs.Services
Scope {
id: root
readonly property var log: Log.scoped("Lock")
property string sharedPasswordBuffer: ""
property bool shouldLock: false
onSharedPasswordBufferChanged: {
if (!powerOffFadeTimer.running)
return;
cancelPowerOffFade();
}
onShouldLockChanged: {
IdleService.isShellLocked = shouldLock;
}
property bool lockInitiatedLocally: false
property bool pendingLock: false
readonly property int maxLockRetries: 3
property int lockRetryAttempts: 0
property bool lockRetryPending: false
property bool lockPowerOffArmed: false
property bool lockWakeAllowed: false
property bool customLockerSpawned: false
readonly property bool powerOffOnLock: SettingsData.lockScreenPowerOffMonitorsOnLock || IdleService.lockPowerOffRequested
property real powerOffFadeTarget: 0
property bool powerOffFadeInstant: false
function beginPowerOff() {
if (!SettingsData.fadeToDpmsEnabled || SettingsData.fadeToDpmsGracePeriod <= 0) {
applyMonitorsOff();
return;
}
powerOffFadeInstant = false;
powerOffFadeTarget = 1;
powerOffFadeTimer.restart();
}
function applyMonitorsOff() {
IdleService.monitorsOff = true;
CompositorService.powerOffMonitors();
lockWakeAllowed = false;
lockWakeDebounce.restart();
dpmsReapplyTimer.start();
}
function resetPowerOffFade() {
powerOffFadeTimer.stop();
powerOffFadeInstant = true;
powerOffFadeTarget = 0;
}
function cancelPowerOffFade() {
resetPowerOffFade();
IdleService.lockPowerOffRequested = false;
}
// Avoid startup lock when using dms-greeter (#2952)
function freshGreeterLogin() {
const authTime = Number(Quickshell.env("DMS_GREETER_AUTH_TIME") || 0);
if (!authTime)
return false;
return (Date.now() / 1000 - authTime) < 120;
}
Component.onCompleted: {
IdleService.lockComponent = this;
IdleService.isSessionLockSecure = sessionLock.secure;
if (SettingsData.lockAtStartup && !freshGreeterLogin())
lock();
}
function notifyLockedHint(locked: bool) {
if (!SettingsData.loginctlLockIntegration || !DMSService.isConnected)
return;
DMSService.setLockedHint(locked, () => {});
}
function notifyLoginctl(lockAction: bool) {
if (!SettingsData.loginctlLockIntegration || !DMSService.isConnected)
return;
if (lockAction)
DMSService.lockSession(() => {});
else
DMSService.unlockSession(() => {});
}
function spawnCustomLocker() {
IdleService.lockPowerOffRequested = false;
Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionLock]);
// The custom locker manages its own surface; DMS never engages
// WlSessionLock here, so isShellLocked stays false and the fade
// overlay would never be dismissed. Hand off by dismissing it now.
IdleService.dismissFadeToLock();
customLockerSpawned = true;
}
function handleLoginctlCustomLock(): bool {
if (!(SettingsData.customPowerActionLock?.length > 0))
return false;
if (!customLockerSpawned)
spawnCustomLocker();
return true;
}
function resetLockRetry() {
lockRetryAttempts = 0;
lockRetryPending = false;
lockRetryTimer.stop();
}
function handleLockLost() {
if (lockRetryAttempts >= maxLockRetries) {
log.error("Compositor refused session lock", maxLockRetries, "times - resetting lock state");
forceReset();
return;
}
lockRetryAttempts++;
lockRetryPending = true;
lockRetryTimer.restart();
log.warn("Session lock lost while lock requested - retry", lockRetryAttempts, "/", maxLockRetries);
}
function lock() {
if (SettingsData.customPowerActionLock?.length > 0) {
spawnCustomLocker();
return;
}
if (shouldLock || pendingLock)
return;
resetLockRetry();
lockInitiatedLocally = true;
lockPowerOffArmed = powerOffOnLock;
if (!SessionService.active && SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration) {
pendingLock = true;
notifyLoginctl(true);
return;
}
shouldLock = true;
notifyLoginctl(true);
}
function lockAndOutputsOff() {
IdleService.lockPowerOffRequested = true;
if (sessionLock.secure) {
beginPowerOff();
return;
}
lockPowerOffArmed = true;
lock();
}
function unlock() {
if (!shouldLock)
return;
resetLockRetry();
lockInitiatedLocally = false;
notifyLoginctl(false);
shouldLock = false;
}
function forceReset() {
lockInitiatedLocally = false;
pendingLock = false;
shouldLock = false;
customLockerSpawned = false;
resetLockRetry();
resetPowerOffFade();
IdleService.lockPowerOffRequested = false;
}
function activate() {
lock();
}
Connections {
target: SessionService
function onSessionLocked() {
if (shouldLock || pendingLock)
return;
if (handleLoginctlCustomLock())
return;
if (!SessionService.active && SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration) {
pendingLock = true;
lockInitiatedLocally = false;
return;
}
lockInitiatedLocally = false;
lockPowerOffArmed = powerOffOnLock;
shouldLock = true;
}
function onSessionUnlocked() {
customLockerSpawned = false;
if (pendingLock) {
pendingLock = false;
lockInitiatedLocally = false;
return;
}
if (!shouldLock || lockInitiatedLocally)
return;
shouldLock = false;
}
function onSessionResumed() {
if (!shouldLock || sessionLock.locked)
return;
resumeRelockTimer.restart();
}
function onLoginctlStateChanged() {
if (SessionService.active && pendingLock) {
pendingLock = false;
lockInitiatedLocally = true;
lockPowerOffArmed = powerOffOnLock;
shouldLock = true;
return;
}
if (SessionService.locked && !shouldLock && !pendingLock) {
if (handleLoginctlCustomLock())
return;
lockInitiatedLocally = false;
lockPowerOffArmed = powerOffOnLock;
shouldLock = true;
}
}
}
Connections {
target: IdleService
function onLockRequested() {
lock();
}
function onMonitorsOffChanged() {
if (!IdleService.monitorsOff)
root.resetPowerOffFade();
}
}
Pam {
id: sharedPam
lockSecured: root.shouldLock
buffer: root.sharedPasswordBuffer
onUnlockRequested: root.unlock()
}
WlSessionLock {
id: sessionLock
locked: shouldLock && !lockRetryPending
WlSessionLockSurface {
id: lockSurface
property string currentScreenName: screen?.name ?? ""
property bool isActiveScreen: {
if (Quickshell.screens.length <= 1)
return true;
return SettingsData.getFilteredScreens("lockScreen").includes(screen);
}
color: isActiveScreen ? "transparent" : SettingsData.lockScreenInactiveColor
LockSurface {
anchors.fill: parent
visible: lockSurface.isActiveScreen
lock: sessionLock
pam: sharedPam
sharedPasswordBuffer: root.sharedPasswordBuffer
screenName: lockSurface.currentScreenName
isLocked: shouldLock
onUnlockRequested: root.unlock()
onPasswordChanged: newPassword => {
root.sharedPasswordBuffer = newPassword;
}
}
Rectangle {
anchors.fill: parent
color: "black"
opacity: root.powerOffFadeTarget
visible: opacity > 0 || powerOffFadeTimer.running
Behavior on opacity {
enabled: !root.powerOffFadeInstant
NumberAnimation {
duration: SettingsData.fadeToDpmsGracePeriod * 1000
easing.type: Easing.OutCubic
}
}
MouseArea {
property real baselineX: -1
property real baselineY: -1
anchors.fill: parent
enabled: powerOffFadeTimer.running
hoverEnabled: enabled
onEnabledChanged: {
baselineX = -1;
baselineY = -1;
}
onPressed: root.cancelPowerOffFade()
onWheel: root.cancelPowerOffFade()
onPositionChanged: mouse => {
if (baselineX < 0) {
baselineX = mouse.x;
baselineY = mouse.y;
return;
}
if (Math.abs(mouse.x - baselineX) < 5 && Math.abs(mouse.y - baselineY) < 5)
return;
root.cancelPowerOffFade();
}
}
}
}
}
Connections {
target: sessionLock
function onSecureChanged() {
IdleService.isSessionLockSecure = sessionLock.secure;
notifyLockedHint(sessionLock.secure);
if (!sessionLock.secure)
return;
SessionService.onSessionLockSecured();
IdleService.dismissFadeToLock();
lockRetryAttempts = 0;
pendingLock = false;
if (lockPowerOffArmed && powerOffOnLock)
beginPowerOff();
lockPowerOffArmed = false;
}
function onLockedChanged() {
if (sessionLock.locked)
return;
if (shouldLock && (IdleService.monitorsOff || lockPowerOffArmed || IdleService.lockPowerOffRequested))
return;
lockWakeAllowed = false;
resetPowerOffFade();
if (IdleService.monitorsOff && powerOffOnLock) {
IdleService.monitorsOff = false;
CompositorService.powerOnMonitors();
}
IdleService.lockPowerOffRequested = false;
if (shouldLock && !lockRetryPending)
handleLockLost();
}
}
LockScreenDemo {
id: demoWindow
}
IpcHandler {
target: "lock"
function lock() {
root.lock();
}
function lockAndOutputsOff() {
root.lockAndOutputsOff();
}
function unlock() {
root.unlock();
}
function forceReset() {
root.forceReset();
}
function demo() {
demoWindow.showDemo();
}
function isLocked(): bool {
return sessionLock.secure;
}
function status(): string {
return JSON.stringify({
shouldLock: root.shouldLock,
sessionLockLocked: sessionLock.locked,
sessionLockSecure: sessionLock.secure,
lockRetryAttempts: root.lockRetryAttempts,
lockInitiatedLocally: root.lockInitiatedLocally,
pendingLock: root.pendingLock,
loginctlLocked: SessionService.locked,
loginctlActive: SessionService.active
});
}
}
Timer {
id: powerOffFadeTimer
interval: SettingsData.fadeToDpmsGracePeriod * 1000
repeat: false
onTriggered: root.applyMonitorsOff()
}
IdleMonitor {
timeout: 1
respectInhibitors: false
enabled: powerOffFadeTimer.running
onIsIdleChanged: {
if (isIdle)
return;
if (!powerOffFadeTimer.running)
return;
root.cancelPowerOffFade();
}
}
Timer {
id: lockRetryTimer
interval: 1000
repeat: false
onTriggered: root.lockRetryPending = false
}
Timer {
id: resumeRelockTimer
interval: 1000
repeat: false
onTriggered: {
if (!root.shouldLock || sessionLock.locked)
return;
if (IdleService.monitorsOff || root.lockPowerOffArmed || IdleService.lockPowerOffRequested)
return;
root.log.warn("Session lock dead after resume - re-locking");
root.resetLockRetry();
root.lockRetryPending = true;
root.lockRetryPending = false;
}
}
Timer {
id: dpmsReapplyTimer
interval: 100
repeat: false
onTriggered: IdleService.reapplyDpmsIfNeeded()
}
Timer {
id: lockWakeDebounce
interval: 200
repeat: false
onTriggered: {
if (!sessionLock.secure)
return;
if (!powerOffOnLock)
return;
if (!IdleService.monitorsOff) {
lockWakeAllowed = true;
return;
}
if (lockWakeAllowed) {
IdleService.monitorsOff = false;
CompositorService.powerOnMonitors();
} else {
lockWakeAllowed = true;
}
}
}
MouseArea {
anchors.fill: parent
enabled: sessionLock.secure
hoverEnabled: enabled
onPressed: lockWakeDebounce.restart()
onPositionChanged: lockWakeDebounce.restart()
onWheel: lockWakeDebounce.restart()
}
FocusScope {
anchors.fill: parent
focus: sessionLock.secure
Keys.onPressed: event => {
if (!sessionLock.secure)
return;
lockWakeDebounce.restart();
}
}
}