mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-16 16:24:00 -04:00
* powermenu: derive shortcuts and extra actions from one source The power menu matched keys with a switch that repeated the key hints getPowerActionData already returns, plus a second key list for release. Look the pressed key up against the hints of the visible actions instead, so every action with a hint gets its shortcut and the two cannot drift. Side effects: - Ctrl+letter no longer triggers actions. Only Ctrl+P was excluded before, so Ctrl+R still rebooted. Ctrl combinations are navigation. - U now triggers switch user when it is shown. Its hint already said U. - Releasing a letter with no visible action no longer cancels a hold. SessionService now owns the custom:N action ids as customPowerActions and extraPowerActions, which the power menu and the launcher both use. Actions resolve by index into that list instead of slicing the prefix. * powermenu: add actions that reboot into a chosen efi boot entry Settings → Power & Sleep gets a "Reboot to another OS" card that lists the firmware's active EFI entries through efibootmgr. Each saved entry becomes a "Reboot to <name>" action in the power menu and the launcher, bound to 1-9 in the order it was added. Picking one lists the entries again, and stops with a toast if the id no longer carries the saved label. Otherwise it sets BootNext through pkexec and reboots through SessionService, so custom reboot commands still apply. BootEntryService reports why entries are unavailable: not booted in UEFI mode, efibootmgr missing, or efibootmgr failing with its stderr. Saved entries stay listed in settings whatever the status, so a synced config on another machine can still remove them. efibootmgr parsing and the picker's filtering live in BootEntries.js, covered by tests/boot-entries.test.mjs. * powermenu: ignore special keys in action shortcuts
1018 lines
32 KiB
QML
1018 lines
32 KiB
QML
pragma Singleton
|
|
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import Quickshell.I3
|
|
import qs.Common
|
|
import qs.Services
|
|
|
|
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 string sessionType: ""
|
|
property string userName: ""
|
|
property string seat: ""
|
|
property string display: ""
|
|
|
|
signal sessionLocked
|
|
signal sessionUnlocked
|
|
signal sessionResumed
|
|
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
|
|
|
|
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
|
|
|
|
Timer {
|
|
id: sessionInitTimer
|
|
interval: 200
|
|
running: true
|
|
repeat: false
|
|
onTriggered: {
|
|
detectUwsmProcess.running = true;
|
|
detectElogindProcess.running = true;
|
|
detectLoginctlProcess.running = true;
|
|
detectSystemctlProcess.running = true;
|
|
detectHibernateProcess.running = true;
|
|
detectPrimeRunProcess.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");
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: detectUwsmProcess
|
|
running: false
|
|
command: ["sh", "-c", "command -v uwsm > /dev/null 2>&1 && systemctl --user is-active --quiet 'wayland-wm@*.service' 2> /dev/null"]
|
|
|
|
onExited: function (exitCode) {
|
|
hasUwsm = (exitCode === 0);
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: detectElogindProcess
|
|
running: false
|
|
command: ["sh", "-c", "ps -eo comm= | grep -E '^(elogind|elogind-daemon)$'"]
|
|
|
|
onExited: function (exitCode) {
|
|
log.debug("Elogind detection exited with code", exitCode);
|
|
isElogind = (exitCode === 0);
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: detectLoginctlProcess
|
|
running: false
|
|
command: ["sh", "-c", "command -v loginctl"]
|
|
|
|
onExited: function (exitCode) {
|
|
loginctlCommandAvailable = (exitCode === 0);
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: detectSystemctlProcess
|
|
running: false
|
|
command: ["sh", "-c", "command -v systemctl > /dev/null || exit 1; systemctl --user show-environment > /dev/null 2>&1 || exit 2"]
|
|
|
|
onExited: function (exitCode) {
|
|
systemctlCommandAvailable = (exitCode === 0 || exitCode === 2);
|
|
userManagerAvailable = (exitCode === 0);
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: detectHibernateProcess
|
|
running: false
|
|
command: ["grep", "-q", "disk", "/sys/power/state"]
|
|
|
|
onExited: function (exitCode) {
|
|
hibernateSupported = (exitCode === 0);
|
|
}
|
|
}
|
|
|
|
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: detectPrimeRunProcess
|
|
running: false
|
|
command: ["sh", "-c", "command -v prime-run"]
|
|
|
|
onExited: function (exitCode) {
|
|
if (exitCode === 0) {
|
|
nvidiaCommand = "prime-run";
|
|
} else {
|
|
detectNvidiaOffloadProcess.running = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: detectNvidiaOffloadProcess
|
|
running: false
|
|
command: ["sh", "-c", "command -v nvidia-offload"]
|
|
|
|
onExited: function (exitCode) {
|
|
if (exitCode === 0) {
|
|
nvidiaCommand = "nvidia-offload";
|
|
}
|
|
}
|
|
}
|
|
|
|
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 = prefix.split(" ").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 = prefix.split(" ").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;
|
|
}
|
|
|
|
HyprlandService.exit();
|
|
} 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 suspend() {
|
|
if (SettingsData.customPowerActionSuspend.length === 0) {
|
|
Quickshell.execDetached(powerManagerCommand("suspend"));
|
|
} else {
|
|
Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionSuspend]);
|
|
}
|
|
}
|
|
|
|
function hibernate() {
|
|
hibernateProcess.errorOutput = "";
|
|
if (SettingsData.customPowerActionHibernate.length > 0) {
|
|
hibernateProcess.command = ["sh", "-c", SettingsData.customPowerActionHibernate];
|
|
} else {
|
|
hibernateProcess.command = powerManagerCommand("hibernate");
|
|
}
|
|
hibernateProcess.running = true;
|
|
}
|
|
|
|
function suspendThenHibernate() {
|
|
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();
|
|
}
|
|
}
|