Files
moonarch/defaults/xdg/quickshell/moonarch/PowerPopout.qml
T
nevaforget bcb82b131b fix(quickshell): quit niri on logout instead of terminating the session
loginctl terminate-session kills the login session scope, which holds the
niri-session script, but niri.service runs in the user manager and survives it
-- along with the cleanup line the script would have run next. The compositor
stayed on seat0, greetd returned to the greeter, and every login after that
died immediately. Quitting niri stops niri.service, lets the script finish and
takes graphical-session.target down with it.
2026-08-31 14:04:08 +02:00

123 lines
4.4 KiB
QML

// ABOUTME: Session power popout — Lock, Logout, Hibernate, Reboot, Shutdown.
// ABOUTME: Replaces moonset; every action except Lock needs a second click to confirm.
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell.Io
PopoutPanel {
id: root
property var popouts: null
title: qsTr("Session")
panelWidth: 220
// Row currently asking for confirmation ("" = none). A fresh open never starts armed.
property string pending: ""
readonly property bool shown: root.popouts && root.popouts.hasCurrent
onShownChanged: if (root.shown)
root.pending = ""
// One process for all five actions: the popout closes on activation, so two
// actions cannot be in flight at once. `swaylock -f` daemonises itself, the
// systemctl and loginctl calls are one-shot.
Process {
id: actionProc
}
function run(cmd) {
if (actionProc.running)
return;
actionProc.command = cmd;
actionProc.running = true;
if (root.popouts)
root.popouts.close();
}
function activate(entry) {
// First click on a destructive row arms it, second one runs it.
if (entry.confirm && root.pending !== entry.name) {
root.pending = entry.name;
return;
}
root.run(entry.cmd);
}
Repeater {
// `auto`, not `self`: the shell process does not always count as belonging
// to a session ("Caller does not belong to any known session"), while `auto`
// falls back to the user's graphical session — verified to resolve to the
// wayland session on systemd 261. Avoids plumbing XDG_SESSION_ID in.
// Glyph codepoints read out of UbuntuSansNerdFont-Regular.ttf, not guessed.
model: [
{
"name": "lock",
"label": qsTr("Lock"),
"icon": 0xf033e, // md-lock
"confirm": false,
"cmd": ["swaylock", "-f"]
},
{
"name": "logout",
"label": qsTr("Logout"),
"icon": 0xf0343, // md-logout
"confirm": true,
"cmd": ["niri", "msg", "action", "quit", "--skip-confirmation"]
},
{
"name": "hibernate",
"label": qsTr("Hibernate"),
"icon": 0xf0904, // md-power_sleep
"confirm": true,
"cmd": ["systemctl", "hibernate"]
},
{
"name": "reboot",
"label": qsTr("Reboot"),
"icon": 0xf0709, // md-restart
"confirm": true,
"cmd": ["systemctl", "reboot"]
},
{
"name": "shutdown",
"label": qsTr("Shutdown"),
"icon": 0xf0425, // md-power
"confirm": true,
"cmd": ["systemctl", "poweroff"]
}
]
delegate: ListRow {
id: actionRow
required property var modelData
readonly property bool armed: root.pending === actionRow.modelData.name
width: root.contentWidth
onClicked: root.activate(actionRow.modelData)
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: 8
// Explicit height: children can only align vertically inside a Row
// that has one (see CLAUDE.md, quickshell geometry notes).
height: 20
Text {
width: 16
height: parent.height
text: String.fromCodePoint(actionRow.modelData.icon)
color: actionRow.armed ? Theme.red : Theme.text
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize + 2
verticalAlignment: Text.AlignVCenter
}
Text {
height: parent.height
text: actionRow.armed ? qsTr("%1 — click again").arg(actionRow.modelData.label) : actionRow.modelData.label
color: actionRow.armed ? Theme.red : Theme.text
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
verticalAlignment: Text.AlignVCenter
}
}
}
}
}