Add key-visualizer panel UI
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user