Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QKxGW1raAWaqeU8WdHsMsp
223 lines
9.1 KiB
Bash
Executable File
223 lines
9.1 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
|
|
|
|
require_command lua
|
|
require_command xkbcli
|
|
|
|
tmpdir=$(mktemp -d) && [[ -n $tmpdir && -d $tmpdir ]] ||
|
|
fail "the test gets a temporary directory to stub Hyprland in"
|
|
trap 'rm -rf "$tmpdir"' EXIT
|
|
|
|
home="$tmpdir/home"
|
|
stub_bin="$tmpdir/bin"
|
|
mkdir -p "$home/.config" "$stub_bin"
|
|
cp -r "$ROOT/config/hypr" "$home/.config/hypr"
|
|
|
|
# The menu reads binds from Hyprland, which is not running here, so stand in for
|
|
# it. A Lua bind reports dispatcher __lua and no arg, and the menu recovers both
|
|
# from the Lua source; an exec bind carries its own command. Both shapes matter:
|
|
# what two chords dispatch is what decides whether they share a row.
|
|
lua_bind() {
|
|
printf 'bind\n\tmodmask: %s\n\tsubmap: \n\tkey: %s\n\tkeycode: 0\n\tcatchall: false\n\tdescription: %s\n\tdispatcher: __lua\n\targ: \n' "$1" "$2" "$3"
|
|
}
|
|
|
|
exec_bind() {
|
|
printf 'bind\n\tmodmask: %s\n\tsubmap: \n\tkey: %s\n\tkeycode: 0\n\tcatchall: false\n\tdescription: %s\n\tdispatcher: exec\n\targ: %s\n' "$1" "$2" "$3" "$4"
|
|
}
|
|
|
|
stub_hyprctl() {
|
|
{
|
|
echo '#!/bin/bash'
|
|
echo 'case "$1" in'
|
|
echo ' binds) cat <<'"'"'BINDS'"'"''
|
|
cat
|
|
echo 'BINDS'
|
|
echo ' ;;'
|
|
echo ' devices) echo "active keymap: English (US)" ;;'
|
|
echo 'esac'
|
|
} >"$stub_bin/hyprctl"
|
|
chmod +x "$stub_bin/hyprctl"
|
|
}
|
|
|
|
keybindings() {
|
|
env -i PATH="$stub_bin:$ROOT/bin:$PATH" HOME="$home" \
|
|
XDG_CACHE_HOME="$tmpdir/cache" OMARCHY_PATH="$ROOT" \
|
|
bash "$ROOT/bin/omarchy-menu-keybindings" --print
|
|
}
|
|
|
|
# Closing a window and toggling the scratchpad are two of the actions Omarchy
|
|
# binds twice on purpose. The last bind carries the longest description Omarchy
|
|
# ships, which is what puts a row closest to the width the menu allows.
|
|
stub_hyprctl <<BINDS
|
|
$(lua_bind 64 "SUPER + W" "关闭窗口")
|
|
$(lua_bind 64 "SUPER + Q" "关闭窗口")
|
|
$(lua_bind 64 "SUPER + F" "全屏")
|
|
$(lua_bind 64 "SUPER + S" "切换便签区")
|
|
$(lua_bind 64 "SUPER + grave" "切换便签区")
|
|
$(exec_bind 73 "SUPER SHIFT ALT + 0" "窗口静默移到工作区 10" "true")
|
|
BINDS
|
|
|
|
rendered=$(keybindings)
|
|
[[ -n $rendered ]] || fail "the keybindings menu renders with a stubbed Hyprland"
|
|
|
|
grep -q 'SUPER + F *→ 全屏' <<<"$rendered" ||
|
|
fail "a chord with no alternative renders on its own" "$rendered"
|
|
pass "the keybindings menu renders its entries"
|
|
|
|
(( $(grep -c '→ 关闭窗口$' <<<"$rendered") == 1 )) ||
|
|
fail "an alternative chord joins the row of the first one" "$rendered"
|
|
grep -q 'SUPER + W / SUPER + Q *→ 关闭窗口' <<<"$rendered" ||
|
|
fail "a shared row names both chords" "$rendered"
|
|
pass "an alternative chord joins the row of the first one"
|
|
|
|
# Which chord leads is the whole point of keeping Hyprland's order: SUPER + W is
|
|
# the documented default and SUPER + Q the alternative bound after it.
|
|
grep -q '^SUPER + W / SUPER + Q' <<<"$rendered" ||
|
|
fail "the chord declared first leads a shared row" "$rendered"
|
|
pass "the chord declared first leads a shared row"
|
|
|
|
# Hyprland calls the key left of 1 "grave". Nobody reads their keyboard that way.
|
|
grep -q 'SUPER + S / SUPER + ~ *→ 切换便签区' <<<"$rendered" ||
|
|
fail "the grave key reads as the symbol printed on it" "$rendered"
|
|
! grep -q 'grave' <<<"$rendered" ||
|
|
fail "no entry still says grave" "$rendered"
|
|
pass "the grave key reads as the symbol printed on it"
|
|
|
|
# Monospace menu: every arrow sits in one column, and nothing is allowed past
|
|
# it. A row that overruns pushes its own arrow out of line.
|
|
[[ $(awk -F '→' '{ print length($1) }' <<<"$rendered" | sort -u) == "36" ]] ||
|
|
fail "every entry pads its chords to the same column" "$rendered"
|
|
pass "every entry pads its chords to the same column"
|
|
|
|
# The menu elides a row that outgrows its card: 754px of label, 78 monospace
|
|
# characters at the heading size. The longest entry Omarchy ships sits at 74, so
|
|
# a row has four characters of room and no more.
|
|
(( $(awk '{ print length($0) }' <<<"$rendered" | sort -rn | head -1) <= 78 )) ||
|
|
fail "no entry outgrows the width the menu gives it" "$rendered"
|
|
pass "no entry outgrows the width the menu gives it"
|
|
|
|
# Priority ordering reads the row, and the chord sharing it must not reclassify
|
|
# the entry: XF86Calculator alone belongs in the tail the menu keeps for media
|
|
# keys, while the calculator itself sits in the body of the list.
|
|
stub_hyprctl <<BINDS
|
|
$(exec_bind 68 "SUPER CTRL + Q" "计算器" "omacalc")
|
|
$(exec_bind 0 "XF86Calculator" "计算器" "omacalc")
|
|
$(exec_bind 8 "ALT + TAB" "活动窗口置顶显示" "true")
|
|
BINDS
|
|
|
|
rendered=$(keybindings)
|
|
(( $(grep -n '→ 计算器$' <<<"$rendered" | cut -d: -f1) <
|
|
$(grep -n '→ 活动窗口置顶显示$' <<<"$rendered" | cut -d: -f1) )) ||
|
|
fail "a shared chord does not change where its entry ranks" "$rendered"
|
|
pass "a shared chord does not change where its entry ranks"
|
|
|
|
# The same key written as a keycode arrives by the other road: Hyprland reports
|
|
# the code and the keymap resolves it, after the rename above has run.
|
|
stub_hyprctl <<'BINDS'
|
|
bind
|
|
modmask: 64
|
|
submap:
|
|
key:
|
|
keycode: 49
|
|
catchall: false
|
|
description: 切换便签区
|
|
dispatcher: exec
|
|
arg: true
|
|
BINDS
|
|
|
|
rendered=$(keybindings)
|
|
grep -q 'SUPER + ~ *→ 切换便签区' <<<"$rendered" ||
|
|
fail "a keycode resolves to the symbol printed on the key too" "$rendered"
|
|
pass "a keycode resolves to the symbol printed on the key too"
|
|
|
|
# A chord refused for width opens a row of its own, and the next chord tries
|
|
# that row rather than reaching back past it and printing out of order.
|
|
stub_hyprctl <<BINDS
|
|
$(exec_bind 64 "SUPER + A" "计算器" "omacalc")
|
|
$(exec_bind 77 "SUPER SHIFT CTRL ALT + BACKSPACE" "计算器" "omacalc")
|
|
$(exec_bind 64 "SUPER + B" "计算器" "omacalc")
|
|
BINDS
|
|
|
|
rendered=$(keybindings)
|
|
(( $(grep -c '→ 计算器$' <<<"$rendered") == 3 )) ||
|
|
fail "a refused chord does not let the next one jump the queue" "$rendered"
|
|
pass "a refused chord does not let the next one jump the queue"
|
|
|
|
# Sharing a row is something Omarchy names an action for, not something two
|
|
# chords earn by looking alike. Alt + Tab and Shift + Alt + Tab both read
|
|
# "活动窗口置顶显示" and cycle opposite ways.
|
|
stub_hyprctl <<BINDS
|
|
$(exec_bind 64 "SUPER + Y" "放大" "omarchy-zoom in")
|
|
$(exec_bind 64 "SUPER + Z" "放大" "omarchy-zoom in")
|
|
BINDS
|
|
|
|
rendered=$(keybindings)
|
|
(( $(grep -c '→ 放大$' <<<"$rendered") == 2 )) ||
|
|
fail "an action Omarchy did not name keeps its chords on separate rows" "$rendered"
|
|
pass "an action Omarchy did not name keeps its chords on separate rows"
|
|
|
|
# Even a named action gives up the shared row rather than overrun the column:
|
|
# two rows in line beat one that juts out of it.
|
|
stub_hyprctl <<BINDS
|
|
$(exec_bind 73 "SUPER SHIFT ALT + BACKSPACE" "计算器" "omacalc")
|
|
$(exec_bind 69 "SUPER SHIFT CTRL + BACKSPACE" "计算器" "omacalc")
|
|
BINDS
|
|
|
|
rendered=$(keybindings)
|
|
(( $(grep -c '→ 计算器$' <<<"$rendered") == 2 )) ||
|
|
fail "chords too wide to share a row stay on their own" "$rendered"
|
|
[[ $(awk -F '→' '{ print length($1) }' <<<"$rendered" | sort -u) == "36" ]] ||
|
|
fail "chords too wide to share a row leave the column alone" "$rendered"
|
|
pass "chords too wide to share a row stay on their own"
|
|
|
|
# A shared label is not a shared action. Two chords that merely read alike have
|
|
# to stay apart, or the menu hides one of them behind the other.
|
|
stub_hyprctl <<BINDS
|
|
$(lua_bind 64 "SUPER + W" "关闭窗口")
|
|
$(exec_bind 64 "SUPER + X" "关闭窗口" "omarchy-hyprland-window-close-all")
|
|
BINDS
|
|
|
|
rendered=$(keybindings)
|
|
(( $(grep -c '→ 关闭窗口$' <<<"$rendered") == 2 )) ||
|
|
fail "chords with the same label but different actions stay apart" "$rendered"
|
|
pass "chords with the same label but different actions stay apart"
|
|
|
|
# An unresolved Lua bind reports no dispatcher at all, so nothing says the two
|
|
# chords run the same thing, whatever their label promises.
|
|
stub_hyprctl <<BINDS
|
|
$(lua_bind 64 "SUPER + Y" "关闭窗口")
|
|
$(lua_bind 64 "SUPER + Z" "关闭窗口")
|
|
BINDS
|
|
|
|
rendered=$(keybindings)
|
|
(( $(grep -c '→ 关闭窗口$' <<<"$rendered") == 2 )) ||
|
|
fail "chords whose dispatch is unknown stay apart" "$rendered"
|
|
pass "chords whose dispatch is unknown stay apart"
|
|
|
|
# What the menu is expected to pair up, written out here rather than read from
|
|
# the script, so dropping an action from the list fails instead of shrinking
|
|
# what gets checked.
|
|
expected_alternatives=(
|
|
"关闭窗口"
|
|
"计算器"
|
|
"切换便签区"
|
|
"窗口移入便签区"
|
|
)
|
|
|
|
eval "$(sed -n '/^alternative_chord_actions()/,/^}/p' "$ROOT/bin/omarchy-menu-keybindings")"
|
|
|
|
[[ $(alternative_chord_actions) == "$(printf '%s\n' "${expected_alternatives[@]}")" ]] ||
|
|
fail "the menu pairs up the actions Omarchy means it to" "$(alternative_chord_actions)"
|
|
pass "the menu pairs up the actions Omarchy means it to"
|
|
|
|
# A renamed description would leave an action named here matching nothing, and
|
|
# the row it was meant to share would quietly split in two. Only real binds
|
|
# count: a commented-out example is not a second chord.
|
|
for action in "${expected_alternatives[@]}"; do
|
|
(( $(grep -rhE '^[[:space:]]*o\.bind\(' "$ROOT/default/hypr/bindings" |
|
|
grep -cF ", \"$action\",") >= 2 )) ||
|
|
fail "every action named as having an alternative is bound twice" "$action"
|
|
done
|
|
pass "every action named as having an alternative is bound twice"
|