import Quickshell import Quickshell.Io import Quickshell.Hyprland import Quickshell.Wayland import QtQuick import QtQuick.Layouts import QtQuick.Controls import "../shared" as Shared Scope { id: root property bool keybindsVisible: false property bool animating: false property int activeTab: 0 property var allBinds: [] property var filteredBinds: [] property string targetMonitor: Shared.Config.monitor readonly property var tabMasks: [64, 65, 68, 72] readonly property var tabLabels: ["SUPER", "SUPER+SHIFT", "SUPER+CTRL", "SUPER+ALT"] IpcHandler { target: "keybinds" function toggle(): void { root.toggle() } } function toggle() { if (root.keybindsVisible) { root.keybindsVisible = false closeTimer.restart() } else { root.targetMonitor = Hyprland.focusedMonitor?.name ?? Shared.Config.monitor root.animating = true root.activeTab = 0 root.keybindsVisible = true bindsProc.running = false bindsProc.running = true } } function updateFilter() { const mask = root.tabMasks[root.activeTab] root.filteredBinds = root.allBinds.filter(function(b) { return b.modmask === mask && !b.mouse && b.submap === "" && !b.key.startsWith("_") }) } function formatKey(k) { if (k.length === 1) return k.toUpperCase() const pretty = { "Return": "↵", "BackSpace": "⌫", "Tab": "⇥", "space": "Space", "Prior": "PgUp", "Next": "PgDn", "comma": ",", "period": ".", "semicolon": ";", "apostrophe": "'", "grave": "`", "minus": "-", "equal": "=", "bracketleft": "[", "bracketright": "]", "backslash": "\\", "slash": "/", } return pretty[k] ?? (k.length > 10 ? k.slice(0, 9) + "…" : k) } function formatAction(b) { if (b.has_description && b.description !== "") return b.description if (b.dispatcher === "exec") { let a = b.arg let idx = a.indexOf("-- ") let cmd = idx >= 0 ? a.slice(idx + 3) : a return cmd.length > 48 ? cmd.slice(0, 45) + "…" : cmd } let s = b.dispatcher + (b.arg ? " " + b.arg : "") return s.length > 48 ? s.slice(0, 45) + "…" : s } onAllBindsChanged: updateFilter() onActiveTabChanged: updateFilter() Timer { id: closeTimer interval: 220 onTriggered: root.animating = false } Process { id: bindsProc command: ["hyprctl", "binds", "-j"] stdout: StdioCollector { onStreamFinished: { try { root.allBinds = JSON.parse(this.text) } catch(e) {} } } } Variants { model: Quickshell.screens delegate: Component { PanelWindow { required property var modelData screen: modelData WlrLayershell.namespace: "quickshell:keybinds" WlrLayershell.layer: WlrLayer.Overlay surfaceFormat { opaque: false } exclusionMode: ExclusionMode.Ignore focusable: true visible: modelData.name === root.targetMonitor && (root.keybindsVisible || root.animating) anchors { top: true; bottom: true; left: true; right: true } color: "transparent" // Dim backdrop Rectangle { anchors.fill: parent color: Qt.rgba(0, 0, 0, 0.45) opacity: root.keybindsVisible ? 1.0 : 0.0 Behavior on opacity { NumberAnimation { duration: 180; easing.type: Easing.OutCubic } } } // Click on backdrop to close MouseArea { anchors.fill: parent onClicked: { root.keybindsVisible = false closeTimer.restart() } } // Card Rectangle { id: card anchors.centerIn: parent width: 760 height: 560 z: 1 radius: Shared.Theme.radiusNormal color: Shared.Theme.popoutBackground border.width: 1 border.color: Shared.Theme.borderSubtle focus: true opacity: root.keybindsVisible ? 1.0 : 0.0 scale: root.keybindsVisible ? 1.0 : 0.97 Behavior on opacity { NumberAnimation { duration: 180; easing.type: Easing.OutCubic } } Behavior on scale { NumberAnimation { duration: 220; easing.type: Easing.OutCubic } } Keys.onEscapePressed: { root.keybindsVisible = false closeTimer.restart() } // Consume clicks so backdrop doesn't close MouseArea { anchors.fill: parent; z: -1 } ColumnLayout { anchors.fill: parent anchors.margins: Shared.Theme.popoutPadding spacing: Shared.Theme.popoutSpacing // ── Header ────────────────────────────── RowLayout { Layout.fillWidth: true spacing: 12 // Icon badge — styled like the key chips Rectangle { implicitWidth: 34 implicitHeight: 28 radius: 6 color: Qt.alpha(Shared.Theme.accent, Shared.Theme.opacityLight) border.width: 1 border.color: Qt.alpha(Shared.Theme.accent, 0.35) Text { anchors.centerIn: parent text: "⌨" color: Shared.Theme.accent font.pixelSize: 16 font.family: Shared.Theme.fontFamily } } Text { text: "Keybinds" color: Shared.Theme.text font.pixelSize: Shared.Theme.fontLarge font.family: Shared.Theme.fontFamily font.bold: true } Item { Layout.fillWidth: true } Text { text: "ESC / click outside to close" color: Shared.Theme.overlay0 font.pixelSize: Shared.Theme.fontSmall font.family: Shared.Theme.fontFamily } } // ── Tabs ──────────────────────────────── RowLayout { Layout.fillWidth: true spacing: 6 Repeater { model: root.tabLabels Rectangle { required property string modelData required property int index readonly property bool current: root.activeTab === index implicitHeight: 30 implicitWidth: tabText.implicitWidth + 20 radius: Shared.Theme.radiusSmall color: current ? Qt.alpha(Shared.Theme.accent, Shared.Theme.opacityLight) : Qt.alpha(Shared.Theme.surface0, 0.5) border.width: 1 border.color: current ? Qt.alpha(Shared.Theme.accent, Shared.Theme.opacityMedium) : Shared.Theme.borderSubtle Behavior on color { ColorAnimation { duration: 120 } } Behavior on border.color { ColorAnimation { duration: 120 } } Text { id: tabText anchors.centerIn: parent text: modelData color: parent.current ? Shared.Theme.accent : Shared.Theme.subtext0 font.pixelSize: Shared.Theme.fontSmall font.family: Shared.Theme.fontFamily font.bold: parent.current Behavior on color { ColorAnimation { duration: 120 } } } MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: root.activeTab = index } } } Item { Layout.fillWidth: true } Text { text: root.filteredBinds.length + " binds" color: Shared.Theme.overlay0 font.pixelSize: Shared.Theme.fontSmall font.family: Shared.Theme.fontFamily } } // ── Divider ───────────────────────────── Rectangle { Layout.fillWidth: true implicitHeight: 1 color: Shared.Theme.borderSubtle } // ── Bind list ─────────────────────────── ListView { id: bindsList Layout.fillWidth: true Layout.fillHeight: true model: root.filteredBinds clip: true spacing: 2 boundsBehavior: Flickable.StopAtBounds ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded width: 6 } delegate: Rectangle { required property var modelData required property int index width: bindsList.width - 8 implicitHeight: 34 radius: Shared.Theme.radiusSmall color: rowMouse.containsMouse ? Qt.alpha(Shared.Theme.surface0, 0.6) : "transparent" Behavior on color { ColorAnimation { duration: 80 } } RowLayout { anchors.fill: parent anchors.leftMargin: 8 anchors.rightMargin: 8 spacing: 12 Rectangle { implicitHeight: 22 implicitWidth: Math.max(keyLabel.implicitWidth + 14, 36) radius: 5 color: Qt.alpha(Shared.Theme.accent, Shared.Theme.opacityLight) border.width: 1 border.color: Qt.alpha(Shared.Theme.accent, 0.25) Text { id: keyLabel anchors.centerIn: parent text: root.formatKey(modelData.key) color: Shared.Theme.accent font.pixelSize: Shared.Theme.fontSmall font.family: Shared.Theme.fontFamily font.bold: true } } Text { Layout.fillWidth: true text: root.formatAction(modelData) color: Shared.Theme.text font.pixelSize: Shared.Theme.fontSize font.family: Shared.Theme.fontFamily elide: Text.ElideRight } Text { visible: modelData.dispatcher !== "exec" && modelData.dispatcher !== "" && modelData.dispatcher !== "lua" text: modelData.dispatcher color: Shared.Theme.overlay0 font.pixelSize: Shared.Theme.fontSmall font.family: Shared.Theme.fontFamily } } MouseArea { id: rowMouse anchors.fill: parent hoverEnabled: true } } } } } } } } }