diff --git a/README.md b/README.md index c07d967..e90d885 100644 --- a/README.md +++ b/README.md @@ -242,6 +242,15 @@ components/ToggleSwitch.qml On/off switch - Quickshell's `DesktopEntry.execute()` intentionally ignores `Terminal=true` and does not strip `Exec` field codes, so entries would be launched with a literal `%U` argument. `services/AppLauncher.qml` handles both instead. +- `Quickshell.execDetached` detaches a launched app from the panel *process*, but + not from its control group. Run as a systemd user unit — which is how tablet + mode starts the panel — stopping the unit signals every process in the group, + so the panel would take down every app it had ever launched. `AppLauncher` puts + each app in a transient scope of its own with `systemd-run --user --scope`, and + only when `INVOCATION_ID` says the panel is a unit: started from a shell or by + the compositor it shares the session's group, where wrapping buys nothing. A + scope rather than a service, so the app inherits the environment it needs — + `WAYLAND_DISPLAY` above all. - Icons are resolved through the Qt icon theme, falling back to the last segment of a reverse-DNS app id (`org.qutebrowser.qutebrowser` → `qutebrowser`), then to a lettered tile. The packaged build adds Adwaita and hicolor to diff --git a/services/AppLauncher.qml b/services/AppLauncher.qml index 4100204..f40d0c5 100644 --- a/services/AppLauncher.qml +++ b/services/AppLauncher.qml @@ -12,6 +12,26 @@ import qs.config 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 = []; @@ -37,7 +57,7 @@ Singleton { const argv = entry.runInTerminal ? [...Config.terminal, ...command] : command; const options = { - command: argv + command: root.inSystemdUnit ? [...root.scopeWrapper, ...argv] : argv }; if (entry.workingDirectory)