Files
dotfiles/dot_config/quickshell/keybinds/KeybindsWindow.qml
T

369 lines
15 KiB
QML

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
}
}
}
}
}
}
}
}
}