From 60eae607ab75988fc441bb4d87ef790175009572 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 14 Sep 2026 12:44:09 +0200 Subject: fix(desktop): honest alwaysActive docs and working Escape in confirm --- desktop/Drawer.qml | 20 ++++++++++---------- desktop/Module.qml | 7 +++---- desktop/README.md | 30 +++++++++++++++++------------- desktop/modules/mail/README.md | 6 +++--- desktop/modules/mail/waybar-mail.sh | 2 +- desktop/modules/vm/README.md | 23 +++++++++++++---------- desktop/modules/vm/VmPage.qml | 4 ++++ 7 files changed, 51 insertions(+), 41 deletions(-) diff --git a/desktop/Drawer.qml b/desktop/Drawer.qml index 368f249..f3b8e6d 100644 --- a/desktop/Drawer.qml +++ b/desktop/Drawer.qml @@ -41,7 +41,7 @@ Scope { } function show(name) { - root.page = name ?? ""; + root.page = root.modules.some(m => m.name === name) ? name : ""; root.open = true; } @@ -94,15 +94,6 @@ Scope { // Keys reach a focused item, never the window: setting // keyboardFocus above is necessary but not sufficient, and // Keys.onEscapePressed on a PanelWindow never fires. See AGENTS.md. - Item { - anchors.fill: parent - focus: true - Keys.onEscapePressed: { - if (root.page) root.page = ""; - else root.close(); - } - } - Rectangle { id: panel anchors { top: parent.top; left: parent.left; bottom: parent.bottom } @@ -113,6 +104,15 @@ Scope { border.width: 1 border.color: Qt.alpha(Theme.text, 0.12) + // The panel is that focused item. As the ancestor of the page + // content, a key pressed in any focused descendant bubbles up + // to it. + focus: true + Keys.onEscapePressed: { + if (root.page) root.page = ""; + else root.close(); + } + // Clicks on the panel must not reach the catcher behind it. MouseArea { anchors.fill: parent } diff --git a/desktop/Module.qml b/desktop/Module.qml index 645c736..eabe434 100644 --- a/desktop/Module.qml +++ b/desktop/Module.qml @@ -30,10 +30,9 @@ QtObject { // Label under the icon. Defaults to the name, capitalised. property string label: name.charAt(0).toUpperCase() + name.slice(1) - // Whether this module's background service runs while the drawer is - // closed. The drawer is closed most of the time, so this is what decides - // whether the shell is cheap to run all session. It governs the service - // only: pages are lazily loaded either way. + // Declarative metadata: whether this module's service is meant to run + // while the drawer is closed. The drawer does not read or enforce it, the + // module implements its own lifetime. Pages are lazily loaded either way. property bool alwaysActive: false // Rendered inside the tile, below the icon: a short state line. Null for diff --git a/desktop/README.md b/desktop/README.md index e443ca0..29223b7 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -43,8 +43,9 @@ drawer needs to show it: null for a tile that says nothing beyond its name. - `page`: a `Component` for the full-height page behind the tile, or null. - `activate()`: what a tile with no page does when clicked. -- `alwaysActive`: whether the module's background service runs while the - drawer is closed, covered below. +- `alwaysActive`: declares whether the module's service is meant to run while + the drawer is closed. The drawer does not read it, the module implements its + own lifetime. Covered below. A module provides a tile, a page, both, or neither. The drawer does not care which: it reads those properties and does the obvious thing. A page is wrapped @@ -53,7 +54,8 @@ fire-and-forget, so its click calls `activate()` and closes the drawer. `modules/appearance/AppearanceModule.qml` is the smallest complete example: a name, an icon, a label, and an `activate()` that launches the separate -appearance shell. It has no page and no service. Sound, mail and vm each add a +appearance shell. It has no page, and it owns only a `Process` that fires the +picker on click, no always-active service. Sound, mail and vm each add a service and a page on top of that same shape. Module files live under `desktop/modules//` and reference root types @@ -63,20 +65,22 @@ directory import does not reach the parent. The moved `TransportButton.qml` and ## alwaysActive -Sound and mail are `alwaysActive: true`; vm is false. The property governs the -module's background service, not its page: pages are lazily loaded either way. +`alwaysActive` is declarative metadata: it documents whether a module's service +is meant to run while the drawer is closed. The drawer does not read or enforce +it. Each module implements its own service lifetime, and the property records +that choice rather than driving it. Sound and mail are `alwaysActive: true`; vm +is false. Sound's OSD has to answer a volume keypress with no drawer open, which is the -whole point of it, so its PipeWire service runs for the session. Mail's unread -count outlives the drawer and its config watcher makes a newly added account -appear without a restart, so its service runs too. The vm module's 2s stats -poll exists only to paint a page nobody is looking at, so it starts and stops -with the page. +whole point of it, so its PipeWire service is instantiated eagerly and runs for +the session. Mail's unread count outlives the drawer and its config watcher +makes a newly added account appear without a restart, so its service is +instantiated eagerly too. The vm module's 2s stats poll exists only to paint a +page nobody is looking at, so it starts and stops with the page. -That last line is not the whole story for vm: its lifecycle event stream runs +vm is not only its poll, however: its lifecycle event stream runs unconditionally, which is what keeps the VM list and the tile's dots current -while the page is closed. `alwaysActive` gates the poll, not everything the -module does. +while the page is closed. Pages are lazily loaded in every module. ## Geometry diff --git a/desktop/modules/mail/README.md b/desktop/modules/mail/README.md index 8ae2bd2..92e4dab 100644 --- a/desktop/modules/mail/README.md +++ b/desktop/modules/mail/README.md @@ -244,9 +244,9 @@ is not worth a timer. ## Theme and blur -`Theme.qml` is the shared one: the palette comes from -`~/.cache/wal/udt-palette.qml` and is watched. It is a fourth identical copy, -which is a known loose end, not a palette to grow. +`Theme.qml` is a symlink to `shared/Theme.qml`: the palette comes from +`~/.cache/wal/udt-palette.qml` and is watched. Sharing one file is what keeps +the palette from drifting into per-component copies. Frosting is Hyprland's, matched on this window's namespace: diff --git a/desktop/modules/mail/waybar-mail.sh b/desktop/modules/mail/waybar-mail.sh index 4942f54..207a86e 100755 --- a/desktop/modules/mail/waybar-mail.sh +++ b/desktop/modules/mail/waybar-mail.sh @@ -16,7 +16,7 @@ # which is why there is no daemon to supervise and no interval to tune. # # The count is the total across every account. A per account breakdown is the -# drawer's job; see modules/mail/README.md. +# drawer's job; see desktop/modules/mail/README.md. set -u diff --git a/desktop/modules/vm/README.md b/desktop/modules/vm/README.md index 79c60f5..fbb7bde 100644 --- a/desktop/modules/vm/README.md +++ b/desktop/modules/vm/README.md @@ -38,10 +38,9 @@ Write those two paths out in full in the real config: `exec_cmd` runs the command directly, with no shell to expand `~`. The drawer opens on the monitor named by the `monitor` property at the top of -`VmPanel.qml`, defaulting to `DP-3`, and falls back to the first screen when -that one is not connected. It dims the rest of that monitor, because the -secondary screen usually has a real window on it, and takes keyboard focus so -Escape closes it and a delete can be confirmed by typing. +`Drawer.qml`, defaulting to `DP-1`, and falls back to the first screen when +that one is not connected. It takes keyboard focus so Escape closes it and a +delete can be confirmed by typing. ## Where the numbers come from @@ -73,9 +72,11 @@ for the whole session, panel open or not. Starting a VM from `virsh` or virt-manager updates the drawer, and opening it shows current state rather than whatever was true last time. -Statistics do need sampling, on a 2 second timer, but only while the drawer -is open: `Virsh.sampling` follows the panel's `open`. A closed panel costs one -idle process waiting on an event socket. +Statistics do need sampling, on a 2 second timer, but only while the vm page +is constructed: `Virsh.sampling` follows the page, set in the page's +`Component.onCompleted` and cleared in `Component.onDestruction`. The page is +lazily loaded, so a closed page costs one idle process waiting on an event +socket. ## Two things that bite @@ -87,9 +88,11 @@ the key then has nothing to talk to and silently does nothing. **Key events reach an item, not a window.** Setting `keyboardFocus` on the layer shell is necessary but not sufficient: `Keys.onEscapePressed` on the -PanelWindow itself never fires, because no item inside holds focus. A filled -`Item` with `focus: true` catches it, and takes focus back when the TextInput -in a delete confirmation gives it up. +PanelWindow itself never fires, because no item inside holds focus. The panel +rectangle is focused as the ancestor of the page content, so a key from any +focused descendant bubbles up to it. The delete confirmation's `TextInput` +also handles Escape itself, cancelling the pending confirmation rather than +navigating away. ## Destructive actions diff --git a/desktop/modules/vm/VmPage.qml b/desktop/modules/vm/VmPage.qml index 5cb4dc7..9fca5f5 100644 --- a/desktop/modules/vm/VmPage.qml +++ b/desktop/modules/vm/VmPage.qml @@ -318,6 +318,10 @@ Column { focus: needsTyping Component.onCompleted: if (needsTyping) forceActiveFocus() + // Escape cancels the pending confirmation rather than + // navigating away from the page; this handler consumes the key. + Keys.onEscapePressed: { page.confirming = null; page.typed = ""; } + Rectangle { anchors.fill: parent anchors.margins: -6 -- cgit v1.2.3