Watch
1
0
Fork
You've already forked souveraine
0
souveraine/surfaces/quickshell/services/SessiondBridge.qml
Fimeg 19fe6b1d0e shell: dock reorder, fullscreen detection fix, idle-power, sessiond, misc shell work
- Dock drag-to-reorder for pinned apps (insertion gap, quick-slide vs dwell)
- Fullscreen detection: scan all windows via HyprlandData.windowList
- IdleCoordinator, GlobalStates, Session.qml updates
- Deploy script, qmldir, settings, wallpaper, visualizer fixes
- sessiond server, memory module updates

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 22:18:20 -04:00

172 lines
6.2 KiB
QML

// Shell side of the souveraine-sessiond handoff protocol.
//
// sessiond takes ext-session-lock before the shell exists; this bridge is
// how the shell (a) announces itself and takes the lock over, (b) keeps the
// heartbeat connection open so sessiond can retake the lock the moment the
// shell dies, and (c) confirms the compositor-acked lock (locked_ack).
//
// The session is never unlocked during the handoff: sessiond abandons its
// lock (connection drop) and misc:allow_session_lock_restore lets our
// WlSessionLock inherit the locked session. See
// souveraine/src/sessiond/protocol.rs — that file is the contract.
//
// No sessiond on the socket (laptop, or bring-up) = everything no-ops and
// the legacy launchOnStartup path decides alone.
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs
Singleton {
id: root
// Only shell.qml calls claimAuthority(). Importing this singleton from a
// utility window must never create a session-authority connection.
property bool authorityScope: false
// Registered = shell_ready was answered ok on the CURRENT connection.
property bool registered: false
// sessiond held the session lock when we registered; we owe it a lock
// and a locked_ack.
property bool oweLock: false
property bool ackSent: false
property var pendingReady: null // callback awaiting the shell_ready response
function claimAuthority() {
root.authorityScope = true
}
function load() {
if (!root.authorityScope)
console.log("[sessiond-bridge] inactive outside authority scope")
}
// Announce the shell. cb(mustLock) fires exactly once: mustLock true
// means sessiond was holding and the session IS locked — the shell must
// raise its own lock surface immediately.
function shellReady(cb) {
if (!root.authorityScope) {
console.log("[sessiond-bridge] shell_ready refused outside authority scope")
cb(false)
return
}
if (!sock.connected) {
cb(false);
return;
}
if (root.pendingReady) {
// Only one handshake in flight; late duplicate answers false.
cb(false);
return;
}
root.pendingReady = cb;
readyTimeout.restart();
sock.write(JSON.stringify({ op: "shell_ready" }) + "\n");
sock.flush();
}
function sendLockedAck() {
if (!sock.connected || !root.registered || root.ackSent) return;
root.ackSent = true;
sock.write(JSON.stringify({ op: "locked_ack" }) + "\n");
sock.flush();
console.log("[sessiond-bridge] locked_ack sent");
}
Timer {
id: readyTimeout
interval: 3000
repeat: false
onTriggered: {
if (root.pendingReady) {
console.log("[sessiond-bridge] shell_ready timed out — proceeding without sessiond");
const cb = root.pendingReady;
root.pendingReady = null;
cb(false);
}
}
}
// Reconnect: sessiond may restart (upgrade) or start late. While
// connected this timer is idle; the Socket does not retry by itself.
Timer {
id: reconnect
interval: 5000
repeat: true
running: root.authorityScope && !sock.connected
onTriggered: sock.connected = true
}
Socket {
id: sock
path: Quickshell.env("XDG_RUNTIME_DIR") + "/souveraine/sessiond.sock"
connected: root.authorityScope
// `connectionStateChanged` is the real Signal on Quickshell.Io.Socket;
// the `onSocketConnected`/`onSocketDisconnected` handler-slots aren't
// reliably attachable across quickshell builds, so branch on
// `connected` here. Re-register on reconnect if we were registered
// before (sessiond restarted underneath us).
onConnectionStateChanged: {
if (sock.connected) {
console.log("[sessiond-bridge] connected");
const wasRegistered = root.registered;
root.registered = false;
root.ackSent = false;
if (wasRegistered) {
root.shellReady(function(mustLock) {
if (mustLock) {
// sessiond locked while we were disconnected (it
// treats our EOF as shell death). Mirror it.
GlobalStates.screenLocked = true;
}
});
}
} else {
console.log("[sessiond-bridge] disconnected");
root.registered = false;
root.pendingReady = null;
}
}
parser: SplitParser {
splitMarker: "\n"
onRead: message => {
let reply;
try {
reply = JSON.parse(message);
} catch (e) {
console.log("[sessiond-bridge] unparseable reply: " + message);
return;
}
if (root.pendingReady) {
// The only request we await a response for.
const cb = root.pendingReady;
root.pendingReady = null;
readyTimeout.stop();
if (reply.ok) {
root.registered = true;
root.oweLock = reply.must_lock === true;
console.log("[sessiond-bridge] registered, must_lock=" + root.oweLock);
cb(root.oweLock);
} else {
console.log("[sessiond-bridge] shell_ready refused: " + reply.reason);
cb(false);
}
}
}
}
}
// The compositor acknowledged OUR lock surface — tell sessiond the
// handoff is complete. Gated on secure, not the request, per doctrine.
Connections {
target: GlobalStates
function onScreenLockSecureChanged() {
if (GlobalStates.screenLockSecure && root.oweLock && !root.ackSent) {
root.sendLockedAck();
}
}
}
}