Draw idle as one timeline, and let the power button answer to its owner

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-24 20:33:44 -04:00
parent 6f0ce639d9
commit e1ff25fc66
23 changed files with 2655 additions and 174 deletions
@@ -63,26 +63,34 @@ PanelWindow {
// withdraw itself (hibernate on a machine with no resume swap).
readonly property var entries: allEntries.filter(entry => entry.available !== false)
// `entryId` is the stable name outside code can address an entry by --
// the power-button bind asks for "poweroff" and gets Power Off wherever it
// happens to sit. Labels are copy and hibernate comes and goes, so neither
// is something an IPC call can be built on.
readonly property var allEntries: [
{
entryId: "lock",
glyph: "󰌾",
label: "Lock",
destructive: false,
cmd: ["loginctl", "lock-session"]
},
{
entryId: "logout",
glyph: "󰗼",
label: "Log Out",
destructive: true,
cmd: ["sh", "-c", win.logoutScript]
},
{
entryId: "suspend",
glyph: "󰒲",
label: "Suspend",
destructive: false,
cmd: ["systemctl", "suspend"]
},
{
entryId: "hibernate",
glyph: "󰋊",
label: "Hibernate",
destructive: false,
@@ -90,12 +98,14 @@ PanelWindow {
cmd: ["systemctl", "hibernate"]
},
{
entryId: "restart",
glyph: "󰜉",
label: "Restart",
destructive: true,
cmd: ["systemctl", "reboot"]
},
{
entryId: "poweroff",
glyph: "󰐥",
label: "Power Off",
destructive: true,
@@ -103,14 +113,70 @@ PanelWindow {
}
]
onVisibleChanged: {
if (!win.visible)
// The entry to arm once the menu is on screen, set by preselect() before
// opening. Cleared as soon as it is applied, and again on close, so an
// interrupted open can never arm something on the next unrelated one.
property string armOnOpen: ""
function indexOfEntry(entryId: string): int {
for (let i = 0; i < win.entries.length; i++) {
if (win.entries[i].entryId === entryId)
return i;
}
return -1;
}
// Open the menu with one entry pre-armed, addressed by its id.
//
// This is what the power button's "Powers off" setting binds to: the first
// press opens the menu with Power Off selected and armed, and the second
// press is the confirm the menu already asks for. Nothing here goes around
// that confirm -- it calls the same trigger() a click calls, so a
// destructive entry still takes two presses and a harmless one still takes
// one. Pre-arming only removes the reach for the mouse, not the question.
function preselect(entryId: string): void {
const index = win.indexOfEntry(entryId);
if (index < 0) {
// An id this machine has no entry for -- hibernate without a
// resume swap. Open the menu rather than doing nothing at all.
win.armOnOpen = "";
ShellState.open("powermenu");
return;
}
if (!win.visible) {
win.armOnOpen = entryId;
ShellState.open("powermenu");
return;
}
// Already open: this press is the next one in the sequence.
win.currentIndex = index;
const button = rep.itemAt(index);
if (button)
button.trigger();
}
onVisibleChanged: {
if (!win.visible) {
win.armOnOpen = "";
return;
}
// Never reopen with a destructive button still armed from last time.
win.currentIndex = 0;
for (let i = 0; i < rep.count; i++)
rep.itemAt(i).disarm();
keys.forceActiveFocus();
const wanted = win.armOnOpen;
win.armOnOpen = "";
if (wanted === "")
return;
const index = win.indexOfEntry(wanted);
if (index < 0)
return;
win.currentIndex = index;
const button = rep.itemAt(index);
if (button)
button.trigger();
}
function run(index: int): void {