From a7357e3840cca1b41ba9800c646dac8616c0bced Mon Sep 17 00:00:00 2001 From: felixzsh Date: Wed, 26 Aug 2026 17:06:57 -0500 Subject: [PATCH 1/6] fix(ui): declare debugFont and debugFontMetrics before use debugFont was referenced by debugText and debugFontMetrics before its declaration, causing "ReferenceError: debugFont is not defined" on panel load. Move both the debugFont property and the debugFontMetrics FontMetrics block above the debug overlay so all references resolve in declaration order; qualify the last use as root.debugFont. --- KeyVisualizer.qml | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/KeyVisualizer.qml b/KeyVisualizer.qml index b971a31..e69cab3 100644 --- a/KeyVisualizer.qml +++ b/KeyVisualizer.qml @@ -995,6 +995,17 @@ Item { // Debug overlay: live readout of the card's position/dimensions and the // movement state, shown next to the card while moving and after release. // Toggle with: omarchy-shell key-visualizer debug + readonly property var debugFont: Qt.font({ + family: Style.font.family, + pixelSize: Style.font.bodySmall, + bold: false + }) + + FontMetrics { + id: debugFontMetrics + font: debugFont + } + BorderSurface { id: debugOverlay visible: root.debugOverlay @@ -1012,7 +1023,7 @@ Item { anchors.fill: parent anchors.margins: root.cardPad verticalAlignment: Text.AlignVCenter - font: debugFont + font: root.debugFont color: Color.popups.text text: { var lb = "\n" @@ -1026,16 +1037,6 @@ Item { } readonly property bool debugDragging: dragArea.dragging - FontMetrics { - id: debugFontMetrics - font: debugFont - } - readonly property var debugFont: Qt.font({ - family: Style.font.family, - pixelSize: Style.font.bodySmall, - bold: false - }) - // Combo mode banner — a separate visual stacked against the history // card (below it for bottom positions, above it for top positions). // Shows the combo counter, multiplier and running score; hue and From 68a75c2ab557e355a7be990394f850a868c65f6f Mon Sep 17 00:00:00 2001 From: felixzsh Date: Wed, 26 Aug 2026 18:03:51 -0500 Subject: [PATCH 2/6] fix(ui): auto-size config panel to content width The config panel used a fixed contentWidth of Style.space(250), the narrowest of all omarchy panels. With a wider font the Filter row (label "Filter" plus the All keys / Bindings buttons) no longer fit and the label overlapped the buttons. Derive contentWidth from the Filter row's implicit width (label + gap + buttons) plus padding and borders, capped to the available screen via fittedContentWidth, so the panel grows or shrinks with the font. --- Panel.qml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Panel.qml b/Panel.qml index ba2d427..17f355d 100644 --- a/Panel.qml +++ b/Panel.qml @@ -225,7 +225,10 @@ Panel { owner: root bar: root.bar open: root.opened - contentWidth: Style.space(250) + contentWidth: panel.fittedContentWidth( + Math.max(Style.space(250), + filterLabel.implicitWidth + Style.spacing.xl + modeButtons.implicitWidth + + panel.padding * 2 + Border.left(panel.borderSpec) + Border.right(panel.borderSpec))) contentHeight: menuColumn.implicitHeight + panel.padding * 2 Column { @@ -295,6 +298,7 @@ Panel { height: modeButtons.height Text { + id: filterLabel anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter text: "Filter" From 9fbdd2b8cfc9d1a7642634d40e08e150ffd8f587 Mon Sep 17 00:00:00 2001 From: felixzsh Date: Wed, 26 Aug 2026 18:34:35 -0500 Subject: [PATCH 3/6] chore: bump to version 1.8.2 --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 644455d..93288f4 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "felixzsh.key-visualizer", "name": "Key Visualizer", - "version": "1.8.1", + "version": "1.8.2", "author": "felixzsh", "description": "Shows the keys you press on screen. Great for keybinding tutorials, demos, and screencasts.", "kinds": [ From 48af33c2a757e78ece3302223585e669229a2445 Mon Sep 17 00:00:00 2001 From: felixzsh Date: Wed, 26 Aug 2026 18:47:29 -0500 Subject: [PATCH 4/6] fix(security): use POSIX-only checks in runtime dir validation --- key-visualizer.lua | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/key-visualizer.lua b/key-visualizer.lua index 089b671..64fd788 100644 --- a/key-visualizer.lua +++ b/key-visualizer.lua @@ -19,12 +19,12 @@ local function is_runtime_secure(r) if r == "/tmp" then return false end if r:sub(1, 1) ~= "/" then return false end local q = shell_quote(r) - -- Must be a directory owned by the current user and, if stat is - -- available, mode 0700 (systemd's XDG_RUNTIME_DIR default). The - -- stat check is skipped when stat is missing so we don't fail-closed - -- on minimal containers. - local cmd = "test -d " .. q .. " && test -O " .. q .. " && { p=$(stat -c %a " .. q .. " 2>/dev/null); [ -z \"$p\" ] || [ \"$p\" = \"700\" ]; }" - local res = os.execute(cmd) + -- POSIX-only checks (test -d, test -w): no GNU `test -O`, no `stat`, + -- no command substitution. They behave identically inside Hyprland's + -- os.execute and in any POSIX shell. XDG_RUNTIME_DIR is always a 0700 + -- dir owned by the user, so "exists + writable by us + absolute + not + -- /tmp" is sufficient; the predictable /tmp fallback is rejected above. + local res = os.execute("test -d " .. q .. " && test -w " .. q) return res == 0 or res == true end @@ -47,9 +47,11 @@ local function secure_write(path, content) if path == "/tmp/omarchy-key-visualizer.json" or path == "/tmp/omarchy-key-visualizer-super" then return false end local q = shell_quote(path) -- Refuse to follow a symlink at the destination (O_NOFOLLOW mitigation). - -- `test ! -L` succeeds when the file does not exist or is not a symlink; - -- it fails only when the destination is a symlink, which we must not follow. - local not_symlink = os.execute("test ! -L " .. q) + -- POSIX `test ! -h` succeeds when the file does not exist or is not a + -- symlink (the `-h` test is POSIX; `-L` is the GNU alias and is not + -- reliable inside Hyprland's os.execute). It fails only when the + -- destination is a symlink, which we must not follow. + local not_symlink = os.execute("test ! -h " .. q) if not (not_symlink == 0 or not_symlink == true) then print("[key-visualizer] refusing to write symlink: " .. path) return false From 48a80209107fec9e833e67c6d5a4314a2ed3d940 Mon Sep 17 00:00:00 2001 From: felixzsh Date: Wed, 26 Aug 2026 18:47:29 -0500 Subject: [PATCH 5/6] fix(ui): clear stale combo when capture stops updating --- KeyVisualizer.qml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/KeyVisualizer.qml b/KeyVisualizer.qml index e69cab3..5455b21 100644 --- a/KeyVisualizer.qml +++ b/KeyVisualizer.qml @@ -29,6 +29,11 @@ Item { // it. The history tick prunes entries whose linger window passed. With // historyCount 1 this is exactly "the current combo, lingering". property var entries: [] + // Epoch (seconds) of the last state payload we successfully parsed. Used + // to detect a dead capture: if no new payload arrives within maxStateAgeMs + // and the top combo was never released, we treat it as released so the + // panel self-heals instead of freezing on a stale combo forever. + property real lastStateT: 0 // How many combos stay on screen (1..5, default 1). Older entries fade // out via the entryOpacity() gradient; a count of 1 is the classic // current-combo-only display. @@ -467,9 +472,10 @@ Item { if (!root.paused) { try { var parsed = JSON.parse(stateFile.text()) - if (parsed && Array.isArray(parsed.keys)) { + 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 + if ((parsed.t || 0) > 0) root.lastStateT = parsed.t } } catch (e) {} } if (next.length > 0 && root.mode === "bindings") { @@ -529,6 +535,16 @@ Item { repeat: true running: root.entries.length > 0 onTriggered: { + // If the capture stopped updating (crashed, plugin unloaded, or a + // config reload disabled it), the top combo may be stuck with + // releasedAt === 0 forever because no empty "all keys up" payload + // ever arrives. Treat a stale state file as a release so the combo + // lingers normally and then clears, instead of freezing on screen. + if (root.entries.length > 0 && root.entries[0].releasedAt === 0 && + root.lastStateT > 0 && + Math.floor(Date.now() / 1000) - root.lastStateT > Math.ceil(root.maxStateAgeMs / 1000)) { + root.entries[0] = { keys: root.entries[0].keys, releasedAt: Date.now() } + } var now = Date.now() var kept = [] for (var i = 0; i < root.entries.length; i++) { From 3a1380c59f7cfb90b923d4bc393684f8f96f7587 Mon Sep 17 00:00:00 2001 From: felixzsh Date: Wed, 26 Aug 2026 18:54:43 -0500 Subject: [PATCH 6/6] chore: bump to version 1.8.3 --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 93288f4..2b143ff 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "felixzsh.key-visualizer", "name": "Key Visualizer", - "version": "1.8.2", + "version": "1.8.3", "author": "felixzsh", "description": "Shows the keys you press on screen. Great for keybinding tutorials, demos, and screencasts.", "kinds": [