// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. import Quickshell import Quickshell.Io import Quickshell.Wayland import QtQuick import "Binds.js" as Binds Scope { id: root property bool open: false property string monitor: "DP-1" property var groups: [] // Non-empty while loading or after a failure; the grid shows only when // this is empty, so a stale list from the last opening never shows. property string status: "Loading..." readonly property var padActions: Binds.padActions(root.groups) readonly property var screenObj: Quickshell.screens.find(s => s.name === root.monitor) ?? Quickshell.screens[0] function toggle() { root.open = !root.open; } function close() { root.open = false; } // Asked afresh on every opening, so the list is what Hyprland has loaded // right now: an edit to keybindings.lua shows after `hyprctl reload`, and // not before, which is when it becomes true. onOpenChanged: if (open) { root.status = "Loading..."; // A Process already running ignores running = true. proc.running = false; proc.running = true; } Process { id: proc command: ["hyprctl", "binds", "-j"] stdout: StdioCollector { onStreamFinished: { try { root.groups = Binds.group(JSON.parse(text)); root.status = ""; } catch (e) { root.status = "hyprctl binds -j gave nothing readable"; } } } } // A config with no visible window exits, reporting nothing. This 1x1 // click-through window holds the shell open while the overlay is hidden. PanelWindow { anchors { top: true; left: true } implicitWidth: 1 implicitHeight: 1 color: "transparent" exclusionMode: ExclusionMode.Ignore mask: Region {} WlrLayershell.keyboardFocus: WlrKeyboardFocus.None } LazyLoader { active: root.open PanelWindow { screen: root.screenObj anchors { top: true; left: true; right: true; bottom: true } color: "transparent" exclusionMode: ExclusionMode.Ignore WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.namespace: "quickshell-keybinds" WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive // Keys reach a focused item, not a window. Item { anchors.fill: parent focus: true Keys.onEscapePressed: root.close() Rectangle { anchors.fill: parent color: Qt.rgba(Theme.base.r, Theme.base.g, Theme.base.b, 0.82) MouseArea { anchors.fill: parent onClicked: root.close() } Text { anchors.centerIn: parent visible: root.status !== "" text: root.status color: Theme.text font.family: Theme.fontFamily font.pixelSize: 22 } // The left fifth is the macropad; the list centres in the // rest. Item { id: listArea x: parent.width * 0.2 width: parent.width * 0.8 height: parent.height } PadPanel { width: parent.width * 0.2 height: parent.height visible: root.status === "" actions: root.padActions } // Groups fill a column down to the screen's height, then // start the next one, so a long group never leaves a row // of short ones sitting on empty space. The width is left // to the content, which is what centres the block. // ponytail: a single group taller than the screen clips; // split it in keybindings.lua if one ever gets there. Flow { anchors.centerIn: listArea height: parent.height * 0.9 flow: Flow.TopToBottom spacing: 28 visible: root.status === "" Repeater { model: root.groups Column { required property var modelData width: 440 spacing: 4 Text { text: modelData.name color: Theme.accent font.family: Theme.fontFamily font.pixelSize: 18 font.bold: true bottomPadding: 6 } Repeater { model: modelData.rows // Fixed widths summing to the column's: // a Row sizes to its children. Row { required property var modelData spacing: 12 Text { width: 240 elide: Text.ElideLeft text: modelData.keys color: Theme.subtext font.family: Theme.fontFamily font.pixelSize: 14 } Text { width: 188 wrapMode: Text.Wrap text: modelData.label color: Theme.text font.family: Theme.fontFamily font.pixelSize: 14 } } } } } } } } } } }