Bring back dispatch from keybindings menu

This commit is contained in:
David Heinemeier Hansson
2026-05-16 15:09:07 +02:00
parent f7f2838776
commit 3b2fc3a080
+219 -38
View File
@@ -18,13 +18,18 @@ declare -A FALLBACK_KEYCODE_SYM_MAP=(
[19]="0" [19]="0"
[20]="MINUS" [20]="MINUS"
[21]="EQUAL" [21]="EQUAL"
[59]="COMMA"
[60]="PERIOD"
[61]="SLASH" [61]="SLASH"
) )
# Hyprland's Lua config provider currently reports code:... binds from hl.bind() # Hyprland's Lua config provider currently reports Lua binds as dispatcher
# as key="" and keycode=0 in `hyprctl -j binds`. Keep a lightweight # __lua and code:... binds from hl.bind() as key="" and keycode=0 in
# source-derived key cache so the menu can still show those bindings. # `hyprctl -j binds`. Keep a lightweight source-derived cache so the menu can
# still show and dispatch those bindings.
declare -A LUA_BIND_KEY_MAP declare -A LUA_BIND_KEY_MAP
declare -A LUA_BIND_DISPATCHER_MAP
declare -A LUA_BIND_ARG_MAP
build_keymap_cache() { build_keymap_cache() {
local keymap local keymap
@@ -114,15 +119,19 @@ parse_keycodes() {
} }
# Supplement `hyprctl -j binds` for Lua-only binds that Hyprland currently # Supplement `hyprctl -j binds` for Lua-only binds that Hyprland currently
# reports without their original key, such as hl.bind("SUPER + code:10", ...). # reports as dispatcher __lua and without their original key for code: binds.
build_lua_bind_key_cache() { build_lua_bind_cache() {
local modmask description key local modmask description key dispatcher arg cache_key
omarchy-cmd-present lua || return 0 omarchy-cmd-present lua || return 0
while IFS=$'\t' read -r modmask description key; do while IFS=$'\t' read -r modmask description key dispatcher arg; do
[[ -z $modmask || -z $description || -z $key ]] && continue [[ -z $modmask || -z $description || -z $key ]] && continue
LUA_BIND_KEY_MAP["$modmask,$description"]="$key" LUA_BIND_KEY_MAP["$modmask,$description"]="$key"
cache_key="$modmask,$description,$key"
LUA_BIND_DISPATCHER_MAP["$cache_key"]="$dispatcher"
LUA_BIND_ARG_MAP["$cache_key"]="$arg"
done < <( done < <(
lua <<'LUA' lua <<'LUA'
local modifiers = { SHIFT = 1, CTRL = 4, CONTROL = 4, ALT = 8, SUPER = 64 } local modifiers = { SHIFT = 1, CTRL = 4, CONTROL = 4, ALT = 8, SUPER = 64 }
@@ -131,7 +140,7 @@ local function split_keys(keys)
local modmask = 0 local modmask = 0
local key = "" local key = ""
for part in string.gmatch(keys, "[^+]+") do for part in string.gmatch(tostring(keys or ""), "[^+]+") do
local value = part:gsub("^%s+", ""):gsub("%s+$", "") local value = part:gsub("^%s+", ""):gsub("%s+$", "")
local modifier = modifiers[string.upper(value)] local modifier = modifiers[string.upper(value)]
@@ -145,45 +154,145 @@ local function split_keys(keys)
return modmask, key return modmask, key
end end
local function proxy() local function lua_literal(value)
local p = {} local value_type = type(value)
return setmetatable(p, { if value_type == "string" then
__index = function() return string.format("%q", value)
return p elseif value_type == "number" or value_type == "boolean" then
return tostring(value)
elseif value_type == "table" then
local parts = {}
local keys = {}
local array_length = #value
for index = 1, array_length do
parts[#parts + 1] = lua_literal(value[index])
end
for key in pairs(value) do
if not (type(key) == "number" and key >= 1 and key <= array_length and math.floor(key) == key) then
keys[#keys + 1] = key
end
end
table.sort(keys, function(left, right)
return tostring(left) < tostring(right)
end)
for _, key in ipairs(keys) do
local key_prefix
if type(key) == "string" and key:match("^[%a_][%w_]*$") then
key_prefix = key .. " = "
else
key_prefix = "[" .. lua_literal(key) .. "] = "
end
parts[#parts + 1] = key_prefix .. lua_literal(value[key])
end
return "{ " .. table.concat(parts, ", ") .. " }"
elseif value_type == "nil" then
return "nil"
else
return "nil"
end
end
local function call_expression(path, ...)
local args = {}
for index = 1, select("#", ...) do
args[index] = lua_literal(select(index, ...))
end
return path .. "(" .. table.concat(args, ", ") .. ")"
end
local function dispatcher(kind, arg, expr)
return {
__omarchy_dispatcher = true,
kind = kind or "",
arg = arg or "",
expr = expr or "",
}
end
local function dsp_proxy(path)
return setmetatable({ path = path }, {
__index = function(self, key)
return dsp_proxy(self.path .. "." .. tostring(key))
end, end,
__call = function() __call = function(self, ...)
return p local first_arg = ...
local expr = call_expression(self.path, ...)
if self.path == "hl.dsp.exec_cmd" and type(first_arg) == "string" then
return dispatcher("exec", first_arg, expr)
end
return dispatcher("lua", expr, expr)
end, end,
}) })
end end
local p = proxy() local noop
noop = setmetatable({}, {
__index = function()
return noop
end,
__call = function()
return noop
end,
})
local function noop_fn()
return noop
end
hl = setmetatable({ hl = setmetatable({
dsp = p, dsp = dsp_proxy("hl.dsp"),
bind = function(keys, _, opts) bind = function(keys, bind_dispatcher, opts)
opts = opts or {} opts = opts or {}
if opts.description and opts.description ~= "" then if opts.description and opts.description ~= "" then
local modmask, key = split_keys(keys) local modmask, key = split_keys(keys)
print(modmask .. "\t" .. opts.description .. "\t" .. key) local kind = ""
local arg = ""
if type(bind_dispatcher) == "table" and bind_dispatcher.__omarchy_dispatcher then
kind = bind_dispatcher.kind or ""
arg = bind_dispatcher.arg or bind_dispatcher.expr or ""
elseif type(bind_dispatcher) == "string" and bind_dispatcher ~= "" then
kind = "exec"
arg = bind_dispatcher
end
print(table.concat({ tostring(modmask), opts.description, key, kind, arg }, "\t"))
end end
return p
return noop
end,
unbind = noop_fn,
config = noop_fn,
env = noop_fn,
monitor = noop_fn,
window_rule = noop_fn,
workspace_rule = noop_fn,
layer_rule = noop_fn,
gesture = noop_fn,
animation = noop_fn,
curve = noop_fn,
exec_cmd = noop_fn,
dispatch = noop_fn,
on = noop_fn,
timer = noop_fn,
get_config = function()
return nil
end, end,
unbind = function() end,
config = function() end,
env = function() end,
monitor = function() end,
window_rule = function() end,
gesture = function() end,
animation = function() end,
curve = function() end,
exec_cmd = function() end,
}, { }, {
__index = function() __index = function()
return function() return noop
return p
end
end, end,
}) })
@@ -226,12 +335,12 @@ modmask_to_text() {
# Fetch dynamic keybindings from Hyprland. # Fetch dynamic keybindings from Hyprland.
# #
# Also do some pre-processing: # Also do some pre-processing:
# - Fill missing Lua code:... keys from the Lua source cache # - Fill missing Lua code:... keys and __lua dispatch metadata from the Lua source cache
# - Remove standard Omarchy bin path prefix # - Remove standard Omarchy bin path prefix
# - Map numeric modifier key mask to a textual rendition # - Map numeric modifier key mask to a textual rendition
# - Output comma-separated values that the parser can understand # - Output comma-separated values that the parser can understand
dynamic_bindings() { dynamic_bindings() {
local modmask key keycode description dispatcher arg modifiers local modmask key keycode description dispatcher arg modifiers cache_key
hyprctl -j binds | hyprctl -j binds |
jq -r '.[] | [.modmask, (.key // ""), (.keycode // 0), (.description // ""), (.dispatcher // ""), (.arg // "") | tostring] | join("\u001f")' | jq -r '.[] | [.modmask, (.key // ""), (.keycode // 0), (.description // ""), (.dispatcher // ""), (.arg // "") | tostring] | join("\u001f")' |
@@ -244,12 +353,24 @@ dynamic_bindings() {
key="${LUA_BIND_KEY_MAP["$modmask,$description"]}" key="${LUA_BIND_KEY_MAP["$modmask,$description"]}"
fi fi
if [[ $dispatcher == "__lua" && -n $description && -n $key ]]; then
cache_key="$modmask,$description,$key"
dispatcher="${LUA_BIND_DISPATCHER_MAP["$cache_key"]}"
arg="${LUA_BIND_ARG_MAP["$cache_key"]}"
fi
[[ -z $description && $dispatcher == "__lua" ]] && continue [[ -z $description && $dispatcher == "__lua" ]] && continue
case "$key" in
comma) key="COMMA" ;;
period) key="PERIOD" ;;
minus) key="MINUS" ;;
equal) key="EQUAL" ;;
slash) key="SLASH" ;;
esac
modifiers=$(modmask_to_text "$modmask") modifiers=$(modmask_to_text "$modmask")
arg="${arg//~\/.local\/share\/omarchy\/bin\//}" arg="${arg//~\/.local\/share\/omarchy\/bin\//}"
arg="${arg//uwsm app -- /}"
arg="${arg//uwsm-app -- /}"
printf '%s,%s,%s,%s,%s\n' "$modifiers" "$key" "$description" "$dispatcher" "$arg" printf '%s,%s,%s,%s,%s\n' "$modifiers" "$key" "$description" "$dispatcher" "$arg"
done done
@@ -377,7 +498,7 @@ prioritize_entries() {
output_binding_records() { output_binding_records() {
build_keymap_cache build_keymap_cache
build_lua_bind_key_cache build_lua_bind_cache
{ {
dynamic_bindings dynamic_bindings
@@ -393,13 +514,73 @@ output_keybindings() {
output_binding_records | cut -f1 output_binding_records | cut -f1
} }
trim() {
local value="$1"
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
printf '%s\n' "$value"
}
lua_string() {
jq -Rnr --arg value "$1" '$value | @json'
}
dispatch_lua_expression() {
local expression="$1"
local output status
output=$(hyprctl dispatch "$expression" 2>&1)
status=$?
if (( status == 0 )) && [[ -z $output || $output == "ok" ]]; then
[[ -n $output ]] && printf '%s\n' "$output"
return 0
fi
return 1
}
dispatch_exec_binding() {
local command="$1"
dispatch_lua_expression "hl.dsp.exec_cmd($(lua_string "$command"))" || hyprctl dispatch exec "$command"
}
dispatch_sendshortcut_binding() {
local arg="$1"
local mods key window rest
IFS=, read -r mods key window rest <<<"$arg"
mods=$(trim "$mods")
key=$(trim "$key")
window=$(trim "$window")
[[ -z $window ]] && window="activewindow"
if [[ -n $key ]] && dispatch_lua_expression "hl.dsp.send_key_state({ mods = $(lua_string "$mods"), key = $(lua_string "$key"), state = \"down\", window = $(lua_string "$window") })"; then
sleep 0.05
dispatch_lua_expression "hl.dsp.send_key_state({ mods = $(lua_string "$mods"), key = $(lua_string "$key"), state = \"up\", window = $(lua_string "$window") })"
return
fi
hyprctl dispatch sendshortcut "$arg"
}
dispatch_binding() { dispatch_binding() {
local dispatcher="$1" local dispatcher="$1"
local arg="$2" local arg="$2"
case "$dispatcher" in case "$dispatcher" in
exec) exec)
[[ -n $arg ]] && hyprctl dispatch exec "$arg" [[ -n $arg ]] && dispatch_exec_binding "$arg"
;;
sendshortcut)
[[ -n $arg ]] && dispatch_sendshortcut_binding "$arg"
;;
lua)
[[ -n $arg ]] && hyprctl dispatch "$arg"
;; ;;
"") return 1 ;; "") return 1 ;;
*) *)