From 516b8b6d5b16e11690de09cce8c87c6bdac87e4f Mon Sep 17 00:00:00 2001 From: rope Date: Fri, 12 Jun 2026 14:23:33 +0100 Subject: [PATCH] quickshell: session menu card backing, keyboard nav with default selection Co-Authored-By: Claude Fable 5 --- settings/quickshell.nix | 119 +++++++++++++++++++++++++--------------- 1 file changed, 75 insertions(+), 44 deletions(-) diff --git a/settings/quickshell.nix b/settings/quickshell.nix index 6ea2b90..d5b2528 100644 --- a/settings/quickshell.nix +++ b/settings/quickshell.nix @@ -475,6 +475,8 @@ in required property var shellRoot screen: modelData WlrLayershell.namespace: "quickshell-bar" + // Keyboard only while the session menu is open (arrow/Enter nav) + WlrLayershell.keyboardFocus: sessionMenu.open ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None anchors { top: true @@ -528,25 +530,54 @@ in } // ── Session menu: icon-only power controls morphing out of - // the right frame column at screen centre (Super+L). + // the right frame column at screen centre (Super+L). Keyboard: + // arrows/Tab move the selection, Enter activates, Esc closes. Item { id: sessionMenu property bool open: false - property real openW: open ? 56 : 0 + property int selIdx: 0 + property real openW: open ? 64 : 0 Behavior on openW { NumberAnimation { duration: 280; easing.type: Easing.OutExpo } } + readonly property var actions: [ + { icon: "lock", danger: false, act: "lock" }, + { icon: "logout", danger: false, act: "logout" }, + { icon: "restart_alt", danger: true, act: "reboot" }, + { icon: "power_settings_new", danger: true, act: "poweroff" } + ] + + function activate(act) { + open = false; + if (act === "lock") Quickshell.execDetached([Commands.hyprlock]); + else if (act === "logout") Hyprland.dispatch("hl.dsp.exit()"); + else if (act === "reboot") Quickshell.execDetached([Commands.systemctl, "reboot"]); + else Quickshell.execDetached([Commands.systemctl, "poweroff"]); + } + + function toggle() { + open = !open; + if (open) { + selIdx = 0; + forceActiveFocus(); + _sessionAutoClose.restart(); + } + } + x: bar.width - Theme.frameWidth - openW y: Math.round((bar.height - height) / 2) width: openW - height: sessionCol.height + 24 + height: sessionCard.height + 24 visible: openW > 0.5 - function toggle() { - open = !open; - if (open) _sessionAutoClose.restart(); - } + focus: open + Keys.onEscapePressed: open = false + Keys.onUpPressed: { selIdx = (selIdx + actions.length - 1) % actions.length; _sessionAutoClose.restart(); } + Keys.onDownPressed: { selIdx = (selIdx + 1) % actions.length; _sessionAutoClose.restart(); } + Keys.onTabPressed: { selIdx = (selIdx + 1) % actions.length; _sessionAutoClose.restart(); } + Keys.onReturnPressed: activate(actions[selIdx].act) + Keys.onEnterPressed: activate(actions[selIdx].act) Timer { id: _sessionAutoClose @@ -570,51 +601,51 @@ in NumberAnimation { duration: 200; easing.type: Easing.OutCubic } } - Column { - id: sessionCol + // Card backing, matching the other dropdowns + Rectangle { + id: sessionCard anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right anchors.rightMargin: 8 - spacing: 4 + width: 48 + height: sessionCol.height + 8 + radius: 8 + color: Theme.base01 - Repeater { - model: [ - { icon: "lock", danger: false, act: "lock" }, - { icon: "logout", danger: false, act: "logout" }, - { icon: "restart_alt", danger: true, act: "reboot" }, - { icon: "power_settings_new", danger: true, act: "poweroff" } - ] + Column { + id: sessionCol + anchors.centerIn: parent + spacing: 4 - Rectangle { - id: sessBtn - required property var modelData - width: 40 - height: 40 - radius: 8 - color: sessBtnMa.containsMouse ? Theme.base02 : "transparent" - Behavior on color { ColorAnimation { duration: 120 } } + Repeater { + model: sessionMenu.actions - Text { - anchors.centerIn: parent - text: sessBtn.modelData.icon - color: sessBtn.modelData.danger && sessBtnMa.containsMouse - ? Theme.base08 : Theme.base05 + Rectangle { + id: sessBtn + required property var modelData + required property int index + width: 40 + height: 40 + radius: 8 + color: sessionMenu.selIdx === index ? Theme.base02 : "transparent" Behavior on color { ColorAnimation { duration: 120 } } - font.family: Theme.iconFont - font.pixelSize: 20 - } - MouseArea { - id: sessBtnMa - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - sessionMenu.open = false; - if (sessBtn.modelData.act === "lock") Quickshell.execDetached([Commands.hyprlock]); - else if (sessBtn.modelData.act === "logout") Hyprland.dispatch("hl.dsp.exit()"); - else if (sessBtn.modelData.act === "reboot") Quickshell.execDetached([Commands.systemctl, "reboot"]); - else Quickshell.execDetached([Commands.systemctl, "poweroff"]); + Text { + anchors.centerIn: parent + text: sessBtn.modelData.icon + color: sessBtn.modelData.danger && sessionMenu.selIdx === sessBtn.index + ? Theme.base08 : Theme.base05 + Behavior on color { ColorAnimation { duration: 120 } } + font.family: Theme.iconFont + font.pixelSize: 20 + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: sessionMenu.selIdx = sessBtn.index + onClicked: sessionMenu.activate(sessBtn.modelData.act) } } }