Preserve user keymaps in the Cua Hyprland plugin
Give background agent seats independent keymaps and check foreground keyboard compatibility per operation. Package the downstream patch with separate source integrity and build provenance, retaining the upstream ABI verifier. Co-Authored-By: Codex XHigh <noreply@openai.com>
This commit is contained in:
1 parent
6c9bdbd0a2
commit
f868f3c767
7 files changed
+1021
-107
No files matched your search
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"files": {
|
||||
"CMakeLists.txt": "4a2027383ae39b052c4cf30732810f93ee1170b7987777aa9e8ea282abb17731",
|
||||
"LICENSE.md": "c0779290c1d4783169aa3dbfb55feb505e563ef8a004bbf55298ceffcfbda8d9",
|
||||
"SOURCE-PROVENANCE.json": "54f514664c84e1358a435f29cd6befd5661b0b133d76997191c000b10f021a75",
|
||||
"cmake/DetectHyprlandAPI.cmake": "216133ec0eb141c3696bf3770a23e63e46521c9e91a0245a7cb75c20a75ba2c5",
|
||||
"cmake/VerifyRuntime.cmake": "5869f79a418e7aa6178d2b9166c36cd01c3093c2579b647624968244db57b761",
|
||||
"include/cua_hyprland/protocol.hpp": "7051463b3c61a2dc93c388e66b6136b7bb524f8694350f249f9d2b5c55826493",
|
||||
"include/cua_hyprland/session.hpp": "e6a968e4f2ac28122cb7413a0e318f6222d2a0a1b7b0c45f4ab419a78639ebea",
|
||||
"include/cua_hyprland/status.hpp": "56a9656647c0f4eeb0c588cd4b98a77df198d1f90421f973e9a80e6802f61495",
|
||||
"src/drag_geometry.hpp": "c5b783d15ff197f22938f08f8d176bab5d45544fe989d150f15cb99295acedb8",
|
||||
"src/foreground_route.hpp": "bf07e9ada1a3e25dd85611d5f3f4f725df50b1b8fc1d4c4903fb5c6dc9c3d5f8",
|
||||
"src/inject_server.cpp": "0935283580c50fcf0e4ad956f858885700536002b1d86d2f078da9c4404ee9e8",
|
||||
"src/inject_server.hpp": "67de008b4d6983371207bb22a57bab154b1dedda9b38d1207d3ac8cb379382eb",
|
||||
"src/input_client_deadline.hpp": "00a91a789ff698820607449ff7152e2fb50d0f315e0fa9c5c3855752bfffe2ef",
|
||||
"src/input_experiment.cpp": "358fa86abd21dbfda8460b21b3568d0199e47ed79ef6c5dd0c7e0c6d4c026290",
|
||||
"src/input_experiment.hpp": "9da2ddab7f0de6e9cf02aea53e9119acef17ef8513af849bb5aa3d137ebd12c3",
|
||||
"src/input_grant.hpp": "90b544b2f559bacd920b85ab5915f09ff201de3c05a052ce1b71ff71f767e6a6",
|
||||
"src/keyboard_layout.hpp": "66d7d1ace6357c5be26f4a9b57730709fe21e30267437051c62b6697eb073300",
|
||||
"src/owned_socket_path.hpp": "8e784656d944c700f3ded383c93a8cadf846dabdaa4cc12673bcb637c26d9fd1",
|
||||
"src/passive_pointer_target.hpp": "ede36fd9fd6e95ae5923c751f12591084392be9d2270eb06ad64eb4f245169fa",
|
||||
"src/plugin.cpp": "e36817f12b47f317dde300cfa6820ce2d408c8fe5bfea3a3e5c727b86d461d3e",
|
||||
"src/primary_trace.cpp": "e9468d1a3f3be2a90d47bf8c4a638ad8a60fe10b6297582ab7825751cb707aa9",
|
||||
"src/primary_trace.hpp": "8d62535fb0b24a02bb80d9a8dcd540b39204afb2f3b4bcb5cabd5275c3b5eaa7",
|
||||
"src/protocol.cpp": "bd083d65efb05e80946566dc535b1a6fadaa581c66ec327eff41796feba795b1",
|
||||
"src/seat_lifetime.hpp": "386cf5c72c178f8eec0824f2a7d46fa755b0bb000861f9a6e00802f6b81fb779",
|
||||
"src/session.cpp": "0105c7ba5f9e2dbdd9a21f48be0bc1f2bde930f6aa19f77e2216403d7790e4df",
|
||||
"src/status.cpp": "e46e81e5e8ae3b1f50af5dcaa6c1776e236321c788fe61b402c9923c797b1270",
|
||||
"tests/agent_keymap_test.py": "9b112f520a97a77a0d55f1009cd2594988d3c1e8e7bcaa39f213ffad2644dc56",
|
||||
"tests/cmake-api/CMakeLists.txt": "6a66c8f98023f029998af917fcd3ec6b1388607bdf7acaf5bdfaef9141962685",
|
||||
"tests/cmake-api/include/src/plugins/PluginAPI.hpp": "86c8ad51e668908d18928cef1b04e8e6d32a33894525640b52f0b31769c870a0",
|
||||
"tests/desktop_fault_policy_fixture.cpp": "ac24d675ebc64cc98148e852eb5aba5858bffc06332678d14276d04b317aaf15",
|
||||
"tests/desktop_fault_policy_test.py": "c3f624c0239036babd23eaaf1bb6b722f3a0c3321a5d3d6668ae7ab4910ca95c",
|
||||
"tests/drag_geometry_test.cpp": "d32ea649d008fc051fe18555d6fbc54ba5d057b61880b648c5df8aa076a53fbc",
|
||||
"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/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/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",
|
||||
"schema": 1,
|
||||
"upstream_manifest_sha256": "54f514664c84e1358a435f29cd6befd5661b0b133d76997191c000b10f021a75",
|
||||
"upstream_revision": "cc54254464c0c9aebfd6547fe7e4a0ceaf0456d7"
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
# Verified upstream kit with a local package-revision profile; source/tooling are unchanged.
|
||||
# Verified upstream kit plus a separately pinned Omarchy keyboard-remap patch.
|
||||
# Normal reruns need a fresh build directory; makepkg -e reuses verified extracted trees.
|
||||
# Profile kit: original source bytes and separately committed packaging tooling.
|
||||
# shellcheck shell=bash disable=SC2034,SC2154
|
||||
pkgname=cua-hyprland-plugin
|
||||
pkgver=0.26.1
|
||||
pkgrel=3
|
||||
pkgdesc='Cua input candidate for reviewed profile omarchy-hyprland-0562r3'
|
||||
pkgrel=4
|
||||
pkgdesc='Cua input candidate for reviewed profile omarchy-hyprland-0562r3-remaps'
|
||||
arch=('x86_64')
|
||||
url='https://github.com/trycua/cua'
|
||||
license=('MIT')
|
||||
depends=('hyprland=0.56.2-3' 'aquamarine=0.15.0-2' 'glibc=2.44+r24+g16be1518495f-1' 'hyprcursor=0.1.13-7' 'hyprgraphics=0.5.1-4' 'hyprlang=0.6.8-5' 'hyprutils=0.14.2-1' 'libgcc=16.2.1+r23+gd564253eb6c8-1' 'libstdc++=16.2.1+r23+gd564253eb6c8-1' 'libxkbcommon=1.13.2-1' 'wayland=1.26.0-1' 'python>=3.11' 'binutils')
|
||||
makedepends=('cmake>=3.30' 'ninja' 'pkgconf' 'gcc')
|
||||
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='1391084a903254fb16d251522dba41ae34a06cfa4b2f6b7ba91a3cceeb895921'
|
||||
_profile_sha256='eebba812a7513b471cf8969364334a4bde1eaa9c4519157701c771c1b6cd1b07'
|
||||
_kit_sha256='aa88498fc9635e493a64aaf4e2a08cdb5249d576d7e00c3cf100799d5c9f23f2'
|
||||
_profile_sha256='ee9b7cf5fbcc78ab169a87b5edb061a85c4425f7ddf41c90328cf020550c5f9a'
|
||||
_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,7 +24,17 @@ source=('https://github.com/trycua/cua/releases/download/cua-hyprland-kit-v1.1.0
|
||||
'PROFILE.json')
|
||||
noextract=("$_download_name")
|
||||
sha256sums=('a89bfa7f6490f598719dfabdd2a5badc137e27b0764fd25e6a05e1f969433520'
|
||||
'eebba812a7513b471cf8969364334a4bde1eaa9c4519157701c771c1b6cd1b07')
|
||||
'ee9b7cf5fbcc78ab169a87b5edb061a85c4425f7ddf41c90328cf020550c5f9a')
|
||||
|
||||
# Downstream inputs are also checked explicitly when makepkg integrity is skipped.
|
||||
declare -gA _downstream_sha256=(
|
||||
['independent-keymaps.patch']='5847cd2c300a9b3c7dc1e836722a0e2b222892e308290195d973300c7f74e0e4'
|
||||
['DOWNSTREAM-PROVENANCE.json']='e87949fa29d8c006f1c829cb81e640980b349a704bedb5f1d4fe7dc47746994e'
|
||||
['downstream.py']='7c9725805ad038737bd4d346c85f27991ce9ddfff436a32ebf515cad1bf98ca1'
|
||||
['downstream_test.py']='7b9a992979b22a5e061fdf011ac6be59a3f1ae6e0d7c0a8ff631f74e71984c5a'
|
||||
)
|
||||
source+=('independent-keymaps.patch' 'DOWNSTREAM-PROVENANCE.json' 'downstream.py' 'downstream_test.py')
|
||||
sha256sums+=('5847cd2c300a9b3c7dc1e836722a0e2b222892e308290195d973300c7f74e0e4' 'e87949fa29d8c006f1c829cb81e640980b349a704bedb5f1d4fe7dc47746994e' '7c9725805ad038737bd4d346c85f27991ce9ddfff436a32ebf515cad1bf98ca1' '7b9a992979b22a5e061fdf011ac6be59a3f1ae6e0d7c0a8ff631f74e71984c5a')
|
||||
|
||||
_verify_download() {
|
||||
python3 -I - "$SRCDEST/$_download_name" "$_download_sha256" "$srcdir" "$1" "$SRCDEST/PROFILE.json" <<'CUA_DOWNLOAD_PY'
|
||||
@@ -65,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) == 'eebba812a7513b471cf8969364334a4bde1eaa9c4519157701c771c1b6cd1b07',
|
||||
require(digest(profile_data) == 'ee9b7cf5fbcc78ab169a87b5edb061a85c4425f7ddf41c90328cf020550c5f9a',
|
||||
'local profile checksum mismatch')
|
||||
profile = json.loads(payload['PROFILE.json'])
|
||||
profile.update(profile_id='omarchy-hyprland-0562r3', package_release=3)
|
||||
profile.update(profile_id='omarchy-hyprland-0562r3-remaps', package_release=4)
|
||||
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
|
||||
@@ -76,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=3\n'), ('omarchy-stable-20260910', profile['profile_id']),
|
||||
for old, new in [('pkgrel=2\n', 'pkgrel=4\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']))]:
|
||||
@@ -84,10 +94,10 @@ for old, new in [('pkgrel=2\n', 'pkgrel=3\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': 'eebba812a7513b471cf8969364334a4bde1eaa9c4519157701c771c1b6cd1b07',
|
||||
'KIT-PROVENANCE.json': '1391084a903254fb16d251522dba41ae34a06cfa4b2f6b7ba91a3cceeb895921',
|
||||
'PKGBUILD': '4c5ef50c5a8d556d461afe283b13b228fb09d2558b37fb76257e13e25846325b',
|
||||
'SHA256SUMS': '2ea22af60f8c86df588fd7db103d9f444d9ac4018b704a48ddff2545ff7ce999'})
|
||||
expected.update({'PROFILE.json': 'ee9b7cf5fbcc78ab169a87b5edb061a85c4425f7ddf41c90328cf020550c5f9a',
|
||||
'KIT-PROVENANCE.json': 'aa88498fc9635e493a64aaf4e2a08cdb5249d576d7e00c3cf100799d5c9f23f2',
|
||||
'PKGBUILD': '0e71ddab36e5d9214c35811f2cbf89cc98e5b4fdf6656f85e6b3885d07a1c021',
|
||||
'SHA256SUMS': '404cf40875ab14b8af84847abd4674945b1c7ab2e0bfb1605fa11e2d85d789db'})
|
||||
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')
|
||||
@@ -132,17 +142,31 @@ _verify() {
|
||||
printf '%s %s\n' "$_profile_sha256" "$srcdir/cua-profile-kit/PROFILE.json" | sha256sum -c - || return 1
|
||||
printf '%s %s\n' "$_verifier_sha256" "$srcdir/cua-profile-kit/profile_verify.py" | sha256sum -c - || return 1
|
||||
python3 "$srcdir/cua-profile-kit/profile_verify.py" --kit "$srcdir/cua-profile-kit" --kit-sha256 "$_kit_sha256" \
|
||||
--archive "$srcdir/cua-profile-kit/${_stem}.tar.gz" --source "$srcdir/$_stem" --cxx "$_cxx" "$@"
|
||||
--archive "$srcdir/cua-profile-kit/${_stem}.tar.gz" --source "$srcdir/$_stem" --cxx "$_cxx"
|
||||
}
|
||||
|
||||
_downstream() {
|
||||
local name
|
||||
for name in independent-keymaps.patch DOWNSTREAM-PROVENANCE.json downstream.py downstream_test.py; do
|
||||
printf '%s %s\n' "${_downstream_sha256[$name]}" "$SRCDEST/$name" | sha256sum -c - || return 1
|
||||
done
|
||||
python3 -B "$SRCDEST/downstream.py" "$1" \
|
||||
--pristine "$srcdir/$_stem" --source "$srcdir/omarchy-source" \
|
||||
--patch "$SRCDEST/independent-keymaps.patch" --manifest "$SRCDEST/DOWNSTREAM-PROVENANCE.json" \
|
||||
--kit "$srcdir/cua-profile-kit" --kit-sha256 "$_kit_sha256" \
|
||||
--archive "$srcdir/cua-profile-kit/${_stem}.tar.gz" --cxx "$_cxx" "${@:2}"
|
||||
}
|
||||
|
||||
prepare() {
|
||||
_verify_download extract || return 1
|
||||
_verify
|
||||
_verify || return 1
|
||||
_downstream prepare
|
||||
}
|
||||
|
||||
build() {
|
||||
_verify || return 1
|
||||
cmake -S "$srcdir/$_stem" -B "$srcdir/build" -G Ninja \
|
||||
_downstream check || return 1
|
||||
cmake -S "$srcdir/omarchy-source" -B "$srcdir/build" -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER="$_cxx" \
|
||||
-DPKG_CONFIG_EXECUTABLE=/usr/bin/pkgconf -DPKG_CONFIG_ARGN= \
|
||||
-DPKG_CONFIG_USE_CMAKE_PREFIX_PATH=OFF -DCMAKE_PREFIX_PATH= \
|
||||
@@ -155,6 +179,8 @@ build() {
|
||||
|
||||
check() {
|
||||
_verify || return 1
|
||||
_downstream check || return 1
|
||||
python3 -B "$SRCDEST/downstream_test.py" || return 1
|
||||
(
|
||||
unset LD_PRELOAD FAKEROOTKEY FAKED_MODE
|
||||
ctest --test-dir "$srcdir/build" --output-on-failure --no-tests=error
|
||||
@@ -163,7 +189,7 @@ check() {
|
||||
|
||||
package() {
|
||||
check || return 1
|
||||
_verify --build "$srcdir/build" --output "$srcdir/BUILD-PROVENANCE.json" || return 1
|
||||
_downstream build --build "$srcdir/build" --output "$srcdir/BUILD-PROVENANCE.json" || return 1
|
||||
install -Dm755 "$srcdir/build/cua-hyprland-plugin.so" \
|
||||
"$pkgdir/usr/lib/cua/hyprland/cua-hyprland-plugin.so" || return 1
|
||||
install -Dm644 "$srcdir/$_stem/LICENSE.md" \
|
||||
@@ -175,4 +201,7 @@ package() {
|
||||
for name in KIT-PROVENANCE.json PROFILE.json profile_verify.py; do
|
||||
install -Dm644 "$srcdir/cua-profile-kit/$name" "$pkgdir/usr/share/$pkgname/$name" || return 1
|
||||
done
|
||||
for name in DOWNSTREAM-PROVENANCE.json independent-keymaps.patch; do
|
||||
install -Dm644 "$SRCDEST/$name" "$pkgdir/usr/share/$pkgname/$name" || return 1
|
||||
done
|
||||
}
|
||||
@@ -12,8 +12,8 @@
|
||||
"sha256": "da8fcacf347bcbed83edc40108c6e2298da095e22246bd764e9bb382786cebb2"
|
||||
},
|
||||
"kit_version": "1.1.0",
|
||||
"package_release": 3,
|
||||
"profile_id": "omarchy-hyprland-0562r3",
|
||||
"package_release": 4,
|
||||
"profile_id": "omarchy-hyprland-0562r3-remaps",
|
||||
"runtime": {
|
||||
"basename": "libstdc++.so.6.0.36",
|
||||
"packages": {
|
||||
|
||||
@@ -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. 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 `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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -10,9 +10,9 @@ The package uses the [Driver 0.26.1 plugin source](https://github.com/trycua/cua
|
||||
including the [desktop-fault cleanup repair](https://github.com/trycua/cua/pull/3702).
|
||||
It is not a repackaging of the unmodified 0.24.0 plugin.
|
||||
|
||||
The qualified 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 this pinned module; its production plugin source is unchanged from the source used here. Discovery protocol v2 is separate. A newer Driver release is a changed pairing and requires affected replay before promotion.
|
||||
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`, kit tooling `1.1.0`, and package release `3` pin:
|
||||
Profile `omarchy-hyprland-0562r3-remaps`, kit tooling `1.1.0`, and package release `4` 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.
|
||||
@@ -24,14 +24,13 @@ its compositor executable and all 498 header/pkg-config files are byte-identical
|
||||
to `-2`. Both executables have SHA-256
|
||||
`da8fcacf347bcbed83edc40108c6e2298da095e22246bd764e9bb382786cebb2`.
|
||||
The checked-in `PROFILE.json` changes only the profile name, package release,
|
||||
and exact Hyprland package version. Compiler, runtime, source, executable,
|
||||
and header identities remain unchanged. The download wrapper verifies the
|
||||
and exact Hyprland package version. Compiler, runtime, upstream source, executable,
|
||||
and header identities remain unchanged; the separately recorded patch changes the build source. The download wrapper verifies the
|
||||
original kit before deriving the updated profile, recipe, and provenance,
|
||||
then verifies every derived member against its recorded digest.
|
||||
|
||||
The native qualification below was recorded with package release `2` and
|
||||
Hyprland `-2`. This packaging update does not claim a new application or
|
||||
Driver replay. The `-3` dependency must reach a destination channel before
|
||||
Hyprland `-2`. The downstream keymap change needs its own application and Driver replay before promotion. The `-3` dependency must reach a destination channel before
|
||||
this artifact can be installed there; publication still follows edge → RC → stable.
|
||||
|
||||
The generated `PKGBUILD` identifies the immutable kit download, outer checksum,
|
||||
@@ -47,7 +46,17 @@ and production flags remain mandatory. Packaging runs all bundled CTests even
|
||||
with `--nocheck` or `--repackage`; `--skipinteg` does not bypass recipe checks.
|
||||
Production input is built in; experimental signed input and tracing are off.
|
||||
|
||||
## What is qualified
|
||||
The pristine upstream archive, manifest, and verifier remain unchanged. `independent-keymaps.patch` is applied to a separate source tree, and `DOWNSTREAM-PROVENANCE.json` pins the patch and every resulting source file. Build, check, and package revalidate both trees, including when makepkg integrity checks are skipped. `BUILD-PROVENANCE.json` records the upstream base under `source`, the applied change under `downstream`, and the final module digest; the downstream manifest and patch are installed beside it. This preserves the existing compiler, headers, runtime, and consumer checks without representing the modified module as an unmodified upstream build.
|
||||
|
||||
## Keyboard behavior
|
||||
|
||||
Each background lane owns a canonical US keymap and independent modifier state. The physical keyboard keeps its layout, Compose key, and remaps. No installation or activation step edits `input:kb_*`. Existing Driver keycodes are interpreted by the agent keyboard, so this does not add Unicode, IME, or new Driver text routes.
|
||||
|
||||
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.
|
||||
|
||||
## Historical upstream qualification
|
||||
|
||||
The initial app scope is native Wayland Inkscape `1.4.4-6` with the canonical
|
||||
US keymap. Two lanes require independent Driver processes and distinct native
|
||||
@@ -131,7 +140,7 @@ kit-provenance digest:
|
||||
```sh
|
||||
python3 /usr/share/cua-hyprland-plugin/profile_verify.py \
|
||||
--kit /usr/share/cua-hyprland-plugin \
|
||||
--kit-sha256 1391084a903254fb16d251522dba41ae34a06cfa4b2f6b7ba91a3cceeb895921 \
|
||||
--kit-sha256 aa88498fc9635e493a64aaf4e2a08cdb5249d576d7e00c3cf100799d5c9f23f2 \
|
||||
--consumer /usr/lib/cua/hyprland/cua-hyprland-plugin.so
|
||||
```
|
||||
|
||||
@@ -146,98 +155,28 @@ hyprctl plugin load /usr/lib/cua/hyprland/cua-hyprland-plugin.so
|
||||
hyprctl -j cua:status
|
||||
```
|
||||
|
||||
Loading alone does not enable input. Before opting in, save open work and save
|
||||
the exact current personal input configuration. The backup command refuses a
|
||||
symlinked input file and refuses to replace an earlier backup:
|
||||
Loading alone does not enable input. Use Omarchy's explicit Cua Input toggle when available; it verifies the installed profile and loaded capability and removes the legacy copied toggle's keyboard override. If it reports an older mapped plugin, disable Cua Input and log out and back in before enabling it again. Never hot-unload and reload the module.
|
||||
|
||||
```sh
|
||||
test -f "$HOME/.config/hypr/input.lua" && \
|
||||
test ! -L "$HOME/.config/hypr/input.lua" && \
|
||||
test ! -e "$HOME/.config/hypr/input.lua.cua-before" && \
|
||||
cp --archive -- "$HOME/.config/hypr/input.lua" \
|
||||
"$HOME/.config/hypr/input.lua.cua-before"
|
||||
```
|
||||
|
||||
If `input.lua` is a symlink, stop here: back up and later restore its resolved
|
||||
target explicitly instead of using the commands below.
|
||||
|
||||
The background-input admission guard requires the exact XKB keymap
|
||||
`rules=evdev`, `model=pc105`, `layout=us`, empty variant and options, and no
|
||||
custom keymap file. Stock Omarchy 4.0.3 English (US) is not that literal
|
||||
configuration: it leaves rules and model empty and sets
|
||||
`compose:caps,shift:both_capslock_cancel`. Those options make Caps Lock the
|
||||
Compose key and both Shift keys the Caps Lock/cancel chord. The required empty
|
||||
options restore ordinary Caps Lock behavior and remove both stock shortcuts
|
||||
while Cua input is enabled. Any other effective value is intentionally refused
|
||||
as `unsupported_layout`; do not weaken or bypass that admission guard.
|
||||
|
||||
Append this override to `~/.config/hypr/input.lua` so it follows any existing
|
||||
input settings. It both selects the exact admitted keymap and enables the
|
||||
trusted local transport:
|
||||
For manual activation, add only this plugin setting to a sourced Hyprland Lua configuration file, preserving all existing input settings:
|
||||
|
||||
```lua
|
||||
hl.config({
|
||||
input = {
|
||||
kb_rules = "evdev",
|
||||
kb_model = "pc105",
|
||||
kb_layout = "us",
|
||||
kb_variant = "",
|
||||
kb_options = "",
|
||||
kb_file = "",
|
||||
},
|
||||
plugin = { cua = { enabled = true } },
|
||||
})
|
||||
```
|
||||
|
||||
Reload, then read back every keymap value rather than relying on the source
|
||||
file alone:
|
||||
Then reload and inspect status:
|
||||
|
||||
```sh
|
||||
hyprctl reload
|
||||
for name in kb_rules kb_model kb_layout kb_variant kb_options kb_file; do
|
||||
value=$(hyprctl -j getoption "input:$name" | jq -r '.str')
|
||||
printf '%s=%s\n' "$name" "$value"
|
||||
done
|
||||
hyprctl -j cua:status
|
||||
```
|
||||
|
||||
The keymap readback must be exactly:
|
||||
|
||||
```text
|
||||
kb_rules=evdev
|
||||
kb_model=pc105
|
||||
kb_layout=us
|
||||
kb_variant=
|
||||
kb_options=
|
||||
kb_file=
|
||||
```
|
||||
|
||||
A runtime keyword or Lua evaluation without `hyprctl reload` does not reconcile
|
||||
the input sockets. Continue only when status also reports input protocol v3,
|
||||
input capability, socket paths, and the expected compositor identity. Do not
|
||||
disable NumLock; that was required only by a strict qualification observer, not
|
||||
by the demonstrated background-input contract.
|
||||
|
||||
Start Driver with `CUA_DRIVER_RS_ENABLE_WAYLAND=1`. For the activation check,
|
||||
use background input in a new disposable Inkscape document to create a text
|
||||
object containing `CUA activation check`. Save it under a new temporary
|
||||
filename, then verify the text in both a fresh Driver snapshot and the reopened
|
||||
saved SVG. Never test against an existing document, and do not automatically
|
||||
replay an action with a partial or unknown outcome.
|
||||
|
||||
After the check, restore the exact saved configuration and reload it:
|
||||
|
||||
```sh
|
||||
command mv --force -- "$HOME/.config/hypr/input.lua.cua-before" \
|
||||
"$HOME/.config/hypr/input.lua"
|
||||
hyprctl reload
|
||||
hyprctl -j cua:status
|
||||
```
|
||||
|
||||
Confirm that the prior keymap values are back and status reports input disabled.
|
||||
Retained inert agent pointers can remain until the compositor exits; disabling
|
||||
input does not unload the mapped module. If you intentionally keep activation,
|
||||
retain the backup until you are ready to perform this exact restoration.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
To disable input, turn the Cua Input toggle off, or remove the manual `plugin.cua.enabled` setting and reload. Confirm that status reports input disabled. Retained inert agent pointers can remain until the compositor exits; disabling input does not unload the mapped module.
|
||||
|
||||
Before an incompatible desktop update, remove operator-added plugin activation
|
||||
settings, save work, and exit the graphical session. From a text console, run
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify the Omarchy patch separately from the unchanged upstream source kit."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path, PurePosixPath
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
|
||||
def require(condition, message):
|
||||
if not condition:
|
||||
raise ValueError(message)
|
||||
|
||||
|
||||
def digest(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def inventory(root):
|
||||
require(root.is_dir() and not root.is_symlink(), "source must be a real directory")
|
||||
result = {}
|
||||
for path in root.rglob("*"):
|
||||
require(not path.is_symlink() and (path.is_dir() or path.is_file()), "nonregular source entry")
|
||||
if path.is_file():
|
||||
result[path.relative_to(root).as_posix()] = digest(path)
|
||||
return result
|
||||
|
||||
|
||||
def verify_inputs(pristine, patch, manifest):
|
||||
require(manifest["schema"] == 1, "unsupported downstream schema")
|
||||
require(patch.is_file() and not patch.is_symlink() and digest(patch) == manifest["patch_sha256"],
|
||||
"downstream patch checksum mismatch")
|
||||
require(digest(pristine / "SOURCE-PROVENANCE.json") == manifest["upstream_manifest_sha256"],
|
||||
"downstream base manifest mismatch")
|
||||
require(manifest["files"], "empty downstream inventory")
|
||||
for name in manifest["files"]:
|
||||
path = PurePosixPath(name)
|
||||
require(name and not path.is_absolute() and path.as_posix() == name and
|
||||
".." not in path.parts and "\\" not in name, "invalid downstream path")
|
||||
|
||||
|
||||
def verify_tree(source, manifest):
|
||||
require(inventory(source) == manifest["files"], "patched source inventory/checksum mismatch")
|
||||
|
||||
|
||||
def prepare(pristine, source, patch, manifest):
|
||||
require(not source.exists() and not source.is_symlink(), "patched source requires a fresh destination")
|
||||
shutil.copytree(pristine, source)
|
||||
subprocess.run(["patch", "--batch", "--fuzz=0", "-p1", "-i", str(patch.resolve())], cwd=source, check=True)
|
||||
verify_tree(source, manifest)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("mode", choices=("prepare", "check", "build"))
|
||||
parser.add_argument("--pristine", required=True, type=Path)
|
||||
parser.add_argument("--source", required=True, type=Path)
|
||||
parser.add_argument("--patch", required=True, type=Path)
|
||||
parser.add_argument("--manifest", required=True, type=Path)
|
||||
parser.add_argument("--kit", required=True, type=Path)
|
||||
parser.add_argument("--kit-sha256", required=True)
|
||||
parser.add_argument("--archive", required=True, type=Path)
|
||||
parser.add_argument("--cxx", required=True, type=Path)
|
||||
parser.add_argument("--build", type=Path)
|
||||
parser.add_argument("--output", type=Path)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
# PKGBUILD authenticates the verifier and this helper before execution.
|
||||
spec = importlib.util.spec_from_file_location("upstream_profile", args.kit / "profile_verify.py")
|
||||
upstream = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(upstream)
|
||||
profile, kit = upstream.verify_kit(args.kit, args.kit_sha256)
|
||||
base = upstream.verify_archive(args.archive, profile)
|
||||
require(upstream.verify_source(args.pristine, profile) == base, "upstream source identity mismatch")
|
||||
manifest = upstream.read_json(args.manifest.read_bytes())
|
||||
verify_inputs(args.pristine, args.patch, manifest)
|
||||
if args.mode == "prepare":
|
||||
prepare(args.pristine, args.source, args.patch, manifest)
|
||||
else:
|
||||
verify_tree(args.source, manifest)
|
||||
if args.mode == "build":
|
||||
require(args.build is not None and args.output is not None, "build evidence requires output")
|
||||
native = upstream.verify_native(args.cxx, profile)
|
||||
native["module_sha256"] = upstream.verify_build(args.build, args.source, args.cxx, profile)
|
||||
native["module_runtime_sha256"] = profile["runtime"]["sha256"]
|
||||
args.output.write_bytes(upstream.json_bytes(dict(native, source=base, profile=profile,
|
||||
kit=kit, downstream=manifest)))
|
||||
except (ValueError, KeyError, TypeError, OSError, subprocess.CalledProcessError) as error:
|
||||
parser.exit(1, f"error: {error}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise downstream integrity with real patch application and tampering."""
|
||||
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import downstream
|
||||
|
||||
|
||||
class DownstreamTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
self.pristine = self.root / "pristine"
|
||||
self.pristine.mkdir()
|
||||
(self.pristine / "SOURCE-PROVENANCE.json").write_text("upstream\n")
|
||||
(self.pristine / "input.cpp").write_text("old\n")
|
||||
self.patch = self.root / "change.patch"
|
||||
self.patch.write_text("--- a/input.cpp\n+++ b/input.cpp\n@@ -1 +1 @@\n-old\n+new\n")
|
||||
self.source = self.root / "patched"
|
||||
self.manifest = {
|
||||
"schema": 1,
|
||||
"patch_sha256": downstream.digest(self.patch),
|
||||
"upstream_manifest_sha256": downstream.digest(self.pristine / "SOURCE-PROVENANCE.json"),
|
||||
"files": {"SOURCE-PROVENANCE.json": downstream.digest(self.pristine / "SOURCE-PROVENANCE.json"),
|
||||
"input.cpp": hashlib.sha256(b"new\n").hexdigest()},
|
||||
}
|
||||
|
||||
def test_applies_patch_without_changing_upstream(self):
|
||||
downstream.verify_inputs(self.pristine, self.patch, self.manifest)
|
||||
downstream.prepare(self.pristine, self.source, self.patch, self.manifest)
|
||||
self.assertEqual((self.pristine / "input.cpp").read_text(), "old\n")
|
||||
self.assertEqual((self.source / "input.cpp").read_text(), "new\n")
|
||||
|
||||
def test_changed_patch_refuses(self):
|
||||
self.patch.write_text(self.patch.read_text().replace("+new", "+bad"))
|
||||
with self.assertRaisesRegex(ValueError, "patch checksum"):
|
||||
downstream.verify_inputs(self.pristine, self.patch, self.manifest)
|
||||
|
||||
def test_changed_base_manifest_refuses(self):
|
||||
(self.pristine / "SOURCE-PROVENANCE.json").write_text("different\n")
|
||||
with self.assertRaisesRegex(ValueError, "base manifest"):
|
||||
downstream.verify_inputs(self.pristine, self.patch, self.manifest)
|
||||
|
||||
def test_tampered_missing_and_extra_files_refuse(self):
|
||||
downstream.prepare(self.pristine, self.source, self.patch, self.manifest)
|
||||
file = self.source / "input.cpp"
|
||||
for content in ("tampered\n", None):
|
||||
if content is None:
|
||||
file.unlink()
|
||||
else:
|
||||
file.write_text(content)
|
||||
with self.assertRaisesRegex(ValueError, "inventory/checksum"):
|
||||
downstream.verify_tree(self.source, self.manifest)
|
||||
file.write_text("new\n")
|
||||
(self.source / "unexpected.cpp").write_text("extra\n")
|
||||
with self.assertRaisesRegex(ValueError, "inventory/checksum"):
|
||||
downstream.verify_tree(self.source, self.manifest)
|
||||
|
||||
def test_symlink_and_reused_destination_refuse(self):
|
||||
downstream.prepare(self.pristine, self.source, self.patch, self.manifest)
|
||||
with self.assertRaisesRegex(ValueError, "fresh destination"):
|
||||
downstream.prepare(self.pristine, self.source, self.patch, self.manifest)
|
||||
file = self.source / "input.cpp"
|
||||
file.unlink()
|
||||
file.symlink_to(self.pristine / "input.cpp")
|
||||
with self.assertRaisesRegex(ValueError, "nonregular"):
|
||||
downstream.verify_tree(self.source, self.manifest)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,719 @@
|
||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||
index 8a80de2..5dedb8f 100644
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -39,8 +39,24 @@ 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)
|
||||
|
||||
+if(BUILD_TESTING OR CUA_HYPRLAND_INPUT OR CUA_HYPRLAND_TEST_INPUT)
|
||||
+ find_package(PkgConfig REQUIRED)
|
||||
+ pkg_check_modules(XKBCOMMON REQUIRED IMPORTED_TARGET xkbcommon)
|
||||
+endif()
|
||||
+
|
||||
if(BUILD_TESTING)
|
||||
+ add_executable(cua_hyprland_keyboard_layout_test tests/keyboard_layout_test.cpp)
|
||||
+ target_compile_features(cua_hyprland_keyboard_layout_test PRIVATE cxx_std_26)
|
||||
+ target_include_directories(cua_hyprland_keyboard_layout_test PRIVATE src)
|
||||
+ target_link_libraries(cua_hyprland_keyboard_layout_test PRIVATE PkgConfig::XKBCOMMON)
|
||||
+ target_compile_options(cua_hyprland_keyboard_layout_test PRIVATE -Wall -Wextra -Wpedantic -Werror)
|
||||
+ add_test(NAME cua_hyprland_keyboard_layout_test COMMAND cua_hyprland_keyboard_layout_test)
|
||||
+
|
||||
find_package(Python3 REQUIRED COMPONENTS Interpreter)
|
||||
+ 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_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)
|
||||
message(FATAL_ERROR "Input is pinned to Hyprland 0.56.2")
|
||||
endif()
|
||||
target_sources(cua_hyprland_plugin PRIVATE src/input_experiment.cpp)
|
||||
+ target_link_libraries(cua_hyprland_plugin PRIVATE PkgConfig::XKBCOMMON)
|
||||
endif()
|
||||
if(CUA_HYPRLAND_INPUT)
|
||||
target_compile_definitions(cua_hyprland_plugin PRIVATE CUA_HYPRLAND_INPUT=1)
|
||||
diff --git a/src/input_experiment.cpp b/src/input_experiment.cpp
|
||||
index fc7e740..f256fc4 100644
|
||||
--- a/src/input_experiment.cpp
|
||||
+++ b/src/input_experiment.cpp
|
||||
@@ -9,6 +9,8 @@
|
||||
#include "seat_lifetime.hpp"
|
||||
#include "owned_socket_path.hpp"
|
||||
#include "foreground_route.hpp"
|
||||
+#include "keyboard_layout.hpp"
|
||||
+#include <sys/mman.h>
|
||||
|
||||
#include <src/Compositor.hpp>
|
||||
#include <src/devices/IKeyboard.hpp>
|
||||
@@ -214,7 +216,11 @@ struct InputExperiment::Impl {
|
||||
xkb_keymap* keymap = nullptr;
|
||||
xkb_state* keyboard_state = nullptr;
|
||||
int keymap_fd = -1;
|
||||
- bool retired = false, suspended = true, us_keymap = false, physical_keymap_present = false;
|
||||
+ // Foreground state uses the actual primary map; never share it with agent seats.
|
||||
+ xkb_keymap* physical_keymap = nullptr;
|
||||
+ xkb_state* physical_state = nullptr;
|
||||
+ std::string physical_keymap_text;
|
||||
+ bool retired = false, suspended = true, physical_keymap_present = false;
|
||||
WP<IKeyboard> physical_keyboard;
|
||||
CHyprSignalListener keymap_listener;
|
||||
unsigned lane;
|
||||
@@ -243,23 +249,37 @@ struct InputExperiment::Impl {
|
||||
// No private key or input-enabled default exists in this component.
|
||||
}
|
||||
|
||||
- static bool canonical_us_keymap(xkb_context* context, xkb_keymap* map) {
|
||||
- // Compare canonical compiled content, not a layout display name. This
|
||||
- // deliberately excludes variants, options, remaps, and multiple groups.
|
||||
- const xkb_rule_names names{"evdev", "pc105", "us", "", ""};
|
||||
- auto* reference = xkb_keymap_new_from_names(context, &names, XKB_KEYMAP_COMPILE_NO_FLAGS);
|
||||
- if (!reference) return false;
|
||||
- char* actual = xkb_keymap_get_as_string(map, XKB_KEYMAP_FORMAT_TEXT_V1);
|
||||
- char* expected = xkb_keymap_get_as_string(reference, XKB_KEYMAP_FORMAT_TEXT_V1);
|
||||
- const bool matches = actual && expected && std::strcmp(actual, expected) == 0;
|
||||
- std::free(actual); std::free(expected); xkb_keymap_unref(reference);
|
||||
- return matches;
|
||||
- }
|
||||
- bool layout_qualified() const {
|
||||
- if (!kProduction) return true;
|
||||
+ bool physical_layout_ready() const {
|
||||
const auto keyboard = g_pSeatManager->m_keyboard.lock();
|
||||
- return physical_keymap_present && us_keymap && keyboard_state && keyboard &&
|
||||
- keyboard->m_xkbKeymapV1FD.get() >= 0 && keyboard->m_xkbKeymapV1String == keymap_text;
|
||||
+ return physical_keymap_present && physical_state && keyboard &&
|
||||
+ keyboard->m_xkbKeymapV1FD.get() >= 0 && keyboard->m_xkbKeymapV1String == physical_keymap_text;
|
||||
+ }
|
||||
+ bool layout_qualified(InputRoute route, std::uint64_t capability) const {
|
||||
+ return keyboard_layout_ready(route, capability, keyboard_state && keymap_fd >= 0, physical_layout_ready());
|
||||
+ }
|
||||
+ void initialize_agent_keymap() {
|
||||
+ if (keymap) return;
|
||||
+ xkb_context_ = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
|
||||
+ keymap = xkb_context_ ? agent_keymap(xkb_context_) : nullptr;
|
||||
+ keyboard_state = keymap ? xkb_state_new(keymap) : nullptr;
|
||||
+ char* text = keymap ? xkb_keymap_get_as_string(keymap, XKB_KEYMAP_FORMAT_TEXT_V1) : nullptr;
|
||||
+ if (!keyboard_state || !text) {
|
||||
+ std::free(text);
|
||||
+ throw std::runtime_error("agent XKB keymap unavailable");
|
||||
+ }
|
||||
+ keymap_text = text;
|
||||
+ std::free(text);
|
||||
+ keymap_fd = memfd_create("cua-agent-keymap", MFD_CLOEXEC | MFD_ALLOW_SEALING);
|
||||
+ if (keymap_fd < 0) throw std::runtime_error("agent keymap fd unavailable");
|
||||
+ std::size_t offset = 0;
|
||||
+ while (offset < keymap_text.size() + 1) {
|
||||
+ const auto count = write(keymap_fd, keymap_text.c_str() + offset, keymap_text.size() + 1 - offset);
|
||||
+ if (count < 0 && errno == EINTR) continue;
|
||||
+ if (count <= 0) throw std::runtime_error("agent keymap write failed");
|
||||
+ offset += static_cast<std::size_t>(count);
|
||||
+ }
|
||||
+ if (fcntl(keymap_fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE | F_SEAL_SEAL) < 0)
|
||||
+ throw std::runtime_error("agent keymap sealing failed");
|
||||
}
|
||||
void sync_keymap() {
|
||||
const auto keyboard = g_pSeatManager->m_keyboard.lock();
|
||||
@@ -278,38 +298,20 @@ struct InputExperiment::Impl {
|
||||
return;
|
||||
}
|
||||
physical_keymap_present = true;
|
||||
- if (keyboard_state && keymap_text == keyboard->m_xkbKeymapV1String) return;
|
||||
- // Prepare a complete replacement before retiring the old independent
|
||||
- // state. Keep our own fd: primary keyboard replacement must not leave
|
||||
- // later seat bindings referring to a closed compositor fd.
|
||||
- auto* context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
|
||||
- auto* map = context ? xkb_keymap_new_from_string(context, keyboard->m_xkbKeymapV1String.c_str(),
|
||||
- XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS) : nullptr;
|
||||
- auto* state = map ? xkb_state_new(map) : nullptr;
|
||||
- const auto fd = fcntl(keyboard->m_xkbKeymapV1FD.get(), F_DUPFD_CLOEXEC, 0);
|
||||
- if (!state || fd < 0) {
|
||||
- if (fd >= 0) close(fd);
|
||||
- if (state) xkb_state_unref(state);
|
||||
- if (map) xkb_keymap_unref(map);
|
||||
- if (context) xkb_context_unref(context);
|
||||
- throw std::runtime_error("independent XKB state unavailable");
|
||||
- }
|
||||
+ if (physical_keymap_text == keyboard->m_xkbKeymapV1String) return;
|
||||
+ // Physical changes still revoke authority, but never replace the map
|
||||
+ // advertised by either background keyboard.
|
||||
desktop_transition();
|
||||
- if (keyboard_state) xkb_state_unref(keyboard_state);
|
||||
- if (keymap) xkb_keymap_unref(keymap);
|
||||
- if (xkb_context_) xkb_context_unref(xkb_context_);
|
||||
- if (keymap_fd >= 0) close(keymap_fd);
|
||||
- keyboard_state = state; keymap = map; xkb_context_ = context; keymap_fd = fd;
|
||||
- us_keymap = !kProduction || canonical_us_keymap(context, map);
|
||||
- keymap_text = keyboard->m_xkbKeymapV1String;
|
||||
- for (auto& k : keyboards)
|
||||
- if (!k->dead && k->wl->resource())
|
||||
- k->wl->sendKeymap(WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1, keymap_fd, keymap_text.size() + 1);
|
||||
- for (auto& seat : seats)
|
||||
- if (!seat->dead && seat->wl->resource())
|
||||
- seat->wl->sendCapabilities(static_cast<wl_seat_capability>(WL_SEAT_CAPABILITY_POINTER | WL_SEAT_CAPABILITY_KEYBOARD));
|
||||
+ auto* map = xkb_keymap_new_from_string(xkb_context_, keyboard->m_xkbKeymapV1String.c_str(),
|
||||
+ XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS);
|
||||
+ auto* state = map ? xkb_state_new(map) : nullptr;
|
||||
+ if (physical_state) xkb_state_unref(physical_state);
|
||||
+ if (physical_keymap) xkb_keymap_unref(physical_keymap);
|
||||
+ physical_state = state; physical_keymap = map;
|
||||
+ physical_keymap_text = keyboard->m_xkbKeymapV1String;
|
||||
}
|
||||
void start() {
|
||||
+ initialize_agent_keymap();
|
||||
sync_keymap();
|
||||
timer = wl_event_loop_add_timer(g_pCompositor->m_wlEventLoop, tick, this);
|
||||
if (!timer) throw std::runtime_error("input timer registration failed");
|
||||
@@ -389,6 +391,8 @@ struct InputExperiment::Impl {
|
||||
cleanup_socket();
|
||||
if (keyboard_state) xkb_state_unref(keyboard_state);
|
||||
if (keymap) xkb_keymap_unref(keymap);
|
||||
+ if (physical_state) xkb_state_unref(physical_state);
|
||||
+ if (physical_keymap) xkb_keymap_unref(physical_keymap);
|
||||
if (xkb_context_) xkb_context_unref(xkb_context_);
|
||||
if (keymap_fd >= 0) close(keymap_fd);
|
||||
}
|
||||
@@ -786,7 +790,7 @@ struct InputExperiment::Impl {
|
||||
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};
|
||||
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 {
|
||||
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) {
|
||||
const auto root = c.surface.lock();
|
||||
@@ -820,7 +826,7 @@ struct InputExperiment::Impl {
|
||||
const auto failure = foreground_guard(c).activation_failure();
|
||||
if (failure != ForegroundFailureReason::none) throw ForegroundFailure{failure};
|
||||
if (!physical) throw ForegroundFailure{ForegroundFailureReason::physical_keyboard};
|
||||
- if (!keyboard_state) throw ForegroundFailure{ForegroundFailureReason::keyboard_state};
|
||||
+ if (needs_keyboard && !physical_layout_ready()) throw ForegroundFailure{ForegroundFailureReason::keyboard_state};
|
||||
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};
|
||||
}
|
||||
- xkb_state_update_mask(keyboard_state, foreground_modifiers[0], foreground_modifiers[1], foreground_modifiers[2], 0, 0, foreground_modifiers[3]);
|
||||
+ 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 {
|
||||
}
|
||||
void foreground_key(Client& c, std::uint32_t code, bool pressed) {
|
||||
require_foreground(c);
|
||||
+ if (!physical_layout_ready()) throw ForegroundFailure{ForegroundFailureReason::unsupported_layout};
|
||||
foreground_keyboard_used = true;
|
||||
- xkb_state_update_key(keyboard_state, code + 8, pressed ? XKB_KEY_DOWN : XKB_KEY_UP);
|
||||
+ xkb_state_update_key(physical_state, code + 8, pressed ? XKB_KEY_DOWN : XKB_KEY_UP);
|
||||
if (pressed) held_keys.push_back(code); else std::erase(held_keys, code);
|
||||
for (const auto& weak : foreground_keyboards) {
|
||||
const auto k = weak.lock(); if (!k || !k->good()) throw ForegroundFailure{ForegroundFailureReason::keyboard_resources};
|
||||
k->sendKey(event_ms(), code, pressed ? WL_KEYBOARD_KEY_STATE_PRESSED : WL_KEYBOARD_KEY_STATE_RELEASED);
|
||||
- k->sendMods(xkb_state_serialize_mods(keyboard_state, XKB_STATE_MODS_DEPRESSED),
|
||||
- xkb_state_serialize_mods(keyboard_state, XKB_STATE_MODS_LATCHED),
|
||||
- xkb_state_serialize_mods(keyboard_state, XKB_STATE_MODS_LOCKED),
|
||||
- xkb_state_serialize_layout(keyboard_state, XKB_STATE_LAYOUT_EFFECTIVE));
|
||||
+ k->sendMods(xkb_state_serialize_mods(physical_state, XKB_STATE_MODS_DEPRESSED),
|
||||
+ xkb_state_serialize_mods(physical_state, XKB_STATE_MODS_LATCHED),
|
||||
+ xkb_state_serialize_mods(physical_state, XKB_STATE_MODS_LOCKED),
|
||||
+ xkb_state_serialize_layout(physical_state, XKB_STATE_LAYOUT_EFFECTIVE));
|
||||
}
|
||||
}
|
||||
bool pointer_enter(Client& c, double x, double y) {
|
||||
@@ -942,8 +949,8 @@ struct InputExperiment::Impl {
|
||||
held_button = pressed ? value : 0;
|
||||
}
|
||||
bool keyboard_enter(Client& c) {
|
||||
- const auto root = c.surface.lock(); const auto physical = g_pSeatManager->m_keyboard.lock();
|
||||
- if (!root || !physical || physical->m_xkbKeymapV1String != keymap_text || !keyboard_state) return false;
|
||||
+ const auto root = c.surface.lock();
|
||||
+ if (!root || !keyboard_state || keymap_fd < 0) return false;
|
||||
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 {
|
||||
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; }
|
||||
- if (!layout_qualified()) { invalidate(c, false); send(c, refusal("unsupported_layout")); return; }
|
||||
+ if (!layout_qualified(route, requested_cap)) { invalidate(c, false); send(c, refusal("unsupported_layout")); return; }
|
||||
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 {
|
||||
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; }
|
||||
- if (!layout_qualified()) { revoke("unsupported_layout", true); send(c, refusal("unsupported_layout")); return; }
|
||||
+ if (!layout_qualified(c.route, cap)) { revoke("unsupported_layout", true); send(c, refusal("unsupported_layout")); return; }
|
||||
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 {
|
||||
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)) {
|
||||
+ 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");
|
||||
if (command == "KEY") {
|
||||
@@ -1227,7 +1238,7 @@ struct InputExperiment::Impl {
|
||||
if (lease) {
|
||||
if (lease->dead) revoke("disconnected", true);
|
||||
else if (Clock::now() >= expires) revoke("lease_expired");
|
||||
- else if (!available() || !layout_qualified()) revoke("cancelled", true);
|
||||
+ else if (!available() || !layout_qualified(lease->route, capabilities)) revoke("cancelled", true);
|
||||
else if (!refresh(*lease) || primary_conflict(*lease) || agent_conflict(*lease) ||
|
||||
(drag && !drag->geometry.matches(lease->revision))) revoke("cancelled");
|
||||
}
|
||||
diff --git a/src/keyboard_layout.hpp b/src/keyboard_layout.hpp
|
||||
new file mode 100644
|
||||
index 0000000..ab7defc
|
||||
--- /dev/null
|
||||
+++ b/src/keyboard_layout.hpp
|
||||
@@ -0,0 +1,102 @@
|
||||
+#pragma once
|
||||
+
|
||||
+#include "foreground_route.hpp"
|
||||
+#include <xkbcommon/xkbcommon.h>
|
||||
+#include <array>
|
||||
+#include <cstdint>
|
||||
+#include <memory>
|
||||
+
|
||||
+namespace cua::hyprland {
|
||||
+// Wire v3 carries evdev key positions plus fixed Shift/Ctrl/Alt/Super bits.
|
||||
+// Background seats advertise this map, independently of the human keyboard.
|
||||
+inline xkb_keymap* agent_keymap(xkb_context* context) {
|
||||
+ const xkb_rule_names names{"evdev", "pc105", "us", "", ""};
|
||||
+ return xkb_keymap_new_from_names(context, &names, XKB_KEYMAP_COMPILE_NO_FLAGS);
|
||||
+}
|
||||
+
|
||||
+inline bool keyboard_layout_ready(InputRoute route, std::uint64_t capability,
|
||||
+ bool agent_ready, bool physical_ready) {
|
||||
+ if (!(capability & 2)) return true; // Pointer/activation needs no key mapping.
|
||||
+ return route == InputRoute::primary_foreground ? physical_ready : agent_ready;
|
||||
+}
|
||||
+
|
||||
+inline bool same_key_symbols(xkb_state* actual, xkb_state* expected, xkb_keycode_t key) {
|
||||
+ const xkb_keysym_t *a = nullptr, *b = nullptr;
|
||||
+ const int na = xkb_state_key_get_syms(actual, key, &a);
|
||||
+ const int nb = xkb_state_key_get_syms(expected, key, &b);
|
||||
+ if (na != nb) return false;
|
||||
+ for (int i = 0; i < na; ++i) if (a[i] != b[i]) return false;
|
||||
+ return true;
|
||||
+}
|
||||
+
|
||||
+inline bool same_consumed_modifiers(xkb_state* actual, xkb_state* expected, xkb_keycode_t key) {
|
||||
+ // Toolkits subtract consumed modifiers when matching shortcuts. Matching
|
||||
+ // symbols and effective state alone does not preserve shortcut meaning.
|
||||
+ for (auto* state : {actual, expected}) {
|
||||
+ auto* map = xkb_state_get_keymap(state);
|
||||
+ for (xkb_mod_index_t i = 0; i < xkb_keymap_num_mods(map); ++i) {
|
||||
+ const char* name = xkb_keymap_mod_get_name(map, i);
|
||||
+ if (xkb_state_mod_name_is_active(state, name, XKB_STATE_MODS_EFFECTIVE) <= 0) continue;
|
||||
+ const auto ai = xkb_keymap_mod_get_index(xkb_state_get_keymap(actual), name);
|
||||
+ const auto bi = xkb_keymap_mod_get_index(xkb_state_get_keymap(expected), name);
|
||||
+ for (auto mode : {XKB_CONSUMED_MODE_XKB, XKB_CONSUMED_MODE_GTK})
|
||||
+ if ((xkb_state_mod_index_is_consumed2(actual, key, ai, mode) > 0) !=
|
||||
+ (xkb_state_mod_index_is_consumed2(expected, key, bi, mode) > 0)) return false;
|
||||
+ }
|
||||
+ }
|
||||
+ return true;
|
||||
+}
|
||||
+
|
||||
+inline bool same_modifier_state(xkb_state* actual, xkb_state* expected) {
|
||||
+ // Modifier indices can differ between maps. Compare names, in both directions,
|
||||
+ // including nonstandard modifiers and lock/latch changes on key release.
|
||||
+ for (auto* state : {actual, expected}) {
|
||||
+ auto* map = xkb_state_get_keymap(state);
|
||||
+ for (xkb_mod_index_t i = 0; i < xkb_keymap_num_mods(map); ++i) {
|
||||
+ const char* name = xkb_keymap_mod_get_name(map, i);
|
||||
+ for (auto component : {XKB_STATE_MODS_DEPRESSED, XKB_STATE_MODS_LATCHED,
|
||||
+ XKB_STATE_MODS_LOCKED, XKB_STATE_MODS_EFFECTIVE}) {
|
||||
+ const bool a = xkb_state_mod_name_is_active(actual, name, component) > 0;
|
||||
+ const bool b = xkb_state_mod_name_is_active(expected, name, component) > 0;
|
||||
+ if (a != b) return false;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ for (auto component : {XKB_STATE_LAYOUT_DEPRESSED, XKB_STATE_LAYOUT_LATCHED,
|
||||
+ XKB_STATE_LAYOUT_LOCKED, XKB_STATE_LAYOUT_EFFECTIVE})
|
||||
+ if (xkb_state_serialize_layout(actual, component) != xkb_state_serialize_layout(expected, component))
|
||||
+ return false;
|
||||
+ return true;
|
||||
+}
|
||||
+
|
||||
+// 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) {
|
||||
+ if (!physical || !canonical) 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;
|
||||
+ 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
|
||||
+ // shifted level; that level is not another press. Releases pair by
|
||||
+ // 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;
|
||||
+ xkb_state_update_key(actual.get(), key + 8, direction);
|
||||
+ xkb_state_update_key(expected.get(), key + 8, direction);
|
||||
+ return same_modifier_state(actual.get(), expected.get());
|
||||
+ };
|
||||
+ constexpr std::array<std::uint32_t, 4> keys{42, 29, 56, 125};
|
||||
+ for (unsigned i = 0; i < keys.size(); ++i)
|
||||
+ if ((mods & (1u << i)) && keys[i] != code && !event(keys[i], XKB_KEY_DOWN)) return false;
|
||||
+ if (!event(code, XKB_KEY_DOWN) || !event(code, XKB_KEY_UP)) return false;
|
||||
+ for (int i = 3; i >= 0; --i)
|
||||
+ if ((mods & (1u << i)) && keys[i] != code && !event(keys[i], XKB_KEY_UP)) return false;
|
||||
+ return true;
|
||||
+}
|
||||
+} // namespace cua::hyprland
|
||||
diff --git a/src/plugin.cpp b/src/plugin.cpp
|
||||
index 88b9900..2d24471 100644
|
||||
--- a/src/plugin.cpp
|
||||
+++ b/src/plugin.cpp
|
||||
@@ -218,6 +218,11 @@ std::string status_output(bool json) {
|
||||
|
||||
if (json) {
|
||||
auto result = cua::hyprland::render_status_json(report);
|
||||
+#ifdef CUA_HYPRLAND_INPUT
|
||||
+ // Installation checks compiled support before enabling input seats.
|
||||
+ result.pop_back();
|
||||
+ result += ",\"keyboard_layout_independent\":true}";
|
||||
+#endif
|
||||
#if defined(CUA_HYPRLAND_TEST_INPUT) || defined(CUA_HYPRLAND_INPUT)
|
||||
if (g_experiment) {
|
||||
#ifdef CUA_HYPRLAND_INPUT
|
||||
diff --git a/tests/agent_keymap_test.py b/tests/agent_keymap_test.py
|
||||
new file mode 100644
|
||||
index 0000000..78a3603
|
||||
--- /dev/null
|
||||
+++ b/tests/agent_keymap_test.py
|
||||
@@ -0,0 +1,86 @@
|
||||
+"""Exercise the production map initializer without a compositor or desktop changes."""
|
||||
+import os
|
||||
+from pathlib import Path
|
||||
+import re
|
||||
+import shlex
|
||||
+import subprocess
|
||||
+import tempfile
|
||||
+import unittest
|
||||
+
|
||||
+ROOT = Path(__file__).resolve().parents[1]
|
||||
+
|
||||
+
|
||||
+class AgentKeymapTest(unittest.TestCase):
|
||||
+ def test_production_keymap_and_fd(self):
|
||||
+ source = (ROOT / 'src/input_experiment.cpp').read_text()
|
||||
+ body = re.search(r'^ void initialize_agent_keymap\(\).*?^ }', source, re.M | re.S)
|
||||
+ self.assertIsNotNone(body)
|
||||
+ fixture = r'''
|
||||
+#include "keyboard_layout.hpp"
|
||||
+#include <sys/mman.h>
|
||||
+#include <sys/stat.h>
|
||||
+#include <fcntl.h>
|
||||
+#include <unistd.h>
|
||||
+#include <cerrno>
|
||||
+#include <cstdlib>
|
||||
+#include <stdexcept>
|
||||
+#include <string>
|
||||
+#include <iostream>
|
||||
+using namespace cua::hyprland;
|
||||
+void check(bool v, const char* m) { if (!v) { std::cerr << m; std::exit(1); } }
|
||||
+struct Lane {
|
||||
+ xkb_context* xkb_context_ = nullptr;
|
||||
+ xkb_keymap* keymap = nullptr;
|
||||
+ xkb_state* keyboard_state = nullptr;
|
||||
+ int keymap_fd = -1;
|
||||
+ std::string keymap_text;
|
||||
+ // INITIALIZER
|
||||
+ ~Lane() {
|
||||
+ if (keyboard_state) xkb_state_unref(keyboard_state);
|
||||
+ if (keymap) xkb_keymap_unref(keymap);
|
||||
+ if (xkb_context_) xkb_context_unref(xkb_context_);
|
||||
+ if (keymap_fd >= 0) close(keymap_fd);
|
||||
+ }
|
||||
+};
|
||||
+int main() {
|
||||
+ Lane a, b;
|
||||
+ a.initialize_agent_keymap(); b.initialize_agent_keymap();
|
||||
+ check(a.keymap_fd != b.keymap_fd && a.keymap != b.keymap && a.keyboard_state != b.keyboard_state,
|
||||
+ "lanes share owned map resources");
|
||||
+ check(a.keymap_text == b.keymap_text, "lanes advertise different layouts");
|
||||
+ for (Lane* lane : {&a, &b}) {
|
||||
+ struct stat status{};
|
||||
+ check(fstat(lane->keymap_fd, &status) == 0 && status.st_size == (off_t)lane->keymap_text.size() + 1,
|
||||
+ "keymap fd is not terminated serialized map");
|
||||
+ std::string text(status.st_size, '\0');
|
||||
+ check(pread(lane->keymap_fd, text.data(), text.size(), 0) == status.st_size, "keymap fd cannot be read");
|
||||
+ check(text == lane->keymap_text + '\0', "keymap fd differs from advertised map");
|
||||
+ check(fcntl(lane->keymap_fd, F_GETFD) & FD_CLOEXEC, "keymap fd inherited by exec");
|
||||
+ const int seals = F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE | F_SEAL_SEAL;
|
||||
+ check((fcntl(lane->keymap_fd, F_GET_SEALS) & seals) == seals, "keymap fd not immutable");
|
||||
+ const int fd = lane->keymap_fd;
|
||||
+ lane->initialize_agent_keymap();
|
||||
+ check(lane->keymap_fd == fd, "initialization replaced advertised map");
|
||||
+ check(xkb_state_key_get_one_sym(lane->keyboard_state, 21 + 8) == XKB_KEY_y,
|
||||
+ "background lane did not initialize US map");
|
||||
+ }
|
||||
+ xkb_state_update_key(a.keyboard_state, 42 + 8, XKB_KEY_DOWN);
|
||||
+ check(xkb_state_key_get_one_sym(a.keyboard_state, 30 + 8) == XKB_KEY_A, "lane modifier not applied");
|
||||
+ check(xkb_state_key_get_one_sym(b.keyboard_state, 30 + 8) == XKB_KEY_a, "lane modifier leaked");
|
||||
+}
|
||||
+'''.replace('// INITIALIZER', body.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-agent-keymap-') 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/desktop_fault_policy_fixture.cpp b/tests/desktop_fault_policy_fixture.cpp
|
||||
index 45c48ef..36be139 100644
|
||||
--- a/tests/desktop_fault_policy_fixture.cpp
|
||||
+++ b/tests/desktop_fault_policy_fixture.cpp
|
||||
@@ -1,6 +1,7 @@
|
||||
// Used by desktop_fault_policy_test.py, which inserts actual production bodies.
|
||||
// Transport effects are counted here; native protocol/app behavior is separate.
|
||||
#include "drag_geometry.hpp"
|
||||
+#include "keyboard_layout.hpp"
|
||||
#include "input_grant.hpp"
|
||||
#include "passive_pointer_target.hpp"
|
||||
|
||||
@@ -18,6 +19,7 @@ struct Window {};
|
||||
struct Surface { int client() const { return 1; } };
|
||||
using Target = PassivePointerTarget<std::weak_ptr<Window>, std::weak_ptr<Surface>>;
|
||||
struct Client {
|
||||
+ InputRoute route = InputRoute::independent;
|
||||
bool dead = false;
|
||||
void* source = nullptr;
|
||||
int fd = -1;
|
||||
@@ -34,8 +36,6 @@ struct Pointer {
|
||||
};
|
||||
struct Drag { Client* client; DragGeometry geometry{1}; };
|
||||
struct Trace { void mark(const char*, unsigned) {} };
|
||||
-enum class ForegroundFailureReason { none };
|
||||
-struct ForegroundFailure { ForegroundFailureReason reason; };
|
||||
void wl_event_source_remove(void*) {}
|
||||
int close(int) { return 0; }
|
||||
std::string refusal(std::string_view reason) { return std::string(reason); }
|
||||
@@ -79,7 +79,7 @@ struct Lane {
|
||||
drag.emplace(lease);
|
||||
}
|
||||
bool available() const { return !suspended && session; }
|
||||
- bool layout_qualified() const { return layout; }
|
||||
+ bool layout_qualified(InputRoute route, uint64_t cap) const { return keyboard_layout_ready(route, cap, layout, layout); }
|
||||
bool refresh(Client&) const { return refresh_ok; }
|
||||
template<typename T> bool primary_conflict(const T&) const { return primary_busy; }
|
||||
template<typename T> bool agent_conflict(const T&) const { return peer_busy; }
|
||||
@@ -122,7 +122,7 @@ int main() {
|
||||
for (bool session_fault : {true, false}) {
|
||||
for (int path = 0; path < 3; ++path) {
|
||||
Lane l;
|
||||
- if (session_fault) l.session = false; else l.layout = false;
|
||||
+ if (session_fault) l.session = false; else { l.layout = false; l.capabilities = 2; }
|
||||
if (path == 0) l.guard_targets();
|
||||
if (path == 1) l.target_refusal(*l.lease);
|
||||
if (path == 2) l.action_refusal(*l.lease);
|
||||
@@ -133,6 +133,19 @@ int main() {
|
||||
l.session = true; l.layout = true; l.guard_targets(); l.check_inert();
|
||||
}
|
||||
}
|
||||
+ // Pointer admission, dispatch and ongoing drag ignore keyboard layout readiness.
|
||||
+ for (auto route : {InputRoute::independent, InputRoute::primary_foreground}) {
|
||||
+ for (uint64_t cap : {1, 4, 8, 16}) {
|
||||
+ Lane l; l.layout = false; l.lease->route = route; l.capabilities = cap;
|
||||
+ l.target_refusal(*l.lease, route, cap);
|
||||
+ check(l.lease && l.responses.empty(), "pointer admission gated on layout");
|
||||
+ l.action_refusal(*l.lease, cap);
|
||||
+ check(l.lease && l.responses.empty(), "pointer dispatch gated on layout");
|
||||
+ l.guard_targets();
|
||||
+ check(l.lease && l.drag && l.held_button && l.responses.empty(),
|
||||
+ "pointer operation was gated by keyboard layout");
|
||||
+ }
|
||||
+ }
|
||||
// Every passive safety guard still applies during either desktop fault.
|
||||
for (bool session_fault : {true, false}) {
|
||||
for (int bad = 0; bad < 8; ++bad) {
|
||||
diff --git a/tests/desktop_fault_policy_test.py b/tests/desktop_fault_policy_test.py
|
||||
index 68ca6c8..7a4d1b4 100644
|
||||
--- a/tests/desktop_fault_policy_test.py
|
||||
+++ b/tests/desktop_fault_policy_test.py
|
||||
@@ -35,10 +35,11 @@ def fixture(source):
|
||||
):
|
||||
block = source.split(start, 1)[1].split(end, 1)[0]
|
||||
refusals = [line.strip() for line in block.splitlines()
|
||||
- if 'if (' in line and ('!available()' in line or '!layout_qualified()' in line)]
|
||||
+ if 'if (' in line and ('!available()' in line or '!layout_qualified(' in line)]
|
||||
if len(refusals) != 2:
|
||||
raise AssertionError(f'production refusal branches not found: {label}')
|
||||
- methods += '\nvoid ' + label + '(Client& c) {\n' + '\n'.join(refusals) + '\n}'
|
||||
+ params = ', InputRoute route = InputRoute::independent, uint64_t requested_cap = 2' if label == 'target_refusal' else ', uint64_t cap = 2'
|
||||
+ methods += '\nvoid ' + label + '(Client& c' + params + ') {\n' + '\n'.join(refusals) + '\n}'
|
||||
return (ROOT / 'tests/desktop_fault_policy_fixture.cpp').read_text().replace(
|
||||
'// PRODUCTION_METHODS', methods)
|
||||
|
||||
diff --git a/tests/keyboard_layout_test.cpp b/tests/keyboard_layout_test.cpp
|
||||
new file mode 100644
|
||||
index 0000000..dd7c962
|
||||
--- /dev/null
|
||||
+++ b/tests/keyboard_layout_test.cpp
|
||||
@@ -0,0 +1,97 @@
|
||||
+#include "keyboard_layout.hpp"
|
||||
+#include <cstdlib>
|
||||
+#include <iostream>
|
||||
+#include <memory>
|
||||
+
|
||||
+using namespace cua::hyprland;
|
||||
+void check(bool condition, const char* message) {
|
||||
+ if (!condition) { std::cerr << message << '\n'; std::exit(1); }
|
||||
+}
|
||||
+int main() {
|
||||
+ using Context = std::unique_ptr<xkb_context, decltype(&xkb_context_unref)>;
|
||||
+ using Map = std::unique_ptr<xkb_keymap, decltype(&xkb_keymap_unref)>;
|
||||
+ using State = std::unique_ptr<xkb_state, decltype(&xkb_state_unref)>;
|
||||
+ Context context{xkb_context_new(XKB_CONTEXT_NO_FLAGS), xkb_context_unref};
|
||||
+ Map agent{agent_keymap(context.get()), xkb_keymap_unref};
|
||||
+ Map second{agent_keymap(context.get()), xkb_keymap_unref};
|
||||
+ check(bool(agent) && bool(second), "canonical maps unavailable");
|
||||
+ const auto map = [&](const char* layout, const char* options) {
|
||||
+ const xkb_rule_names names{"evdev", "pc105", layout, "", options};
|
||||
+ return Map{xkb_keymap_new_from_names(context.get(), &names, XKB_KEYMAP_COMPILE_NO_FLAGS), xkb_keymap_unref};
|
||||
+ };
|
||||
+ auto stock = map("us", "compose:caps,shift:both_capslock_cancel");
|
||||
+ auto nocaps = map("us", "ctrl:nocaps");
|
||||
+ auto swapctrl = map("us", "ctrl:swapcaps");
|
||||
+ auto swapalt = map("us", "altwin:swap_alt_win");
|
||||
+ 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(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");
|
||||
+ // 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");
|
||||
+ // 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};
|
||||
+ State human{xkb_state_new(german.get()), xkb_state_unref};
|
||||
+ xkb_state_update_key(first.get(), 42 + 8, XKB_KEY_DOWN);
|
||||
+ check(xkb_state_key_get_one_sym(first.get(), 30 + 8) == XKB_KEY_A, "agent shift not active");
|
||||
+ check(xkb_state_key_get_one_sym(other.get(), 30 + 8) == XKB_KEY_a, "agent shift leaked across lanes");
|
||||
+ check(xkb_state_key_get_one_sym(human.get(), 21 + 8) == XKB_KEY_z, "human layout changed");
|
||||
+ // Same symbols can hide a changed XKB action. Simulate an A that sets Mod3.
|
||||
+ char* serialized = xkb_keymap_get_as_string(agent.get(), XKB_KEYMAP_FORMAT_TEXT_V1);
|
||||
+ std::string changed = serialized;
|
||||
+ std::free(serialized);
|
||||
+ const auto at = changed.find("key <AC01>");
|
||||
+ const auto end = changed.find(';', at);
|
||||
+ check(at != std::string::npos && end != std::string::npos, "test action fixture unavailable");
|
||||
+ changed.replace(at, end - at + 1,
|
||||
+ "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");
|
||||
+ // 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;
|
||||
+ std::free(serialized);
|
||||
+ const auto type_at = changed.find("type \"ALPHABETIC\"");
|
||||
+ const auto type_end = changed.find("};", type_at);
|
||||
+ check(type_at != std::string::npos && type_end != std::string::npos, "key type fixture unavailable");
|
||||
+ 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");
|
||||
+ // 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;
|
||||
+ std::free(serialized);
|
||||
+ const auto shift_at = changed.find("key <LFSH>");
|
||||
+ const auto shift_end = changed.find(';', shift_at);
|
||||
+ check(shift_at != std::string::npos && shift_end != std::string::npos, "shift fixture unavailable");
|
||||
+ changed.replace(shift_at, shift_end - shift_at + 1,
|
||||
+ "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");
|
||||
+ 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");
|
||||
+ }
|
||||
+ check(keyboard_layout_ready(InputRoute::independent, 2, true, false), "background depends on human map");
|
||||
+ check(!keyboard_layout_ready(InputRoute::primary_foreground, 2, true, false), "foreground uses agent readiness");
|
||||
+ 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
|
||||
--- a/tests/plugin_input_lifetime_test.cpp
|
||||
+++ b/tests/plugin_input_lifetime_test.cpp
|
||||
@@ -51,12 +51,17 @@ int main() {
|
||||
check(setenv("XDG_RUNTIME_DIR", directory, 1) == 0 &&
|
||||
setenv("HYPRLAND_INSTANCE_SIGNATURE", "mock", 1) == 0,
|
||||
"select runtime");
|
||||
- Config::Values::configured_bool_override = true;
|
||||
+ Config::Values::configured_bool_override = false;
|
||||
static_cast<void>(pluginInit(nullptr));
|
||||
const auto toggle = [](bool enabled) {
|
||||
HyprlandAPI::registered_bool->set_mock_value(enabled);
|
||||
Event::bus()->m_events.config.reloaded.emit();
|
||||
};
|
||||
+#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,
|
||||
+ "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() {
|
||||
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("operator") == std::string::npos,
|
||||
"v3 status advertises its actual admission mode");
|
||||
#endif
|
||||
Reference in new issue
Block a user