Stop push-to-talk even when other keys are pressed during the hold (#14645)
Hyprland skips a release bind once any other key or mouse button is released while its key is held (KeybindManager::shadowKeybinds), and when the modifiers changed since the press. That is what makes a bind like "bindr = SUPER, SUPER_L" tap-only, but it means typing or clicking while holding F9 or Right Alt never runs omarchy-dictation stop, so the recording keeps going. o.bind_hold binds a press and a release action together. The release half is transparent (it can't be shadowed), ignores modifiers, and is non-consuming, so it doesn't take the key from apps when pressed with other modifiers (Shift+F9 still reaches the app). It runs only after its own press ran, so releasing the key without the rest of a chord does nothing. Dictation's push-to-talk keys use it. Co-authored-by: superduck00 <294879536+superduck00@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4 files changed
+143
-4
No files matched your search
@@ -0,0 +1,71 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
require_command lua
|
||||
|
||||
HOME="$(mktemp -d)" OMARCHY_PATH="$ROOT" lua <<'LUA'
|
||||
package.path = os.getenv("OMARCHY_PATH") .. "/?.lua;" .. package.path
|
||||
|
||||
local binds, ran = {}, {}
|
||||
hl = setmetatable({
|
||||
dsp = setmetatable({}, {
|
||||
__index = function(_, name)
|
||||
return function(arg) return { dsp = name, arg = arg } end
|
||||
end,
|
||||
}),
|
||||
bind = function(keys, dispatcher, opts)
|
||||
table.insert(binds, { keys = keys, dispatcher = dispatcher, opts = opts })
|
||||
end,
|
||||
exec_cmd = function(command) table.insert(ran, command) end,
|
||||
dispatch = function(dispatcher) table.insert(ran, dispatcher.dsp .. " " .. dispatcher.arg) end,
|
||||
}, {
|
||||
__index = function()
|
||||
return function() return {} end
|
||||
end,
|
||||
})
|
||||
|
||||
require("default.hypr.helpers")
|
||||
|
||||
local function ran_since(fn)
|
||||
local before = #ran
|
||||
fn()
|
||||
return table.concat({ table.unpack(ran, before + 1) }, "\n")
|
||||
end
|
||||
|
||||
local options = { locked = true }
|
||||
o.bind_hold("SUPER + X", "Start", "start-cmd", "Stop", "stop-cmd", options)
|
||||
local press, release = binds[1], binds[2]
|
||||
assert(press.keys == "SUPER + X" and release.keys == "SUPER + X", "both halves bind the same keys")
|
||||
assert(press.opts.description == "Start" and release.opts.description == "Stop", "each half keeps its description")
|
||||
assert(not press.opts.release and not press.opts.transparent, "the press half is an ordinary bind")
|
||||
assert(release.opts.release and release.opts.transparent and release.opts.ignore_mods,
|
||||
"the release half survives keys typed and modifiers changed during the hold")
|
||||
assert(release.opts.non_consuming, "the release half leaves the key to apps when its press did not run")
|
||||
assert(press.opts.locked and release.opts.locked, "caller options apply to both halves")
|
||||
assert(options.release == nil and options.description == nil, "caller options are not modified")
|
||||
assert(o.bind_commands[press.dispatcher] == "start-cmd" and o.bind_commands[release.dispatcher] == "stop-cmd",
|
||||
"the keybindings menu can run each half")
|
||||
|
||||
assert(ran_since(release.dispatcher) == "", "a release without its press does nothing")
|
||||
assert(ran_since(press.dispatcher) == "start-cmd", "the press runs its command")
|
||||
assert(ran_since(release.dispatcher) == "stop-cmd", "the release ends what the press started")
|
||||
assert(ran_since(release.dispatcher) == "", "a release runs once per press")
|
||||
|
||||
o.bind_hold("SUPER + R", "Start", "start-cmd", "Stop", "stop-cmd",
|
||||
{ repeating = true, long_press = true, click = true, drag = true, locked = true })
|
||||
local repeat_release, repeat_press = table.remove(binds), table.remove(binds)
|
||||
assert(repeat_press.opts.repeating, "the press half keeps its options")
|
||||
for _, key in ipairs({ "repeating", "long_press", "click", "drag" }) do
|
||||
assert(repeat_release.opts[key] == nil, "the release half drops " .. key)
|
||||
end
|
||||
assert(repeat_release.opts.locked, "the release half keeps other options")
|
||||
|
||||
o.bind_hold("SUPER + Z", "Start", hl.dsp.exec_cmd("start-dsp"), "Stop", function() table.insert(ran, "stop-fn") end)
|
||||
assert(ran_since(binds[3].dispatcher) == "exec_cmd start-dsp", "dispatchers are dispatched")
|
||||
assert(ran_since(binds[4].dispatcher) == "stop-fn", "functions are called")
|
||||
assert(ran_since(binds[2].dispatcher) == "", "each hold binding tracks its own press")
|
||||
LUA
|
||||
pass "hold bindings pair each release with its press"
|
||||
@@ -219,12 +219,25 @@ o = {
|
||||
return configured
|
||||
end,
|
||||
bind = function(...) table.insert(bindings, {...}) end,
|
||||
bind_hold = function(keys, _, press, _, release, options)
|
||||
table.insert(bindings, { keys, press, options })
|
||||
table.insert(bindings, { keys, release, options })
|
||||
end,
|
||||
}
|
||||
dofile(root .. "/default/hypr/bindings/dictation.lua")
|
||||
assert(#bindings == 0, "unconfigured dictation leaves application keys available")
|
||||
configured = true
|
||||
dofile(root .. "/default/hypr/bindings/dictation.lua")
|
||||
assert(#bindings == 5, "any configured backend receives the same bindings")
|
||||
local function binds(keys, command)
|
||||
for _, binding in ipairs(bindings) do
|
||||
if binding[1] == keys and binding[2] == command then return true end
|
||||
end
|
||||
end
|
||||
for _, keys in ipairs({ "F9", "ALT + Alt_R" }) do
|
||||
assert(binds(keys, "omarchy-dictation start") and binds(keys, "omarchy-dictation stop"),
|
||||
keys .. " starts dictation on press and stops it on release")
|
||||
end
|
||||
LUA
|
||||
pass "dictation shortcuts require a selection without limiting backend names"
|
||||
|
||||
|
||||
Reference in new issue
Block a user