* Unify input methods with the installer keyboard selection * Preserve inherited input keys and later keyboard changes * Exercise input engines across desktop toolkits * Show input modes in the keyboard indicator * Keep Pinyin setup prompts visible * Remember input selections before an app gains focus * Remove the Japanese input welcome notification * Show explicit choices on notification cards * Suppress the Cloudpinyin first-use prompt * Show the input switching shortcut in keyboard hints * Verify input setup and composition stay quiet * Use Super+I for input switching independently of Fcitx keys * Restore linked input settings and attribute test notifications to Fcitx * Correct the input setup shortcut hint * Keep input tooltips to the name and switching shortcut * Place the input language selector with the right bar widgets * Show input switching only for multiple layouts or methods * Unify input method and keyboard layout switching * Select keyboard layouts and input methods in the Typing menu * Apply typing selections immediately from the menu * Use installer keyboard choices and keep typing selections accurate * Queue typing changes and wait for saved selections * Keep queued typing selections when reopening the menu * Cycle keyboard layouts alongside composition engines * Avoid shadowing Python typing and retain pending mixed input selections * Keep input clicks working through controller and compositor failures * Move the keyboard layout widget to the right side of existing bars Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Map keyboard choices to the layouts the installer sets Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep the switching order when changing input methods Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Set input method fonts only after the selection saves Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Restore confirmed picker checks when an immediate change fails Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Route Super+I through the shell's input reader so unfocused choices stick Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Restore the input group when the font preference cannot be written Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Drop the azerty conversion now that the installer no longer offers it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Apply the owner's keyboard layout once before persisting the input choice A quattro merge kept a second apply_keyboard after the retry loop, which re-ran loadkeys and firstboot, ignored their failure, and overwrote the XKB layout setup supplies for keymaps kbd-model-map lacks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Give the Lao keyboard choice a Lao desktop layout The row used la-latin1, kbd's Latin American Spanish keymap, so Lao installs typed Spanish. There is no Lao console keymap; like Korean, the console stays US and the desktop gets la, which Omarchy keeps behind US as a non-Latin layout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Load engines installed after Fcitx started when they are selected The Typing menu refused an engine whose package an update had installed, since Fcitx discovers engines only at startup. Restart input once and wait for the engine; name the package when it really is not installed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Cycle input methods back with Super+Shift+I Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Drop notification action buttons now that the Pinyin prompt is suppressed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Cycle input only through the shell's reader Without the shell there is no bar or menu either, so the one-shot cycling path only duplicated the reader's logic. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add engines from the command line through the Typing menu's path omarchy setup input <engine> had its own restart, append, select and live-rollback code in configure.py, parallel to the menu's. It now installs the packages and appends through typing_setup, leaving configure.py with install-time seeding and update defaults. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep Ctrl+Space out of Fcitx's toggles instead of probing for it The terminal prefix bind read the Fcitx config and profile on every press to avoid fighting a Ctrl+Space toggle. configure --defaults now removes that key from every trigger list, so the bind only checks for a terminal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Run the input-method scripts with the system Python A bare python resolves through PATH, so a mise, pyenv or venv interpreter could run them without the system's PyGObject. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
696 lines
22 KiB
Bash
Executable File
696 lines
22 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# omarchy:summary=Display Hyprland keybindings defined in your configuration using an interactive search menu.
|
|
|
|
# Hyprland's Lua config provider currently reports Lua binds as dispatcher
|
|
# __lua in `hyprctl 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_DISPATCHER_MAP
|
|
declare -A LUA_BIND_ARG_MAP
|
|
|
|
# A platform package can name keys the way its keyboards print them: one
|
|
# "<keysym> <name>" per line (on a MacBook, XF86MonBrightnessUp is F2), in the
|
|
# fixed platform root, which no environment variable moves.
|
|
key_names_file=/usr/share/omarchy-platform/key-names
|
|
|
|
# Hyprland reports XKB keycodes for code: bindings. Resolve them to symbols
|
|
# via the compiled keymap, with a small fallback for common keys so the menu
|
|
# remains readable if xkbcli cannot resolve a symbol.
|
|
parse_keycodes() {
|
|
awk -v key_names="$key_names_file" '
|
|
BEGIN {
|
|
while ((getline line < key_names) > 0) {
|
|
# Blanks around a line and a CRLF line end read the same, as in
|
|
# displays.conf.
|
|
gsub(/^[ \t\r]+|[ \t\r]+$/, "", line)
|
|
if (split(line, name_parts, /[ \t]+/) == 2 && name_parts[1] ~ /^[A-Za-z0-9_]+$/ && name_parts[2] ~ /^[A-Za-z0-9_]+$/) key_name[name_parts[1]] = name_parts[2]
|
|
}
|
|
close(key_names)
|
|
|
|
split("10=1 11=2 12=3 13=4 14=5 15=6 16=7 17=8 18=9 19=0 20=MINUS 21=EQUAL 59=COMMA 60=PERIOD 61=SLASH", fallbacks, " ")
|
|
for (i in fallbacks) {
|
|
separator = index(fallbacks[i], "=")
|
|
keycode_symbol[substr(fallbacks[i], 1, separator - 1)] = substr(fallbacks[i], separator + 1)
|
|
}
|
|
|
|
# </dev/null keeps xkbcli from consuming the binding records on stdin
|
|
keymap_cmd = "xkbcli compile-keymap </dev/null"
|
|
section = ""
|
|
while ((keymap_cmd | getline line) > 0) {
|
|
if (line ~ /xkb_keycodes/) { section = "codes"; continue }
|
|
if (line ~ /xkb_symbols/) { section = "syms"; continue }
|
|
if (section == "codes" && match(line, /<([A-Za-z0-9_]+)>\s*=\s*([0-9]+)\s*;/, m)) code_by_name[m[1]] = m[2]
|
|
if (section == "syms" && match(line, /key\s*<([A-Za-z0-9_]+)>\s*\{\s*\[\s*([^, \]]+)/, m)) sym_by_name[m[1]] = m[2]
|
|
}
|
|
close(keymap_cmd)
|
|
|
|
for (name in code_by_name) {
|
|
code = code_by_name[name]
|
|
symbol = sym_by_name[name]
|
|
if (code != "" && symbol != "" && symbol != "NoSymbol") keycode_symbol[code] = toupper(symbol)
|
|
}
|
|
|
|
# Read off the keysym rather than the keycode, so the key left of 1 reads
|
|
# as the symbol printed on it whichever key the active layout puts it on.
|
|
for (code in keycode_symbol) {
|
|
if (keycode_symbol[code] == "GRAVE") keycode_symbol[code] = "~"
|
|
}
|
|
|
|
mouse_symbol["272"] = "LEFT MOUSE BUTTON"
|
|
mouse_symbol["273"] = "RIGHT MOUSE BUTTON"
|
|
mouse_symbol["274"] = "MIDDLE MOUSE BUTTON"
|
|
}
|
|
{
|
|
if (match($0, /code:([0-9]+)/, match_parts)) {
|
|
code = match_parts[1]
|
|
symbol = keycode_symbol[code]
|
|
if (symbol == "") symbol = "code:" code
|
|
sub("code:" code, symbol)
|
|
} else if (match($0, /mouse:([0-9]+)/, match_parts)) {
|
|
code = match_parts[1]
|
|
symbol = mouse_symbol[code]
|
|
if (symbol == "") symbol = "mouse:" code
|
|
sub("mouse:" code, symbol)
|
|
} else if ((first = index($0, ",")) > 0) {
|
|
# Only the key (the second field) is renamed: the description, the
|
|
# dispatcher and its argument are left as they are.
|
|
rest = substr($0, first + 1)
|
|
second = index(rest, ",")
|
|
key = second > 0 ? substr(rest, 1, second - 1) : rest
|
|
if (key in key_name) $0 = substr($0, 1, first) key_name[key] (second > 0 ? substr(rest, second) : "")
|
|
}
|
|
|
|
print
|
|
}
|
|
'
|
|
}
|
|
|
|
# Supplement `hyprctl binds` for Lua-only binds that Hyprland currently
|
|
# reports as dispatcher __lua and without their original key for code: binds.
|
|
build_lua_bind_cache() {
|
|
local modmask description key dispatcher arg cache_key
|
|
|
|
omarchy-cmd-present lua || return 0
|
|
|
|
while IFS=$'\t' read -r modmask description key dispatcher arg; do
|
|
[[ -z $modmask || -z $description || -z $key ]] && continue
|
|
|
|
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 < <(
|
|
# -E keeps LUA_INIT and LUA_PATH from the environment out of the scan, which
|
|
# loads the user's config.
|
|
lua -E - <<'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(tostring(keys or ""), "[^+]+") 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 lua_literal(value)
|
|
local value_type = type(value)
|
|
|
|
if value_type == "string" then
|
|
return string.format("%q", value)
|
|
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,
|
|
__call = function(self, ...)
|
|
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
|
|
|
|
local noop
|
|
noop = setmetatable({}, {
|
|
__index = function()
|
|
return noop
|
|
end,
|
|
__call = function()
|
|
return noop
|
|
end,
|
|
})
|
|
|
|
hl = setmetatable({
|
|
dsp = dsp_proxy("hl.dsp"),
|
|
bind = function(keys, bind_dispatcher, opts)
|
|
opts = opts or {}
|
|
|
|
if opts.description and opts.description ~= "" then
|
|
local modmask, key = split_keys(keys)
|
|
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
|
|
elseif o and o.bind_commands and o.bind_commands[bind_dispatcher] then
|
|
kind = "exec"
|
|
arg = o.bind_commands[bind_dispatcher]
|
|
end
|
|
|
|
print(table.concat({ tostring(modmask), opts.description, key, kind, arg }, "\t"))
|
|
end
|
|
|
|
return noop
|
|
end,
|
|
get_config = function()
|
|
return nil
|
|
end,
|
|
}, {
|
|
__index = function()
|
|
return noop
|
|
end,
|
|
})
|
|
|
|
local config = os.getenv("HOME") .. "/.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 and __lua dispatch metadata 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 cache_key
|
|
|
|
# Parse the plain `hyprctl binds` output rather than `hyprctl -j binds`:
|
|
# Hyprland 0.56.0 emits invalid JSON for binds (misaligned fields in
|
|
# bindsRequest), and older versions broke on quotes in bind args.
|
|
hyprctl binds | awk '
|
|
function emit() {
|
|
if (!seen) return
|
|
seen = 0
|
|
printf "%s\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s\n", f["modmask"], f["key"], f["keycode"], f["description"], f["dispatcher"], f["arg"]
|
|
}
|
|
/^bind/ { emit(); seen = 1; delete f; next }
|
|
seen && match($0, /^\t[a-z]+: /) { f[substr($0, 2, RLENGTH - 3)] = substr($0, RLENGTH + 1) }
|
|
END { emit() }
|
|
' |
|
|
while IFS=$'\x1f' read -r modmask key keycode description dispatcher arg; do
|
|
# Lua binds report their full display key ("SUPER + code:20"); the
|
|
# modifiers are already carried separately in modmask.
|
|
key="${key##* + }"
|
|
|
|
if [[ -z $key && $keycode != "0" ]]; then
|
|
key="code:$keycode"
|
|
fi
|
|
|
|
if [[ -z $key && -n $description ]]; then
|
|
key="${LUA_BIND_KEY_MAP["$modmask,$description"]}"
|
|
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
|
|
|
|
# The Copilot key just duplicates an existing binding, so keep it hidden
|
|
[[ $key == "code:201" ]] && continue
|
|
|
|
case "$key" in
|
|
comma) key="COMMA" ;;
|
|
grave) key="~" ;;
|
|
period) key="PERIOD" ;;
|
|
minus) key="MINUS" ;;
|
|
equal) key="EQUAL" ;;
|
|
slash) key="SLASH" ;;
|
|
esac
|
|
|
|
modifiers=$(modmask_to_text "$modmask")
|
|
arg="${arg//~\/.local\/share\/omarchy\/bin\//}"
|
|
|
|
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,"
|
|
echo "SHIFT ALT,D,Download Video from Web App,sendshortcut,SHIFT ALT,D,"
|
|
}
|
|
|
|
# Actions Omarchy binds to a second chord meant as an alternative, named one at
|
|
# a time. A rule would be wrong here: Alt + Tab and Shift + Alt + Tab both say
|
|
# "Reveal active window on top" while cycling opposite ways, and a media key is
|
|
# nobody's idea of an alternative to a Super chord. Only chords a user reaches
|
|
# for interchangeably belong on one row.
|
|
alternative_chord_actions() {
|
|
cat <<'ACTIONS'
|
|
Close window
|
|
Calculator
|
|
Toggle scratchpad
|
|
Move window to scratchpad
|
|
ACTIONS
|
|
}
|
|
|
|
# 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.
|
|
# - Puts an action's alternative chord on the row with the first one.
|
|
# - Prints display text and dispatch metadata as tab-separated fields.
|
|
parse_binding_records() {
|
|
awk -F, -v alternatives="$(alternative_chord_actions)" '
|
|
BEGIN {
|
|
# The column every row pads its chords to. Nothing is allowed past it: the
|
|
# menu renders in monospace, and a row that overruns pushes its arrow out
|
|
# of a column the eye reads straight down.
|
|
column = 35;
|
|
|
|
split(alternatives, named, "\n");
|
|
for (i in named) {
|
|
if (named[i] != "") shares_a_row[named[i]] = 1;
|
|
}
|
|
}
|
|
{
|
|
# 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
|
|
}
|
|
|
|
# An alternative chord joins the row the first one opened: "SUPER + W /
|
|
# SUPER + Q -> Close window". Both halves have to agree on what they run,
|
|
# since a label is only what a chord is called; an unresolved dispatcher
|
|
# says nothing at all, so it never counts as agreement.
|
|
if (action != "") {
|
|
action_key = action SUBSEP dispatcher SUBSEP arg;
|
|
together = "";
|
|
|
|
if (action in shares_a_row && dispatcher != "" && action_key in leads) {
|
|
together = chords[leads[action_key]] " / " key_combo;
|
|
}
|
|
|
|
if (together != "" && length(together) <= column) {
|
|
chords[leads[action_key]] = together;
|
|
} else {
|
|
# Also the path a pair too wide for the column takes: two rows in
|
|
# line beat one that juts out of it.
|
|
entries++;
|
|
chords[entries] = key_combo;
|
|
actions[entries] = action;
|
|
dispatchers[entries] = dispatcher;
|
|
args[entries] = arg;
|
|
leads[action_key] = entries;
|
|
}
|
|
}
|
|
}
|
|
END {
|
|
for (entry = 1; entry <= entries; entry++) {
|
|
printf "%-*s → %s\t%s\t%s\n", column, chords[entry], actions[entry], dispatchers[entry], args[entry];
|
|
}
|
|
}'
|
|
}
|
|
|
|
prioritize_entries() {
|
|
awk -F '\t' '
|
|
{
|
|
# Alternative chords are display only. Classifying on them would read
|
|
# "SUPER SHIFT + RETURN / SUPER SHIFT + B" as the alternate browser chord
|
|
# and drop the browser 20 rows down its own list, so match the chord that
|
|
# leads the row and ignore the rest.
|
|
line = $1
|
|
sub(/ \/ [^→]*→/, " →", line)
|
|
|
|
prio = 50
|
|
if (match(line, /Keybindings/)) prio = 0
|
|
if (match(line, /Omarchy menu/)) prio = 1
|
|
if (match(line, /Terminal/)) prio = 2
|
|
if (match(line, /Browser/) && !match(line, /Browser[[:space:]]*\(/) && !match(line, /SUPER SHIFT.*\+.*B.*→.*Browser/)) prio = 3
|
|
if (match(line, /File manager/) && !match(line, /File manager \(cwd\)/)) prio = 4
|
|
if (match(line, /Launch apps/)) 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, /Emojis/)) prio = 21
|
|
if (match(line, /Color picker/)) prio = 22
|
|
if (match(line, /Screenshot/)) prio = 23
|
|
if (match(line, /Screenrecording/)) prio = 24
|
|
if (match(line, /Tmux/)) prio = 25
|
|
if (match(line, /Herdr/)) prio = 26
|
|
if (match(line, /SUPER SHIFT.*\+.*B.*→.*Browser/)) prio = 27
|
|
if (match(line, /File manager \(cwd\)/)) prio = 28
|
|
if (match(line, /(Switch|Next|Former|Previous).*workspace/)) prio = 29
|
|
if (match(line, /Move window to workspace/)) prio = 30
|
|
if (match(line, /Move window silently to workspace/)) prio = 31
|
|
if (match(line, /Swap window/)) prio = 32
|
|
if (match(line, /Focus/)) prio = 33
|
|
if (match(line, /Move window$/)) prio = 34
|
|
if (match(line, /Resize window/)) prio = 35
|
|
if (match(line, /Expand window/)) prio = 36
|
|
if (match(line, /Shrink window/)) prio = 37
|
|
if (match(line, /scratchpad/)) prio = 38
|
|
if (match(line, /notification/)) prio = 39
|
|
if (match(line, /Toggle window transparency/)) prio = 40
|
|
if (match(line, /Toggle workspace gaps/)) prio = 41
|
|
if (match(line, /Toggle nightlight/)) prio = 42
|
|
if (match(line, /Toggle locking/)) prio = 43
|
|
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
|
|
if (match(line, /Tmux keybindings/)) prio = 100
|
|
if (match(line, /Herdr keybindings/)) prio = 101
|
|
|
|
# print "priority<TAB>record"
|
|
printf "%d\t%s\n", prio, $0
|
|
}' |
|
|
sort -k1,1n -k2,2 |
|
|
cut -f2-
|
|
}
|
|
|
|
# Drop repeated records while keeping the order Hyprland reported them in, so
|
|
# the chord a user declared first is the one that leads a merged entry.
|
|
dedupe_binding_records() {
|
|
awk '!seen[$0]++'
|
|
}
|
|
|
|
output_binding_records_uncached() {
|
|
local dynamic
|
|
|
|
build_lua_bind_cache
|
|
dynamic=$(dynamic_bindings)
|
|
|
|
{
|
|
[[ -n $dynamic ]] && printf '%s\n' "$dynamic"
|
|
static_bindings
|
|
} |
|
|
dedupe_binding_records |
|
|
parse_keycodes |
|
|
parse_binding_records |
|
|
prioritize_entries
|
|
|
|
# Fail when Hyprland reported no binds so the fallout of a broken hyprctl
|
|
# is never cached.
|
|
[[ -n $dynamic ]]
|
|
}
|
|
|
|
keybindings_cache_key() {
|
|
{
|
|
printf 'v15\n'
|
|
hyprctl devices 2>/dev/null | grep -F 'active keymap:'
|
|
cat "$key_names_file" 2>/dev/null
|
|
hyprctl binds 2>/dev/null
|
|
} | sha256sum | awk '{ print $1 }'
|
|
}
|
|
|
|
refresh_keybindings_cache() {
|
|
local cache_dir="$1"
|
|
local cache_file="$2"
|
|
local cache_name="$3"
|
|
local tmp_file
|
|
|
|
tmp_file=$(mktemp "$cache_dir/keybindings.XXXXXX") || return 1
|
|
|
|
if output_binding_records_uncached >"$tmp_file"; then
|
|
mv "$tmp_file" "$cache_file"
|
|
find "$cache_dir" -maxdepth 1 -type f -name 'keybindings-*.records' ! -name "$cache_name" -delete 2>/dev/null || true
|
|
else
|
|
rm -f "$tmp_file"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
output_binding_records() {
|
|
local cache_dir cache_file cache_name
|
|
|
|
cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/omarchy"
|
|
cache_name="keybindings-$(keybindings_cache_key).records"
|
|
cache_file="$cache_dir/$cache_name"
|
|
|
|
if [[ -s $cache_file ]]; then
|
|
cat "$cache_file"
|
|
elif mkdir -p "$cache_dir" 2>/dev/null && refresh_keybindings_cache "$cache_dir" "$cache_file" "$cache_name"; then
|
|
cat "$cache_file"
|
|
else
|
|
output_binding_records_uncached
|
|
fi
|
|
}
|
|
|
|
output_keybindings() {
|
|
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() {
|
|
local dispatcher="$1"
|
|
local arg="$2"
|
|
|
|
case "$dispatcher" in
|
|
exec)
|
|
[[ -n $arg ]] && dispatch_exec_binding "$arg"
|
|
;;
|
|
sendshortcut)
|
|
[[ -n $arg ]] && dispatch_sendshortcut_binding "$arg"
|
|
;;
|
|
lua)
|
|
[[ -n $arg ]] && hyprctl dispatch "$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)
|
|
selection=$(cut -f1 <<<"$records" |
|
|
omarchy-menu-select 'Keybindings' -- --width 800 --height 500)
|
|
|
|
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
|