#!/usr/bin/env bash set -euo pipefail layouts=${1:-} variants=${2:-} active_index=${3:-0} keyboard=${4:-} remap_b64=${5:-} config="${HOME}/.config/hypr/input.lua" symbols_dir="${HOME}/.config/xkb/symbols" symbols_file="${symbols_dir}/omarchy-keymaps" if [[ ! "$layouts" =~ ^[[:alnum:]_,-]+$ ]] || [[ -z "$layouts" ]]; then printf '%s\n' "Invalid keyboard layout list" >&2 exit 2 fi if [[ ! "$variants" =~ ^[[:alnum:]_,-]*$ ]]; then printf '%s\n' "Invalid keyboard variant list" >&2 exit 2 fi if [[ ! "$active_index" =~ ^[0-9]+$ ]]; then printf '%s\n' "Invalid active layout index" >&2 exit 2 fi if [[ ! "$remap_b64" =~ ^[A-Za-z0-9+/=]*$ ]]; then printf '%s\n' "Invalid keyboard remap payload" >&2 exit 2 fi if [[ ! -f "$config" ]]; then printf '%s\n' "Hyprland input config not found: $config" >&2 exit 1 fi remap_body="" if [[ -n "$remap_b64" ]]; then remap_body=$(base64 -d <<<"$remap_b64") fi # The remap symbols come from Model.js's curated KEY_TABLE, never free text, # so this layout string is always safe to hand to xkbcli/Hyprland as-is. # # The component is prefixed to the first layout only, and the symbols file # spells out all four XKB groups itself. Appending it per layout instead # collides with the ":N" group suffix the XKB rules add to each element, # collapsing every group into group 1 and losing the per-group layout names. augmented_layouts="$layouts" if [[ -n "$remap_body" ]]; then augmented_layouts="omarchy-keymaps(remap)+${layouts}" fi tmp="" symbols_scratch="" cleanup() { if [[ -n "$tmp" ]]; then rm -f "$tmp"; fi if [[ -n "$symbols_scratch" ]]; then rm -rf "$symbols_scratch"; fi } trap cleanup EXIT # kb_options is never written by this plugin, only read, so that the keymap is # validated against the options actually in force. current_options=$(hyprctl getoption input:kb_options -j | jq -r '.str // ""') if [[ -n "$remap_body" ]]; then # Validate against a scratch copy of the new symbols file, which shadows any # real one through --include, before touching anything real. A remap that # does not compile can then never leave the keyboard unusable. symbols_scratch=$(mktemp -d) mkdir -p "$symbols_scratch/symbols" { printf '%s\n' 'xkb_symbols "remap" {' printf '%s\n' "$remap_body" printf '%s\n' '};' } >"$symbols_scratch/symbols/omarchy-keymaps" fi compile_args=(--test --include-defaults --layout "$augmented_layouts" --variant "$variants" --options "$current_options") if [[ -n "$symbols_scratch" ]]; then compile_args=(--include "$symbols_scratch" "${compile_args[@]}") fi # xkbcli only exits non-zero when the keymap cannot be built at all (a missing # component, say); it recovers from a bad rule inside an included file and still # reports success, so treat anything it logs as an error as a failure too. validation=$(xkbcli compile-keymap "${compile_args[@]}" 2>&1 >/dev/null) || validation_failed=1 if [[ -n "${validation_failed:-}" ]] || [[ "$validation" == *ERROR* ]]; then printf '%s\n' "Could not validate the requested keyboard settings" >&2 printf '%s\n' "$validation" >&2 exit 1 fi if [[ -n "$remap_body" ]]; then mkdir -p "$symbols_dir" mv "$symbols_scratch/symbols/omarchy-keymaps" "$symbols_file" else # No remaps left: drop the generated file so nothing stale is reported back # to the UI, which reads its configured pairs straight out of it. rm -f "$symbols_file" fi tmp=$(mktemp "${config}.tmp.XXXXXX") awk -v layouts="$augmented_layouts" -v variants="$variants" ' function indent(line, result) { result = line sub(/[^[:space:]].*/, "", result) return result } { if ($0 ~ /^[[:space:]]*kb_layout[[:space:]]*=/) { print indent($0) "kb_layout = \"" layouts "\","; has_layout = 1 next } if ($0 ~ /^[[:space:]]*kb_variant[[:space:]]*=/) { print indent($0) "kb_variant = \"" variants "\","; has_variant = 1 next } if ($0 ~ /^[[:space:]]*kb_options[[:space:]]*=/) { if (!has_layout) { print indent($0) "kb_layout = \"" layouts "\","; has_layout = 1 } if (!has_variant) { print indent($0) "kb_variant = \"" variants "\","; has_variant = 1 } } print } END { if (!has_layout || !has_variant) { print "" print "-- Managed by omarchy-keymaps." print "hl.config({" print " input = {" if (!has_layout) print " kb_layout = \"" layouts "\","; if (!has_variant) print " kb_variant = \"" variants "\","; print " }," print "})" } } ' "$config" > "$tmp" chmod --reference="$config" "$tmp" mv "$tmp" "$config" tmp="" hyprctl reload >/dev/null if [[ -n "$keyboard" ]]; then hyprctl switchxkblayout "$keyboard" "$active_index" >/dev/null || true fi