Files
omarchy-pkgs/pkgbuilds/cua-hyprland-plugin/PKGBUILD
T
Spencer BullandCodex XHigh a2c3571672 Rebase the Cua Hyprland plugin on Driver 0.32.0 for Hyprland 0.56.2-4 (#772)
* Rebase the Cua Hyprland plugin on Driver 0.32.0 source

Driver 0.31.0 took the independent agent keymaps and Num Lock handling from #473 upstream, so independent-keymaps.patch goes. Upstream also added an up-front check that every key the KEY command can press types the US keysym, modifier keys included, which refuses all foreground typing under ctrl:swapcaps, compose:ralt, altwin:swap_alt_win or compose:102. foreground-remaps.patch makes that check run the per-chord check every key already passes over exactly the chords Driver types text with, with Num Lock off and on, so a string is still admitted or refused before its first key and other remaps are left to the per-chord check.

0.32.0 also gives agent keyboards a real repeat rate, which stops single-seat clients like imv crashing on an agent seat (trycua/cua#4257).

cua-driver-bin stays at 0.28.2, which speaks the same input protocol v3: Driver 0.28.3 through 0.32.0 refuse desktop capture on Hyprland with more than one output or with one away from the origin (trycua/cua#4161).

Co-Authored-By: Codex XHigh <noreply@openai.com>

* Keep restarting fcitx5 from crashing Hyprland under the Cua plugin

With plugin input enabled, fcitx5 requests an input method for each of the three seats, and when it exits Hyprland 0.56.2 can still hold an input-method popup that is mapped although its wl_surface is gone: the popup's surface-destroy handler emits unmap but never clears m_mapped. Tearing down the input method then updates every popup, CInputPopup::updateBox dereferences the missing surface and Hyprland restarts in safe mode. Omarchy's omarchy-restart-xcompose reaches this in normal use.

The plugin now hooks CInputPopup::updateBox and CInputPopup::shouldBeRendered so a popup whose getSurface() is empty is neither placed nor rendered, reports the guard in cua:status, and removes the hooks on unload. The guard is compiled into the module only, so the mock-based tests are unchanged. foreground-remaps.patch becomes downstream.patch now that it carries both changes.

* Pin the Cua Hyprland plugin to Arch's hyprland 0.56.2-4 rebuild

Arch rebuilt Hyprland 0.56.2 against vulkan-sdk 1.4.363 and glslang. The executable and two headers change, so the plugin's exact compositor pin and hashes would block hyprland upgrades for anyone with it installed, and Cua's kit has no profile for it (trycua/cua#4216). The release, compiler and runtime are unchanged, so the derivation now also takes the re-measured compositor executable and header inventory; the build's verifier checks both against the installed package.

* Qualify the Cua plugin README on its input-method guard

The guard is installed best effort, so the README promises the fix only while it is active, and the activation checklist now asks for ime_popup_guard: true. 'No hooks' meant package-manager hooks, which the plugin's new function hooks made ambiguous.

Co-Authored-By: Codex XHigh <noreply@openai.com>

---------

Co-authored-by: Codex XHigh <noreply@openai.com>
2026-10-03 22:37:44 -05:00

240 lines
15 KiB
Bash

# Verified upstream kit plus a separately pinned Omarchy 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.32.0
# Profile validation starts package releases at 2, above upstream's generic -1 build of the same Driver.
pkgrel=3
pkgdesc='Cua input candidate for reviewed profile omarchy-hyprland-0562r4-remaps'
arch=('x86_64')
url='https://github.com/trycua/cua'
license=('MIT')
depends=('hyprland=0.56.2-4' 'aquamarine=0.15.1-1' 'glibc=2.44+r50+g1848099f063e-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' 'patch')
options=('!strip' '!debug' '!lto')
_stem='cua-hyprland-plugin-0.32.0-58aba84b5b83d77e7e2b0f006547699eb594e50d'
_archive_sha256='25bc3b2d353ba6bb6ecd17e4b67b11b0d6b731a5ae01025197442c97e50acf7a'
_kit_sha256='3c2141dcb2583d918ef2cdb6ff3805dec62119a439a29c811a93cc980d621423'
_profile_sha256='b20d98979137fb5216768d95215f80d372d9bfd2269b541d7bcf664a12f80b97'
_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'
_download_sha256='a89bfa7f6490f598719dfabdd2a5badc137e27b0764fd25e6a05e1f969433520'
source=('https://github.com/trycua/cua/releases/download/cua-hyprland-kit-v1.1.0-omarchy-stable-20260910/cua-hyprland-plugin-0.26.1-cc54254464c0c9aebfd6547fe7e4a0ceaf0456d7-profile-omarchy-stable-20260910-kit-1.1.0-5dacea79a6a5927e59fca7b51e6c04e94fa45133ac1b8fb7f4e1f442d113eb9a-67f89ceb47edd973aa748820eadd02ddbfb3d3e4.tar.gz'
"https://github.com/trycua/cua/releases/download/cua-driver-rs-v${pkgver}/${_stem}.tar.gz"
'PROFILE.json')
noextract=("$_download_name" "${_stem}.tar.gz")
sha256sums=('a89bfa7f6490f598719dfabdd2a5badc137e27b0764fd25e6a05e1f969433520'
'25bc3b2d353ba6bb6ecd17e4b67b11b0d6b731a5ae01025197442c97e50acf7a'
'b20d98979137fb5216768d95215f80d372d9bfd2269b541d7bcf664a12f80b97')
# Downstream inputs are also checked explicitly when makepkg integrity is skipped.
declare -gA _downstream_sha256=(
['downstream.patch']='87d3f560cfbd195748321a7e7ad2cb921c77cc022877de7890d3a711940f6e3c'
['DOWNSTREAM-PROVENANCE.json']='814e136f4684886107b5ccfa7ffc773a1ad0cea7dcddebd977c6f35c6e87ce00'
['downstream.py']='7c9725805ad038737bd4d346c85f27991ce9ddfff436a32ebf515cad1bf98ca1'
['downstream_test.py']='7b9a992979b22a5e061fdf011ac6be59a3f1ae6e0d7c0a8ff631f74e71984c5a'
)
source+=('downstream.patch' 'DOWNSTREAM-PROVENANCE.json' 'downstream.py' 'downstream_test.py')
sha256sums+=('87d3f560cfbd195748321a7e7ad2cb921c77cc022877de7890d3a711940f6e3c' '814e136f4684886107b5ccfa7ffc773a1ad0cea7dcddebd977c6f35c6e87ce00' '7c9725805ad038737bd4d346c85f27991ce9ddfff436a32ebf515cad1bf98ca1' '7b9a992979b22a5e061fdf011ac6be59a3f1ae6e0d7c0a8ff631f74e71984c5a')
_verify_download() {
python3 -I - "$SRCDEST/$_download_name" "$_download_sha256" "$srcdir" "$1" "$SRCDEST/PROFILE.json" \
"$SRCDEST/${_stem}.tar.gz" <<'CUA_DOWNLOAD_PY'
import hashlib
import io
import json
from pathlib import Path, PurePosixPath
import sys
import tarfile
expected = {'KIT-PROVENANCE.json': '7beb736adfd334eed52e84070177634269e3a09f8bb25971b38606933ff4c997', 'PKGBUILD': 'b945a6a6eda13d0e382770edd5419485e3196041956663eb38f5183b05d30db3', 'PROFILE-PKGBUILD.in': 'c350d1b2375946cb0166893d67a1fc01344ee5e3215bbe801b4d54bb361d6bce', 'PROFILE-USAGE.md': 'c14d8e8103fccab59e558758f4249f86bce700a8723cd4ba1b97b1102e02bbd2', 'PROFILE.json': '5dacea79a6a5927e59fca7b51e6c04e94fa45133ac1b8fb7f4e1f442d113eb9a', 'SHA256SUMS': '34a2126bcfab983f171382aafac3ef218475b652f4583c58f4a04088044cb935', 'SOURCE-PROVENANCE.json': '54f514664c84e1358a435f29cd6befd5661b0b133d76997191c000b10f021a75', 'cua-hyprland-plugin-0.26.1-cc54254464c0c9aebfd6547fe7e4a0ceaf0456d7.tar.gz': '47bca9e018f32f4fcfe683f91c7475c60368f3b65d318cc35c1f2de88a4ee9ab', 'lifecycle.py': 'b18dceb8b8e05b93586ddd3a2f1d90d70ae1c36088e54fc05c89e08585290990', 'profile_bundle.py': 'ac883883814787da477c037f017939ee92c9fb5f46f373af7de1331b24f1f6da', 'profile_verify.py': '480f649cbd35a0ddc7f49dc0a3a44785402c9e0ec9653b0f8ea4965d52d7f900'}
kit_stem = 'cua-hyprland-plugin-0.26.1-cc54254464c0c9aebfd6547fe7e4a0ceaf0456d7'
stem = 'cua-hyprland-plugin-0.32.0-58aba84b5b83d77e7e2b0f006547699eb594e50d'
def require(condition, message):
if not condition:
raise SystemExit(message)
def digest(data):
return hashlib.sha256(data).hexdigest()
archive, checksum, srcdir, mode, profile_path, source_path = sys.argv[1:]
archive, srcdir = Path(archive), Path(srcdir)
require(mode in {'check', 'extract'}, 'invalid kit verification mode')
require(archive.is_file() and not archive.is_symlink(), 'outer archive must be a regular file')
data = archive.read_bytes()
require(digest(data) == checksum, 'outer archive checksum mismatch')
payload = {}
with tarfile.open(fileobj=io.BytesIO(data), mode='r:gz') as contents:
for member in contents:
require(member.isfile() and not member.issparse() and not member.pax_headers,
'nonregular outer kit member')
require(member.name in expected and member.name not in payload, 'outer kit inventory mismatch')
content = contents.extractfile(member).read()
require(digest(content) == expected[member.name], 'outer kit member checksum mismatch')
payload[member.name] = content
require(payload.keys() == expected.keys(), 'outer kit inventory mismatch')
# Rebase the kit onto the Driver 0.32.0 source release, whose manifest has the fields
# this verifier checks, and derive the reviewed Omarchy edge profile for it.
# Tooling, compiler and runtime hashes stay upstream's. The compositor and its
# headers are re-measured from Arch's 0.56.2-4 rebuild of the same Hyprland release.
source_file = Path(source_path)
require(source_file.is_file() and not source_file.is_symlink(), 'source archive must be a regular file')
source_data = source_file.read_bytes()
require(digest(source_data) == '25bc3b2d353ba6bb6ecd17e4b67b11b0d6b731a5ae01025197442c97e50acf7a',
'source archive checksum mismatch')
with tarfile.open(fileobj=io.BytesIO(source_data), mode='r:gz') as contents:
manifest_data = contents.extractfile(stem + '/SOURCE-PROVENANCE.json').read()
manifest = json.loads(manifest_data)
del payload[kit_stem + '.tar.gz']
payload[stem + '.tar.gz'] = source_data
payload['SOURCE-PROVENANCE.json'] = manifest_data
identity = {'revision': manifest['source_revision'], 'driver_version': manifest['driver_version'],
'archive_sha256': digest(source_data), 'manifest_sha256': digest(manifest_data)}
profile_data = Path(profile_path).read_bytes()
require(digest(profile_data) == 'b20d98979137fb5216768d95215f80d372d9bfd2269b541d7bcf664a12f80b97',
'local profile checksum mismatch')
profile = json.loads(payload['PROFILE.json'])
profile.update(profile_id='omarchy-hyprland-0562r4-remaps', package_release=3, source=identity)
profile['hyprland'].update(package_version='0.56.2-4',
sha256='55da553be71222566ee73b973d2f56dbb9939044d8f22f81aa54b66c83f2f6d1',
headers_sha256='1fdefe6ac027a159d04a5dfee4928ec7ebd15544a9a25b5f66d2f5a46fcf364a')
profile['runtime']['packages'].update(aquamarine='0.15.1-1', glibc='2.44+r50+g1848099f063e-1')
require(json.loads(profile_data) == profile,
'local profile changes more than the reviewed source, package versions and re-measured compositor')
payload['PROFILE.json'] = profile_data
provenance = json.loads(payload['KIT-PROVENANCE.json'])
provenance.update(profile_sha256=digest(profile_data), source=identity)
payload['KIT-PROVENANCE.json'] = (json.dumps(provenance, sort_keys=True, indent=2) + '\n').encode()
# Render the recipe the way the kit's own profile_verify.render_recipe does.
recipe = payload['PROFILE-PKGBUILD.in'].decode()
for key, value in {'DRIVER_VERSION': identity['driver_version'], 'PKGREL': str(profile['package_release']),
'PROFILE_ID': profile['profile_id'], 'STEM': stem,
'HYPRLAND_PACKAGE': profile['hyprland']['package_version'],
'RUNTIME_DEPENDS': ' '.join(f"'{name}={version}'" for name, version
in sorted(profile['runtime']['packages'].items())),
'ARCHIVE_SHA256': identity['archive_sha256'], 'PROFILE_SHA256': digest(profile_data),
'KIT_SHA256': digest(payload['KIT-PROVENANCE.json']),
'VERIFIER_SHA256': provenance['tooling_files']['profile_verify.py']}.items():
recipe = recipe.replace(f'@{key}@', value)
payload['PKGBUILD'] = recipe.encode()
payload['SHA256SUMS'] = ''.join(f'{digest(body)} {name}\n' for name, body in sorted(payload.items())
if name != 'SHA256SUMS').encode()
del expected[kit_stem + '.tar.gz']
expected.update({stem + '.tar.gz': '25bc3b2d353ba6bb6ecd17e4b67b11b0d6b731a5ae01025197442c97e50acf7a',
'SOURCE-PROVENANCE.json': '292ac8fa09bec96cbd499d5d57cc8ea9b1c00fe6347162fb6960c45f972bdb9d',
'PROFILE.json': 'b20d98979137fb5216768d95215f80d372d9bfd2269b541d7bcf664a12f80b97',
'KIT-PROVENANCE.json': '3c2141dcb2583d918ef2cdb6ff3805dec62119a439a29c811a93cc980d621423',
'PKGBUILD': '610d426d6f88238fc6fae56afc1d54ba7866800d14e97777d9b47ddc9b347cc0',
'SHA256SUMS': '093dc8708a9b440977662d7f00bccace24b0b9253e6bf0d1decad2655b886881'})
require(payload.keys() == expected.keys(), 'derived kit inventory mismatch')
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')
kit, source = srcdir / 'cua-profile-kit', srcdir / stem
if mode == 'extract':
require(not kit.exists() and not kit.is_symlink() and not source.exists() and not source.is_symlink(),
'prepare requires fresh kit and source destinations; use a clean srcdir')
source_payload = {}
with tarfile.open(fileobj=io.BytesIO(payload[stem + '.tar.gz']), mode='r:gz') as contents:
for member in contents:
require(member.isfile() and not member.issparse() and not member.pax_headers and
member.name.startswith(stem + '/'), 'invalid source member')
name = member.name[len(stem) + 1:]
path = PurePosixPath(name)
require(name and path.as_posix() == name and not path.is_absolute() and
'..' not in path.parts and '\\' not in name and name not in source_payload,
'unsafe or duplicate source path')
source_payload[name] = contents.extractfile(member).read()
kit.mkdir()
source.mkdir()
for name, content in payload.items():
(kit / name).write_bytes(content)
for name, content in source_payload.items():
destination = source / name
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_bytes(content)
require(kit.is_dir() and not kit.is_symlink(), 'kit must be a real directory')
require({path.name for path in kit.iterdir()} == expected.keys(), 'extracted kit inventory mismatch')
for name, checksum in expected.items():
path = kit / name
require(path.is_file() and not path.is_symlink() and digest(path.read_bytes()) == checksum,
'extracted kit checksum mismatch: ' + name)
CUA_DOWNLOAD_PY
}
_verify() {
printf '%s %s\n' "$_download_sha256" "$SRCDEST/$_download_name" | sha256sum -c - || return 1
_verify_download check || return 1
# Explicit checks still apply to --skipinteg, --noextract and --repackage.
printf '%s %s\n' "$_archive_sha256" "$srcdir/cua-profile-kit/${_stem}.tar.gz" | sha256sum -c - || return 1
printf '%s %s\n' "$_kit_sha256" "$srcdir/cua-profile-kit/KIT-PROVENANCE.json" | sha256sum -c - || return 1
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"
}
_downstream() {
local name
for name in downstream.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/downstream.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 || return 1
_downstream prepare
}
build() {
_verify || return 1
_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= \
-DBUILD_TESTING=ON -DCUA_HYPRLAND_BUILD_PLUGIN=ON \
-DCUA_HYPRLAND_EXPECTED_VERSION=0.56.2 \
-DCUA_HYPRLAND_INPUT=ON -DCUA_HYPRLAND_TEST_INPUT=OFF \
-DCUA_HYPRLAND_INPUT_TRACE=OFF -DCUA_HYPRLAND_TEST_OPERATOR_KEY= || return 1
cmake --build "$srcdir/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
)
}
package() {
check || 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" \
"$pkgdir/usr/share/licenses/$pkgname/LICENSE" || return 1
install -Dm644 "$srcdir/$_stem/SOURCE-PROVENANCE.json" \
"$pkgdir/usr/share/$pkgname/SOURCE-PROVENANCE.json" || return 1
local name
install -Dm644 "$srcdir/BUILD-PROVENANCE.json" "$pkgdir/usr/share/$pkgname/BUILD-PROVENANCE.json" || return 1
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 downstream.patch; do
install -Dm644 "$SRCDEST/$name" "$pkgdir/usr/share/$pkgname/$name" || return 1
done
}