fix(quickshell): start launched apps in their own systemd scopes

app.execute() spawns the app as a child of quickshell, and a child
inherits its parent's cgroup, so everything started from the launcher
ended up inside quickshell.service. systemd kills a unit's whole cgroup
on stop, so a crash or restart of the bar took the open apps with it —
Librewolf counted each one as a crash and eventually stopped restoring
the session at all.

app2unit wraps systemd-run and gives each app its own transient
app-*.scope, the same shape niri gives the windows it spawns itself.
Quickshell 0.3.0 exports no desktop-entry id, only the command, so the
prefix takes that; Terminal=true entries need the -term variant, and a
Path= from the entry travels as a unit property because app2unit
otherwise starts the app in the bar's directory.
This commit is contained in:
nevaforget committed 2026-09-09 15:53:03 +02:00
1 parent 435364d2e8
commit 924d5c07f4
2 files changed
+12 -2

No files matched your search

@@ -37,7 +37,15 @@ Item {
function launch(app) { function launch(app) {
if (!app) if (!app)
return; return;
app.execute(); // Started through app2unit, not app.execute(): execute() spawns the app as a child of quickshell,
// and a child inherits its parent's cgroup, so the app lands inside quickshell.service and
// systemd kills it along with the unit on every restart of the bar. app2unit wraps systemd-run
// and hands each app its own transient app-*.scope — the same shape niri gives the windows it
// spawns itself. Terminal entries need the -term variant, and a Path= from the entry has to
// travel as a unit property because app2unit otherwise starts the app in the bar's directory.
const unit = app.runInTerminal ? "app2unit-term" : "app2unit";
const props = app.workingDirectory ? ["-p", "WorkingDirectory=" + app.workingDirectory] : [];
Quickshell.execDetached([unit].concat(props, ["--"], app.command));
if (root.popouts) if (root.popouts)
root.popouts.close(); root.popouts.close();
} }
@@ -408,7 +408,9 @@ PopoutPanel {
id: managerRow id: managerRow
width: root.contentWidth width: root.contentWidth
onClicked: { onClicked: {
Quickshell.execDetached(["nm-connection-editor"]); // Through app2unit for the same reason as the launcher: a child of quickshell shares its
// cgroup, so a restart of the bar would close the editor mid-edit. See LauncherPopout.
Quickshell.execDetached(["app2unit", "--", "nm-connection-editor"]);
if (root.popouts) if (root.popouts)
root.popouts.close(); root.popouts.close();
} }