Watch
1
0
Fork
You've already forked souveraine
0
souveraine/surfaces/quickshell/modules/ii/lock/Lock.qml
2026-08-10 20:10:50 -04:00

217 lines
8.8 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.
//
// `session` is the one lifecycle authority across phone, laptop, and
// desktop. The visual power-menu toggles live at `sessionMenu`; a menu is
// a presentation concern, whereas this target is the system contract.
// The ii screen is vendored with that one target rename so Quickshell
// never has to silently choose between duplicate `session` handlers.
// 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: "session"
// 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));
}
}
// Preview-only diagnostic ingress. The physical FPC1020 producer publishes
// its root-owned pulse record for LockContext to watch; it cannot use
// generic XF86WakeUp here because the touch controller emits that key too.
// This target lets us exercise the same visual-only path manually. It
// never unlocks, reveals Personal content, or mints step-up. Task 41 owns
// replacing this with an attested, source-specific producer.
IpcHandler {
target: "fingerprint"
function signal(): string {
return JSON.stringify(root.context.noteProvisionalFingerprintPulse());
}
}
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])
}
}
}
// Which surface, decided WITHOUT waiting for the config file.
//
// `Config.options` is a JsonAdapter, so it answers with its QML defaults
// from the instant it exists — `touchKeypad` reads `false` until `onLoaded`
// flips `ready`. That is survivable at boot, where `initIfReady()` waits for
// `Config.ready` before requesting a lock, and fatal on a reload, where the
// lock is adopted at construction: the surface would bind to the DESKTOP
// keypad on the phone, and `WlSessionLock.surfaceComponent` cannot be
// changed while the lock is active — quickshell qCritical's and keeps the
// old one. The result is a lock screen that will not take your PIN, which
// is the worst of the three failures on this path because you cannot get
// back in.
//
// So the last known-good answer rides through the reload in-process, and
// Config takes over the moment it is genuinely loaded.
//
// `PersistentProperties` is in-process, so it covers a reload and not a
// cold start. sessiond takes the lock before the shell exists, so a shell
// started by greetd adopts a live lock at construction with `ready` still
// false — the fatal path above, reached without any reload. `SOUVERAINE_
// TOUCH_KEYPAD` is read from the environment, which is answerable at that
// instant; unset it reads exactly as before.
PersistentProperties {
id: lockPrefs
reloadableId: "souveraineLockPrefs"
property bool touchKeypad: Quickshell.env("SOUVERAINE_TOUCH_KEYPAD") === "1"
}
Connections {
target: Config
function onReadyChanged() {
if (Config.ready)
lockPrefs.touchKeypad = Config.options.lock.touchKeypad;
}
}
lockSurface: (Config.ready ? Config.options.lock.touchKeypad : lockPrefs.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
}
}
}