mirror of
https://github.com/arthur-pbty/arthur-os.git
synced 2026-08-01 20:28:16 +02:00
432 lines
12 KiB
Bash
Executable File
432 lines
12 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# omarchy:summary=Display Hyprland keybindings defined in your configuration using walker for an interactive search menu.
|
|
|
|
declare -A KEYCODE_SYM_MAP
|
|
# Hyprland reports XKB keycodes for code: bindings. Keep a small fallback for
|
|
# common keys so the menu remains readable if xkbcli cannot resolve a symbol.
|
|
declare -A FALLBACK_KEYCODE_SYM_MAP=(
|
|
[10]="1"
|
|
[11]="2"
|
|
[12]="3"
|
|
[13]="4"
|
|
[14]="5"
|
|
[15]="6"
|
|
[16]="7"
|
|
[17]="8"
|
|
[18]="9"
|
|
[19]="0"
|
|
[20]="MINUS"
|
|
[21]="EQUAL"
|
|
[61]="SLASH"
|
|
)
|
|
|
|
# Hyprland's Lua config provider currently reports code:... binds from hl.bind()
|
|
# as key="" and keycode=0 in `hyprctl -j binds`. Keep a lightweight
|
|
# source-derived key cache so the menu can still show those bindings.
|
|
declare -A LUA_BIND_KEY_MAP
|
|
|
|
build_keymap_cache() {
|
|
local keymap
|
|
keymap="$(xkbcli compile-keymap)" || {
|
|
echo "Failed to compile keymap" >&2
|
|
return 1
|
|
}
|
|
|
|
while IFS=, read -r code sym; do
|
|
[[ -z $code || -z $sym ]] && continue
|
|
KEYCODE_SYM_MAP["$code"]="$sym"
|
|
done < <(
|
|
awk '
|
|
BEGIN { sec = "" }
|
|
/xkb_keycodes/ { sec = "codes"; next }
|
|
/xkb_symbols/ { sec = "syms"; next }
|
|
sec == "codes" {
|
|
if (match($0, /<([A-Za-z0-9_]+)>\s*=\s*([0-9]+)\s*;/, m)) code_by_name[m[1]] = m[2]
|
|
}
|
|
sec == "syms" {
|
|
if (match($0, /key\s*<([A-Za-z0-9_]+)>\s*\{\s*\[\s*([^, \]]+)/, m)) sym_by_name[m[1]] = m[2]
|
|
}
|
|
END {
|
|
for (k in code_by_name) {
|
|
c = code_by_name[k]
|
|
s = sym_by_name[k]
|
|
if (c != "" && s != "" && s != "NoSymbol") print c "," s
|
|
}
|
|
}
|
|
' <<<"$keymap"
|
|
)
|
|
}
|
|
|
|
lookup_keycode_cached() {
|
|
local symbol
|
|
symbol="${KEYCODE_SYM_MAP[$1]}"
|
|
|
|
if [[ -z $symbol ]]; then
|
|
symbol="${FALLBACK_KEYCODE_SYM_MAP[$1]}"
|
|
fi
|
|
|
|
if [[ -z $symbol ]]; then
|
|
symbol="code:$1"
|
|
fi
|
|
|
|
printf '%s\n' "${symbol^^}"
|
|
}
|
|
|
|
parse_keycodes() {
|
|
local start end elapsed
|
|
[[ ${DEBUG:-0} == "1" ]] && start=$(date +%s.%N)
|
|
while IFS= read -r line; do
|
|
if [[ $line =~ code:([0-9]+) ]]; then
|
|
code="${BASH_REMATCH[1]}"
|
|
if [[ $code == "201" ]]; then
|
|
echo "${line/SUPER SHIFT,code:201/,COPILOT KEY}"
|
|
else
|
|
symbol=$(lookup_keycode_cached "$code" "$XKB_KEYMAP_CACHE")
|
|
echo "${line/code:${code}/$symbol}"
|
|
fi
|
|
elif [[ $line =~ mouse:([0-9]+) ]]; then
|
|
code="${BASH_REMATCH[1]}"
|
|
|
|
case "$code" in
|
|
272) symbol="LEFT MOUSE BUTTON" ;;
|
|
273) symbol="RIGHT MOUSE BUTTON" ;;
|
|
274) symbol="MIDDLE MOUSE BUTTON" ;;
|
|
*) symbol="mouse:${code}" ;;
|
|
esac
|
|
|
|
echo "${line/mouse:${code}/$symbol}"
|
|
else
|
|
echo "$line"
|
|
fi
|
|
done
|
|
|
|
if [[ $DEBUG == "1" ]]; then
|
|
end=$(date +%s.%N)
|
|
# fall back to awk if bc is missing
|
|
if command -v bc >/dev/null 2>&1; then
|
|
elapsed=$(echo "$end - $start" | bc)
|
|
else
|
|
elapsed=$(awk -v s="$start" -v e="$end" 'BEGIN{printf "%.6f", (e - s)}')
|
|
fi
|
|
echo "[DEBUG] parse_keycodes elapsed: ${elapsed}s" >&2
|
|
fi
|
|
}
|
|
|
|
# Supplement `hyprctl -j binds` for Lua-only binds that Hyprland currently
|
|
# reports without their original key, such as hl.bind("SUPER + code:10", ...).
|
|
build_lua_bind_key_cache() {
|
|
local modmask description key
|
|
|
|
omarchy-cmd-present lua || return 0
|
|
|
|
while IFS=$'\t' read -r modmask description key; do
|
|
[[ -z $modmask || -z $description || -z $key ]] && continue
|
|
LUA_BIND_KEY_MAP["$modmask,$description"]="$key"
|
|
done < <(
|
|
lua <<'LUA'
|
|
local modifiers = { SHIFT = 1, CTRL = 4, CONTROL = 4, ALT = 8, SUPER = 64 }
|
|
|
|
local function split_keys(keys)
|
|
local modmask = 0
|
|
local key = ""
|
|
|
|
for part in string.gmatch(keys, "[^+]+") do
|
|
local value = part:gsub("^%s+", ""):gsub("%s+$", "")
|
|
local modifier = modifiers[string.upper(value)]
|
|
|
|
if modifier then
|
|
modmask = modmask + modifier
|
|
else
|
|
key = value
|
|
end
|
|
end
|
|
|
|
return modmask, key
|
|
end
|
|
|
|
local function proxy()
|
|
local p = {}
|
|
|
|
return setmetatable(p, {
|
|
__index = function()
|
|
return p
|
|
end,
|
|
__call = function()
|
|
return p
|
|
end,
|
|
})
|
|
end
|
|
|
|
local p = proxy()
|
|
|
|
hl = setmetatable({
|
|
dsp = p,
|
|
bind = function(keys, _, opts)
|
|
opts = opts or {}
|
|
if opts.description and opts.description ~= "" then
|
|
local modmask, key = split_keys(keys)
|
|
print(modmask .. "\t" .. opts.description .. "\t" .. key)
|
|
end
|
|
return p
|
|
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()
|
|
return function()
|
|
return p
|
|
end
|
|
end,
|
|
})
|
|
|
|
local config = (os.getenv("HOME") or "") .. "/.config/hypr/hyprland.lua"
|
|
local file = io.open(config, "r")
|
|
|
|
if file then
|
|
file:close()
|
|
local ok, err = pcall(dofile, config)
|
|
if not ok and os.getenv("DEBUG") == "1" then
|
|
io.stderr:write("[DEBUG] lua bind scan failed: " .. tostring(err) .. "\n")
|
|
end
|
|
end
|
|
LUA
|
|
)
|
|
}
|
|
|
|
modmask_to_text() {
|
|
case "$1" in
|
|
0) printf '' ;;
|
|
1) printf 'SHIFT' ;;
|
|
4) printf 'CTRL' ;;
|
|
5) printf 'SHIFT CTRL' ;;
|
|
8) printf 'ALT' ;;
|
|
9) printf 'SHIFT ALT' ;;
|
|
12) printf 'CTRL ALT' ;;
|
|
13) printf 'SHIFT CTRL ALT' ;;
|
|
64) printf 'SUPER' ;;
|
|
65) printf 'SUPER SHIFT' ;;
|
|
68) printf 'SUPER CTRL' ;;
|
|
69) printf 'SUPER SHIFT CTRL' ;;
|
|
72) printf 'SUPER ALT' ;;
|
|
73) printf 'SUPER SHIFT ALT' ;;
|
|
76) printf 'SUPER CTRL ALT' ;;
|
|
77) printf 'SUPER SHIFT CTRL ALT' ;;
|
|
*) printf '%s' "$1" ;;
|
|
esac
|
|
}
|
|
|
|
# Fetch dynamic keybindings from Hyprland.
|
|
#
|
|
# Also do some pre-processing:
|
|
# - Fill missing Lua code:... keys from the Lua source cache
|
|
# - Remove standard Omarchy bin path prefix
|
|
# - Map numeric modifier key mask to a textual rendition
|
|
# - Output comma-separated values that the parser can understand
|
|
dynamic_bindings() {
|
|
local modmask key keycode description dispatcher arg modifiers
|
|
|
|
hyprctl -j binds |
|
|
jq -r '.[] | [.modmask, (.key // ""), (.keycode // 0), (.description // ""), (.dispatcher // ""), (.arg // "") | tostring] | join("\u001f")' |
|
|
while IFS=$'\x1f' read -r modmask key keycode description dispatcher arg; do
|
|
if [[ -z $key && $keycode != "0" ]]; then
|
|
key="code:$keycode"
|
|
fi
|
|
|
|
if [[ -z $key && -n $description ]]; then
|
|
key="${LUA_BIND_KEY_MAP["$modmask,$description"]}"
|
|
fi
|
|
|
|
[[ -z $description && $dispatcher == "__lua" ]] && continue
|
|
|
|
modifiers=$(modmask_to_text "$modmask")
|
|
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"
|
|
done
|
|
}
|
|
|
|
# Hardcoded bindings, like the copy-url extension and such
|
|
static_bindings() {
|
|
echo "SHIFT ALT,L,Copy URL from Web App,sendshortcut,SHIFT ALT,L,"
|
|
}
|
|
|
|
# Parse and format keybindings
|
|
#
|
|
# `awk` does the heavy lifting:
|
|
# - Set the field separator to a comma ','.
|
|
# - Joins the key combination (e.g., "SUPER + Q").
|
|
# - Joins the command that the key executes.
|
|
# - Prints display text and dispatch metadata as tab-separated fields.
|
|
parse_binding_records() {
|
|
awk -F, '
|
|
{
|
|
# Combine the modifier and key (first two fields)
|
|
key_combo = $1 " + " $2;
|
|
|
|
# Clean up: strip leading "+" if present, trim spaces
|
|
gsub(/^[ \t]*\+?[ \t]*/, "", key_combo);
|
|
gsub(/[ \t]+$/, "", key_combo);
|
|
|
|
# Use description, if set
|
|
action = $3;
|
|
dispatcher = $4;
|
|
|
|
# Reconstruct the dispatcher arg from the remaining fields
|
|
arg = "";
|
|
for (i = 5; i <= NF; i++) {
|
|
arg = arg $i (i < NF ? "," : "");
|
|
}
|
|
|
|
if (action == "") {
|
|
# Reconstruct the command from the remaining fields
|
|
for (i = 4; i <= NF; i++) {
|
|
action = action $i (i < NF ? "," : "");
|
|
}
|
|
|
|
# Clean up trailing commas, remove leading "exec, ", and trim
|
|
sub(/,$/, "", action);
|
|
gsub(/(^|,)[[:space:]]*exec[[:space:]]*,?/, "", action);
|
|
gsub(/(^|[[:space:]])uwsm(-app| app)[[:space:]]+--[[:space:]]+/, "", action);
|
|
gsub(/^[ \t]+|[ \t]+$/, "", action);
|
|
gsub(/[ \t]+/, " ", key_combo); # Collapse multiple spaces to one
|
|
|
|
# Escape XML entities
|
|
gsub(/&/, "\\&", action);
|
|
gsub(/</, "\\<", action);
|
|
gsub(/>/, "\\>", action);
|
|
gsub(/"/, "\\"", action);
|
|
gsub(/'"'"'/, "\\'", action);
|
|
}
|
|
|
|
if (action != "") {
|
|
printf "%-35s → %s\t%s\t%s\n", key_combo, action, dispatcher, arg;
|
|
}
|
|
}'
|
|
}
|
|
|
|
prioritize_entries() {
|
|
awk -F '\t' '
|
|
{
|
|
line = $1
|
|
prio = 50
|
|
if (match(line, /Terminal/)) prio = 0
|
|
if (match(line, /Tmux/)) prio = 1
|
|
if (match(line, /Browser/) && !match(line, /Browser[[:space:]]*\(/) && !match(line, /SUPER SHIFT.*\+.*B.*→.*Browser/)) prio = 2
|
|
if (match(line, /File manager/) && !match(line, /File manager \(cwd\)/)) prio = 3
|
|
if (match(line, /Launch apps/)) prio = 4
|
|
if (match(line, /Omarchy menu/)) prio = 5
|
|
if (match(line, /System menu/)) prio = 6
|
|
if (match(line, /Theme menu/)) prio = 7
|
|
if (match(line, /Full screen/)) prio = 8
|
|
if (match(line, /Full width/)) prio = 9
|
|
if (match(line, /Close window/)) prio = 10
|
|
if (match(line, /Close all windows/)) prio = 11
|
|
if (match(line, /Lock system/)) prio = 12
|
|
if (match(line, /Toggle window floating/)) prio = 13
|
|
if (match(line, /Toggle window split/)) prio = 14
|
|
if (match(line, /Pop window/)) prio = 15
|
|
if (match(line, /Universal/)) prio = 16
|
|
if (match(line, /Clipboard/)) prio = 17
|
|
if (match(line, /Audio controls/)) prio = 18
|
|
if (match(line, /Bluetooth controls/)) prio = 19
|
|
if (match(line, /Wifi controls/)) prio = 20
|
|
if (match(line, /Emoji picker/)) prio = 21
|
|
if (match(line, /Color picker/)) prio = 22
|
|
if (match(line, /Screenshot/)) prio = 23
|
|
if (match(line, /Screenrecording/)) prio = 24
|
|
if (match(line, /SUPER SHIFT.*\+.*B.*→.*Browser/)) prio = 25
|
|
if (match(line, /File manager \(cwd\)/)) prio = 26
|
|
if (match(line, /(Switch|Next|Former|Previous).*workspace/)) prio = 27
|
|
if (match(line, /Move window to workspace/)) prio = 28
|
|
if (match(line, /Move window silently to workspace/)) prio = 29
|
|
if (match(line, /Swap window/)) prio = 30
|
|
if (match(line, /Focus/)) prio = 31
|
|
if (match(line, /Move window$/)) prio = 32
|
|
if (match(line, /Resize window/)) prio = 33
|
|
if (match(line, /Expand window/)) prio = 34
|
|
if (match(line, /Shrink window/)) prio = 35
|
|
if (match(line, /scratchpad/)) prio = 36
|
|
if (match(line, /notification/)) prio = 37
|
|
if (match(line, /Toggle window transparency/)) prio = 38
|
|
if (match(line, /Toggle workspace gaps/)) prio = 39
|
|
if (match(line, /Toggle nightlight/)) prio = 40
|
|
if (match(line, /Toggle locking/)) prio = 41
|
|
if (match(line, /group/)) prio = 94
|
|
if (match(line, /Scroll active workspace/)) prio = 95
|
|
if (match(line, /Cycle to/)) prio = 96
|
|
if (match(line, /Reveal active/)) prio = 97
|
|
if (match(line, /Apple Display/)) prio = 98
|
|
if (match(line, /XF86/)) prio = 99
|
|
|
|
# print "priority<TAB>record"
|
|
printf "%d\t%s\n", prio, $0
|
|
}' |
|
|
sort -k1,1n -k2,2 |
|
|
cut -f2-
|
|
}
|
|
|
|
output_binding_records() {
|
|
build_keymap_cache
|
|
build_lua_bind_key_cache
|
|
|
|
{
|
|
dynamic_bindings
|
|
static_bindings
|
|
} |
|
|
sort -u |
|
|
parse_keycodes |
|
|
parse_binding_records |
|
|
prioritize_entries
|
|
}
|
|
|
|
output_keybindings() {
|
|
output_binding_records | cut -f1
|
|
}
|
|
|
|
dispatch_binding() {
|
|
local dispatcher="$1"
|
|
local arg="$2"
|
|
|
|
case "$dispatcher" in
|
|
exec)
|
|
[[ -n $arg ]] && hyprctl dispatch exec "$arg"
|
|
;;
|
|
"") return 1 ;;
|
|
*)
|
|
if [[ -n $arg ]]; then
|
|
hyprctl dispatch "$dispatcher" "$arg"
|
|
else
|
|
hyprctl dispatch "$dispatcher"
|
|
fi
|
|
;;
|
|
esac
|
|
}
|
|
|
|
if [[ $1 == "--print" || $1 == "-p" ]]; then
|
|
output_keybindings
|
|
else
|
|
records=$(output_binding_records)
|
|
monitor_height=$(hyprctl monitors -j | jq -r '.[] | select(.focused == true) | .height')
|
|
menu_height=$((monitor_height * 40 / 100))
|
|
selection=$(cut -f1 <<<"$records" |
|
|
walker --dmenu -p 'Keybindings' --width 800 --height "$menu_height")
|
|
|
|
if [[ -n $selection ]]; then
|
|
record=$(awk -F '\t' -v selection="$selection" '$1 == selection { print; exit }' <<<"$records")
|
|
dispatcher=$(cut -f2 <<<"$record")
|
|
arg=$(cut -f3- <<<"$record")
|
|
|
|
dispatch_binding "$dispatcher" "$arg"
|
|
fi
|
|
fi
|