Watch
1
0
Fork
You've already forked souveraine
0
souveraine/surfaces/quickshell/modules/ii/lock/Lock.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

166 lines
6.5 KiB
QML

// Souveraine patch to ii's stock Lock.qml.
//
// Selects the lock surface by config: lock.touchKeypad picks the
// TouchLockSurface (PIN pad for the phone's touch panel) instead of the
// stock keyboard-driven LockSurface. Everything else is unchanged stock ii.
pragma ComponentBehavior: Bound
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.panels.lock
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
LockScreen {
id: root
// Monitor name -> workspace id to restore on unlock (set when locking)
property var savedWorkspaces: ({})
// Session arbiter surface. Lives here beside the lock because the lock IS
// the session gate on this device — LockScreen already owns WlSessionLock,
// and the session verbs (suspend/poweroff/inhibit) are the other half of
// the same lifecycle. The logic lives in the Session singleton
// (modules/common/functions/Session.qml); this is only the IPC face.
//
// Every mutating method returns {ok, reason?} rather than throwing, so a
// refusal is information the agent learns from — same contract as dock.*,
// shell.* and apps.*. `lock` (target: "lock") stays as it was: it is the
// surface's own activate/focus pair, not the session lifecycle.
//
// Named "sessionctl", not "session": ii's SessionScreen already owns the
// "session" target (its toggle/open/close for the power menu). Quickshell
// does not merge duplicate IPC targets — the second one is silently
// dropped, with no error — so a collision here would have quietly produced
// a surface that simply is not there. The menu is "session"; the session
// lifecycle is "sessionctl".
// Every method returns `string`, not `var`, and the payload is JSON.
// This is not a style choice — quickshell marshals exactly five types over
// IPC (string, int, bool, double, color; see src/io/ipc.cpp ipcType()) and
// maps a `var` return to VOID, discarding the value with no error. A
// method declared `: var` therefore looks correct in QML, registers as
// `(): void`, and silently returns nothing to the caller. JSON-over-string
// is the only way a structured {ok, reason} result actually crosses.
IpcHandler {
target: "sessionctl"
// Read-only projection: lock state (read through from the compositor,
// never a cached bool), idle inhibitors with their reasons, and what
// this machine can actually do.
function state(): string {
return JSON.stringify(Session.state());
}
function capabilities(): string {
return JSON.stringify(Session.caps());
}
function lock(): string {
return JSON.stringify(Session.lock());
}
// Refuses by design — unlocking is the credential gate.
function unlock(): string {
return JSON.stringify(Session.unlock());
}
function suspend(): string {
return JSON.stringify(Session.suspend());
}
function hibernate(): string {
return JSON.stringify(Session.hibernate());
}
function poweroff(): string {
return JSON.stringify(Session.poweroff());
}
function reboot(): string {
return JSON.stringify(Session.reboot());
}
function logout(): string {
return JSON.stringify(Session.logout());
}
// Reason is mandatory: an inhibitor nobody can explain is exactly the
// thing that leaves the phone awake in a pocket at 3am.
function inhibit(what: string, reason: string): string {
return JSON.stringify(Session.inhibit(what, reason));
}
function uninhibit(cookie: string): string {
return JSON.stringify(Session.uninhibit(cookie));
}
}
Timer {
id: restoreTimer
interval: 150
repeat: false
onTriggered: {
var batch = ""
for (var j = 0; j < Quickshell.screens.length; ++j) {
var monName = Quickshell.screens[j].name
var wsId = root.savedWorkspaces[monName]
if (wsId !== undefined) {
batch += `hyprctl dispatch 'hl.dsp.focus({monitor="${monName}"})'; hyprctl dispatch 'hl.dsp.focus({workspace=${wsId}})';`
}
}
if (batch.length > 0) {
Quickshell.execDetached(["bash", "-c", batch])
}
}
}
lockSurface: Config.options.lock.touchKeypad ? touchSurfaceComponent : desktopSurfaceComponent
property Component desktopSurfaceComponent: LockSurface {
context: root.context
}
property Component touchSurfaceComponent: TouchLockSurface {
context: root.context
}
// Single batch for lock and unlock so we don't race multiple hyprctl calls
Connections {
target: GlobalStates
function onScreenLockedChanged() {
if (GlobalStates.screenLocked) {
// Lock: save workspace per monitor and move all to temp workspace in one batch
var next = {}
var batch = "keyword animation workspaces,1,7,menu_decel,slidevert; "
for (var i = 0; i < Quickshell.screens.length; ++i) {
var mon = Quickshell.screens[i].name
var mData = HyprlandData.monitors.find(m => m.name === mon)
if (mData?.activeWorkspace == undefined) {
return;
}
var ws = (mData?.activeWorkspace?.id ?? 1)
next[mon] = ws
batch += `hyprctl dispatch 'hl.dsp.focus({monitor="${mon}"})'; hyprctl dispatch 'hl.dsp.focus({workspace=${2147483647 - ws}})';`
}
root.savedWorkspaces = next
Quickshell.execDetached(["bash", "-c", batch])
} else {
restoreTimer.start()
}
}
}
// Push everything down (visual only; workspace switch is in Connections above)
Variants {
model: Quickshell.screens
delegate: Scope {
required property ShellScreen modelData
property bool shouldPush: GlobalStates.screenLocked
property string targetMonitorName: modelData.name
property int verticalMovementDistance: modelData.height
property int horizontalSqueeze: modelData.width * 0.2
}
}
}