69 lines
2.9 KiB
QML
69 lines
2.9 KiB
QML
pragma Singleton
|
|
|
|
import Quickshell
|
|
// Required: execDetached's context overload takes a processContext value type
|
|
// from this module, and without the import the call fails to resolve.
|
|
import Quickshell.Io
|
|
import qs.config
|
|
|
|
// Quickshell's DesktopEntry.execute() deliberately ignores both Terminal=true
|
|
// and Exec field codes, so entries are launched here instead. Without this,
|
|
// entries like `firefox %U` are started with a literal "%U" argument.
|
|
Singleton {
|
|
id: root
|
|
|
|
// Set only when att_menu itself was started by systemd, which is the one
|
|
// case where the control group is worth escaping. Started from a shell or
|
|
// by the compositor the panel shares the session's group, and wrapping
|
|
// would buy nothing.
|
|
readonly property bool inSystemdUnit: !!Quickshell.env("INVOCATION_ID")
|
|
|
|
// Moves a launched app into a transient scope of its own, outside att_menu's
|
|
// control group. Stopping a systemd unit signals every process in its group
|
|
// and execDetached's fork does not leave it, so without this the panel takes
|
|
// down everything it ever launched — which is what happens each time tablet
|
|
// mode ends and the unit is stopped.
|
|
//
|
|
// A scope, not a service: the app keeps the environment, working directory
|
|
// and stdio it was given, so WAYLAND_DISPLAY and friends still reach it.
|
|
// The exec'd shell leaves no process of its own behind, and passing the app
|
|
// as "$@" keeps its arguments intact. systemd-run is only assumed to be on
|
|
// PATH when it is actually there — the panel runs on systems packaged
|
|
// without it too.
|
|
readonly property var scopeWrapper: ["sh", "-c", "command -v systemd-run >/dev/null 2>&1 && exec systemd-run --user --scope --collect --quiet -- \"$@\"; exec \"$@\"", "att_menu"]
|
|
|
|
function withoutFieldCodes(command: var): var {
|
|
const out = [];
|
|
|
|
for (const arg of command) {
|
|
// "%%" is an escaped percent and collapses to one. Every other
|
|
// %<char> is a field code the spec says to substitute with files,
|
|
// URLs or icons — launching from a menu has nothing to substitute,
|
|
// so they are removed.
|
|
const cleaned = arg.replace(/%(.)/g, (match, char) => char === "%" ? "%" : "");
|
|
|
|
// An argument that was nothing but a field code is dropped.
|
|
if (cleaned !== "")
|
|
out.push(cleaned);
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
function launch(entry: var): void {
|
|
const command = root.withoutFieldCodes(entry.command);
|
|
if (command.length === 0)
|
|
return;
|
|
|
|
const argv = entry.runInTerminal ? [...Config.terminal, ...command] : command;
|
|
const options = {
|
|
command: root.inSystemdUnit ? [...root.scopeWrapper, ...argv] : argv
|
|
};
|
|
|
|
if (entry.workingDirectory)
|
|
options.workingDirectory = entry.workingDirectory;
|
|
|
|
Quickshell.execDetached(options);
|
|
}
|
|
}
|