diff --git a/config/dot/hypr/input.lua b/config/dot/hypr/input.lua index 9fcbe58..13d1c89 100644 --- a/config/dot/hypr/input.lua +++ b/config/dot/hypr/input.lua @@ -10,9 +10,9 @@ local prefs = require("prefs") hl.config({ input = { kb_layout = prefs.get("keyboardLayout", "us"), - kb_variant = "", + kb_variant = prefs.get("keyboardVariant", ""), kb_model = "", - kb_options = "", + kb_options = prefs.get("keyboardOptions", ""), kb_rules = "", numlock_by_default = prefs.get("numlockByDefault", true), diff --git a/config/dot/quickshell/modules/settings/ShortcutsPage.qml b/config/dot/quickshell/modules/settings/ShortcutsPage.qml index 38453d7..db6f505 100644 --- a/config/dot/quickshell/modules/settings/ShortcutsPage.qml +++ b/config/dot/quickshell/modules/settings/ShortcutsPage.qml @@ -21,6 +21,25 @@ SettingsPage { // when nothing is being captured. Held here rather than per row so that // starting a new capture cancels any other. property string capturingChord: "" + readonly property string storedXkbOptions: String(DesktopPreferences.get("keyboardOptions") ?? "") + + function xkbOptions(): var { + return root.storedXkbOptions + .split(",") + .map(option => option.trim()) + .filter(option => option !== ""); + } + + function currentXkbOption(prefix: string): string { + return root.xkbOptions().find(option => option.indexOf(prefix) === 0) ?? ""; + } + + function setXkbOption(prefix: string, option: string): void { + const options = root.xkbOptions().filter(option => option.indexOf(prefix) !== 0); + if (option !== "") + options.push(option); + SystemSettings.commitPreference("keyboardOptions", options.join(",")); + } title: "Input & Shortcuts" lede: "The Forge mental model, carried forward into native tiling." @@ -35,6 +54,52 @@ SettingsPage { // they are real controls. TextEntryRow { setting: "keyboardLayout"; placeholder: "us" } TextEntryRow { setting: "keyboardVariant"; placeholder: "none" } + + ChoiceGrid { + width: parent.width + label: "Caps Lock" + detail: "Keep it conventional, or turn a prime keyboard position into Escape or Control" + current: root.currentXkbOption("caps:") + options: [ + { value: "", label: "Standard" }, + { value: "caps:escape_shifted_capslock", label: "Esc ยท Shift for Caps" }, + { value: "caps:escape", label: "Escape" }, + { value: "caps:ctrl_modifier", label: "Control" } + ] + onPicked: value => root.setXkbOption("caps:", value) + } + + ChoiceGrid { + width: parent.width + label: "Compose key" + detail: "Type accented characters and symbols with memorable key sequences" + current: root.currentXkbOption("compose:") + options: [ + { value: "", label: "Off" }, + { value: "compose:ralt", label: "Right Alt" }, + { value: "compose:rwin", label: "Right Super" }, + { value: "compose:menu", label: "Menu" } + ] + onPicked: value => root.setXkbOption("compose:", value) + } + + ChoiceGrid { + width: parent.width + label: "Layout switching" + detail: "Used when Keyboard layout contains more than one comma-separated layout" + current: root.currentXkbOption("grp:") + options: [ + { value: "", label: "Off" }, + { value: "grp:win_space_toggle", label: "Super + Space" }, + { value: "grp:alt_shift_toggle", label: "Alt + Shift" }, + { value: "grp:ctrl_shift_toggle", label: "Ctrl + Shift" }, + { value: "grp:caps_toggle", label: "Caps Lock" } + ] + onPicked: value => root.setXkbOption("grp:", value) + } + + // Presets preserve every option outside their own category. The raw + // value remains visible for less common xkeyboard-config features. TextEntryRow { setting: "keyboardOptions"; placeholder: "compose:ralt" } SliderRow { setting: "keyRepeatDelay" } SliderRow { setting: "keyRepeatRate" } diff --git a/tests/quickshell/xkb-presets-contract.sh b/tests/quickshell/xkb-presets-contract.sh new file mode 100755 index 0000000..48b36db --- /dev/null +++ b/tests/quickshell/xkb-presets-contract.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +# Common XKB behavior should be discoverable without hiding the raw option +# string from advanced users. This is source-only so it never remaps the live +# keyboard while the desktop is in use. + +set -euo pipefail + +repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +page="$repo_dir/config/dot/quickshell/modules/settings/ShortcutsPage.qml" +input="$repo_dir/config/dot/hypr/input.lua" + +fail() { + printf 'xkb presets contract: %s\n' "$1" >&2 + exit 1 +} + +for needle in \ + 'function currentXkbOption(prefix: string): string' \ + 'function setXkbOption(prefix: string, option: string): void' \ + 'label: "Caps Lock"' \ + 'value: "caps:escape_shifted_capslock"' \ + 'value: "caps:ctrl_modifier"' \ + 'label: "Compose key"' \ + 'value: "compose:ralt"' \ + 'label: "Layout switching"' \ + 'value: "grp:win_space_toggle"' \ + 'SystemSettings.commitPreference("keyboardOptions"' \ + 'TextEntryRow { setting: "keyboardOptions"'; do + rg -Fq "$needle" "$page" || fail "Shortcuts is missing $needle" +done + +# Picking one category must replace only that category, preserving advanced +# options from every other group. +rg -Fq 'option.indexOf(prefix) !== 0' "$page" \ + || fail 'preset updates do not preserve unrelated XKB options' + +rg -Fq 'kb_variant = prefs.get("keyboardVariant", "")' "$input" \ + || fail 'Hyprland does not replay the stored keyboard variant' +rg -Fq 'kb_options = prefs.get("keyboardOptions", "")' "$input" \ + || fail 'Hyprland does not replay the stored keyboard options' + +printf 'xkb presets contract: PASS\n'