diff --git a/KeyVisualizer.qml b/KeyVisualizer.qml index b971a31..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++) { @@ -995,6 +1011,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 +1039,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 +1053,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 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" diff --git a/key-visualizer.lua b/key-visualizer.lua index b6d03c8..e94b1e1 100644 --- a/key-visualizer.lua +++ b/key-visualizer.lua @@ -32,9 +32,8 @@ local function is_runtime_secure(r) if not r or r == "" then return false end if r == "/tmp" then return false end if r:sub(1, 1) ~= "/" then return false end - -- Shell ownership probes fail from Hyprland's embedded Lua environment. -- /run/user is root-controlled, so accepting only this process's systemd - -- runtime path still excludes shared or caller-supplied directories. + -- runtime path avoids shell probes that fail in Hyprland's Lua runtime. local uid = effective_uid() return uid ~= nil and r == "/run/user/" .. uid end @@ -53,10 +52,8 @@ local SUPER_FLAG = runtime and (runtime .. "/omarchy-key-visualizer-super") or n local function secure_write(path, content) if not runtime or (path ~= STATE_FILE and path ~= SUPER_FLAG) then return false end - -- FileView watches the existing inode, so replacing the path on every key - -- leaves Quickshell attached to an unlinked file. Updating in place keeps - -- the watcher live. The containing runtime directory is private to the - -- effective user and the two accepted paths are fixed above. + -- FileView watches the existing inode. These are fixed paths inside the + -- current user's private runtime directory, so update them in place. local f = io.open(path, "w") if not f then return false end f:write(content) @@ -206,9 +203,8 @@ local function emit_super() secure_write(SUPER_FLAG, down and "1" or "0") end --- Create both files when the hook loads. This clears stale state after a --- compositor restart and gives FileView stable paths to watch before the --- first keyboard event arrives. +-- Create stable files before Quickshell starts watching them and clear stale +-- state left by a compositor restart. emit() emit_super() diff --git a/manifest.json b/manifest.json index 644455d..2b143ff 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.3", "author": "felixzsh", "description": "Shows the keys you press on screen. Great for keybinding tutorials, demos, and screencasts.", "kinds": [