From 25e33522a74d9ffb71e399d9060d9fdef42e792d Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Fri, 18 Sep 2026 12:26:48 +0200 Subject: feat(status): add a screen lock mode dnd and presentation were the only modes, and presentation was the sole holder of the Wayland idle inhibitor. That inhibitor is the only thing that stops hypridle's loginctl lock-session at the 600s timeout, so screen locking is on by default and there was no way to turn it off on its own. nolock is stored as "auto-lock disabled" so the file keeps the registry's "missing means off" rule, and the row inverts it so the switch reads as the default: lock on. Presentation now drives nolock the same way it drives dnd, holding the prior value in memory and restoring it on exit, so it needs no second inhibitor and the inhibitor has a single holder. The page sets the disable permanently or for a number of minutes. The deadline and its timer live in the singleton rather than the row because the drawer page is destroyed on close while the effect has to outlive it. The timer is shell-lifetime, the same class of limit as dndBeforePresentation: a restart during a timed disable leaves the lock off until it is toggled, which is safe because the user opted into the disable. The mode governs idle-triggered locking only. The SUPER+l bind and the lock on suspend are separate paths and stay live. --- shared/Status.qml | 58 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 8 deletions(-) (limited to 'shared') diff --git a/shared/Status.qml b/shared/Status.qml index 8f80b9a..be090b3 100644 --- a/shared/Status.qml +++ b/shared/Status.qml @@ -32,34 +32,71 @@ Singleton { readonly property bool dnd: dndFile.value readonly property bool presentation: presFile.value + // Stored as "auto-lock disabled", so the missing-file default of off is + // the wanted default of locking on. The row inverts it for the label. + readonly property bool nolock: nolockFile.value readonly property int activeCount: (root.dnd ? 1 : 0) + (root.presentation ? 1 : 0) + + (root.nolock ? 1 : 0) // Set once by shell.qml. IdleInhibitor does nothing with a null window, // and the singleton has no window of its own to offer. property var inhibitWindow: null - // What dnd was before presentation mode turned it on, so turning - // presentation mode off restores it rather than clearing it. Held here - // rather than in a file: it is meaningful only while presentation mode is - // on, and presentation mode does not survive a reboot. + // What dnd and nolock were before presentation mode drove them, so turning + // presentation mode off restores them rather than clearing them. Held here + // rather than in a file: they are meaningful only while presentation mode + // is on, and presentation mode does not survive a reboot. // // Known limit: on a shell restart while presentation is already on, - // onPresentationChanged can run before the dnd FileView has loaded, so the - // recorded prior value depends on which file loads first. + // onPresentationChanged can run before these FileViews have loaded, so the + // recorded prior values depend on which file loads first. property bool dndBeforePresentation: false + property bool nolockBeforePresentation: false + + // Deadline for a timed disable, owned by the singleton rather than the row + // because the drawer page (and so the row) is destroyed on close while the + // effect has to outlive it. Shell-lifetime only: a restart while a timed + // disable is pending leaves the lock off until it is toggled, which is + // safe because the user opted into the disable. + property double nolockUntil: 0 function setMode(name, on) { if (name === "dnd") { dndFile.write(on); } else if (name === "presentation") { presFile.write(on); + } else if (name === "nolock") { + // Any write clears a pending deadline; the timed path sets it back. + root.nolockUntil = 0; + nolockFile.write(on); } } + // Disable auto-lock, optionally for a number of minutes. Zero or a + // non-number means until toggled back. + function disableLockFor(minutes) { + const m = Number(minutes); + root.setMode("nolock", true); + if (m > 0) root.nolockUntil = Date.now() + m * 60000; + } + function toggleMode(name) { if (name === "dnd") root.setMode("dnd", !root.dnd); else if (name === "presentation") root.setMode("presentation", !root.presentation); + else if (name === "nolock") root.setMode("nolock", !root.nolock); + } + + // The timed disable re-enables itself. Runs only while a deadline is + // pending, so no tick burns while the mode is off or permanently on. + Timer { + id: nolockTimer + interval: 1000 + repeat: true + running: root.nolockUntil > 0 + onTriggered: { + if (Date.now() >= root.nolockUntil) root.setMode("nolock", false); + } } // Effects follow the mode rather than the setter, so a mode set by @@ -67,10 +104,13 @@ Singleton { onPresentationChanged: { if (root.presentation) { root.dndBeforePresentation = root.dnd; + root.nolockBeforePresentation = root.nolock; root.setMode("dnd", true); + root.setMode("nolock", true); root.runBreaktimer("pause"); } else { root.setMode("dnd", root.dndBeforePresentation); + root.setMode("nolock", root.nolockBeforePresentation); root.runBreaktimer("resume"); } } @@ -94,10 +134,11 @@ Singleton { // Wayland idle inhibit. The compositor advertises // zwp_idle_inhibit_manager_v1 and hypridle honours it, so no D-Bus path - // is needed even though elogind runs here. + // is needed even though elogind runs here. nolock is the sole holder; + // presentation drives nolock, so it needs no second inhibitor. IdleInhibitor { window: root.inhibitWindow - enabled: root.presentation && root.inhibitWindow !== null + enabled: root.nolock && root.inhibitWindow !== null } component ModeFile: FileView { @@ -136,4 +177,5 @@ Singleton { ModeFile { id: dndFile; path: root.dir + "/status.dnd" } ModeFile { id: presFile; path: root.dir + "/status.presentation" } + ModeFile { id: nolockFile; path: root.dir + "/status.nolock" } } -- cgit v1.2.3