diff --git a/KeyVisualizer.qml b/KeyVisualizer.qml index d35ee75..b16bafc 100644 --- a/KeyVisualizer.qml +++ b/KeyVisualizer.qml @@ -96,6 +96,22 @@ Item { return (runtime && runtime.length > 0 ? runtime : "/tmp") + "/omarchy-key-visualizer.json" } + // Super-held flag written by the Lua capture hook. While Super is down the + // overlay's input mask covers the card, so a SUPER+drag moves the visualizer + // itself (the Lua has unbound the compositor's SUPER+mouse while it is mapped). + property bool superHeld: false + readonly property string superPath: { + var runtime = Quickshell.env("XDG_RUNTIME_DIR") + return (runtime && runtime.length > 0 ? runtime : "/tmp") + "/omarchy-key-visualizer-super" + } + // True while the cursor hovers the card with Super held: the moment when the + // compositor's SUPER+mouse move/resize binds are temporarily unbound so the + // drag captures the visualizer instead of a window underneath. + property bool overCard: false + // Whether the SUPER+mouse binds are currently unbound by us (avoids redundant + // hyprctl eval calls on every hover transition). + property bool dragArmed: false + // Pause flag shared with the bar widget: while the file holds "1" the // display is frozen (keys are ignored). The bar button writes/removes the // file; both sides watch it, so a click on any monitor updates all of them. @@ -548,6 +564,15 @@ Item { onFileChanged: reload() } + FileView { + id: superFile + path: root.superPath + watchChanges: true + printErrors: false + onLoaded: root.superHeld = (text() === "1") + onFileChanged: reload() + } + onPausedChanged: if (root.paused) { root.entries = [] root.opened = false @@ -589,6 +614,68 @@ Item { if (isFinite(cfg.offsetY)) root.offsetY = Math.round(root.clamp(cfg.offsetY, -2000, 2000)) } + // Round-trips the current options to the shared config. Used by the SUPER+drag + // to persist the offset on release (it updates offsetX/offsetY live while + // dragging, then commits once). Mirrors the panel's writeConfig. + function persistConfig() { + var cfg = { + mode: root.mode, + position: root.position, + margin: root.margin, + lingerMs: root.lingerMs, + historyCount: root.historyCount, + comboMode: root.comboMode, + offsetX: root.offsetX, + offsetY: root.offsetY + } + persistProc.command = ["sh", "-c", + "printf '%s\\n' '" + JSON.stringify(cfg) + "' > " + Util.shellQuote(root.configPath)] + persistProc.running = true + } + + Process { + id: persistProc + } + + // While the cursor hovers the card with Super held, temporarily unbind the + // compositor's SUPER+mouse move/resize so the drag reaches the visualizer + // instead of a window underneath. Restored the moment the cursor leaves or + // Super is released, so normal window dragging keeps working elsewhere. + // Done via `hyprctl eval` so the shell can toggle the Lua-defined binds live. + function armSuperDrag() { + dragBindProc.command = ["sh", "-c", + "hyprctl eval \"hl.unbind('SUPER + mouse:272'); hl.unbind('SUPER + mouse:273')\""] + dragBindProc.running = true + } + + function disarmSuperDrag() { + dragBindProc.command = ["sh", "-c", + "hyprctl eval \"hl.unbind('SUPER + mouse:272'); hl.unbind('SUPER + mouse:273'); hl.bind('SUPER + mouse:272', hl.dsp.window.drag(), {mouse=true}); hl.bind('SUPER + mouse:273', hl.dsp.window.resize(), {mouse=true})\""] + dragBindProc.running = true + } + + Process { + id: dragBindProc + } + + function updateSuperDrag() { + if (dragArea.dragging) return + var shouldArm = root.superHeld && root.overCard && root.opened + if (shouldArm && !root.dragArmed) { + root.dragArmed = true + root.armSuperDrag() + } else if (!shouldArm && root.dragArmed) { + root.dragArmed = false + root.disarmSuperDrag() + } + } + + onSuperHeldChanged: { + if (!root.superHeld) root.overCard = false + root.updateSuperDrag() + } + onOverCardChanged: root.updateSuperDrag() + function migrateConfig() { // First load with the new location: carry over values from the old // plugin-dir config (if any) and remove it, or seed the defaults. @@ -710,8 +797,28 @@ Item { WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.keyboardFocus: WlrKeyboardFocus.None exclusionMode: ExclusionMode.Ignore - // Visual-only surface: never block clicks to the desktop below. - mask: Region {} + // Click-through normally; while Super is held the card's area captures the + // pointer so a SUPER+drag moves the visualizer (see dragArea). While + // dragging, capture the whole surface so the drag does not cut off when + // there is no window underneath. + mask: root.superHeld ? (dragArea.dragging ? fullMask : cardMask) : emptyMask + + // Two pre-declared regions so the mask can switch between "click-through" + // (empty) and "capture the card" without rebuilding a region per frame. + Region { + id: emptyMask + } + Region { + id: cardMask + item: card + } + Region { + id: fullMask + x: 0 + y: 0 + width: panel.width + height: panel.height + } BorderSurface { id: card @@ -787,6 +894,49 @@ Item { } } + // SUPER+drag: when Super is held the card's mask captures the pointer, so + // pressing and dragging on the card moves the visualizer live. While the + // cursor hovers the card with Super held, the compositor's SUPER+mouse + // move/resize binds are temporarily unbound so the drag reaches us instead + // of a window underneath; they are restored when the cursor leaves (or + // Super is released), so normal window dragging keeps working elsewhere. + MouseArea { + id: dragArea + x: card.x + y: card.y + width: card.width + height: card.height + enabled: root.superHeld && root.opened + acceptedButtons: Qt.LeftButton + hoverEnabled: true + cursorShape: dragging ? Qt.ClosedHandCursor : Qt.SizeAllCursor + + property bool dragging: false + property real grabX: 0 + property real grabY: 0 + property int grabOffsetX: 0 + property int grabOffsetY: 0 + + onEntered: root.overCard = true + onExited: root.overCard = false + onPressed: { + var p = dragArea.mapToItem(null, mouse.x, mouse.y) + grabX = p.x + grabY = p.y + grabOffsetX = root.offsetX + grabOffsetY = root.offsetY + dragging = true + } + onPositionChanged: { + if (!(mouse.buttons & Qt.LeftButton)) return + var p = dragArea.mapToItem(null, mouse.x, mouse.y) + root.offsetX = Math.round(root.clamp(grabOffsetX + (p.x - grabX), -2000, 2000)) + root.offsetY = Math.round(root.clamp(grabOffsetY + (p.y - grabY), -2000, 2000)) + } + onReleased: { dragging = false; root.persistConfig(); root.updateSuperDrag() } + onCanceled: { dragging = false; root.persistConfig(); root.updateSuperDrag() } + } + // 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/key-visualizer.lua b/key-visualizer.lua index 4c707de..160ea1a 100644 --- a/key-visualizer.lua +++ b/key-visualizer.lua @@ -145,6 +145,25 @@ local function emit() end end +-- Super-held flag: the panel/display watches this to know when to capture the +-- SUPER+drag on the overlay (instead of a window underneath). Written only on +-- transitions so it does not spam the filesystem on every key. +local SUPER_FLAG = runtime .. "/omarchy-key-visualizer-super" +local last_super = nil +local function super_down() + return pressed[133] or pressed[134] +end +local function emit_super() + local down = super_down() + if down == last_super then return end + last_super = down + local f = io.open(SUPER_FLAG, "w") + if f then + f:write(down and "1" or "0") + f:close() + end +end + -- Combos: a combination of keys is treated as a unit. The display only -- updates on key-down (the combo grows as you press) and when the last key -- is released (the empty payload starts the panel's linger with the last @@ -165,9 +184,11 @@ hl.on("input.keyboard.key", function(keycode, timeMs, state) end if not found then combo[#combo + 1] = keycode end end + emit_super() emit() else pressed[keycode] = false + emit_super() if not MODS[keycode] then for i, kc in ipairs(combo) do if kc == keycode then