From bf12665adb83bb52ebb30d44dc6b391c2deac191 Mon Sep 17 00:00:00 2001 From: bbedward Date: Fri, 17 Jul 2026 15:24:35 -0400 Subject: [PATCH] lock: add lockAndOutputsOff IPC function --- quickshell/Common/KeybindActions.js | 1 + quickshell/Modules/Lock/Lock.qml | 149 ++++++++++++++++++++++++---- quickshell/Services/IdleService.qml | 3 +- 3 files changed, 134 insertions(+), 19 deletions(-) diff --git a/quickshell/Common/KeybindActions.js b/quickshell/Common/KeybindActions.js index ab08b72dc..0f9136114 100644 --- a/quickshell/Common/KeybindActions.js +++ b/quickshell/Common/KeybindActions.js @@ -63,6 +63,7 @@ const DMS_ACTIONS = [ { id: "spawn dms ipc call keybinds open niri", label: "Keybinds Cheatsheet: Open", compositor: "niri" }, { id: "spawn dms ipc call keybinds close", label: "Keybinds Cheatsheet: Close" }, { id: "spawn dms ipc call lock lock", label: "Lock Screen" }, + { id: "spawn dms ipc call lock lockAndOutputsOff", label: "Lock Screen & Outputs Off" }, { id: "spawn dms ipc call lock demo", label: "Lock Screen: Demo" }, { id: "spawn dms ipc call inhibit toggle", label: "Idle Inhibit: Toggle" }, { id: "spawn dms ipc call inhibit enable", label: "Idle Inhibit: Enable" }, diff --git a/quickshell/Modules/Lock/Lock.qml b/quickshell/Modules/Lock/Lock.qml index de28b81b6..182c9de55 100644 --- a/quickshell/Modules/Lock/Lock.qml +++ b/quickshell/Modules/Lock/Lock.qml @@ -13,6 +13,12 @@ Scope { property string sharedPasswordBuffer: "" property bool shouldLock: false + onSharedPasswordBufferChanged: { + if (!powerOffFadeTimer.running) + return; + cancelPowerOffFade(); + } + onShouldLockChanged: { IdleService.isShellLocked = shouldLock; if (shouldLock && lockPowerOffArmed) { @@ -27,12 +33,8 @@ Scope { onTriggered: { if (sessionLock.locked && lockPowerOffArmed) { pendingLock = false; - IdleService.monitorsOff = true; - CompositorService.powerOffMonitors(); - lockWakeAllowed = false; - lockWakeDebounce.restart(); lockPowerOffArmed = false; - dpmsReapplyTimer.start(); + beginPowerOff(); } } } @@ -42,6 +44,38 @@ Scope { 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; + } Component.onCompleted: { IdleService.lockComponent = this; @@ -65,6 +99,7 @@ Scope { } 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 @@ -90,7 +125,7 @@ Scope { return; lockInitiatedLocally = true; - lockPowerOffArmed = SettingsData.lockScreenPowerOffMonitorsOnLock; + lockPowerOffArmed = powerOffOnLock; if (!SessionService.active && SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration) { pendingLock = true; @@ -102,6 +137,16 @@ Scope { notifyLoginctl(true); } + function lockAndOutputsOff() { + IdleService.lockPowerOffRequested = true; + if (sessionLock.locked) { + beginPowerOff(); + return; + } + lockPowerOffArmed = true; + lock(); + } + function unlock() { if (!shouldLock) return; @@ -115,6 +160,8 @@ Scope { pendingLock = false; shouldLock = false; customLockerSpawned = false; + resetPowerOffFade(); + IdleService.lockPowerOffRequested = false; } function activate() { @@ -135,7 +182,7 @@ Scope { return; } lockInitiatedLocally = false; - lockPowerOffArmed = SettingsData.lockScreenPowerOffMonitorsOnLock; + lockPowerOffArmed = powerOffOnLock; shouldLock = true; } @@ -155,7 +202,7 @@ Scope { if (SessionService.active && pendingLock) { pendingLock = false; lockInitiatedLocally = true; - lockPowerOffArmed = SettingsData.lockScreenPowerOffMonitorsOnLock; + lockPowerOffArmed = powerOffOnLock; shouldLock = true; return; } @@ -163,7 +210,7 @@ Scope { if (handleLoginctlCustomLock()) return; lockInitiatedLocally = false; - lockPowerOffArmed = SettingsData.lockScreenPowerOffMonitorsOnLock; + lockPowerOffArmed = powerOffOnLock; shouldLock = true; } } @@ -175,6 +222,11 @@ Scope { function onLockRequested() { lock(); } + + function onMonitorsOffChanged() { + if (!IdleService.monitorsOff) + root.resetPowerOffFade(); + } } Pam { @@ -214,6 +266,46 @@ Scope { 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(); + } + } + } } } @@ -224,22 +316,19 @@ Scope { notifyLockedHint(sessionLock.locked); if (sessionLock.locked) { pendingLock = false; - if (lockPowerOffArmed && SettingsData.lockScreenPowerOffMonitorsOnLock) { - IdleService.monitorsOff = true; - CompositorService.powerOffMonitors(); - lockWakeAllowed = false; - lockWakeDebounce.restart(); - } + if (lockPowerOffArmed && powerOffOnLock) + beginPowerOff(); lockPowerOffArmed = false; - dpmsReapplyTimer.start(); return; } lockWakeAllowed = false; - if (IdleService.monitorsOff && SettingsData.lockScreenPowerOffMonitorsOnLock) { + resetPowerOffFade(); + if (IdleService.monitorsOff && powerOffOnLock) { IdleService.monitorsOff = false; CompositorService.powerOnMonitors(); } + IdleService.lockPowerOffRequested = false; } } @@ -254,6 +343,10 @@ Scope { root.lock(); } + function lockAndOutputsOff() { + root.lockAndOutputsOff(); + } + function unlock() { root.unlock(); } @@ -282,6 +375,26 @@ Scope { } } + 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: dpmsReapplyTimer interval: 100 @@ -296,7 +409,7 @@ Scope { onTriggered: { if (!sessionLock.locked) return; - if (!SettingsData.lockScreenPowerOffMonitorsOnLock) + if (!powerOffOnLock) return; if (!IdleService.monitorsOff) { lockWakeAllowed = true; diff --git a/quickshell/Services/IdleService.qml b/quickshell/Services/IdleService.qml index 38516dc04..8e61f12ab 100644 --- a/quickshell/Services/IdleService.qml +++ b/quickshell/Services/IdleService.qml @@ -67,6 +67,7 @@ Singleton { property var lockComponent: null property bool monitorsOff: false property bool isShellLocked: false + property bool lockPowerOffRequested: false function reapplyDpmsIfNeeded() { if (monitorsOff) @@ -146,7 +147,7 @@ Singleton { id: lockWakeMonitor timeout: 1 respectInhibitors: false - enabled: root.enabled && root.isShellLocked && root.monitorsOff && SettingsData.lockScreenPowerOffMonitorsOnLock + enabled: root.enabled && root.isShellLocked && root.monitorsOff && (SettingsData.lockScreenPowerOffMonitorsOnLock || root.lockPowerOffRequested) onIsIdleChanged: { if (!isIdle && root.monitorsOff) root.requestMonitorOn();