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.
166 lines
6.5 KiB
QML
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
|
|
}
|
|
}
|
|
}
|