Files

1059 lines
34 KiB
QML

pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.I3
import qs.Common
import qs.Services
import "../Common/SleepLock.js" as SleepLock
import "../Common/LaunchUnit.js" as LaunchUnit
Singleton {
id: root
readonly property var log: Log.scoped("SessionService")
// Qt.platform.os is "unix" on the BSDs (qtdeclarative qqmlplatform.cpp)
readonly property bool isBSD: Qt.platform.os === "unix"
property bool hasUwsm: false
property bool isElogind: false
property bool loginctlCommandAvailable: false
property bool systemctlCommandAvailable: false
property bool userManagerAvailable: false
property bool hibernateSupported: false
readonly property bool softRebootSupported: systemctlCommandAvailable
property bool inhibitorAvailable: true
property bool idleInhibited: false
property string inhibitReason: "Keep system awake"
property string nvidiaCommand: ""
property bool loginctlAvailable: false
property string sessionId: ""
property string sessionPath: ""
property bool locked: false
property bool active: false
property bool idleHint: false
property bool lockedHint: false
property bool preparingForSleep: false
property bool sleepRequested: false
property string sessionType: ""
property string userName: ""
property string seat: ""
property string display: ""
signal sessionLocked
signal sessionUnlocked
signal sessionResumed
signal lockEditorRequested
signal greeterEditorRequested
signal lidOpened
signal loginctlStateChanged
property bool stateInitialized: false
property string prepareForSleepSubscriptionId: ""
property bool prepareForSleepSubscriptionPending: false
property string lidSubscriptionId: ""
property bool lidSubscriptionPending: false
property double lastResumeSignalTimestamp: 0
property var pendingSleepFn: null
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
Timer {
id: sessionInitTimer
interval: 200
running: true
repeat: false
onTriggered: {
detectEnvironmentProcess.running = true;
if (!SettingsData.loginctlLockIntegration) {
log.debug("loginctl lock integration disabled by user");
return;
}
if (socketPath && socketPath.length > 0) {
checkDMSCapabilities();
} else {
log.debug("DMS_SOCKET not set");
}
}
}
Timer {
id: lockBeforeSleepFallback
// Custom lockers never set WlSessionLock.secure.
interval: 2000
repeat: false
onTriggered: root.flushPendingSleep()
}
Timer {
id: sleepRequestTimeout
// A refused sleep or a command that skips logind never sends PrepareForSleep.
interval: 10000
repeat: false
onTriggered: root.sleepRequested = false
}
onPreparingForSleepChanged: {
if (!preparingForSleep)
return;
sleepRequestTimeout.stop();
sleepRequested = false;
}
Process {
id: detectEnvironmentProcess
running: false
command: ["sh", "-c", `uwsm=0; command -v uwsm >/dev/null 2>&1 && systemctl --user is-active --quiet 'wayland-wm@*.service' 2>/dev/null && uwsm=1
elogind=0; ps -eo comm= | grep -qE '^(elogind|elogind-daemon)$' && elogind=1
loginctl=0; command -v loginctl >/dev/null 2>&1 && loginctl=1
systemctl=0; usermgr=0
if command -v systemctl >/dev/null 2>&1; then systemctl=1; systemctl --user --no-pager show-environment >/dev/null 2>&1 && usermgr=1; fi
hibernate=0; grep -q disk /sys/power/state 2>/dev/null && hibernate=1
nvidia=; command -v prime-run >/dev/null 2>&1 && nvidia=prime-run
[ -z "$nvidia" ] && command -v nvidia-offload >/dev/null 2>&1 && nvidia=nvidia-offload
printf 'uwsm=%s\nelogind=%s\nloginctl=%s\nsystemctl=%s\nusermgr=%s\nhibernate=%s\nnvidia=%s\n' "$uwsm" "$elogind" "$loginctl" "$systemctl" "$usermgr" "$hibernate" "$nvidia"`]
stdout: SplitParser {
splitMarker: "\n"
onRead: line => {
const eq = line.indexOf("=");
if (eq <= 0)
return;
const value = line.substring(eq + 1).trim();
switch (line.substring(0, eq)) {
case "uwsm":
root.hasUwsm = value === "1";
return;
case "elogind":
root.isElogind = value === "1";
return;
case "loginctl":
root.loginctlCommandAvailable = value === "1";
return;
case "systemctl":
root.systemctlCommandAvailable = value === "1";
return;
case "usermgr":
root.userManagerAvailable = value === "1";
return;
case "hibernate":
root.hibernateSupported = !root.isBSD && value === "1";
return;
case "nvidia":
root.nvidiaCommand = value;
return;
}
}
}
}
Process {
id: hibernateProcess
running: false
property string errorOutput: ""
stderr: SplitParser {
splitMarker: "\n"
onRead: data => hibernateProcess.errorOutput += data.trim()
}
onExited: function (exitCode) {
if (exitCode === 0) {
errorOutput = "";
return;
}
ToastService.showError(I18n.tr("Hibernate failed"), errorOutput);
errorOutput = "";
}
}
Process {
id: uwsmLogout
property bool notRunning: false
command: ["uwsm", "stop"]
running: false
stdout: SplitParser {
splitMarker: "\n"
onRead: data => {
if (data.trim().toLowerCase().includes("not running"))
uwsmLogout.notRunning = true;
}
}
onExited: function (exitCode) {
if (exitCode === 0 && !notRunning) {
return;
}
_logout();
}
}
function escapeShellArg(arg) {
return "'" + arg.replace(/'/g, "'\\''") + "'";
}
function needsShellExecution(prefix) {
if (!prefix || prefix.length === 0)
return false;
return /[;&|<>()$`\\"']/.test(prefix);
}
function parseEnvVars(envVarsStr) {
if (!envVarsStr || envVarsStr.trim().length === 0)
return {};
const envObj = {};
const pairs = envVarsStr.trim().split(/\s+/);
for (const pair of pairs) {
const eqIndex = pair.indexOf("=");
if (eqIndex > 0) {
const key = pair.substring(0, eqIndex);
const value = pair.substring(eqIndex + 1);
envObj[key] = value;
}
}
return envObj;
}
function splitShellArgs(str) {
const args = [];
let current = "";
let hasToken = false;
let quote = "";
let escaped = false;
for (const ch of str) {
if (escaped) {
current += ch;
escaped = false;
continue;
}
switch (quote) {
case "'":
if (ch === "'") {
quote = "";
continue;
}
current += ch;
continue;
case "\"":
switch (ch) {
case "\"":
quote = "";
continue;
case "\\":
escaped = true;
continue;
}
current += ch;
continue;
}
switch (ch) {
case "\\":
escaped = true;
continue;
case "'":
case "\"":
quote = ch;
hasToken = true;
continue;
case " ":
case "\t":
case "\n":
if (!hasToken && current.length === 0)
continue;
args.push(current);
current = "";
hasToken = false;
continue;
}
current += ch;
}
if (current.length > 0 || hasToken)
args.push(current);
return args;
}
// Restore pre-wrap Qt paths (Nix) so launched apps use their own.
function restoreWrapperEnv(env) {
const restore = (target, snapshot) => {
const orig = Quickshell.env(snapshot);
if (orig === null)
return;
env[target] = orig.length > 0 ? orig : null;
};
restore("NIXPKGS_QT6_QML_IMPORT_PATH", "DMS_ORIG_NIXPKGS_QT6_QML_IMPORT_PATH");
restore("QT_PLUGIN_PATH", "DMS_ORIG_QT_PLUGIN_PATH");
return env;
}
function resolveDesktopId(desktopId) {
if (!desktopId)
return null;
const entry = DesktopEntries.heuristicLookup(desktopId);
if (entry || !desktopId.endsWith(".desktop"))
return entry;
return DesktopEntries.heuristicLookup(desktopId.slice(0, -8));
}
// xdg-open's generic opener ignores Terminal=true, so resolve the handler here and launch it ourselves
function openPath(path) {
if (!path)
return;
const fallback = () => Qt.openUrlExternally("file://" + path);
if (!DMSService.isConnected) {
fallback();
return;
}
DMSService.sendRequest("mime.getDefault", {
"path": path
}, response => {
const entry = resolveDesktopId(response?.result?.desktopId);
if (!entry) {
fallback();
return;
}
launchDesktopEntry(entry, false, [path]);
});
}
function launchDesktopEntry(desktopEntry, useNvidia, args) {
if (!desktopEntry || !desktopEntry.command)
return;
CompositorService.closeNiriOverviewOnWindowFocus();
let cmd = args?.length ? [...desktopEntry.command, ...args] : desktopEntry.command;
const appId = desktopEntry.id || desktopEntry.execString || desktopEntry.exec || "";
const override = SessionData.getAppOverride(appId);
const dgpu = useNvidia || (override?.launchOnDgpu && nvidiaCommand);
if (dgpu && nvidiaCommand)
cmd = [nvidiaCommand].concat(cmd);
if (override?.extraFlags) {
cmd = cmd.concat(splitShellArgs(override.extraFlags));
}
const userPrefix = SettingsData.launchPrefix?.trim() || "";
const defaultPrefix = Quickshell.env("DMS_DEFAULT_LAUNCH_PREFIX") || "";
const prefix = userPrefix.length > 0 ? userPrefix : defaultPrefix;
const workDir = desktopEntry.workingDirectory || Quickshell.env("HOME");
const cursorEnv = typeof SettingsData.getCursorEnvironment === "function" ? SettingsData.getCursorEnvironment() : {};
const overrideEnv = override?.envVars ? parseEnvVars(override.envVars) : {};
const finalEnv = restoreWrapperEnv(Object.assign({}, cursorEnv, overrideEnv));
if (desktopEntry.runInTerminal) {
const terminal = SessionData.resolveTerminal() || "xterm";
const escapedCmd = cmd.map(arg => escapeShellArg(arg)).join(" ");
const shellCmd = prefix.length > 0 ? `${prefix} ${escapedCmd}` : escapedCmd;
Quickshell.execDetached({
command: [terminal, "-e", "sh", "-c", shellCmd],
workingDirectory: workDir,
environment: finalEnv
});
return;
}
if (prefix.length > 0 && needsShellExecution(prefix)) {
const escapedCmd = cmd.map(arg => escapeShellArg(arg)).join(" ");
Quickshell.execDetached({
command: ["sh", "-c", `${prefix} ${escapedCmd}`],
workingDirectory: workDir,
environment: finalEnv
});
return;
}
if (prefix.length > 0)
cmd = LaunchUnit.prefixArgs(prefix, desktopEntry.id, LaunchUnit.randomSuffix()).concat(cmd);
Quickshell.execDetached({
command: cmd,
workingDirectory: workDir,
environment: finalEnv
});
}
function launchDesktopAction(desktopEntry, action, useNvidia) {
if (!desktopEntry || !action || !action.command)
return;
CompositorService.closeNiriOverviewOnWindowFocus();
let cmd = action.command;
const appId = desktopEntry.id || desktopEntry.execString || desktopEntry.exec || "";
const override = SessionData.getAppOverride(appId);
const dgpu = useNvidia || (override?.launchOnDgpu && nvidiaCommand);
if (dgpu && nvidiaCommand)
cmd = [nvidiaCommand].concat(cmd);
const userPrefix = SettingsData.launchPrefix?.trim() || "";
const defaultPrefix = Quickshell.env("DMS_DEFAULT_LAUNCH_PREFIX") || "";
const prefix = userPrefix.length > 0 ? userPrefix : defaultPrefix;
const workDir = desktopEntry.workingDirectory || Quickshell.env("HOME");
const cursorEnv = typeof SettingsData.getCursorEnvironment === "function" ? SettingsData.getCursorEnvironment() : {};
const finalEnv = restoreWrapperEnv(Object.assign({}, cursorEnv));
if (prefix.length > 0 && needsShellExecution(prefix)) {
const escapedCmd = cmd.map(arg => escapeShellArg(arg)).join(" ");
Quickshell.execDetached({
command: ["sh", "-c", `${prefix} ${escapedCmd}`],
workingDirectory: workDir,
environment: finalEnv
});
return;
}
if (prefix.length > 0)
cmd = LaunchUnit.prefixArgs(prefix, desktopEntry.id, LaunchUnit.randomSuffix()).concat(cmd);
Quickshell.execDetached({
command: cmd,
workingDirectory: workDir,
environment: finalEnv
});
}
// * Session management
function logout() {
if (hasUwsm) {
if (uwsmLogout.running)
return;
uwsmLogout.notRunning = false;
uwsmLogout.running = true;
return;
}
_logout();
}
function _logout() {
if (SettingsData.customPowerActionLogout.length === 0) {
if (CompositorService.isAqueous) {
AqueousService.quit();
return;
}
if (CompositorService.isNiri) {
NiriService.quit();
return;
}
if (CompositorService.isMango) {
MangoService.quit();
return;
}
if (CompositorService.isLabwc) {
LabwcService.quit();
return;
}
if (CompositorService.isUmbriel) {
UmbrielService.action("session-quit:skip-confirmation");
return;
}
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
try {
I3.dispatch("exit");
} catch (_) {}
return;
}
if (CompositorService.isHyprland) {
HyprlandService.exit();
return;
}
DMSService.sendRequest("loginctl.terminate", null, response => {
if (response.error)
log.warn("Failed to terminate session:", response.error);
});
} else {
Quickshell.execDetached(customActionCommand(SettingsData.customPowerActionLogout));
}
}
// systemd-run escapes the shell's cgroup so session teardown can't kill the command mid-run (#3250)
function customActionCommand(cmd) {
if (!userManagerAvailable)
return ["sh", "-c", cmd];
return ["systemd-run", "--user", "--scope", "--collect", "--quiet", "sh", "-c", cmd];
}
function powerManagerCommand(action) {
if (isBSD)
return bsdPowerCommand(action);
const useLoginctl = isElogind || (loginctlCommandAvailable && !systemctlCommandAvailable);
return [useLoginctl ? "loginctl" : "systemctl", action];
}
function bsdPowerCommand(action) {
switch (action) {
case "suspend":
case "suspend-then-hibernate":
return ["acpiconf", "-s", "3"];
case "hibernate":
return ["acpiconf", "-s", "4"];
case "reboot":
return ["shutdown", "-r", "now"];
default:
return ["shutdown", "-p", "now"];
}
}
function cancelPendingSleep() {
lockBeforeSleepFallback.stop();
pendingSleepFn = null;
}
function flushPendingSleep() {
lockBeforeSleepFallback.stop();
const run = pendingSleepFn;
pendingSleepFn = null;
if (run)
run();
}
function onSessionLockSecured() {
if (pendingSleepFn)
flushPendingSleep();
}
function runSleepAction(run) {
cancelPendingSleep();
sleepRequested = true;
sleepRequestTimeout.restart();
if (!SleepLock.shouldWaitForLock(SettingsData.lockBeforeSuspend, IdleService.isSessionLockSecure)) {
run();
return;
}
// Custom hibernate skips logind PrepareForSleep; dump RAM only after the lock is up.
// Assign before lockRequested: a synchronous secure must see this closure.
// A second call before the first runs drops the first (last wins).
pendingSleepFn = run;
IdleService.lockRequested();
lockBeforeSleepFallback.restart();
}
function suspend() {
runSleepAction(() => {
if (SettingsData.customPowerActionSuspend.length === 0) {
Quickshell.execDetached(powerManagerCommand("suspend"));
} else {
Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionSuspend]);
}
});
}
function hibernate() {
runSleepAction(() => {
hibernateProcess.errorOutput = "";
if (SettingsData.customPowerActionHibernate.length > 0) {
hibernateProcess.command = ["sh", "-c", SettingsData.customPowerActionHibernate];
} else {
hibernateProcess.command = powerManagerCommand("hibernate");
}
hibernateProcess.running = true;
});
}
function suspendThenHibernate() {
runSleepAction(() => {
Quickshell.execDetached(powerManagerCommand("suspend-then-hibernate"));
});
}
function suspendWithBehavior(behavior) {
if (behavior === SettingsData.SuspendBehavior.Hibernate) {
hibernate();
} else if (behavior === SettingsData.SuspendBehavior.SuspendThenHibernate) {
suspendThenHibernate();
} else {
suspend();
}
}
function reboot() {
if (SettingsData.customPowerActionReboot.length === 0) {
Quickshell.execDetached(powerManagerCommand("reboot"));
} else {
Quickshell.execDetached(customActionCommand(SettingsData.customPowerActionReboot));
}
}
function softReboot() {
Quickshell.execDetached(["systemctl", "soft-reboot"]);
}
function poweroff() {
if (SettingsData.customPowerActionPowerOff.length === 0) {
Quickshell.execDetached(powerManagerCommand("poweroff"));
} else {
Quickshell.execDetached(customActionCommand(SettingsData.customPowerActionPowerOff));
}
}
// Actions added in settings, listed after the built-in ones: boot entries, then custom buttons.
// Each list lines up index for index with its setting.
readonly property var bootEntryActions: (SettingsData.powerMenuBootEntries || []).map(entry => "bootnext:" + entry.id)
readonly property var customPowerActions: (SettingsData.customPowerButtons || []).map((button, i) => "custom:" + i)
readonly property var extraPowerActions: bootEntryActions.concat(customPowerActions)
function isPowerActionSupported(action) {
switch (action) {
case "hibernate":
return hibernateSupported;
case "softreboot":
return softRebootSupported;
default:
return true;
}
}
function executePowerAction(action) {
const customIndex = customPowerActions.indexOf(action);
if (customIndex >= 0) {
const button = SettingsData.customPowerButtons[customIndex];
if (!button?.command)
return false;
Quickshell.execDetached(customActionCommand(button.command));
return true;
}
const bootIndex = bootEntryActions.indexOf(action);
if (bootIndex >= 0) {
BootEntryService.rebootTo(SettingsData.powerMenuBootEntries[bootIndex]);
return true;
}
switch (action) {
case "logout":
logout();
return true;
case "suspend":
suspend();
return true;
case "hibernate":
hibernate();
return true;
case "reboot":
reboot();
return true;
case "softreboot":
softReboot();
return true;
case "poweroff":
poweroff();
return true;
case "restart":
Quickshell.execDetached(["dms", "restart"]);
return true;
default:
return false;
}
}
function getPowerActionData(action) {
const customIndex = customPowerActions.indexOf(action);
if (customIndex >= 0) {
const button = SettingsData.customPowerButtons[customIndex];
return {
"icon": button?.icon || "terminal",
"label": button?.label || button?.command || "",
"key": ""
};
}
const bootIndex = bootEntryActions.indexOf(action);
if (bootIndex >= 0) {
// Boot entries take the digit keys in the order they were added
return {
"icon": "restart_alt",
"label": I18n.tr("Reboot to %1", "power menu action, %1 is a boot entry such as Windows Boot Manager").arg(SettingsData.powerMenuBootEntries[bootIndex].label),
"key": bootIndex < 9 ? String(bootIndex + 1) : ""
};
}
switch (action) {
case "reboot":
return {
"icon": "restart_alt",
"label": I18n.tr("Reboot", "verb, power menu action"),
"key": "R"
};
case "softreboot":
return {
"icon": "autorenew",
"label": I18n.tr("Soft reboot"),
"key": "B"
};
case "logout":
return {
"icon": "logout",
"label": I18n.tr("Log out"),
"key": "X"
};
case "poweroff":
return {
"icon": "power_settings_new",
"label": I18n.tr("Power off"),
"key": "P"
};
case "lock":
return {
"icon": "lock",
"label": I18n.tr("Lock", "verb, power menu action that locks the screen"),
"key": "L"
};
case "suspend":
return {
"icon": "bedtime",
"label": I18n.tr("Suspend", "verb, power menu action"),
"key": "S"
};
case "hibernate":
return {
"icon": "ac_unit",
"label": I18n.tr("Hibernate", "verb, power menu action"),
"key": "H"
};
case "restart":
return {
"icon": "refresh",
"label": I18n.tr("Restart DMS"),
"key": "D"
};
case "switchuser":
return {
"icon": "switch_account",
"label": I18n.tr("Switch User"),
"key": "U"
};
default:
return {
"icon": "help",
"label": action,
"key": "?"
};
}
}
// * Idle Inhibitor
signal inhibitorChanged
readonly property int _maxExpireInterval: 86400000
Timer {
id: inhibitExpireTimer
repeat: false
running: false
onTriggered: root._armExpireTimer()
}
// Timers use a monotonic clock that stops across suspend, so the deadline is
// re-checked against the wall clock on resume and after each chunk.
function _armExpireTimer() {
inhibitExpireTimer.stop();
if (!idleInhibited || SessionData.idleInhibitedUntil <= 0)
return;
const remaining = SessionData.idleInhibitedUntil - Date.now();
if (remaining <= 0) {
disableIdleInhibit();
return;
}
inhibitExpireTimer.interval = Math.min(remaining, _maxExpireInterval);
inhibitExpireTimer.start();
}
onSessionResumed: _armExpireTimer()
function _setIdleInhibited(enabled) {
if (idleInhibited === enabled)
return;
idleInhibited = enabled;
inhibitorChanged();
}
// Both entry points are deliberate: the singletons are created lazily in no fixed order, so
// whichever of these runs once the session is loaded does the restore; the other no-ops.
function _restoreIdleInhibit() {
if (!SessionData._hasLoaded)
return;
_setIdleInhibited(SessionData.idleInhibited);
_armExpireTimer();
}
Connections {
target: SessionData
function onLoaded() {
root._restoreIdleInhibit();
}
}
Component.onCompleted: _restoreIdleInhibit()
function enableIdleInhibit(durationMinutes) {
SessionData.setIdleInhibited(true, durationMinutes);
_setIdleInhibited(true);
_armExpireTimer();
}
function disableIdleInhibit() {
SessionData.setIdleInhibited(false);
_setIdleInhibited(false);
_armExpireTimer();
}
function toggleIdleInhibit(durationMinutes) {
if (idleInhibited) {
disableIdleInhibit();
} else {
enableIdleInhibit(durationMinutes);
}
}
function setInhibitReason(reason) {
inhibitReason = reason;
}
Connections {
target: DMSService
function onConnectionStateChanged() {
if (DMSService.isConnected) {
checkDMSCapabilities();
} else {
clearPowerSubscriptionState();
}
}
function onCapabilitiesReceived() {
syncSleepInhibitor();
}
}
Connections {
target: DMSService
enabled: DMSService.isConnected
function onCapabilitiesChanged() {
checkDMSCapabilities();
}
function onDbusSignalReceived(subscriptionId, data) {
if (subscriptionId === prepareForSleepSubscriptionId) {
handlePrepareForSleepSignal(data);
} else if (subscriptionId === lidSubscriptionId) {
handleLidPropertiesChanged(data);
}
}
}
Connections {
target: SettingsData
function onLoginctlLockIntegrationChanged() {
if (SettingsData.loginctlLockIntegration) {
if (socketPath && socketPath.length > 0 && loginctlAvailable) {
if (!stateInitialized) {
stateInitialized = true;
getLoginctlState();
syncLockBeforeSuspend();
}
}
} else {
stateInitialized = false;
}
syncSleepInhibitor();
}
function onLockBeforeSuspendChanged() {
if (SettingsData.loginctlLockIntegration) {
syncLockBeforeSuspend();
}
syncSleepInhibitor();
}
}
Connections {
target: DMSService
enabled: SettingsData.loginctlLockIntegration
function onLoginctlStateUpdate(data) {
updateLoginctlState(data);
}
}
function checkDMSCapabilities() {
if (!DMSService.isConnected) {
return;
}
if (DMSService.capabilities.length === 0) {
return;
}
if (DMSService.capabilities.includes("loginctl")) {
loginctlAvailable = true;
if (SettingsData.loginctlLockIntegration && !stateInitialized) {
stateInitialized = true;
getLoginctlState();
syncLockBeforeSuspend();
}
} else {
loginctlAvailable = false;
log.debug("loginctl capability not available in DMS");
}
if (DMSService.capabilities.includes("dbus")) {
ensurePrepareForSleepSubscription();
ensureLidSubscription();
} else {
clearPowerSubscriptionState();
}
}
function clearPowerSubscriptionState() {
prepareForSleepSubscriptionId = "";
prepareForSleepSubscriptionPending = false;
lidSubscriptionId = "";
lidSubscriptionPending = false;
}
function ensureLidSubscription() {
if (!DMSService.isConnected || !DMSService.capabilities.includes("dbus"))
return;
if (lidSubscriptionId || lidSubscriptionPending)
return;
lidSubscriptionPending = true;
DMSService.dbusSubscribe("system", "org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.DBus.Properties", "PropertiesChanged", response => {
lidSubscriptionPending = false;
if (response.error) {
log.warn("Failed to subscribe to lid changes:", response.error);
return;
}
lidSubscriptionId = response.result?.subscriptionId || "";
});
}
function handleLidPropertiesChanged(data) {
if (data?.path !== "/org/freedesktop/UPower" || data.body?.[0] !== "org.freedesktop.UPower")
return;
if (data.body?.[1]?.LidIsClosed === false)
lidOpened();
}
function ensurePrepareForSleepSubscription() {
if (!DMSService.isConnected || !DMSService.capabilities.includes("dbus")) {
return;
}
if (prepareForSleepSubscriptionId || prepareForSleepSubscriptionPending) {
return;
}
prepareForSleepSubscriptionPending = true;
DMSService.dbusSubscribe("system", "org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", "PrepareForSleep", response => {
prepareForSleepSubscriptionPending = false;
if (response.error) {
log.warn("Failed to subscribe to PrepareForSleep:", response.error);
return;
}
prepareForSleepSubscriptionId = response.result?.subscriptionId || "";
});
}
function emitSessionResumedOnce() {
const now = Date.now();
if ((now - lastResumeSignalTimestamp) < 1000) {
return;
}
lastResumeSignalTimestamp = now;
sessionResumed();
}
function handlePrepareForSleepSignal(data) {
if (!data?.body || data.body.length === 0) {
return;
}
const wasSleeping = preparingForSleep;
preparingForSleep = data.body[0] === true;
if (wasSleeping && !preparingForSleep) {
emitSessionResumedOnce();
}
}
function getLoginctlState() {
if (!loginctlAvailable)
return;
DMSService.sendRequest("loginctl.getState", null, response => {
if (response.result) {
updateLoginctlState(response.result);
}
});
}
function syncLockBeforeSuspend() {
if (!loginctlAvailable)
return;
DMSService.sendRequest("loginctl.setLockBeforeSuspend", {
enabled: SettingsData.lockBeforeSuspend
}, response => {
if (response.error) {
log.warn("Failed to sync lock before suspend:", response.error);
} else {
log.debug("Synced lock before suspend:", SettingsData.lockBeforeSuspend);
}
});
}
function syncSleepInhibitor() {
if (!loginctlAvailable)
return;
if (!DMSService.apiVersion || DMSService.apiVersion < 4)
return;
DMSService.sendRequest("loginctl.setSleepInhibitorEnabled", {
enabled: SettingsData.loginctlLockIntegration && SettingsData.lockBeforeSuspend
}, response => {
if (response.error) {
log.warn("Failed to sync sleep inhibitor:", response.error);
} else {
log.debug("Synced sleep inhibitor:", SettingsData.loginctlLockIntegration);
}
});
}
function updateLoginctlState(state) {
const wasLocked = locked;
const wasSleeping = preparingForSleep;
sessionId = state.sessionId || "";
sessionPath = state.sessionPath || "";
locked = state.locked || false;
active = state.active || false;
idleHint = state.idleHint || false;
lockedHint = state.lockedHint || false;
preparingForSleep = state.preparingForSleep || false;
sessionType = state.sessionType || "";
userName = state.userName || "";
seat = state.seat || "";
display = state.display || "";
if (locked && !wasLocked) {
sessionLocked();
} else if (!locked && wasLocked) {
sessionUnlocked();
}
if (wasSleeping && !preparingForSleep) {
emitSessionResumedOnce();
}
loginctlStateChanged();
}
}