Preserve Num Lock during compatible Cua foreground input
This commit is contained in:
5 files changed
+488
-77
No files matched your search
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"files": {
|
||||
"CMakeLists.txt": "4a2027383ae39b052c4cf30732810f93ee1170b7987777aa9e8ea282abb17731",
|
||||
"CMakeLists.txt": "7e874a595e1abd708cb0626bd9f0f58d79b4b4bbc6d99b45a0cbe1c5c53b43bc",
|
||||
"LICENSE.md": "c0779290c1d4783169aa3dbfb55feb505e563ef8a004bbf55298ceffcfbda8d9",
|
||||
"SOURCE-PROVENANCE.json": "54f514664c84e1358a435f29cd6befd5661b0b133d76997191c000b10f021a75",
|
||||
"cmake/DetectHyprlandAPI.cmake": "216133ec0eb141c3696bf3770a23e63e46521c9e91a0245a7cb75c20a75ba2c5",
|
||||
@@ -9,17 +9,17 @@
|
||||
"include/cua_hyprland/session.hpp": "e6a968e4f2ac28122cb7413a0e318f6222d2a0a1b7b0c45f4ab419a78639ebea",
|
||||
"include/cua_hyprland/status.hpp": "56a9656647c0f4eeb0c588cd4b98a77df198d1f90421f973e9a80e6802f61495",
|
||||
"src/drag_geometry.hpp": "c5b783d15ff197f22938f08f8d176bab5d45544fe989d150f15cb99295acedb8",
|
||||
"src/foreground_route.hpp": "bf07e9ada1a3e25dd85611d5f3f4f725df50b1b8fc1d4c4903fb5c6dc9c3d5f8",
|
||||
"src/foreground_route.hpp": "4aa016c237b33c15e352a9f5f64bbbca7e1a9c1671ffd593a95a0d87994fb519",
|
||||
"src/inject_server.cpp": "0935283580c50fcf0e4ad956f858885700536002b1d86d2f078da9c4404ee9e8",
|
||||
"src/inject_server.hpp": "67de008b4d6983371207bb22a57bab154b1dedda9b38d1207d3ac8cb379382eb",
|
||||
"src/input_client_deadline.hpp": "00a91a789ff698820607449ff7152e2fb50d0f315e0fa9c5c3855752bfffe2ef",
|
||||
"src/input_experiment.cpp": "358fa86abd21dbfda8460b21b3568d0199e47ed79ef6c5dd0c7e0c6d4c026290",
|
||||
"src/input_experiment.cpp": "7017748c782b64b0bc1d257f4ade1a8d46fd19ce940ecc21f22d628614fcef37",
|
||||
"src/input_experiment.hpp": "9da2ddab7f0de6e9cf02aea53e9119acef17ef8513af849bb5aa3d137ebd12c3",
|
||||
"src/input_grant.hpp": "90b544b2f559bacd920b85ab5915f09ff201de3c05a052ce1b71ff71f767e6a6",
|
||||
"src/keyboard_layout.hpp": "66d7d1ace6357c5be26f4a9b57730709fe21e30267437051c62b6697eb073300",
|
||||
"src/keyboard_layout.hpp": "bc2ec039ac1974caebbb66fe4acb7a2a81832c9054b3aa644a9adcc8954a2467",
|
||||
"src/owned_socket_path.hpp": "8e784656d944c700f3ded383c93a8cadf846dabdaa4cc12673bcb637c26d9fd1",
|
||||
"src/passive_pointer_target.hpp": "ede36fd9fd6e95ae5923c751f12591084392be9d2270eb06ad64eb4f245169fa",
|
||||
"src/plugin.cpp": "e36817f12b47f317dde300cfa6820ce2d408c8fe5bfea3a3e5c727b86d461d3e",
|
||||
"src/plugin.cpp": "712fd73ef8e9046e0fd91531b7bf5da50ce37ccca9df174160137e1924a74b09",
|
||||
"src/primary_trace.cpp": "e9468d1a3f3be2a90d47bf8c4a638ad8a60fe10b6297582ab7825751cb707aa9",
|
||||
"src/primary_trace.hpp": "8d62535fb0b24a02bb80d9a8dcd540b39204afb2f3b4bcb5cabd5275c3b5eaa7",
|
||||
"src/protocol.cpp": "bd083d65efb05e80946566dc535b1a6fadaa581c66ec327eff41796feba795b1",
|
||||
@@ -32,24 +32,25 @@
|
||||
"tests/desktop_fault_policy_fixture.cpp": "ac24d675ebc64cc98148e852eb5aba5858bffc06332678d14276d04b317aaf15",
|
||||
"tests/desktop_fault_policy_test.py": "c3f624c0239036babd23eaaf1bb6b722f3a0c3321a5d3d6668ae7ab4910ca95c",
|
||||
"tests/drag_geometry_test.cpp": "d32ea649d008fc051fe18555d6fbc54ba5d057b61880b648c5df8aa076a53fbc",
|
||||
"tests/foreground_modifiers_test.py": "abf0ddde2d51c6639c8bdca8fdda51cdc8f922b57575a00fc8925c683ccc3bb2",
|
||||
"tests/foreground_route_test.cpp": "1024168828b13ee6abd8242941e73c042e9381b39108e3ada74823039c7e7932",
|
||||
"tests/input_client_deadline_test.cpp": "d62373a7815d531f1269c9a838773595f43e8bcef6482fa140edb162e59a6cac",
|
||||
"tests/input_grant_test.cpp": "1f327b7ee678189ebad6a50bb1b9bd06767521cebc9cfb478d92de4a8bf7e7fe",
|
||||
"tests/keyboard_layout_test.cpp": "80f76d16896bbb77269a52e12c18c079d642618d29d0608f23a3d7ae2844f21f",
|
||||
"tests/keyboard_layout_test.cpp": "3bb0fade4675d7ad92a81eb4a1c5201dd1d01bcc418b7ea59a4284dc235e5fb2",
|
||||
"tests/mock-hyprland/mock.hpp": "3aeb1a4b9d6b83506b66c129d3fa812330fad4509fe218a0dbb99dd2bb5b6319",
|
||||
"tests/mock-hyprland/src/config/values/types/BoolValue.hpp": "47cf2cca89f71a273573968cb9b8ba46a1496841d6c892756ebf123500a7ecb3",
|
||||
"tests/mock-hyprland/src/plugins/PluginAPI.hpp": "5654d90ec9090a88bea3d31f8a79617d4c79742b09068648e64448319d395110",
|
||||
"tests/owned_socket_path_test.cpp": "eaff6b5c6f148eca6c8650ee3dc212a5e892f002ee4f68b2a1290c7205ee7e42",
|
||||
"tests/passive_pointer_target_test.cpp": "2aeef1de1dc8932b26ab8c41b83fb16a4289ff96c177a5816088f07b9a168948",
|
||||
"tests/plugin_api_test.cpp": "1e7e200c309996ee945c88e172273ae942be2837e24564d422dee79d8b77d8a2",
|
||||
"tests/plugin_input_lifetime_test.cpp": "9d291fc6d7de5e9a80a07c2de16de85739e9ba6fe2ceace371f64b94af4724c0",
|
||||
"tests/plugin_input_lifetime_test.cpp": "82e57b335ea1216ea24cca07fe4feebafebdebb779785fde20b8dab6ee222e1b",
|
||||
"tests/protocol_test.cpp": "119cfe0df81c0c00036a2d181764eda7601d6ee72459c2275a96226d4f670447",
|
||||
"tests/seat_lifetime_test.cpp": "b07570edbe0a142f97c54560eeb93e8327c435ab3b8cbc7496d55175e387b78a",
|
||||
"tests/status_test.cpp": "b8990efc53ec3820cfe498c920b9220c4b70615ad585468558e032e7619e32f3",
|
||||
"tests/transport_test.cpp": "deef114a950a27eaff0a530165ddf7db0bfc0fe7e8fb55bdbb66135c8e0c04c9",
|
||||
"verify.py": "fb35d62313ff4661f892f88666919b33b160f8b6d4fb2d5d52610708bf7f4a54"
|
||||
},
|
||||
"patch_sha256": "5847cd2c300a9b3c7dc1e836722a0e2b222892e308290195d973300c7f74e0e4",
|
||||
"patch_sha256": "e74d9021111a5c217b12f3e13c1bbdd260e829c60db5fc2f7293ba4846e1a7d7",
|
||||
"schema": 1,
|
||||
"upstream_manifest_sha256": "54f514664c84e1358a435f29cd6befd5661b0b133d76997191c000b10f021a75",
|
||||
"upstream_revision": "cc54254464c0c9aebfd6547fe7e4a0ceaf0456d7"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# shellcheck shell=bash disable=SC2034,SC2154
|
||||
pkgname=cua-hyprland-plugin
|
||||
pkgver=0.26.1
|
||||
pkgrel=4
|
||||
pkgrel=5
|
||||
pkgdesc='Cua input candidate for reviewed profile omarchy-hyprland-0562r3-remaps'
|
||||
arch=('x86_64')
|
||||
url='https://github.com/trycua/cua'
|
||||
@@ -14,8 +14,8 @@ makedepends=('cmake>=3.30' 'ninja' 'pkgconf' 'gcc' 'patch')
|
||||
options=('!strip' '!debug' '!lto')
|
||||
_stem='cua-hyprland-plugin-0.26.1-cc54254464c0c9aebfd6547fe7e4a0ceaf0456d7'
|
||||
_archive_sha256='47bca9e018f32f4fcfe683f91c7475c60368f3b65d318cc35c1f2de88a4ee9ab'
|
||||
_kit_sha256='aa88498fc9635e493a64aaf4e2a08cdb5249d576d7e00c3cf100799d5c9f23f2'
|
||||
_profile_sha256='ee9b7cf5fbcc78ab169a87b5edb061a85c4425f7ddf41c90328cf020550c5f9a'
|
||||
_kit_sha256='089f447e11cacd8c2d3d6cd56528776417c51d353b9677c47c42bba1ef79c9f9'
|
||||
_profile_sha256='fc3034649af98f7f81178ef575660143c249664b9ff5d0e1ccaf79d7580df17b'
|
||||
_verifier_sha256='480f649cbd35a0ddc7f49dc0a3a44785402c9e0ec9653b0f8ea4965d52d7f900'
|
||||
_cxx="${CUA_RELEASE_CXX:-/usr/bin/g++}"
|
||||
_download_name='cua-hyprland-plugin-0.26.1-cc54254464c0c9aebfd6547fe7e4a0ceaf0456d7-profile-omarchy-stable-20260910-kit-1.1.0-5dacea79a6a5927e59fca7b51e6c04e94fa45133ac1b8fb7f4e1f442d113eb9a-67f89ceb47edd973aa748820eadd02ddbfb3d3e4.tar.gz'
|
||||
@@ -24,17 +24,17 @@ source=('https://github.com/trycua/cua/releases/download/cua-hyprland-kit-v1.1.0
|
||||
'PROFILE.json')
|
||||
noextract=("$_download_name")
|
||||
sha256sums=('a89bfa7f6490f598719dfabdd2a5badc137e27b0764fd25e6a05e1f969433520'
|
||||
'ee9b7cf5fbcc78ab169a87b5edb061a85c4425f7ddf41c90328cf020550c5f9a')
|
||||
'fc3034649af98f7f81178ef575660143c249664b9ff5d0e1ccaf79d7580df17b')
|
||||
|
||||
# Downstream inputs are also checked explicitly when makepkg integrity is skipped.
|
||||
declare -gA _downstream_sha256=(
|
||||
['independent-keymaps.patch']='5847cd2c300a9b3c7dc1e836722a0e2b222892e308290195d973300c7f74e0e4'
|
||||
['DOWNSTREAM-PROVENANCE.json']='e87949fa29d8c006f1c829cb81e640980b349a704bedb5f1d4fe7dc47746994e'
|
||||
['independent-keymaps.patch']='e74d9021111a5c217b12f3e13c1bbdd260e829c60db5fc2f7293ba4846e1a7d7'
|
||||
['DOWNSTREAM-PROVENANCE.json']='e0c95350ec3ff2dd54a05e9377d79ddffffdb1cf494d05553a90207a3a919f7e'
|
||||
['downstream.py']='7c9725805ad038737bd4d346c85f27991ce9ddfff436a32ebf515cad1bf98ca1'
|
||||
['downstream_test.py']='7b9a992979b22a5e061fdf011ac6be59a3f1ae6e0d7c0a8ff631f74e71984c5a'
|
||||
)
|
||||
source+=('independent-keymaps.patch' 'DOWNSTREAM-PROVENANCE.json' 'downstream.py' 'downstream_test.py')
|
||||
sha256sums+=('5847cd2c300a9b3c7dc1e836722a0e2b222892e308290195d973300c7f74e0e4' 'e87949fa29d8c006f1c829cb81e640980b349a704bedb5f1d4fe7dc47746994e' '7c9725805ad038737bd4d346c85f27991ce9ddfff436a32ebf515cad1bf98ca1' '7b9a992979b22a5e061fdf011ac6be59a3f1ae6e0d7c0a8ff631f74e71984c5a')
|
||||
sha256sums+=('e74d9021111a5c217b12f3e13c1bbdd260e829c60db5fc2f7293ba4846e1a7d7' 'e0c95350ec3ff2dd54a05e9377d79ddffffdb1cf494d05553a90207a3a919f7e' '7c9725805ad038737bd4d346c85f27991ce9ddfff436a32ebf515cad1bf98ca1' '7b9a992979b22a5e061fdf011ac6be59a3f1ae6e0d7c0a8ff631f74e71984c5a')
|
||||
|
||||
_verify_download() {
|
||||
python3 -I - "$SRCDEST/$_download_name" "$_download_sha256" "$srcdir" "$1" "$SRCDEST/PROFILE.json" <<'CUA_DOWNLOAD_PY'
|
||||
@@ -75,10 +75,10 @@ require(payload.keys() == expected.keys(), 'outer kit inventory mismatch')
|
||||
# files as -2. Derive a version-only profile with the original source/tooling
|
||||
# and byte checks intact; record its own profile and kit provenance digests.
|
||||
profile_data = Path(profile_path).read_bytes()
|
||||
require(digest(profile_data) == 'ee9b7cf5fbcc78ab169a87b5edb061a85c4425f7ddf41c90328cf020550c5f9a',
|
||||
require(digest(profile_data) == 'fc3034649af98f7f81178ef575660143c249664b9ff5d0e1ccaf79d7580df17b',
|
||||
'local profile checksum mismatch')
|
||||
profile = json.loads(payload['PROFILE.json'])
|
||||
profile.update(profile_id='omarchy-hyprland-0562r3-remaps', package_release=4)
|
||||
profile.update(profile_id='omarchy-hyprland-0562r3-remaps', package_release=5)
|
||||
profile['hyprland']['package_version'] = '0.56.2-3'
|
||||
require(json.loads(profile_data) == profile, 'local profile changes more than the reviewed package revision')
|
||||
payload['PROFILE.json'] = profile_data
|
||||
@@ -86,7 +86,7 @@ provenance = json.loads(payload['KIT-PROVENANCE.json'])
|
||||
provenance['profile_sha256'] = digest(profile_data)
|
||||
payload['KIT-PROVENANCE.json'] = (json.dumps(provenance, sort_keys=True, indent=2) + '\n').encode()
|
||||
recipe = payload['PKGBUILD'].decode()
|
||||
for old, new in [('pkgrel=2\n', 'pkgrel=4\n'), ('omarchy-stable-20260910', profile['profile_id']),
|
||||
for old, new in [('pkgrel=2\n', 'pkgrel=5\n'), ('omarchy-stable-20260910', profile['profile_id']),
|
||||
('hyprland=0.56.2-2', 'hyprland=0.56.2-3'),
|
||||
('5dacea79a6a5927e59fca7b51e6c04e94fa45133ac1b8fb7f4e1f442d113eb9a', digest(profile_data)),
|
||||
('7beb736adfd334eed52e84070177634269e3a09f8bb25971b38606933ff4c997', digest(payload['KIT-PROVENANCE.json']))]:
|
||||
@@ -94,10 +94,10 @@ for old, new in [('pkgrel=2\n', 'pkgrel=4\n'), ('omarchy-stable-20260910', profi
|
||||
payload['PKGBUILD'] = recipe.encode()
|
||||
payload['SHA256SUMS'] = ''.join(f'{digest(body)} {name}\n' for name, body in sorted(payload.items())
|
||||
if name != 'SHA256SUMS').encode()
|
||||
expected.update({'PROFILE.json': 'ee9b7cf5fbcc78ab169a87b5edb061a85c4425f7ddf41c90328cf020550c5f9a',
|
||||
'KIT-PROVENANCE.json': 'aa88498fc9635e493a64aaf4e2a08cdb5249d576d7e00c3cf100799d5c9f23f2',
|
||||
'PKGBUILD': '0e71ddab36e5d9214c35811f2cbf89cc98e5b4fdf6656f85e6b3885d07a1c021',
|
||||
'SHA256SUMS': '404cf40875ab14b8af84847abd4674945b1c7ab2e0bfb1605fa11e2d85d789db'})
|
||||
expected.update({'PROFILE.json': 'fc3034649af98f7f81178ef575660143c249664b9ff5d0e1ccaf79d7580df17b',
|
||||
'KIT-PROVENANCE.json': '089f447e11cacd8c2d3d6cd56528776417c51d353b9677c47c42bba1ef79c9f9',
|
||||
'PKGBUILD': '0cbf2cd34c3c5038a5ed51e6bf84acd81844e4c2bb08ad7203959201bba61da9',
|
||||
'SHA256SUMS': 'd01b9e0be4c5bcf84cc2ecef9f11f44cedfc1ca5b31afbfcf52aa2efbd636a09'})
|
||||
for name, content in payload.items():
|
||||
require(digest(content) == expected[name], 'derived kit checksum mismatch: ' + name)
|
||||
require(srcdir.is_dir() and not srcdir.is_symlink(), 'srcdir must be a real directory')
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"sha256": "da8fcacf347bcbed83edc40108c6e2298da095e22246bd764e9bb382786cebb2"
|
||||
},
|
||||
"kit_version": "1.1.0",
|
||||
"package_release": 4,
|
||||
"package_release": 5,
|
||||
"profile_id": "omarchy-hyprland-0562r3-remaps",
|
||||
"runtime": {
|
||||
"basename": "libstdc++.so.6.0.36",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Optional Cua Hyprland plugin
|
||||
|
||||
This package targets **Omarchy stable x86_64**, with Inkscape `1.4.4-6` and two independent background-input lanes. Package release `4` adds an Omarchy patch for independent agent keymaps and operation-specific foreground checks; the upstream native qualification below covers the unpatched source, not this change. Cua's native qualification is recorded in [the kit's qualification record](https://github.com/trycua/cua/releases/download/cua-hyprland-kit-v1.1.0-omarchy-stable-20260910/QUALIFICATION.md) and [Cua #3698](https://github.com/trycua/cua/pull/3698). Omabot replay and Omarchy's merge decision are recorded in [omarchy-pkgs #346](https://github.com/omacom/omarchy-pkgs/pull/346). Scheduling the recipe does not expand the qualified stable target.
|
||||
This package targets **Omarchy stable x86_64**, with Inkscape `1.4.4-6` and two independent background-input lanes. Package release `5` includes the Omarchy patch for independent agent keymaps, operation-specific foreground checks, and compatible Num Lock state; the upstream native qualification below covers the unpatched source, not this change. Cua's native qualification is recorded in [the kit's qualification record](https://github.com/trycua/cua/releases/download/cua-hyprland-kit-v1.1.0-omarchy-stable-20260910/QUALIFICATION.md) and [Cua #3698](https://github.com/trycua/cua/pull/3698). Omabot replay and Omarchy's merge decision are recorded in [omarchy-pkgs #346](https://github.com/omacom/omarchy-pkgs/pull/346). Scheduling the recipe does not expand the qualified stable target.
|
||||
|
||||
The plugin is optional. Cua Driver works independently, and installation does not load the plugin or enable input. The package follows the normal edge-to-RC-to-stable promotion path instead of the fast release ring. Its PKGBUILD limits builds to x86_64; only stable x86_64 is a qualified target.
|
||||
|
||||
@@ -12,7 +12,7 @@ It is not a repackaging of the unmodified 0.24.0 plugin.
|
||||
|
||||
The qualified upstream Driver pairing is `cua-driver-bin 0.27.0-1`, with input protocol v3. Driver 0.27.0 contains the bounded stale-geometry retry validated with the upstream module; its production plugin source is the base for the downstream patch used here. Discovery protocol v2 is separate. A newer Driver release is a changed pairing and requires affected replay before promotion.
|
||||
|
||||
Profile `omarchy-hyprland-0562r3-remaps`, kit tooling `1.1.0`, and package release `4` pin:
|
||||
Profile `omarchy-hyprland-0562r3-remaps`, kit tooling `1.1.0`, and package release `5` pin:
|
||||
|
||||
- Hyprland `0.56.2-3`, headers `0.56.2`, and measured executable/header hashes.
|
||||
- GCC `16.2.1 20260810`, including compiler bytes and emitted ELF identity.
|
||||
@@ -54,7 +54,9 @@ Each background lane owns a canonical US keymap and independent modifier state.
|
||||
|
||||
Plain click, scroll, drag, and foreground activation do not require a canonical keyboard layout. Foreground keys still use the primary seat: the plugin checks the requested key and modifier sequence against its actual XKB map before activation or input. Unrelated remaps are accepted; a sequence whose symbols or modifier/lock transitions differ from the canonical meaning is refused with `unsupported_layout`. Arbitrary foreground layout translation remains outside protocol v3.
|
||||
|
||||
Both routes retain target/conflict checks and cancellation on desktop/keymap changes. `hyprctl -j cua:status` exposes `keyboard_layout_independent: true` for installers to distinguish this implementation from an older mapped module. The marker does not identify every future package revision; plugin updates still require a fresh desktop session.
|
||||
Foreground typing preserves Num Lock and admits a requested key sequence only when its symbols and shortcut semantics still match the canonical meaning. Num Lock does not block unaffected letters, top-row digits, Enter, or compatible shortcuts; a keypad sequence whose meaning changes is refused. Caps Lock, other unsupported lock states, held or latched modifiers, and nonzero layout groups remain guarded.
|
||||
|
||||
Both routes retain target/conflict checks and cancellation on desktop/keymap changes. `hyprctl -j cua:status` exposes `keyboard_layout_independent: true` and `foreground_numlock_compatible: true` for installers to distinguish this implementation from an older mapped module. The marker does not identify every future package revision; plugin updates still require a fresh desktop session.
|
||||
|
||||
## Historical upstream qualification
|
||||
|
||||
@@ -102,7 +104,7 @@ background refusal never authorizes a hidden foreground fallback or unlock.
|
||||
Build the unsigned candidate in edge:
|
||||
|
||||
```sh
|
||||
bin/repo build --package cua-hyprland-plugin --arch x86_64 --mirror edge
|
||||
./bin/build --package cua-hyprland-plugin --arch x86_64 --mirror edge
|
||||
```
|
||||
|
||||
In a fresh worker matching the reviewed profile:
|
||||
@@ -140,7 +142,7 @@ kit-provenance digest:
|
||||
```sh
|
||||
python3 /usr/share/cua-hyprland-plugin/profile_verify.py \
|
||||
--kit /usr/share/cua-hyprland-plugin \
|
||||
--kit-sha256 aa88498fc9635e493a64aaf4e2a08cdb5249d576d7e00c3cf100799d5c9f23f2 \
|
||||
--kit-sha256 089f447e11cacd8c2d3d6cd56528776417c51d353b9677c47c42bba1ef79c9f9 \
|
||||
--consumer /usr/lib/cua/hyprland/cua-hyprland-plugin.so
|
||||
```
|
||||
|
||||
@@ -172,7 +174,7 @@ hyprctl reload
|
||||
hyprctl -j cua:status
|
||||
```
|
||||
|
||||
Continue only when status reports `keyboard_layout_independent: true`, input protocol v3, input capability, socket paths, and the expected compositor identity. Do not change `kb_layout`, `kb_options`, or NumLock for background input. If you previously followed the stock-US override instructions, remove only that Cua-specific override and reload to restore your underlying personal settings.
|
||||
Continue only when status reports `keyboard_layout_independent: true`, `foreground_numlock_compatible: true`, input protocol v3, input capability, socket paths, and the expected compositor identity. Do not change `kb_layout`, `kb_options`, or NumLock for background input. If you previously followed the stock-US override instructions, remove only that Cua-specific override and reload to restore your underlying personal settings.
|
||||
|
||||
Start Driver with `CUA_DRIVER_RS_ENABLE_WAYLAND=1`. In a new disposable Inkscape document, test an admitted background key operation and pointer operation, then verify the result in both a fresh snapshot and a saved/reopened SVG. Driver text-route restrictions still apply. Never test against an existing document or automatically replay an action with a partial or unknown outcome.
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||
index 8a80de2..5dedb8f 100644
|
||||
index 8a80de2..2d48237 100644
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -39,8 +39,24 @@ target_compile_options(cua_hyprland_protocol PRIVATE -Wall -Wextra -Wpedantic -W
|
||||
@@ -39,8 +39,28 @@ target_compile_options(cua_hyprland_protocol PRIVATE -Wall -Wextra -Wpedantic -W
|
||||
cua_hyprland_harden(cua_hyprland_protocol)
|
||||
set_target_properties(cua_hyprland_protocol PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
@@ -23,11 +23,15 @@ index 8a80de2..5dedb8f 100644
|
||||
+ add_test(NAME cua_hyprland_agent_keymap_test
|
||||
+ COMMAND ${Python3_EXECUTABLE} -B ${CMAKE_CURRENT_SOURCE_DIR}/tests/agent_keymap_test.py)
|
||||
+ set_tests_properties(cua_hyprland_agent_keymap_test PROPERTIES
|
||||
+ ENVIRONMENT "CXX=${CMAKE_CXX_COMPILER}")
|
||||
+ add_test(NAME cua_hyprland_foreground_modifiers_test
|
||||
+ COMMAND ${Python3_EXECUTABLE} -B ${CMAKE_CURRENT_SOURCE_DIR}/tests/foreground_modifiers_test.py)
|
||||
+ set_tests_properties(cua_hyprland_foreground_modifiers_test PROPERTIES
|
||||
+ ENVIRONMENT "CXX=${CMAKE_CXX_COMPILER}")
|
||||
add_test(NAME cua_hyprland_desktop_fault_policy_test
|
||||
COMMAND ${Python3_EXECUTABLE} -B
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests/desktop_fault_policy_test.py)
|
||||
@@ -218,6 +234,7 @@ if(CUA_HYPRLAND_BUILD_PLUGIN)
|
||||
@@ -218,6 +238,7 @@ if(CUA_HYPRLAND_BUILD_PLUGIN)
|
||||
message(FATAL_ERROR "Input is pinned to Hyprland 0.56.2")
|
||||
endif()
|
||||
target_sources(cua_hyprland_plugin PRIVATE src/input_experiment.cpp)
|
||||
@@ -35,8 +39,29 @@ index 8a80de2..5dedb8f 100644
|
||||
endif()
|
||||
if(CUA_HYPRLAND_INPUT)
|
||||
target_compile_definitions(cua_hyprland_plugin PRIVATE CUA_HYPRLAND_INPUT=1)
|
||||
diff --git a/src/foreground_route.hpp b/src/foreground_route.hpp
|
||||
index 0b675d3..c3af461 100644
|
||||
--- a/src/foreground_route.hpp
|
||||
+++ b/src/foreground_route.hpp
|
||||
@@ -68,11 +68,13 @@ struct ForegroundSeatBindings {
|
||||
bool unique() const { return primary_candidates == 1; }
|
||||
};
|
||||
|
||||
-inline ForegroundFailureReason foreground_key_modifier_failure(const std::array<std::uint32_t, 4>& modifiers) {
|
||||
- // The KEY mapping assumes a neutral US state, including layout group zero.
|
||||
+inline ForegroundFailureReason foreground_key_modifier_failure(const std::array<std::uint32_t, 4>& modifiers,
|
||||
+ std::uint32_t allowed_locked = 0) {
|
||||
+ // The caller may admit a keymap-resolved ambient Num Lock mask. All other
|
||||
+ // human modifiers and nonzero layout groups remain unsupported.
|
||||
if (modifiers[0]) return ForegroundFailureReason::keyboard_depressed;
|
||||
if (modifiers[1]) return ForegroundFailureReason::keyboard_latched;
|
||||
- if (modifiers[2]) return ForegroundFailureReason::keyboard_locked;
|
||||
+ if (modifiers[2] & ~allowed_locked) return ForegroundFailureReason::keyboard_locked;
|
||||
if (modifiers[3]) return ForegroundFailureReason::keyboard_group;
|
||||
return ForegroundFailureReason::none;
|
||||
}
|
||||
diff --git a/src/input_experiment.cpp b/src/input_experiment.cpp
|
||||
index fc7e740..f256fc4 100644
|
||||
index fc7e740..5d80d3e 100644
|
||||
--- a/src/input_experiment.cpp
|
||||
+++ b/src/input_experiment.cpp
|
||||
@@ -9,6 +9,8 @@
|
||||
@@ -174,25 +199,75 @@ index fc7e740..f256fc4 100644
|
||||
if (xkb_context_) xkb_context_unref(xkb_context_);
|
||||
if (keymap_fd >= 0) close(keymap_fd);
|
||||
}
|
||||
@@ -786,7 +790,7 @@ struct InputExperiment::Impl {
|
||||
@@ -782,12 +786,40 @@ struct InputExperiment::Impl {
|
||||
.exact_pointer_focus = root && g_pSeatManager->m_state.pointerFocus == root,
|
||||
};
|
||||
}
|
||||
+ std::array<std::uint32_t, 4> capture_foreground_modifiers(bool needs_keyboard) const {
|
||||
+ const auto physical = g_pSeatManager->m_keyboard.lock();
|
||||
+ if (!physical) throw ForegroundFailure{ForegroundFailureReason::physical_keyboard};
|
||||
+ std::array<std::uint32_t, 4> result{};
|
||||
+ const auto observe = [&](const auto& kb, bool primary) {
|
||||
+ const std::array<std::uint32_t, 4> state{kb->m_modifiersState.depressed, kb->m_modifiersState.latched,
|
||||
+ kb->m_modifiersState.locked, kb->m_modifiersState.group};
|
||||
+ if (needs_keyboard) {
|
||||
+ const auto failure = foreground_key_modifier_failure(state, foreground_numlock_mask(kb->m_xkbKeymap));
|
||||
+ if (failure != ForegroundFailureReason::none) throw ForegroundFailure{failure};
|
||||
+ // Hyprland combines shared raw masks. A lock from a different
|
||||
+ // encoding must not be reinterpreted as the primary Num Lock.
|
||||
+ if (state[2] && state[2] != foreground_numlock_mask(physical_keymap))
|
||||
+ throw ForegroundFailure{ForegroundFailureReason::keyboard_locked};
|
||||
+ }
|
||||
+ for (unsigned i = 0; i < 3; ++i) result[i] |= state[i];
|
||||
+ if (primary) result[3] = state[3];
|
||||
+ };
|
||||
+ observe(physical, true);
|
||||
+ for (const auto& kb : g_pInputManager->m_keyboards) {
|
||||
+ if (kb == physical || !kb->m_enabled || !kb->shareStates() ||
|
||||
+ (kb->isVirtual() && g_pInputManager->shouldIgnoreVirtualKeyboard(kb))) continue;
|
||||
+ observe(kb, false);
|
||||
+ }
|
||||
+ return result;
|
||||
+ }
|
||||
void require_foreground(Client& c) {
|
||||
if (lease != &c) throw ForegroundFailure{ForegroundFailureReason::lease};
|
||||
if (c.dead) throw ForegroundFailure{ForegroundFailureReason::client_dead};
|
||||
if (!available()) throw ForegroundFailure{ForegroundFailureReason::session_unavailable};
|
||||
- if (!layout_qualified()) throw ForegroundFailure{ForegroundFailureReason::unsupported_layout};
|
||||
+ if (foreground_keyboard_used && !physical_layout_ready()) throw ForegroundFailure{ForegroundFailureReason::unsupported_layout};
|
||||
if (Clock::now() >= expires) throw ForegroundFailure{ForegroundFailureReason::lease_expired};
|
||||
+ if (foreground_keyboard_used && capture_foreground_modifiers(true) != foreground_modifiers)
|
||||
+ throw ForegroundFailure{ForegroundFailureReason::keyboard_state};
|
||||
const auto failure = foreground_guard(c).dispatch_failure(foreground_needs_pointer);
|
||||
if (failure != ForegroundFailureReason::none) throw ForegroundFailure{failure};
|
||||
@@ -813,6 +817,8 @@ struct InputExperiment::Impl {
|
||||
}
|
||||
@@ -803,24 +835,31 @@ struct InputExperiment::Impl {
|
||||
p->sendButton(event_ms(), held_button, WL_POINTER_BUTTON_STATE_RELEASED);
|
||||
p->sendFrame();
|
||||
}
|
||||
+ // Synthetic events only change our private state. If human input
|
||||
+ // cancelled the action, restore the current real state, not stale locks.
|
||||
+ const auto restore_modifiers = g_pSeatManager->m_keyboard.lock() ?
|
||||
+ capture_foreground_modifiers(false) : foreground_modifiers;
|
||||
if (foreground_keyboard_used && root && root->good() && g_pSeatManager->m_state.keyboardFocus == root)
|
||||
for (const auto& weak : foreground_keyboards)
|
||||
if (const auto k = weak.lock(); k && k->good()) {
|
||||
for (auto code : held_keys) k->sendKey(event_ms(), code, WL_KEYBOARD_KEY_STATE_RELEASED);
|
||||
- k->sendMods(foreground_modifiers[0], foreground_modifiers[1], foreground_modifiers[2], foreground_modifiers[3]);
|
||||
+ k->sendMods(restore_modifiers[0], restore_modifiers[1], restore_modifiers[2], restore_modifiers[3]);
|
||||
}
|
||||
held_button = 0; held_keys.clear();
|
||||
foreground_pointers.clear(); foreground_keyboards.clear(); foreground_surface.reset(); foreground_seat.reset();
|
||||
foreground_started = false;
|
||||
foreground_keyboard_used = false;
|
||||
+ if (physical_state) xkb_state_unref(physical_state);
|
||||
+ physical_state = physical_keymap ? xkb_state_new(physical_keymap) : nullptr;
|
||||
}
|
||||
void start_foreground(Client& c, double x, double y, bool needs_pointer, bool needs_keyboard) {
|
||||
- void start_foreground(Client& c, double x, double y, bool needs_pointer, bool needs_keyboard) {
|
||||
+ void start_foreground(Client& c, double x, double y, bool needs_pointer, bool needs_keyboard,
|
||||
+ const std::array<std::uint32_t, 4>& modifiers) {
|
||||
const auto root = c.surface.lock();
|
||||
@@ -820,7 +826,7 @@ struct InputExperiment::Impl {
|
||||
const auto physical = g_pSeatManager->m_keyboard.lock();
|
||||
const auto failure = foreground_guard(c).activation_failure();
|
||||
if (failure != ForegroundFailureReason::none) throw ForegroundFailure{failure};
|
||||
if (!physical) throw ForegroundFailure{ForegroundFailureReason::physical_keyboard};
|
||||
@@ -201,16 +276,55 @@ index fc7e740..f256fc4 100644
|
||||
if (needs_pointer && !g_pSeatManager->m_mouse) throw ForegroundFailure{ForegroundFailureReason::physical_pointer};
|
||||
const Vector2D local{x + c.geometry[0] - c.geometry[4], y + c.geometry[1] - c.geometry[5]};
|
||||
if (needs_pointer && (!point(c, x, y) || root->at(local, true).first != root)) throw ForegroundFailure{ForegroundFailureReason::pointer_target};
|
||||
@@ -843,7 +849,7 @@ struct InputExperiment::Impl {
|
||||
const auto modifier_failure = foreground_key_modifier_failure(foreground_modifiers);
|
||||
if (modifier_failure != ForegroundFailureReason::none) throw ForegroundFailure{modifier_failure};
|
||||
}
|
||||
@@ -831,24 +870,16 @@ struct InputExperiment::Impl {
|
||||
for (const auto& k : seat->m_keyboards) if (k && k->good()) foreground_keyboards.push_back(k);
|
||||
if (needs_pointer && foreground_pointers.empty()) throw ForegroundFailure{ForegroundFailureReason::pointer_resources};
|
||||
if (foreground_keyboards.empty()) throw ForegroundFailure{ForegroundFailureReason::keyboard_resources};
|
||||
- foreground_modifiers = {physical->m_modifiersState.depressed, physical->m_modifiersState.latched,
|
||||
- physical->m_modifiersState.locked, physical->m_modifiersState.group};
|
||||
- for (const auto& kb : g_pInputManager->m_keyboards) {
|
||||
- if (!kb->m_enabled || !kb->shareStates() || (kb->isVirtual() && g_pInputManager->shouldIgnoreVirtualKeyboard(kb))) continue;
|
||||
- foreground_modifiers[0] |= kb->m_modifiersState.depressed;
|
||||
- foreground_modifiers[1] |= kb->m_modifiersState.latched;
|
||||
- foreground_modifiers[2] |= kb->m_modifiersState.locked;
|
||||
- }
|
||||
- if (needs_keyboard) {
|
||||
- const auto modifier_failure = foreground_key_modifier_failure(foreground_modifiers);
|
||||
- if (modifier_failure != ForegroundFailureReason::none) throw ForegroundFailure{modifier_failure};
|
||||
- }
|
||||
- xkb_state_update_mask(keyboard_state, foreground_modifiers[0], foreground_modifiers[1], foreground_modifiers[2], 0, 0, foreground_modifiers[3]);
|
||||
+ foreground_modifiers = modifiers;
|
||||
+ if (needs_keyboard && capture_foreground_modifiers(true) != foreground_modifiers)
|
||||
+ throw ForegroundFailure{ForegroundFailureReason::keyboard_state};
|
||||
+ if (needs_keyboard) xkb_state_update_mask(physical_state, foreground_modifiers[0], foreground_modifiers[1], foreground_modifiers[2], 0, 0, foreground_modifiers[3]);
|
||||
foreground_surface = root;
|
||||
foreground_seat = seat;
|
||||
foreground_needs_pointer = needs_pointer;
|
||||
@@ -893,16 +899,17 @@ struct InputExperiment::Impl {
|
||||
c.foreground_attempted = true;
|
||||
foreground_started = true;
|
||||
+ foreground_keyboard_used = needs_keyboard;
|
||||
foreground_activating = true;
|
||||
// Activation intentionally persists. Never save, borrow, or restore focus.
|
||||
if (g_pSeatManager->m_state.keyboardFocus != root || Desktop::focusState()->window() != c.window.lock() ||
|
||||
@@ -858,7 +889,16 @@ struct InputExperiment::Impl {
|
||||
if (lease != &c) throw ForegroundFailure{ForegroundFailureReason::lease};
|
||||
const auto focus_failure = foreground_guard(c).dispatch_failure(false);
|
||||
if (focus_failure != ForegroundFailureReason::none) throw ForegroundFailure{focus_failure};
|
||||
- if (!needs_pointer) { require_foreground(c); return; }
|
||||
+ if (!needs_pointer) {
|
||||
+ require_foreground(c);
|
||||
+ if (needs_keyboard)
|
||||
+ for (const auto& weak : foreground_keyboards) {
|
||||
+ const auto k = weak.lock();
|
||||
+ if (!k || !k->good()) throw ForegroundFailure{ForegroundFailureReason::keyboard_resources};
|
||||
+ k->sendMods(foreground_modifiers[0], foreground_modifiers[1], foreground_modifiers[2], foreground_modifiers[3]);
|
||||
+ }
|
||||
+ return;
|
||||
+ }
|
||||
foreground_activating = true;
|
||||
::Pointer::mgr()->warpTo({x + c.geometry[0], y + c.geometry[1]});
|
||||
if (lease != &c) throw ForegroundFailure{ForegroundFailureReason::lease};
|
||||
@@ -893,16 +933,17 @@ struct InputExperiment::Impl {
|
||||
}
|
||||
void foreground_key(Client& c, std::uint32_t code, bool pressed) {
|
||||
require_foreground(c);
|
||||
@@ -233,7 +347,7 @@ index fc7e740..f256fc4 100644
|
||||
}
|
||||
}
|
||||
bool pointer_enter(Client& c, double x, double y) {
|
||||
@@ -942,8 +949,8 @@ struct InputExperiment::Impl {
|
||||
@@ -942,8 +983,8 @@ struct InputExperiment::Impl {
|
||||
held_button = pressed ? value : 0;
|
||||
}
|
||||
bool keyboard_enter(Client& c) {
|
||||
@@ -244,7 +358,7 @@ index fc7e740..f256fc4 100644
|
||||
unsigned count = 0;
|
||||
for (auto& k : keyboards) {
|
||||
if (k->dead || !k->wl->resource() || k->wl->client() != root->client()) continue;
|
||||
@@ -1030,7 +1037,7 @@ struct InputExperiment::Impl {
|
||||
@@ -1030,7 +1071,7 @@ struct InputExperiment::Impl {
|
||||
if (kProduction && (!InputGrant::single_operation(requested_cap) ||
|
||||
(requested_cap == 16 && route != InputRoute::primary_foreground))) { invalidate(c); send(c, refusal("unsupported")); return; }
|
||||
if (kProduction && !available()) { invalidate(c, false); send(c, refusal("session_unavailable")); return; }
|
||||
@@ -253,7 +367,7 @@ index fc7e740..f256fc4 100644
|
||||
const auto pid = number(f[1]); const auto address = number(f[2], 16);
|
||||
PHLWINDOW window;
|
||||
for (const auto& w : Desktop::windowState()->windows())
|
||||
@@ -1090,7 +1097,7 @@ struct InputExperiment::Impl {
|
||||
@@ -1090,7 +1131,7 @@ struct InputExperiment::Impl {
|
||||
if (c.token.empty() || f[2] != c.token || !refresh(c)) { send(c, refusal("stale_target")); return; }
|
||||
if (number(f[3]) != c.revision) { if (kProduction) revoke("stale_geometry"); send(c, refusal("stale_geometry")); return; }
|
||||
if (!available()) { revoke("session_unavailable", true); send(c, refusal("session_unavailable")); return; }
|
||||
@@ -262,18 +376,22 @@ index fc7e740..f256fc4 100644
|
||||
if (lease && Clock::now() >= expires) revoke("lease_expired");
|
||||
if (drag) { send(c, refusal("lease_busy")); return; }
|
||||
if (lease != &c || !(capabilities & cap) || (kProduction && !grant.permits(cap, Clock::now()))) {
|
||||
@@ -1184,6 +1191,10 @@ struct InputExperiment::Impl {
|
||||
@@ -1184,8 +1225,13 @@ struct InputExperiment::Impl {
|
||||
if (Clock::now() + std::chrono::milliseconds(duration + 50) >= expires) { send(c, refusal("lease_expired")); return; }
|
||||
}
|
||||
}
|
||||
+ if (command == "KEY" && !foreground_chord_compatible(physical_keymap, keymap, code, mods)) {
|
||||
+ const auto modifiers = capture_foreground_modifiers(command == "KEY");
|
||||
+ if (command == "KEY" && !foreground_chord_compatible(physical_keymap, keymap, code, mods, modifiers)) {
|
||||
+ revoke("unsupported_layout", true);
|
||||
+ send(c, refusal("unsupported_layout")); return;
|
||||
+ }
|
||||
if (!consume_grant(c, cap)) return;
|
||||
start_foreground(c, x, y, command != "KEY" && command != "ACTIVATE", command == "KEY");
|
||||
- start_foreground(c, x, y, command != "KEY" && command != "ACTIVATE", command == "KEY");
|
||||
+ start_foreground(c, x, y, command != "KEY" && command != "ACTIVATE", command == "KEY", modifiers);
|
||||
if (command == "KEY") {
|
||||
@@ -1227,7 +1238,7 @@ struct InputExperiment::Impl {
|
||||
const std::array<std::uint32_t, 4> keys{42, 29, 56, 125};
|
||||
for (unsigned i = 0; i < 4; ++i) if ((mods & (1u << i)) && keys[i] != code) foreground_key(c, keys[i], true);
|
||||
@@ -1227,7 +1273,7 @@ struct InputExperiment::Impl {
|
||||
if (lease) {
|
||||
if (lease->dead) revoke("disconnected", true);
|
||||
else if (Clock::now() >= expires) revoke("lease_expired");
|
||||
@@ -284,14 +402,15 @@ index fc7e740..f256fc4 100644
|
||||
}
|
||||
diff --git a/src/keyboard_layout.hpp b/src/keyboard_layout.hpp
|
||||
new file mode 100644
|
||||
index 0000000..ab7defc
|
||||
index 0000000..f5f51c8
|
||||
--- /dev/null
|
||||
+++ b/src/keyboard_layout.hpp
|
||||
@@ -0,0 +1,102 @@
|
||||
@@ -0,0 +1,127 @@
|
||||
+#pragma once
|
||||
+
|
||||
+#include "foreground_route.hpp"
|
||||
+#include <xkbcommon/xkbcommon.h>
|
||||
+#include <xkbcommon/xkbcommon-names.h>
|
||||
+#include <array>
|
||||
+#include <cstdint>
|
||||
+#include <memory>
|
||||
@@ -359,16 +478,37 @@ index 0000000..ab7defc
|
||||
+ return true;
|
||||
+}
|
||||
+
|
||||
+// Resolve the virtual modifier through this map; Num Lock is not necessarily
|
||||
+// encoded as Mod2. Ambiguous encodings are deliberately not admitted.
|
||||
+inline xkb_mod_mask_t foreground_numlock_mask(xkb_keymap* map) {
|
||||
+ const auto mask = map ? xkb_keymap_mod_get_mask(map, XKB_VMOD_NAME_NUM) : 0;
|
||||
+ return mask && !(mask & (mask - 1)) &&
|
||||
+ !(mask & xkb_keymap_mod_get_mask(map, XKB_MOD_NAME_CAPS)) ? mask : 0;
|
||||
+}
|
||||
+
|
||||
+// Simulate the complete chord before delivering any events or changing focus.
|
||||
+// This is compatibility checking, not layout translation: physical key positions
|
||||
+// remain unchanged. Unrelated remaps are harmless, requested remaps fail closed.
|
||||
+inline bool foreground_chord_compatible(xkb_keymap* physical, xkb_keymap* canonical,
|
||||
+ std::uint32_t code, std::uint32_t mods) {
|
||||
+ std::uint32_t code, std::uint32_t mods,
|
||||
+ const std::array<std::uint32_t, 4>& modifiers) {
|
||||
+ if (!physical || !canonical) return false;
|
||||
+ if (foreground_key_modifier_failure(modifiers, foreground_numlock_mask(physical)) !=
|
||||
+ ForegroundFailureReason::none) return false;
|
||||
+ using State = std::unique_ptr<xkb_state, decltype(&xkb_state_unref)>;
|
||||
+ State actual{xkb_state_new(physical), xkb_state_unref};
|
||||
+ State expected{xkb_state_new(canonical), xkb_state_unref};
|
||||
+ if (!actual || !expected) return false;
|
||||
+ // The client retains Num Lock, but wire keys still mean the neutral US
|
||||
+ // chord. A third state rejects keypad/navigation changes caused by the lock.
|
||||
+ State intended{xkb_state_new(canonical), xkb_state_unref};
|
||||
+ if (!actual || !expected || !intended) return false;
|
||||
+ if (modifiers[2]) {
|
||||
+ const auto canonical_lock = foreground_numlock_mask(canonical);
|
||||
+ if (!canonical_lock) return false;
|
||||
+ xkb_state_update_mask(actual.get(), 0, 0, modifiers[2], 0, 0, 0);
|
||||
+ xkb_state_update_mask(expected.get(), 0, 0, canonical_lock, 0, 0, 0);
|
||||
+ if (!same_modifier_state(actual.get(), expected.get())) return false;
|
||||
+ }
|
||||
+ const auto event = [&](std::uint32_t key, xkb_key_direction direction) {
|
||||
+ // A client interprets the press using the preceding modifiers event.
|
||||
+ // Stock both_capslock_cancel gives Shift a Caps_Lock symbol at its
|
||||
@@ -376,9 +516,12 @@ index 0000000..ab7defc
|
||||
+ // keycode, but their lock/latch/group effects still must agree.
|
||||
+ if (direction == XKB_KEY_DOWN &&
|
||||
+ (!same_key_symbols(actual.get(), expected.get(), key + 8) ||
|
||||
+ !same_consumed_modifiers(actual.get(), expected.get(), key + 8))) return false;
|
||||
+ !same_consumed_modifiers(actual.get(), expected.get(), key + 8) ||
|
||||
+ !same_key_symbols(expected.get(), intended.get(), key + 8) ||
|
||||
+ !same_consumed_modifiers(expected.get(), intended.get(), key + 8))) return false;
|
||||
+ xkb_state_update_key(actual.get(), key + 8, direction);
|
||||
+ xkb_state_update_key(expected.get(), key + 8, direction);
|
||||
+ xkb_state_update_key(intended.get(), key + 8, direction);
|
||||
+ return same_modifier_state(actual.get(), expected.get());
|
||||
+ };
|
||||
+ constexpr std::array<std::uint32_t, 4> keys{42, 29, 56, 125};
|
||||
@@ -391,7 +534,7 @@ index 0000000..ab7defc
|
||||
+}
|
||||
+} // namespace cua::hyprland
|
||||
diff --git a/src/plugin.cpp b/src/plugin.cpp
|
||||
index 88b9900..2d24471 100644
|
||||
index 88b9900..ae7fc7c 100644
|
||||
--- a/src/plugin.cpp
|
||||
+++ b/src/plugin.cpp
|
||||
@@ -218,6 +218,11 @@ std::string status_output(bool json) {
|
||||
@@ -401,7 +544,7 @@ index 88b9900..2d24471 100644
|
||||
+#ifdef CUA_HYPRLAND_INPUT
|
||||
+ // Installation checks compiled support before enabling input seats.
|
||||
+ result.pop_back();
|
||||
+ result += ",\"keyboard_layout_independent\":true}";
|
||||
+ result += ",\"keyboard_layout_independent\":true,\"foreground_numlock_compatible\":true}";
|
||||
+#endif
|
||||
#if defined(CUA_HYPRLAND_TEST_INPUT) || defined(CUA_HYPRLAND_INPUT)
|
||||
if (g_experiment) {
|
||||
@@ -583,12 +726,243 @@ index 68ca6c8..7a4d1b4 100644
|
||||
return (ROOT / 'tests/desktop_fault_policy_fixture.cpp').read_text().replace(
|
||||
'// PRODUCTION_METHODS', methods)
|
||||
|
||||
diff --git a/tests/foreground_modifiers_test.py b/tests/foreground_modifiers_test.py
|
||||
new file mode 100644
|
||||
index 0000000..013c9bf
|
||||
--- /dev/null
|
||||
+++ b/tests/foreground_modifiers_test.py
|
||||
@@ -0,0 +1,225 @@
|
||||
+"""Exercise production foreground state admission, dispatch and unwind with XKB."""
|
||||
+import os
|
||||
+from pathlib import Path
|
||||
+import re
|
||||
+import shlex
|
||||
+import subprocess
|
||||
+import tempfile
|
||||
+import unittest
|
||||
+
|
||||
+ROOT = Path(__file__).resolve().parents[1]
|
||||
+
|
||||
+
|
||||
+class ForegroundModifiersTest(unittest.TestCase):
|
||||
+ def test_production_modifiers(self):
|
||||
+ source = (ROOT / 'src/input_experiment.cpp').read_text()
|
||||
+ methods = []
|
||||
+ for name in ('capture_foreground_modifiers', 'require_foreground', 'finish_foreground',
|
||||
+ 'start_foreground', 'foreground_key'):
|
||||
+ body = re.search(r'^ \S[^\n]*\b' + name + r'\(.*?^ }', source, re.M | re.S)
|
||||
+ self.assertIsNotNone(body, name)
|
||||
+ methods.append(body.group())
|
||||
+ preflight = re.search(r' const auto modifiers = capture_foreground_modifiers\(command == "KEY"\);.*?'
|
||||
+ r' start_foreground\([^\n]*;', source, re.S)
|
||||
+ self.assertIsNotNone(preflight)
|
||||
+ fixture = r'''
|
||||
+#include "keyboard_layout.hpp"
|
||||
+#include <algorithm>
|
||||
+#include <chrono>
|
||||
+#include <cstdlib>
|
||||
+#include <iostream>
|
||||
+#include <memory>
|
||||
+#include <string>
|
||||
+#include <vector>
|
||||
+using namespace cua::hyprland;
|
||||
+using Clock = std::chrono::steady_clock;
|
||||
+void check(bool ok, const char* why) { if (!ok) { std::cerr << why; std::exit(1); } }
|
||||
+struct Vector2D { double x, y; };
|
||||
+constexpr int WL_KEYBOARD_KEY_STATE_PRESSED=1, WL_KEYBOARD_KEY_STATE_RELEASED=0;
|
||||
+constexpr int WL_POINTER_BUTTON_STATE_RELEASED=0;
|
||||
+struct Root : std::enable_shared_from_this<Root> {
|
||||
+ bool good() const { return true; }
|
||||
+ int client() const { return 0; }
|
||||
+ auto at(Vector2D, bool) { return std::pair{shared_from_this(), 0}; }
|
||||
+};
|
||||
+struct Resource {
|
||||
+ xkb_state* client = nullptr;
|
||||
+ std::vector<xkb_keysym_t> symbols;
|
||||
+ std::array<unsigned, 4> last{};
|
||||
+ std::vector<std::array<unsigned, 4>> history;
|
||||
+ bool good() const { return true; }
|
||||
+ void sendKey(unsigned, unsigned code, unsigned down) {
|
||||
+ if (down) symbols.push_back(xkb_state_key_get_one_sym(client, code + 8));
|
||||
+ }
|
||||
+ void sendMods(unsigned d, unsigned l, unsigned k, unsigned g) {
|
||||
+ last={d,l,k,g}; history.push_back(last); xkb_state_update_mask(client,d,l,k,0,0,g);
|
||||
+ }
|
||||
+ void sendButton(unsigned, unsigned, unsigned) {}
|
||||
+ void sendFrame() {}
|
||||
+};
|
||||
+struct Keyboard {
|
||||
+ struct { unsigned depressed=0,latched=0,locked=0,group=0; } m_modifiersState;
|
||||
+ xkb_keymap* m_xkbKeymap = nullptr;
|
||||
+ bool m_enabled=true, shared=true, virt=false;
|
||||
+ bool shareStates() const { return shared; }
|
||||
+ bool isVirtual() const { return virt; }
|
||||
+};
|
||||
+struct Seat {
|
||||
+ std::vector<std::shared_ptr<Resource>> m_keyboards, m_pointers;
|
||||
+ bool good() const { return true; }
|
||||
+};
|
||||
+struct SeatManager {
|
||||
+ std::weak_ptr<Keyboard> m_keyboard;
|
||||
+ bool m_mouse=true;
|
||||
+ struct { std::shared_ptr<Root> keyboardFocus, pointerFocus; } m_state;
|
||||
+ std::shared_ptr<Seat> seat;
|
||||
+ auto seatResourceForClient(int) { return seat; }
|
||||
+ void setPointerFocus(std::shared_ptr<Root> root, Vector2D) { m_state.pointerFocus=root; }
|
||||
+} manager, *g_pSeatManager=&manager;
|
||||
+struct InputManager {
|
||||
+ std::vector<std::shared_ptr<Keyboard>> m_keyboards;
|
||||
+ bool shouldIgnoreVirtualKeyboard(const std::shared_ptr<Keyboard>&) { return false; }
|
||||
+} input, *g_pInputManager=&input;
|
||||
+namespace Desktop {
|
||||
+constexpr int FOCUS_REASON_OTHER=0;
|
||||
+struct Focus {
|
||||
+ std::shared_ptr<Root> root;
|
||||
+ std::shared_ptr<Keyboard> change_on_focus;
|
||||
+ auto window() { return root; }
|
||||
+ auto surface() { return root; }
|
||||
+ void fullWindowFocus(std::shared_ptr<Root>, int, std::shared_ptr<Root> r) {
|
||||
+ root=r; manager.m_state.keyboardFocus=r;
|
||||
+ if (change_on_focus) change_on_focus->m_modifiersState.locked=0;
|
||||
+ }
|
||||
+} focus;
|
||||
+auto focusState() { return &focus; }
|
||||
+}
|
||||
+namespace Pointer { struct Manager { void warpTo(Vector2D) {} } pointer; auto mgr(){ return &pointer; } }
|
||||
+struct Client {
|
||||
+ bool dead=false, foreground_attempted=false;
|
||||
+ std::weak_ptr<Root> surface, window;
|
||||
+ std::array<double, 6> geometry{};
|
||||
+};
|
||||
+struct Lane {
|
||||
+ xkb_keymap* physical_keymap=nullptr;
|
||||
+ xkb_keymap* keymap=nullptr;
|
||||
+ xkb_state* physical_state=nullptr;
|
||||
+ std::array<unsigned,4> foreground_modifiers{};
|
||||
+ std::vector<std::weak_ptr<Resource>> foreground_keyboards, foreground_pointers;
|
||||
+ std::weak_ptr<Root> foreground_surface;
|
||||
+ std::weak_ptr<Seat> foreground_seat;
|
||||
+ std::vector<unsigned> held_keys;
|
||||
+ unsigned held_button=0;
|
||||
+ bool foreground_started=false,foreground_activating=false,foreground_keyboard_used=false;
|
||||
+ bool foreground_needs_pointer=false, layout_ready=true, physical_held=false;
|
||||
+ Client* lease=nullptr;
|
||||
+ Clock::time_point expires=Clock::now()+std::chrono::hours(1);
|
||||
+ unsigned consumed=0, refused=0;
|
||||
+ bool available() { return true; }
|
||||
+ bool physical_layout_ready() { return layout_ready; }
|
||||
+ bool point(Client&, double,double) { return true; }
|
||||
+ unsigned event_ms() { return 0; }
|
||||
+ void foreground_motion(Client&,double,double) {}
|
||||
+ ForegroundGuard foreground_guard(Client&) {
|
||||
+ return {.exact_root=true,.physical_keys=physical_held,.exact_keyboard_focus=true,.exact_pointer_focus=true};
|
||||
+ }
|
||||
+ void revoke(const char*, bool) {}
|
||||
+ auto refusal(const char*) { return std::string{}; }
|
||||
+ void send(Client&, const std::string&) { ++refused; }
|
||||
+ bool consume_grant(Client&, unsigned) { ++consumed; return true; }
|
||||
+ // METHODS
|
||||
+ void preflight(Client& c, unsigned code, unsigned mods) {
|
||||
+ const std::string command="KEY";
|
||||
+ const unsigned cap=2;
|
||||
+ const double x=0,y=0;
|
||||
+ // PREFLIGHT
|
||||
+ }
|
||||
+ ~Lane() { if (physical_state) xkb_state_unref(physical_state); }
|
||||
+};
|
||||
+int main() {
|
||||
+ auto context=xkb_context_new(XKB_CONTEXT_NO_FLAGS);
|
||||
+ const xkb_rule_names names{"evdev","pc105","us","","ctrl:nocaps"};
|
||||
+ auto physical=xkb_keymap_new_from_names(context,&names,XKB_KEYMAP_COMPILE_NO_FLAGS);
|
||||
+ auto canonical=agent_keymap(context);
|
||||
+ auto keyboard=std::make_shared<Keyboard>(); keyboard->m_xkbKeymap=physical;
|
||||
+ manager.m_keyboard=keyboard; input.m_keyboards={keyboard};
|
||||
+ auto root=std::make_shared<Root>();
|
||||
+ manager.m_state.keyboardFocus=root; manager.m_state.pointerFocus=root; Desktop::focus.root=root;
|
||||
+ manager.seat=std::make_shared<Seat>(); auto resource=std::make_shared<Resource>();
|
||||
+ resource->client=xkb_state_new(physical); manager.seat->m_keyboards={resource};
|
||||
+ Client client; client.surface=root; client.window=root;
|
||||
+ Lane lane; lane.physical_keymap=physical; lane.keymap=canonical;
|
||||
+ lane.physical_state=xkb_state_new(physical); lane.lease=&client;
|
||||
+ const auto num=foreground_numlock_mask(physical);
|
||||
+ keyboard->m_modifiersState.locked=num;
|
||||
+ lane.preflight(client,30,1);
|
||||
+ check(lane.foreground_started && lane.foreground_modifiers[2]==num && resource->last[2]==num,
|
||||
+ "actual Num Lock state was not captured and published before input");
|
||||
+ lane.foreground_key(client,42,true); lane.foreground_key(client,30,true);
|
||||
+ lane.foreground_key(client,30,false); lane.foreground_key(client,42,false);
|
||||
+ check(resource->symbols.back()==XKB_KEY_A,"client did not receive intended shifted text");
|
||||
+ for (const auto& state : resource->history)
|
||||
+ check(state[2]==num,"dispatch transiently cleared Num Lock");
|
||||
+ lane.finish_foreground();
|
||||
+ check(resource->last==std::array<unsigned,4>{0,0,num,0} && keyboard->m_modifiersState.locked==num,
|
||||
+ "completion changed real or client Num Lock state");
|
||||
+ const auto consumed=lane.consumed;
|
||||
+ lane.preflight(client,79,0);
|
||||
+ check(lane.refused==1 && lane.consumed==consumed && !lane.foreground_started,
|
||||
+ "caller checked neutral state and admitted Num Lock keypad semantics");
|
||||
+ keyboard->m_modifiersState.locked=0;
|
||||
+ lane.preflight(client,79,0); lane.foreground_key(client,79,true); lane.foreground_key(client,79,false);
|
||||
+ check(resource->symbols.back()==XKB_KEY_KP_End,"neutral keypad contract changed"); lane.finish_foreground();
|
||||
+ keyboard->m_modifiersState.locked=num; lane.preflight(client,30,1); lane.foreground_key(client,42,true);
|
||||
+ keyboard->m_modifiersState.locked=0;
|
||||
+ try { lane.foreground_key(client,30,true); check(false,"ambient state change accepted"); }
|
||||
+ catch (ForegroundFailure f) { check(f.reason==ForegroundFailureReason::keyboard_state,"wrong state change refusal"); }
|
||||
+ lane.finish_foreground(); check(resource->last==std::array<unsigned,4>{},"cancellation restored stale Num Lock");
|
||||
+ keyboard->m_modifiersState.locked=num; lane.preflight(client,30,1); lane.foreground_key(client,42,true);
|
||||
+ keyboard->m_modifiersState.locked=xkb_keymap_mod_get_mask(physical,XKB_MOD_NAME_CAPS);
|
||||
+ keyboard->m_modifiersState.depressed=xkb_keymap_mod_get_mask(physical,XKB_MOD_NAME_CTRL);
|
||||
+ lane.finish_foreground();
|
||||
+ check(resource->last[0]==keyboard->m_modifiersState.depressed && resource->last[2]==keyboard->m_modifiersState.locked,
|
||||
+ "cancellation failed to restore current human Caps and held Control");
|
||||
+ keyboard->m_modifiersState.depressed=0; keyboard->m_modifiersState.locked=num;
|
||||
+ lane.physical_held=true;
|
||||
+ try { lane.preflight(client,30,0); check(false,"held physical key admitted"); }
|
||||
+ catch (ForegroundFailure f) { check(f.reason==ForegroundFailureReason::physical_keys,"wrong held-key refusal"); }
|
||||
+ lane.physical_held=false;
|
||||
+ auto shared=std::make_shared<Keyboard>(); shared->m_xkbKeymap=physical;
|
||||
+ shared->m_modifiersState.locked=num;
|
||||
+ keyboard->m_modifiersState.locked=0;
|
||||
+ input.m_keyboards.push_back(shared);
|
||||
+ lane.preflight(client,30,0);
|
||||
+ check(lane.foreground_modifiers[2]==num && resource->last[2]==num,"shared Num Lock was ignored");
|
||||
+ lane.foreground_key(client,30,true); lane.foreground_key(client,30,false); lane.finish_foreground();
|
||||
+ check(resource->last[2]==num,"shared Num Lock not restored");
|
||||
+ shared->m_modifiersState.locked=xkb_keymap_mod_get_mask(physical,XKB_MOD_NAME_CAPS);
|
||||
+ try { lane.preflight(client,30,0); check(false,"shared Caps state admitted"); }
|
||||
+ catch (ForegroundFailure f) { check(f.reason==ForegroundFailureReason::keyboard_locked,"wrong shared lock refusal"); }
|
||||
+ shared->shared=false; lane.preflight(client,30,0); lane.finish_foreground();
|
||||
+ // Focus callbacks can change real state between preflight and first event.
|
||||
+ keyboard->m_modifiersState.locked=num;
|
||||
+ Desktop::focus.root.reset(); Desktop::focus.change_on_focus=keyboard;
|
||||
+ try { lane.preflight(client,30,0); check(false,"focus-time state change accepted"); }
|
||||
+ catch (ForegroundFailure f) { check(f.reason==ForegroundFailureReason::keyboard_state,"wrong focus change refusal"); }
|
||||
+ lane.finish_foreground();
|
||||
+ check(resource->last[2]==0,"focus-time cancellation restored stale lock");
|
||||
+ xkb_state_unref(resource->client); xkb_keymap_unref(physical); xkb_keymap_unref(canonical); xkb_context_unref(context);
|
||||
+}
|
||||
+'''.replace('// METHODS', '\n'.join(methods)).replace('// PREFLIGHT', preflight.group())
|
||||
+ compiler = shlex.split(os.environ.get('CXX', 'c++'))
|
||||
+ flags = shlex.split(subprocess.check_output(['pkg-config', '--cflags', '--libs', 'xkbcommon'], text=True))
|
||||
+ with tempfile.TemporaryDirectory(prefix='cua-foreground-modifiers-') as directory:
|
||||
+ cpp, binary = Path(directory) / 'fixture.cpp', Path(directory) / 'fixture'
|
||||
+ cpp.write_text(fixture)
|
||||
+ build = subprocess.run([*compiler, '-std=c++20', '-Wall', '-Wextra', '-Wpedantic', '-Werror',
|
||||
+ '-I', str(ROOT / 'src'), str(cpp), '-o', str(binary), *flags],
|
||||
+ capture_output=True, text=True, timeout=60)
|
||||
+ self.assertEqual(build.returncode, 0, build.stdout + build.stderr)
|
||||
+ result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
|
||||
+ self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
+
|
||||
+
|
||||
+if __name__ == '__main__':
|
||||
+ unittest.main()
|
||||
diff --git a/tests/keyboard_layout_test.cpp b/tests/keyboard_layout_test.cpp
|
||||
new file mode 100644
|
||||
index 0000000..dd7c962
|
||||
index 0000000..6d57616
|
||||
--- /dev/null
|
||||
+++ b/tests/keyboard_layout_test.cpp
|
||||
@@ -0,0 +1,97 @@
|
||||
@@ -0,0 +1,130 @@
|
||||
+#include "keyboard_layout.hpp"
|
||||
+#include <cstdlib>
|
||||
+#include <iostream>
|
||||
@@ -617,24 +991,53 @@ index 0000000..dd7c962
|
||||
+ auto german = map("de", "");
|
||||
+ check(stock && nocaps && swapctrl && swapalt && german, "test keymaps unavailable");
|
||||
+ for (auto* physical : {stock.get(), nocaps.get(), swapctrl.get(), swapalt.get(), german.get()}) {
|
||||
+ check(foreground_chord_compatible(physical, agent.get(), 30, 0), "unrelated remap blocked A");
|
||||
+ check(foreground_chord_compatible(physical, agent.get(), 30, 1), "unrelated remap blocked Shift+A");
|
||||
+ check(foreground_chord_compatible(physical, agent.get(), 28, 0), "unrelated remap blocked Return");
|
||||
+ check(foreground_chord_compatible(physical, agent.get(), 30, 0, {}), "unrelated remap blocked A");
|
||||
+ check(foreground_chord_compatible(physical, agent.get(), 30, 1, {}), "unrelated remap blocked Shift+A");
|
||||
+ check(foreground_chord_compatible(physical, agent.get(), 28, 0, {}), "unrelated remap blocked Return");
|
||||
+ }
|
||||
+ check(foreground_chord_compatible(stock.get(), agent.get(), 30, 2), "stock Omarchy blocked Ctrl+A");
|
||||
+ check(foreground_chord_compatible(nocaps.get(), agent.get(), 30, 2), "Caps to Ctrl blocked Ctrl+A");
|
||||
+ check(!foreground_chord_compatible(swapctrl.get(), agent.get(), 30, 2), "Ctrl/Caps swap sent wrong Ctrl+A");
|
||||
+ check(!foreground_chord_compatible(swapctrl.get(), agent.get(), 29, 0), "remapped modifier key accepted");
|
||||
+ check(!foreground_chord_compatible(swapalt.get(), agent.get(), 30, 4), "Alt/Super swap sent wrong Alt+A");
|
||||
+ check(!foreground_chord_compatible(swapalt.get(), agent.get(), 30, 8), "Alt/Super swap sent wrong Super+A");
|
||||
+ check(!foreground_chord_compatible(german.get(), agent.get(), 21, 0), "German Z accepted as US Y");
|
||||
+ check(!foreground_chord_compatible(german.get(), agent.get(), 3, 1), "German shifted punctuation accepted");
|
||||
+ check(!foreground_chord_compatible(nullptr, agent.get(), 30, 0), "missing physical map accepted");
|
||||
+ check(foreground_chord_compatible(stock.get(), agent.get(), 30, 2, {}), "stock Omarchy blocked Ctrl+A");
|
||||
+ check(foreground_chord_compatible(nocaps.get(), agent.get(), 30, 2, {}), "Caps to Ctrl blocked Ctrl+A");
|
||||
+ check(!foreground_chord_compatible(swapctrl.get(), agent.get(), 30, 2, {}), "Ctrl/Caps swap sent wrong Ctrl+A");
|
||||
+ check(!foreground_chord_compatible(swapctrl.get(), agent.get(), 29, 0, {}), "remapped modifier key accepted");
|
||||
+ check(!foreground_chord_compatible(swapalt.get(), agent.get(), 30, 4, {}), "Alt/Super swap sent wrong Alt+A");
|
||||
+ check(!foreground_chord_compatible(swapalt.get(), agent.get(), 30, 8, {}), "Alt/Super swap sent wrong Super+A");
|
||||
+ check(!foreground_chord_compatible(german.get(), agent.get(), 21, 0, {}), "German Z accepted as US Y");
|
||||
+ check(!foreground_chord_compatible(german.get(), agent.get(), 3, 1, {}), "German shifted punctuation accepted");
|
||||
+ check(!foreground_chord_compatible(nullptr, agent.get(), 30, 0, {}), "missing physical map accepted");
|
||||
+ for (auto* physical : {agent.get(), stock.get(), nocaps.get()}) {
|
||||
+ const auto numlock = foreground_numlock_mask(physical);
|
||||
+ check(numlock != 0, "Num Lock encoding missing");
|
||||
+ for (const auto locked : {0u, numlock}) {
|
||||
+ const std::array<std::uint32_t, 4> ambient{0, 0, locked, 0};
|
||||
+ for (const auto key : {30u, 48u, 44u, 2u, 11u, 28u, 57u})
|
||||
+ for (const auto mods : {0u, 1u, 2u, 3u})
|
||||
+ check(foreground_chord_compatible(physical, agent.get(), key, mods, ambient),
|
||||
+ "Num Lock blocked ordinary text or shortcut");
|
||||
+ for (const auto key : {71u, 72u, 75u, 79u, 82u, 83u})
|
||||
+ check(foreground_chord_compatible(physical, agent.get(), key, 0, ambient) == !locked,
|
||||
+ "Num Lock keypad semantic change was ignored");
|
||||
+ }
|
||||
+ const auto caps = xkb_keymap_mod_get_mask(physical, XKB_MOD_NAME_CAPS);
|
||||
+ for (const auto locked : {caps, caps | numlock, 0x80000000u})
|
||||
+ check(!foreground_chord_compatible(physical, agent.get(), 30, 0, {0, 0, locked, 0}),
|
||||
+ "unsupported lock accepted");
|
||||
+ check(!foreground_chord_compatible(physical, agent.get(), 30, 0, {numlock, 0, numlock, 0}),
|
||||
+ "held modifier accepted with Num Lock");
|
||||
+ check(!foreground_chord_compatible(physical, agent.get(), 30, 0, {0, numlock, numlock, 0}),
|
||||
+ "latched modifier accepted with Num Lock");
|
||||
+ check(!foreground_chord_compatible(physical, agent.get(), 30, 0, {0, 0, numlock, 1}),
|
||||
+ "nonzero group accepted with Num Lock");
|
||||
+ }
|
||||
+ const auto numlock = foreground_numlock_mask(agent.get());
|
||||
+ check(!foreground_chord_compatible(swapctrl.get(), agent.get(), 30, 2, {0, 0, numlock, 0}),
|
||||
+ "Num Lock hid Ctrl remap");
|
||||
+ check(!foreground_chord_compatible(german.get(), agent.get(), 21, 0, {0, 0, numlock, 0}),
|
||||
+ "Num Lock hid layout mismatch");
|
||||
+ // All supported wire chords remain compatible on the independent map.
|
||||
+ for (unsigned code = 1; code <= 247; ++code)
|
||||
+ if (code != 58 && code != 69 && code != 70)
|
||||
+ for (unsigned mods = 0; mods < 16; ++mods)
|
||||
+ check(foreground_chord_compatible(agent.get(), agent.get(), code, mods), "canonical chord rejected");
|
||||
+ check(foreground_chord_compatible(agent.get(), agent.get(), code, mods, {}), "canonical chord rejected");
|
||||
+ // Two lanes never share modifier state and never mutate the human state.
|
||||
+ State first{xkb_state_new(agent.get()), xkb_state_unref};
|
||||
+ State other{xkb_state_new(second.get()), xkb_state_unref};
|
||||
@@ -654,7 +1057,9 @@ index 0000000..dd7c962
|
||||
+ "key <AC01> { type=\"ALPHABETIC\", symbols[Group1]=[a,A], actions[Group1]=[LockMods(modifiers=Mod3),LockMods(modifiers=Mod3)] };");
|
||||
+ Map lock{xkb_keymap_new_from_string(context.get(), changed.c_str(), XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS), xkb_keymap_unref};
|
||||
+ check(bool(lock), "action fixture failed to compile");
|
||||
+ check(!foreground_chord_compatible(lock.get(), agent.get(), 30, 0), "hidden lock action accepted");
|
||||
+ check(!foreground_chord_compatible(lock.get(), agent.get(), 30, 0, {}), "hidden lock action accepted");
|
||||
+ check(!foreground_chord_compatible(lock.get(), agent.get(), 30, 0, {0, 0, numlock, 0}),
|
||||
+ "Num Lock hid an unexpected lock action");
|
||||
+ // Equal symbols and modifier state can still alter toolkit shortcut matching.
|
||||
+ serialized = xkb_keymap_get_as_string(agent.get(), XKB_KEYMAP_FORMAT_TEXT_V1);
|
||||
+ changed = serialized;
|
||||
@@ -665,7 +1070,9 @@ index 0000000..dd7c962
|
||||
+ changed.insert(type_end, "preserve[Shift] = Shift;\n");
|
||||
+ Map preserved{xkb_keymap_new_from_string(context.get(), changed.c_str(), XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS), xkb_keymap_unref};
|
||||
+ check(bool(preserved), "preserved modifier fixture failed to compile");
|
||||
+ check(!foreground_chord_compatible(preserved.get(), agent.get(), 30, 1), "consumed modifier mismatch accepted");
|
||||
+ check(!foreground_chord_compatible(preserved.get(), agent.get(), 30, 1, {}), "consumed modifier mismatch accepted");
|
||||
+ check(!foreground_chord_compatible(preserved.get(), agent.get(), 30, 1, {0, 0, numlock, 0}),
|
||||
+ "Num Lock hid changed shortcut consumption");
|
||||
+ // A modifier that preserves Shift state but emits another symbol must fail.
|
||||
+ serialized = xkb_keymap_get_as_string(agent.get(), XKB_KEYMAP_FORMAT_TEXT_V1);
|
||||
+ changed = serialized;
|
||||
@@ -677,7 +1084,7 @@ index 0000000..dd7c962
|
||||
+ "key <LFSH> { symbols[Group1]=[Delete], actions[Group1]=[SetMods(modifiers=Shift)] };");
|
||||
+ Map badshift{xkb_keymap_new_from_string(context.get(), changed.c_str(), XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS), xkb_keymap_unref};
|
||||
+ check(bool(badshift), "shift fixture failed to compile");
|
||||
+ check(!foreground_chord_compatible(badshift.get(), agent.get(), 30, 1), "modifier press symbols not checked");
|
||||
+ check(!foreground_chord_compatible(badshift.get(), agent.get(), 30, 1, {}), "modifier press symbols not checked");
|
||||
+ for (auto route : {InputRoute::independent, InputRoute::primary_foreground}) {
|
||||
+ for (uint64_t cap : {1, 4, 8, 16}) check(keyboard_layout_ready(route, cap, false, false), "pointer gated on layout");
|
||||
+ check(!keyboard_layout_ready(route, 2, false, false), "key accepted without map");
|
||||
@@ -687,7 +1094,7 @@ index 0000000..dd7c962
|
||||
+ std::cout << "keyboard layout tests passed\n";
|
||||
+}
|
||||
diff --git a/tests/plugin_input_lifetime_test.cpp b/tests/plugin_input_lifetime_test.cpp
|
||||
index ff09451..b999925 100644
|
||||
index ff09451..456da44 100644
|
||||
--- a/tests/plugin_input_lifetime_test.cpp
|
||||
+++ b/tests/plugin_input_lifetime_test.cpp
|
||||
@@ -51,12 +51,17 @@ int main() {
|
||||
@@ -703,17 +1110,18 @@ index ff09451..b999925 100644
|
||||
};
|
||||
+#ifdef CUA_HYPRLAND_INPUT
|
||||
+ const auto initial_status = HyprlandAPI::registered_legacy_command->fn(FORMAT_JSON, {});
|
||||
+ check(created == 0 && initial_status.find("\"configured\":false") != std::string::npos && initial_status.find("\"keyboard_layout_independent\":true") != std::string::npos,
|
||||
+ check(created == 0 && initial_status.find("\"configured\":false") != std::string::npos && initial_status.find("\"keyboard_layout_independent\":true") != std::string::npos && initial_status.find("\"foreground_numlock_compatible\":true") != std::string::npos,
|
||||
+ "disabled production module advertises compiled keyboard support before enable");
|
||||
+#endif
|
||||
for (unsigned i = 0; i < 20; ++i) {
|
||||
toggle(true);
|
||||
#ifdef CUA_HYPRLAND_INPUT
|
||||
@@ -64,6 +69,7 @@ int main() {
|
||||
@@ -64,6 +69,8 @@ int main() {
|
||||
check(status.find("\"state\":\"input_v3_candidate\"") != std::string::npos &&
|
||||
status.find("trusted_local_per_action") != std::string::npos &&
|
||||
status.find("\"input\":{}") != std::string::npos &&
|
||||
+ status.find("\"keyboard_layout_independent\":true") != std::string::npos &&
|
||||
+ status.find("\"foreground_numlock_compatible\":true") != std::string::npos &&
|
||||
status.find("operator") == std::string::npos,
|
||||
"v3 status advertises its actual admission mode");
|
||||
#endif
|
||||
Reference in new issue
Block a user