// Captures a key chord for rebinding. // // Shown in place of a shortcut's chord while it is being changed. It takes // keyboard focus, waits for a non-modifier key, and reports the chord in the // form hypr/keybinds.lua uses. // // Modifier-only presses are ignored rather than accepted, because every press // of a chord passes through them: holding Super to type Super+K would otherwise // be captured as "SUPER" the moment the modifier went down. // // Escape cancels. Not every Qt key has a keysym name Hyprland would accept, so // an unmapped key is refused with a message rather than written as something // that would silently fail to bind. import QtQuick import qs.config FocusScope { id: root signal captured(string chord) signal cancelled property string message: "" implicitWidth: 230 implicitHeight: 30 // Qt key codes to the keysym names Hyprland expects. Letters and digits // fall out of the ASCII range; these are the rest that come up in practice. readonly property var namedKeys: ({ 0x01000000: "Escape", 0x01000001: "Tab", 0x01000004: "Return", 0x01000005: "Return", 0x01000003: "BackSpace", 0x01000006: "Insert", 0x01000007: "Delete", 0x01000010: "Home", 0x01000011: "End", 0x01000016: "Page_Up", 0x01000017: "Page_Down", 0x01000012: "left", 0x01000013: "up", 0x01000014: "right", 0x01000015: "down", 0x20: "space", 0x2c: "comma", 0x2e: "period", 0x2f: "slash", 0x3b: "semicolon", 0x27: "apostrophe", 0x5b: "bracketleft", 0x5d: "bracketright", 0x5c: "backslash", 0x60: "grave", 0x2d: "minus", 0x3d: "equal", 0x01000009: "Print" }) function keysymFor(key: int): string { if (key >= 0x41 && key <= 0x5a) // A-Z return String.fromCharCode(key); if (key >= 0x30 && key <= 0x39) // 0-9 return String.fromCharCode(key); if (key >= 0x01000030 && key <= 0x0100003b) // F1-F12 return "F" + (key - 0x01000030 + 1); return root.namedKeys[key] ?? ""; } function isModifierOnly(key: int): bool { return key === 0x01000020 || key === 0x01000021 || key === 0x01000022 || key === 0x01000023 || key === 0x01000024; } Rectangle { anchors.fill: parent radius: 9 color: Theme.alpha(Theme.accent, 0.14) border.width: 1 border.color: Theme.alpha(Theme.accent, 0.5) Text { anchors.centerIn: parent width: parent.width - 16 horizontalAlignment: Text.AlignHCenter elide: Text.ElideRight text: root.message !== "" ? root.message : "Press a shortcut… Esc to cancel" color: root.message !== "" ? Theme.warn : Theme.fg font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall } } Keys.onPressed: event => { event.accepted = true; if (event.key === 0x01000000) { // Escape root.cancelled(); return; } if (root.isModifierOnly(event.key)) return; const keysym = root.keysymFor(event.key); if (keysym === "") { root.message = "That key cannot be used"; return; } const parts = []; if (event.modifiers & Qt.MetaModifier) parts.push("SUPER"); if (event.modifiers & Qt.ControlModifier) parts.push("CTRL"); if (event.modifiers & Qt.AltModifier) parts.push("ALT"); if (event.modifiers & Qt.ShiftModifier) parts.push("SHIFT"); if (parts.length === 0 && keysym.length === 1) { // A bare letter or digit would swallow ordinary typing. root.message = "Add a modifier"; return; } parts.push(keysym); root.captured(parts.join(" + ")); } onActiveFocusChanged: { if (!activeFocus) root.cancelled(); } }