Add key-visualizer panel UI

This commit is contained in:
felixzsh
2026-08-11 09:53:48 -05:00
parent 40bd0dd1f1
commit d0a36e79dd
+169
View File
@@ -0,0 +1,169 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import qs.Commons
import qs.Ui
// Key Visualizer — shows the keys you press as small chips at the bottom of
// the screen. The capture side is key-visualizer.lua (Hyprland Lua): it listens
// to the compositor's key events and writes the current combination to
// $XDG_RUNTIME_DIR/omarchy-key-visualizer.json. This panel watches that file
// and renders. No images, no animations: the combo appears while held and
// disappears the moment the keys are released.
Item {
id: root
property bool opened: false
property var keys: []
// Grace period after the last key is released. Small enough to feel
// immediate, long enough that a quick tap is still readable.
property int hideDelayMs: 120
// Drop state written more than this long ago (e.g. from a previous shell
// session after a restart) so a stale combo never sticks on screen.
readonly property int maxStateAgeMs: 1500
readonly property string statePath: {
var runtime = Quickshell.env("XDG_RUNTIME_DIR")
return (runtime && runtime.length > 0 ? runtime : "/tmp") + "/omarchy-key-visualizer.json"
}
// ------------------------------------------------------------- layout
readonly property int cardPad: Style.space(10)
readonly property int chipGap: Style.space(8)
readonly property int chipPadX: Style.space(9)
readonly property int chipPadY: Style.space(4)
readonly property int chipHeight: Math.ceil(chipTextMetrics.height) + 2 * chipPadY
function chipWidth(label) {
chipMetrics.text = String(label)
return Math.ceil(chipMetrics.advanceWidth) + 2 * chipPadX
}
function contentWidth() {
var w = 0
for (var i = 0; i < root.keys.length; i++) w += chipWidth(root.keys[i])
return w + Math.max(0, root.keys.length - 1) * chipGap
}
TextMetrics {
id: chipMetrics
font: chipFont
}
FontMetrics {
id: chipTextMetrics
font: chipFont
}
readonly property var chipFont: Qt.font({
family: Style.font.family,
pixelSize: Style.font.title,
bold: true
})
// ------------------------------------------------------------- state
function apply() {
var next = []
try {
var parsed = JSON.parse(stateFile.text())
if (parsed && Array.isArray(parsed.keys)) {
var age = Math.floor(Date.now() / 1000) - (parsed.t || 0)
if (age <= Math.ceil(root.maxStateAgeMs / 1000)) next = parsed.keys
}
} catch (e) {}
root.keys = next
if (next.length === 0) {
hideTimer.restart()
} else {
hideTimer.stop()
root.opened = true
}
}
Timer {
id: hideTimer
interval: root.hideDelayMs
onTriggered: root.opened = false
}
FileView {
id: stateFile
path: root.statePath
watchChanges: true
printErrors: false
onLoaded: root.apply()
onFileChanged: reload()
}
// Lifecycle required for panel plugins: summoning is a no-op because the
// display is driven by the state file; hiding closes the window.
function open(payloadJson) {}
function close() { root.opened = false }
IpcHandler {
target: "key-visualizer"
function ping(): string { return "ok" }
function state(): string { return root.opened ? "open" : "closed" }
}
// ------------------------------------------------------------- display
PanelWindow {
id: panel
visible: root.opened
anchors { top: true; bottom: true; left: true; right: true }
color: "transparent"
WlrLayershell.namespace: "key-visualizer"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
exclusionMode: ExclusionMode.Ignore
// Visual-only surface: never block clicks to the desktop below.
mask: Region {}
BorderSurface {
id: card
visible: root.keys.length > 0
width: card.borderLeft + root.cardPad + root.contentWidth() + root.cardPad + card.borderRight
height: card.borderTop + root.cardPad + root.chipHeight + root.cardPad + card.borderBottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Style.space(67)
color: Util.alpha(Color.popups.background, 0.97)
borderSpec: Border.surfaceSpec("popups", "border", Color.popups.border, Math.max(1, Style.space(2)))
radius: Style.cornerRadius
Row {
anchors.fill: parent
anchors.topMargin: card.borderTop + root.cardPad
anchors.leftMargin: card.borderLeft + root.cardPad
anchors.bottomMargin: card.borderBottom + root.cardPad
anchors.rightMargin: card.borderRight + root.cardPad
spacing: root.chipGap
Repeater {
model: root.keys
delegate: Rectangle {
required property string modelData
width: root.chipWidth(modelData)
height: root.chipHeight
radius: Math.max(3, Style.cornerRadius - 1)
color: Util.alpha(Color.popups.text, 0.10)
border.color: Util.alpha(Color.popups.text, 0.35)
border.width: 1
Text {
anchors.centerIn: parent
text: parent.modelData
font: root.chipFont
color: Color.popups.text
}
}
}
}
}
}
}