Watch
1
0
Fork
You've already forked souveraine
0
souveraine/surfaces/quickshell/services/Idle.qml
Fimeg cecde3bac2 quickshell: session arbiter + fix IPC returns silently dropping payloads
Fork ii's Session singleton and add a sessionctl.* surface.

The IPC bug is the important half. Quickshell marshals exactly five types
(string/int/bool/double/color) and maps a `var` return to VOID, discarding
the payload with no error -- src/io/ipc.cpp, "void and var get mixed by qml
engine". dock.*, shell.* and apps.* were all declared `: var`, so they
registered as `(): void` and returned nothing at all. The {ok, reason}
contract has never once reached a caller. All of them now return JSON as a
string, which is what actually crosses the socket.

Session: upstream fires `systemctl X || loginctl X` detached and throws the
exit code away. Fine on a desktop with someone at the keyboard, not fine on
a phone where the shell is the session manager and a verb that silently does
nothing leaves you believing the machine is suspending when it isn't. So:
probe loginctl/systemctl/hibernate once instead of assuming, run verbs
through a Process that logs the exit code, and refuse honestly when the
machine can't do the thing (the phone has no swap -- hibernate now says so
instead of no-opping). Every upstream verb keeps its name and call sites.

Inhibits carry a mandatory reason and get a cookie; state() lists who is
holding the machine awake and why. "Why didn't it sleep" is now answerable.
unlock() is refused by design -- the lock is the credential gate, so no IPC
caller routes around the PIN pad.

Named sessionctl, not session: ii's SessionScreen already owns "session",
and quickshell drops duplicate targets silently rather than erroring.

Idle: drop the 2>/dev/null and run hypridle through a Process, so a unit
that fails to come back is a log line instead of a flat battery.

Verified on the laptop: inhibit stops hypridle, uninhibit brings it back.
2026-07-14 18:31:59 -04:00

87 lines
2.9 KiB
QML

pragma Singleton
import qs.modules.common
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
/**
* A nice wrapper for date and time strings.
*/
Singleton {
id: root
property alias inhibit: idleInhibitor.enabled
inhibit: false
// Pixel 3: the Wayland idle-inhibitor on an invisible 0x0 surface is
// not honored by our compositor build, and screen-off is driven by
// hypridle scripts anyway — so Keep System Awake stops/starts hypridle.
onInhibitChanged: {
// Keep System Awake toggles the systemd-managed hypridle unit.
// (Was pkill/setsid, which orphaned hypridle across Hyprland restarts
// and wedged wake — see hyprland.lua hyprland.start.)
//
// Runs through a Process, not execDetached, because this is the
// mechanism that decides whether the phone sleeps: if the unit fails
// to come back we need to know, not discover it via a flat battery.
// reset-failed is expected to be a no-op when the unit is healthy, so
// its failure is not interesting — but the restart's is, so the two
// are separated rather than hidden behind one silencing redirect.
hypridleProc.action = root.inhibit ? "stop" : "restart";
hypridleProc.command = ["sh", "-c", root.inhibit
? "systemctl --user stop hypridle.service"
: "systemctl --user reset-failed hypridle.service || true; systemctl --user restart hypridle.service"];
hypridleProc.running = true;
}
Process {
id: hypridleProc
property string action: ""
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0)
console.log(`[idle] hypridle ${hypridleProc.action} failed (exit ${exitCode}); `
+ `idle handling may be wedged inhibit=${root.inhibit}`);
}
}
Connections {
target: Persistent
function onReadyChanged() {
if (!Persistent.isNewHyprlandInstance) {
root.inhibit = Persistent.states.idle.inhibit;
} else {
Persistent.states.idle.inhibit = root.inhibit;
}
}
}
function toggleInhibit(active = null) {
if (active !== null) {
root.inhibit = active;
} else {
root.inhibit = !root.inhibit;
}
Persistent.states.idle.inhibit = root.inhibit;
}
IdleInhibitor {
id: idleInhibitor
window: PanelWindow {
// Inhibitor requires a "visible" surface
// Actually not lol
implicitWidth: 0
implicitHeight: 0
color: "transparent"
// Just in case...
anchors {
right: true
bottom: true
}
// Make it not interactable
mask: Region {
item: null
}
}
}
}