feat(quickshell): power popout replaces moonset
Update PKGBUILD version / update-pkgver (push) Successful in 4s
Update PKGBUILD version / update-pkgver (push) Successful in 4s
Five rows over a model, registered from Launcher.qml under "power" with the borrowed anchor the clipboard popout uses, so shell.qml and every user copy of it stay unchanged. Mod+Escape calls the popout by IPC. Logout goes through `loginctl terminate-session self`, which needs no XDG_SESSION_ID in the shell environment. Glyph codepoints were read out of UbuntuSansNerdFont-Regular.ttf. The reserve Waybar's power button keeps the IPC call and says in its tooltip that it needs the Quickshell bar; there is no way to reach a Quickshell popout from a session where Quickshell was killed.
This commit is contained in:
@@ -92,7 +92,7 @@ quickshell is the **default bar and notification daemon**, spawned by niri as `q
|
||||
|
||||
- **Own popouts:** a composed widget carries its own popout as a `Component` and renders it in the shared morphing host. Pattern (see any indicator, e.g. `AudioIndicator.qml`): an inline `Component { AudioPopout { popouts: root.popouts } }`; the widget declares a `popoutName` and registers on completion (`popouts.register(popoutName, popoutComponent, root, payloadFn)`), then its click handler calls `popouts.toggleByName(popoutName)`. The widget file needs `pragma ComponentBehavior: Bound` so the popout resolves `root` when the shared host instantiates it. The low-level `popouts.open(component, rect, payload)` still exists for non-registered popouts (Tray). The host anchors the popout body to the trigger's on-screen box and picks the side from it: **left-aligned** when the trigger sits in the left half of the screen (so left-cluster popouts grow rightward and stay on-screen), **right-aligned** otherwise (`PopoutHost.qml` `frame.leftAnchored`). The anchor rect carries the trigger's left edge + width; both `openByName` and Tray's direct `open()` pass that box.
|
||||
|
||||
- **Popouts by keybind:** registered popouts are addressable by name through the `PopoutRouter` singleton, which exposes an `IpcHandler { target: "popout" }` with `toggle`/`open`/`close`. A niri keybind fires `qs -c moonarch ipc call popout toggle <name>` (e.g. `Mod+A` → `audio`, in `defaults/xdg/niri/config.kdl`). The router routes to the focused monitor's `PopoutState` via `Niri.activeOutput` (workspace `is_focused` → `output`, matched against `screen.name`); an open popout grabs the keyboard (Escape closes). Names: `audio backlight network bluetooth nightlight cpugov battery notifications media launcher camera calendar clipboard`. Tray is not keybind-addressable.
|
||||
- **Popouts by keybind:** registered popouts are addressable by name through the `PopoutRouter` singleton, which exposes an `IpcHandler { target: "popout" }` with `toggle`/`open`/`close`. A niri keybind fires `qs -c moonarch ipc call popout toggle <name>` (e.g. `Mod+A` → `audio`, in `defaults/xdg/niri/config.kdl`). The router routes to the focused monitor's `PopoutState` via `Niri.activeOutput` (workspace `is_focused` → `output`, matched against `screen.name`); an open popout grabs the keyboard (Escape closes). Names: `audio backlight network bluetooth nightlight cpugov battery notifications media launcher camera calendar clipboard power`. Tray is not keybind-addressable.
|
||||
|
||||
- **Launcher:** `Launcher.qml` is the first widget in the left cluster (an apps glyph) and owns `LauncherPopout.qml` — an application launcher over `DesktopEntries.applications` (search field + keyboard-navigable icon+name list; ↑/↓ select, Enter/click launch via `entry.execute()`, Esc close; app icons via `Quickshell.iconPath`). It registers under `launcher` (so it is IPC-addressable) and is bound to `Mod+Space` (`qs -c moonarch ipc call popout toggle launcher`, in `config.kdl`) in addition to icon-click. Scope is app-launch (drun) only; `walker` stays installed and keeps the other modes (run-command, calc, files), but has no keybind of its own left — what still calls it is dmenu duty for the Waybar-reserve scripts (`moonarch-sink-switcher`, `moonarch-cpugov`, `moonarch-vpn`), which is why the package cannot be dropped.
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Decisions
|
||||
|
||||
## 2026-08-14 – Power menu becomes a Quickshell popout; moonset and moonlock retired
|
||||
- **Who**: Dominik, ClaudeCode
|
||||
- **Why**: With the greeter and the locker gone (entry below), `moonset` was the last self-maintained GTK4 program — a keybind-invoked overlay with five buttons, which the Quickshell config can do with the popout machinery it already has. It was also the only thing still calling `moonlock`, so replacing it is what finally retires that too.
|
||||
- **Tradeoffs**: (1) **Form** — moonset is a fullscreen overlay with wallpaper, blur and an avatar; a popout is a small panel anchored under a bar element. Dominik chose the popout. The fullscreen look, the avatar and the DE/EN string tables are dropped rather than reimplemented; `qsTr()` is in place, so German can come back through Qt translations if it is ever wanted. (2) **Icons** — the codepoints were read out of `UbuntuSansNerdFont-Regular.ttf` (`md-lock` `0xf033e`, `md-logout` `0xf0343`, `md-power_sleep` `0xf0904`, `md-restart` `0xf0709`, `md-power` `0xf0425`) instead of taken from an MDI cheat sheet, because a wrong codepoint renders as a box and nothing warns about it. (3) **Confirmation** — moonset had an inline confirm step; kept, as arming the row on the first click and running on the second, with the label turning red. Lock still acts immediately, as decided in `moonset/DECISIONS.md` (2026-03-27). (4) **Logout** — `loginctl terminate-session self` resolves the caller's own session, which avoids plumbing `XDG_SESSION_ID` into the shell; moonset needed the variable. (5) **Reserve Waybar** — `Alt+W` kills Quickshell and starts Waybar, where a Quickshell IPC call cannot work. The `custom/power` click now fires that call anyway and the tooltip says so: a dead command with an honest label beats a button that silently does nothing, and the reserve bar is a fallback, not a second desktop.
|
||||
- **How**: `defaults/xdg/quickshell/moonarch/PowerPopout.qml` — a `PopoutPanel` with five `ListRow`s over a model of (name, label, icon, confirm, cmd). Registered from `Launcher.qml` under `power`, the same borrowed-anchor trick the clipboard popout uses, so `shell.qml` and every user copy of it stay unchanged. `Mod+Escape` in `defaults/xdg/niri/config.kdl` now fires `qs -c moonarch ipc call popout toggle power` instead of spawning moonset. `qmldir` regenerated.
|
||||
|
||||
## 2026-08-14 – Greeter and lock screen move to upstream packages: regreet + swaylock
|
||||
- **Who**: Dominik, ClaudeCode
|
||||
- **Why**: moongreet (4372 lines of Rust) and moonlock (2386) were self-maintained for a login screen and a lock screen — a cost Dominik no longer wants to carry. moonlock additionally sits on an unfixed unlock SIGSEGV that is a GTK 4.22 regression in gtk4-layer-shell, not our code (`moonlock/DECISIONS.md` 2026-07-08), so the toolkit switch removes a defect we cannot fix ourselves. Both replacements come from `extra`: `greetd-regreet` and `swaylock`. greetd stays.
|
||||
|
||||
@@ -15,7 +15,7 @@ desktop that can be rebuilt from scratch in minutes.
|
||||
| **Compositor** | [Niri](https://github.com/YaLTeR/niri) | Scrollable tiling — no fixed grid, infinite horizontal workspace. Purpose-built Wayland compositor, not an X11 port. |
|
||||
| **Greeter** | [greetd](https://sr.ht/~kennylevinsen/greetd/) + [ReGreet](https://github.com/rharish101/ReGreet) | Minimal, Wayland-native login. ReGreet is a GTK4 greeter from the official repos, running inside its own Niri instance and inheriting the system GTK theme. |
|
||||
| **Lock Screen** | [swaylock](https://github.com/swaywm/swaylock) | ext-session-lock-v1 protocol — compositor guarantees lock on crash. PAM authentication, one Wayland protocol, no toolkit. |
|
||||
| **Power Menu** | [moonset](https://gitea.moonarch.de/nevaforget/moonset) | GTK4 Layer Shell overlay above Waybar. Lock, logout, hibernate, reboot, shutdown with confirmation. |
|
||||
| **Power Menu** | Quickshell popout (`PowerPopout.qml`) | Lock, logout, hibernate, reboot, shutdown; every action except lock needs a second click. Part of the bar, so it needs no own package. |
|
||||
| **Idle Manager** | [stasis](https://aur.archlinux.org/packages/stasis) + [wayland-pipewire-idle-inhibit](https://github.com/rafaelrc7/wayland-pipewire-idle-inhibit) | Separate AC/battery power plans. Brightness dimming, DPMS, lock (via swaylock), and suspend on configurable timeouts. The companion inhibitor holds a Wayland idle-inhibitor while audio plays — keeps the screen awake during windowed browser video, which stasis' pactl detection skips by design. |
|
||||
| **Bar** | [Waybar](https://github.com/Alexays/Waybar) | Wayland-native, highly customizable. Niri workspace/window modules via community plugins. |
|
||||
| **Launcher** | [Walker](https://github.com/abenz1267/walker) + [Elephant](https://github.com/abenz1267/elephant) | Wayland-native GTK4 launcher with built-in providers for apps, bluetooth, audio, files, and calculator. Dmenu mode for custom scripts (VPN, CPU governor). |
|
||||
@@ -32,7 +32,7 @@ desktop that can be rebuilt from scratch in minutes.
|
||||
| **Wallpaper** | [waypaper](https://github.com/anufrievroman/waypaper) + [awww](https://codeberg.org/LGFae/awww) | waypaper provides GUI selection, awww handles animated transitions (60 FPS, 2s crossfade). |
|
||||
| **Night Light** | [wlsunset](https://sr.ht/~kennylevinsen/wlsunset/) | Wayland-native blue light filter via wlr-gamma-control. Toggleable from Waybar brightness group. |
|
||||
| **Firewall** | UFW | Simple deny-incoming/allow-outgoing default. Sane baseline without iptables complexity. |
|
||||
| **AUR Helper** | [paru](https://github.com/Morganamilo/paru) | Rust-based, pacman-compatible. Supports custom PKGBUILD repos for moonset and the moonarch package itself. |
|
||||
| **AUR Helper** | [paru](https://github.com/Morganamilo/paru) | Rust-based, pacman-compatible. Supports the custom package repo that ships the moonarch package itself. |
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -165,7 +165,7 @@ greetd ─► niri (greeter instance) ─► regreet ─► user authenticates
|
||||
| `Super+E` | File manager |
|
||||
| `Super+A` | Audio control (walker) |
|
||||
| `Super+N` | VPN manager |
|
||||
| `Mod+Escape` | Session menu (moonset) |
|
||||
| `Mod+Escape` | Session menu (Quickshell popout) |
|
||||
| `Print` | Screenshot |
|
||||
| `Ctrl+Print` | Screenshot screen |
|
||||
| `Alt+Print` | Screenshot window |
|
||||
@@ -212,21 +212,19 @@ are part of the system and updated via `paru -Syu`.
|
||||
|
||||
## Moonarch Ecosystem
|
||||
|
||||
Moonarch is the deployment and configuration layer. One companion project provides
|
||||
session UI that has no upstream equivalent, built with Rust + gtk4-rs and sharing
|
||||
Catppuccin Mocha theming:
|
||||
Moonarch is the deployment and configuration layer. It no longer carries companion
|
||||
programs of its own — the session UI comes from the official repos or from the
|
||||
Quickshell config in this repository:
|
||||
|
||||
| Project | Role | Installed via |
|
||||
|---------|------|---------------|
|
||||
| [moonset](https://gitea.moonarch.de/nevaforget/moonset) | Power menu overlay (GTK4 Layer Shell) | `paru -S moonset` |
|
||||
| Was | Now |
|
||||
|-----|-----|
|
||||
| moongreet (greetd greeter) | [ReGreet](https://github.com/rharish101/ReGreet), `greetd-regreet` |
|
||||
| moonlock (screen locker) | [swaylock](https://github.com/swaywm/swaylock) |
|
||||
| moonset (power menu) | `PowerPopout.qml` in the Quickshell config |
|
||||
|
||||
Greeter and lock screen used to be own projects too (moongreet, moonlock) and were
|
||||
replaced by ReGreet and swaylock from the official repos. `moonlock` stays installed
|
||||
for now: moonset's Lock action still invokes it by name. It goes away with the moonset
|
||||
release that points that action at swaylock.
|
||||
|
||||
These are built from a [custom paru package repository](https://gitea.moonarch.de/nevaforget/moonarch-pkgbuilds)
|
||||
added during post-install.
|
||||
What remains in the [custom package repository](https://gitea.moonarch.de/nevaforget/moonarch-pkgbuilds)
|
||||
is `moonarch-git` itself plus the cursor theme; the retired projects keep their
|
||||
PKGBUILD directories there, unbuilt, so the way back stays open.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ binds {
|
||||
|
||||
Mod+Return hotkey-overlay-title="Open a Terminal: foot" { spawn "foot"; }
|
||||
Mod+Space hotkey-overlay-title="Run an Application: quickshell launcher" { spawn "qs" "-c" "moonarch" "ipc" "call" "popout" "toggle" "launcher"; }
|
||||
Mod+Escape hotkey-overlay-title="Session Menu: moonset" { spawn "moonset"; }
|
||||
Mod+Escape hotkey-overlay-title="Session Menu: quickshell popout" { spawn "qs" "-c" "moonarch" "ipc" "call" "popout" "toggle" "power"; }
|
||||
|
||||
Mod+A hotkey-overlay-title="Audio: quickshell popout" { spawn "qs" "-c" "moonarch" "ipc" "call" "popout" "toggle" "audio"; }
|
||||
|
||||
|
||||
@@ -9,13 +9,14 @@ Item {
|
||||
property string popoutName: "launcher"
|
||||
|
||||
// Register with the shared popout state so this popout is addressable by keybind (niri IPC).
|
||||
// The clipboard popout rides along: it has no bar widget of its own (it is keybind-only, Super+C),
|
||||
// but register() needs an on-screen item for the anchor rect, so it borrows this one and unfolds
|
||||
// under the launcher glyph. Keeping it here also keeps shell.qml — and therefore every user copy
|
||||
// of shell.qml — unchanged.
|
||||
// The clipboard and power popouts ride along: neither has a bar widget of its own (both are
|
||||
// keybind-only, Super+C and Mod+Escape), but register() needs an on-screen item for the anchor
|
||||
// rect, so they borrow this one and unfold under the launcher glyph. Keeping them here also keeps
|
||||
// shell.qml — and therefore every user copy of shell.qml — unchanged.
|
||||
Component.onCompleted: if (root.popouts) {
|
||||
root.popouts.register(popoutName, popoutComponent, root, null);
|
||||
root.popouts.register("clipboard", clipboardComponent, root, null);
|
||||
root.popouts.register("power", powerComponent, root, null);
|
||||
}
|
||||
|
||||
// This widget's popout — handed to the shared animated host on click.
|
||||
@@ -33,6 +34,13 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: powerComponent
|
||||
PowerPopout {
|
||||
popouts: root.popouts
|
||||
}
|
||||
}
|
||||
|
||||
implicitWidth: Theme.barHeight
|
||||
implicitHeight: Theme.barHeight
|
||||
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
// 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 {
|
||||
// `loginctl terminate-session self` resolves to the caller's own session,
|
||||
// so the logout works without plumbing XDG_SESSION_ID into the shell.
|
||||
// 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": ["loginctl", "terminate-session", "self"]
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ NotifToast NotifToast.qml
|
||||
PopoutHost PopoutHost.qml
|
||||
PopoutPanel PopoutPanel.qml
|
||||
PopoutState PopoutState.qml
|
||||
PowerPopout PowerPopout.qml
|
||||
PrivacyIndicator PrivacyIndicator.qml
|
||||
singleton Backlight Backlight.qml
|
||||
singleton CameraService CameraService.qml
|
||||
|
||||
@@ -170,8 +170,8 @@
|
||||
},
|
||||
"custom/power": {
|
||||
"format": "",
|
||||
"on-click": "moonset",
|
||||
"tooltip-format": "Session Menu"
|
||||
"on-click": "qs -c moonarch ipc call popout toggle power",
|
||||
"tooltip-format": "Session Menu (needs the Quickshell bar running)"
|
||||
},
|
||||
"gamemode": {
|
||||
"format": "{glyph}<small>{count}</small>",
|
||||
|
||||
Reference in New Issue
Block a user