Compare commits
11
Commits
050ae25b69
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9dd2eb2bf | ||
|
|
d4bec38c18 | ||
|
|
e779ba20ff | ||
|
|
ae255f27cb | ||
|
|
a02388c7fe | ||
|
|
8d81d8bcac | ||
|
|
5863b2f0ec | ||
|
|
191317eb1f | ||
|
|
fcd10069ae | ||
|
|
ed80fb0ea7 | ||
|
|
ddea36c211 |
+7
-4
@@ -73,10 +73,13 @@ debug.log
|
|||||||
*.pot
|
*.pot
|
||||||
|
|
||||||
# Package artifacts
|
# Package artifacts
|
||||||
*.pkg.tar.zst
|
*.pkg.tar.*
|
||||||
distro-packages/*/cthulhu/
|
distro-packages/**/.SRCINFO
|
||||||
distro-packages/*/cthulhu-git/
|
distro-packages/**/*.log
|
||||||
distro-packages/*/pkg/
|
distro-packages/*/*/cthulhu/
|
||||||
|
distro-packages/*/*/cthulhu-git/
|
||||||
|
distro-packages/*/*/pkg/
|
||||||
|
distro-packages/*/*/src/
|
||||||
|
|
||||||
# Generated makefiles (should not be committed)
|
# Generated makefiles (should not be committed)
|
||||||
Makefile
|
Makefile
|
||||||
|
|||||||
@@ -125,5 +125,12 @@ Run the narrowest relevant checks first, then broaden testing based on the affec
|
|||||||
- In particular, update both Arch Linux `PKGBUILD` files and, when relevant, Slint's `cthulhu-info` and `README`. Review the Slackware and Slint build files for any corresponding build-dependency changes.
|
- In particular, update both Arch Linux `PKGBUILD` files and, when relevant, Slint's `cthulhu-info` and `README`. Review the Slackware and Slint build files for any corresponding build-dependency changes.
|
||||||
- Preserve the distinction between required, optional, and build-only dependencies so package builds install what Cthulhu actually needs without forcing optional features on every user.
|
- Preserve the distinction between required, optional, and build-only dependencies so package builds install what Cthulhu actually needs without forcing optional features on every user.
|
||||||
|
|
||||||
|
## Arch package verification
|
||||||
|
- After changing an Arch Linux `PKGBUILD`, build every affected recipe in a clean chroot before considering the packaging work complete. Use `pkgctl build --clean` from the `devtools` package; a normal `makepkg` or `yay` build is not a substitute for this check.
|
||||||
|
- Validate both stable and `-git` recipes when a shared packaging change affects both.
|
||||||
|
- Build dependency packages first. When a split helper depends on a locally built package that is unavailable in the configured repositories, pass that package artifact to the helper build with `pkgctl build --clean -I /path/to/package.pkg.tar.zst`.
|
||||||
|
- Run `bash -n`, `shellcheck`, and `makepkg --printsrcinfo` on each changed recipe before the clean-chroot build.
|
||||||
|
- Report which recipes completed clean-chroot builds. If sudo access, network access, or an unavailable dependency prevents a build, state that explicitly instead of describing the package as verified.
|
||||||
|
|
||||||
## Common Cthulhu agent mistakes
|
## Common Cthulhu agent mistakes
|
||||||
- Do not edit or diagnose a stale installed copy under `~/.local/...`; update the repo and rebuild with `./build-local.sh`.
|
- Do not edit or diagnose a stale installed copy under `~/.local/...`; update the repo and rebuild with `./build-local.sh`.
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
# shellcheck shell=bash disable=SC2034,SC2154,SC2164
|
# shellcheck shell=bash disable=SC2034,SC2154,SC2164
|
||||||
|
|
||||||
pkgbase=cthulhu-git
|
pkgbase=cthulhu-git
|
||||||
pkgname=(cthulhu-git cthulhu-wine-access-git)
|
pkgname=cthulhu-git
|
||||||
_pkgname=cthulhu
|
_pkgname=cthulhu
|
||||||
pkgver=2026.05.25.r423.g0576b6f
|
pkgver=2026.09.03.r475.gd4bec38
|
||||||
pkgrel=2
|
pkgrel=1
|
||||||
pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca"
|
pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca"
|
||||||
url="https://git.stormux.org/storm/cthulhu"
|
url="https://git.stormux.org/storm/cthulhu"
|
||||||
arch=(x86_64 aarch64 armv7h)
|
arch=(x86_64 aarch64 armv7h)
|
||||||
@@ -67,16 +67,13 @@ optdepends=(
|
|||||||
'tesseract: OCR engine for text recognition'
|
'tesseract: OCR engine for text recognition'
|
||||||
)
|
)
|
||||||
optdepends_x86_64=(
|
optdepends_x86_64=(
|
||||||
'cthulhu-wine-access-git: Wine, Proton, NVDA Controller, and Tolk integration'
|
# The helper is x86_64-only, but this package is architecture-independent.
|
||||||
|
'cthulhu-wine-access: Wine, Proton, NVDA Controller, and Tolk integration'
|
||||||
)
|
)
|
||||||
makedepends=(
|
makedepends=(
|
||||||
git
|
git
|
||||||
meson
|
meson
|
||||||
)
|
)
|
||||||
makedepends_x86_64=(
|
|
||||||
cmake
|
|
||||||
wine
|
|
||||||
)
|
|
||||||
source=("${_pkgname}::git+https://git.stormux.org/storm/${_pkgname}.git#branch=master")
|
source=("${_pkgname}::git+https://git.stormux.org/storm/${_pkgname}.git#branch=master")
|
||||||
sha512sums=('SKIP')
|
sha512sums=('SKIP')
|
||||||
|
|
||||||
@@ -96,35 +93,19 @@ pkgver() {
|
|||||||
|
|
||||||
build() {
|
build() {
|
||||||
cd "${_pkgname}"
|
cd "${_pkgname}"
|
||||||
arch-meson _build
|
arch-meson _build -Dwine-access=disabled
|
||||||
meson compile -C _build
|
meson compile -C _build
|
||||||
}
|
}
|
||||||
|
|
||||||
package_cthulhu-git() {
|
package() {
|
||||||
arch=(any)
|
arch=(any)
|
||||||
provides=("cthulhu=${pkgver}")
|
provides=("cthulhu=${pkgver}")
|
||||||
conflicts=(cthulhu)
|
conflicts=(cthulhu)
|
||||||
cd "${_pkgname}"
|
cd "${_pkgname}"
|
||||||
meson install -C _build --destdir "$pkgdir"
|
meson install -C _build --destdir "$pkgdir"
|
||||||
|
|
||||||
rm -rf "$pkgdir/usr/libexec/cthulhu/wine"
|
|
||||||
|
|
||||||
# Remove icon cache - it will be generated by post-install hooks
|
# Remove icon cache - it will be generated by post-install hooks
|
||||||
rm -f "$pkgdir/usr/share/icons/hicolor/icon-theme.cache"
|
rm -f "$pkgdir/usr/share/icons/hicolor/icon-theme.cache"
|
||||||
}
|
}
|
||||||
|
|
||||||
package_cthulhu-wine-access-git() {
|
|
||||||
pkgdesc="Wine and Proton accessibility bridge for Cthulhu"
|
|
||||||
arch=(x86_64)
|
|
||||||
depends=("cthulhu-git=${pkgver}" glib2 wine)
|
|
||||||
provides=(cthulhu-wine-access)
|
|
||||||
conflicts=(cthulhu-wine-access)
|
|
||||||
|
|
||||||
cd "${_pkgname}"
|
|
||||||
install -Dm755 _build/cthulhu-wine-access.exe \
|
|
||||||
"$pkgdir/usr/libexec/cthulhu/wine/cthulhu-wine-access.exe"
|
|
||||||
install -Dm755 _build/cthulhu-wine-access.exe.so \
|
|
||||||
"$pkgdir/usr/libexec/cthulhu/wine/cthulhu-wine-access.exe.so"
|
|
||||||
}
|
|
||||||
|
|
||||||
# vim:set sw=2 sts=-1 et:
|
# vim:set sw=2 sts=-1 et:
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
|
||||||
|
# shellcheck shell=bash disable=SC2034,SC2154,SC2164
|
||||||
|
|
||||||
|
pkgname=cthulhu-wine-access-git
|
||||||
|
_pkgname=cthulhu
|
||||||
|
pkgver=2026.09.03.r475.gd4bec38
|
||||||
|
pkgrel=1
|
||||||
|
pkgdesc="Optional Wine and Proton accessibility bridge for Cthulhu"
|
||||||
|
url="https://git.stormux.org/storm/cthulhu"
|
||||||
|
arch=(x86_64)
|
||||||
|
license=(LGPL-2.1-or-later)
|
||||||
|
depends=(
|
||||||
|
glib2
|
||||||
|
wine
|
||||||
|
)
|
||||||
|
makedepends=(
|
||||||
|
cmake
|
||||||
|
git
|
||||||
|
)
|
||||||
|
provides=(cthulhu-wine-access)
|
||||||
|
conflicts=(cthulhu-wine-access)
|
||||||
|
source=("${_pkgname}::git+https://git.stormux.org/storm/${_pkgname}.git#branch=master")
|
||||||
|
sha512sums=('SKIP')
|
||||||
|
options=(!lto)
|
||||||
|
|
||||||
|
pkgver() {
|
||||||
|
cd "${_pkgname}"
|
||||||
|
local projectVersion revisionCount commitHash
|
||||||
|
|
||||||
|
projectVersion=$(sed -n "s/^[[:space:]]*version: '\([^']*\)'.*/\1/p" meson.build)
|
||||||
|
projectVersion=${projectVersion%-master}
|
||||||
|
projectVersion=${projectVersion//-/.}
|
||||||
|
[[ -n "${projectVersion}" ]] || return 1
|
||||||
|
|
||||||
|
revisionCount=$(git rev-list --count HEAD)
|
||||||
|
commitHash=$(git rev-parse --short=7 HEAD)
|
||||||
|
printf "%s.r%s.g%s\n" "${projectVersion}" "${revisionCount}" "${commitHash}"
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
local sourceDir="${srcdir}/${_pkgname}"
|
||||||
|
bash "${sourceDir}/scripts/build-wine-access.sh" \
|
||||||
|
"${sourceDir}/wine-access" \
|
||||||
|
"${sourceDir}/_build/wine-access-build" \
|
||||||
|
"${sourceDir}/_build/cthulhu-wine-access.exe" \
|
||||||
|
"${sourceDir}/_build/cthulhu-wine-access.exe.so"
|
||||||
|
}
|
||||||
|
|
||||||
|
package() {
|
||||||
|
cd "${_pkgname}"
|
||||||
|
install -Dm755 _build/cthulhu-wine-access.exe \
|
||||||
|
"$pkgdir/usr/libexec/cthulhu/wine/cthulhu-wine-access.exe"
|
||||||
|
install -Dm755 _build/cthulhu-wine-access.exe.so \
|
||||||
|
"$pkgdir/usr/libexec/cthulhu/wine/cthulhu-wine-access.exe.so"
|
||||||
|
}
|
||||||
|
|
||||||
|
# vim:set sw=2 sts=-1 et:
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
|
||||||
|
# shellcheck shell=bash disable=SC2034,SC2154,SC2164
|
||||||
|
|
||||||
|
pkgname=cthulhu-wine-access
|
||||||
|
pkgver=2026.09.09
|
||||||
|
pkgrel=1
|
||||||
|
pkgdesc="Optional Wine and Proton accessibility bridge for Cthulhu"
|
||||||
|
url="https://git.stormux.org/storm/cthulhu"
|
||||||
|
arch=(x86_64)
|
||||||
|
license=(LGPL-2.1-or-later)
|
||||||
|
depends=(
|
||||||
|
glib2
|
||||||
|
wine
|
||||||
|
)
|
||||||
|
makedepends=(
|
||||||
|
cmake
|
||||||
|
git
|
||||||
|
)
|
||||||
|
source=("git+https://git.stormux.org/storm/cthulhu.git#tag=${pkgver}")
|
||||||
|
sha512sums=('SKIP')
|
||||||
|
options=(!lto)
|
||||||
|
|
||||||
|
build() {
|
||||||
|
local sourceDir="${srcdir}/cthulhu"
|
||||||
|
bash "${sourceDir}/scripts/build-wine-access.sh" \
|
||||||
|
"${sourceDir}/wine-access" \
|
||||||
|
"${sourceDir}/_build/wine-access-build" \
|
||||||
|
"${sourceDir}/_build/cthulhu-wine-access.exe" \
|
||||||
|
"${sourceDir}/_build/cthulhu-wine-access.exe.so"
|
||||||
|
}
|
||||||
|
|
||||||
|
package() {
|
||||||
|
cd cthulhu
|
||||||
|
install -Dm755 _build/cthulhu-wine-access.exe \
|
||||||
|
"$pkgdir/usr/libexec/cthulhu/wine/cthulhu-wine-access.exe"
|
||||||
|
install -Dm755 _build/cthulhu-wine-access.exe.so \
|
||||||
|
"$pkgdir/usr/libexec/cthulhu/wine/cthulhu-wine-access.exe.so"
|
||||||
|
}
|
||||||
|
|
||||||
|
# vim:set sw=2 sts=-1 et:
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
# shellcheck shell=bash disable=SC2034,SC2154,SC2164
|
# shellcheck shell=bash disable=SC2034,SC2154,SC2164
|
||||||
|
|
||||||
pkgbase=cthulhu
|
pkgbase=cthulhu
|
||||||
pkgname=(cthulhu cthulhu-wine-access)
|
pkgname=cthulhu
|
||||||
pkgver=2026.07.18
|
pkgver=2026.09.09
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca"
|
pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca"
|
||||||
url="https://git.stormux.org/storm/cthulhu"
|
url="https://git.stormux.org/storm/cthulhu"
|
||||||
@@ -70,44 +70,24 @@ makedepends=(
|
|||||||
git
|
git
|
||||||
meson
|
meson
|
||||||
)
|
)
|
||||||
makedepends_x86_64=(
|
|
||||||
cmake
|
|
||||||
wine
|
|
||||||
)
|
|
||||||
source=("git+https://git.stormux.org/storm/cthulhu.git#tag=${pkgver}")
|
source=("git+https://git.stormux.org/storm/cthulhu.git#tag=${pkgver}")
|
||||||
sha512sums=('SKIP')
|
sha512sums=('SKIP')
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
cd cthulhu
|
cd cthulhu
|
||||||
arch-meson _build
|
arch-meson _build -Dwine-access=disabled
|
||||||
meson compile -C _build
|
meson compile -C _build
|
||||||
}
|
}
|
||||||
|
|
||||||
package_cthulhu() {
|
package() {
|
||||||
arch=(any)
|
arch=(any)
|
||||||
provides=("cthulhu=${pkgver}")
|
provides=("cthulhu=${pkgver}")
|
||||||
conflicts=(cthulhu)
|
conflicts=(cthulhu)
|
||||||
cd cthulhu
|
cd cthulhu
|
||||||
meson install -C _build --destdir "$pkgdir"
|
meson install -C _build --destdir "$pkgdir"
|
||||||
|
|
||||||
rm -rf "$pkgdir/usr/libexec/cthulhu/wine"
|
|
||||||
|
|
||||||
# Remove icon cache - it will be generated by post-install hooks
|
# Remove icon cache - it will be generated by post-install hooks
|
||||||
rm -f "$pkgdir/usr/share/icons/hicolor/icon-theme.cache"
|
rm -f "$pkgdir/usr/share/icons/hicolor/icon-theme.cache"
|
||||||
}
|
}
|
||||||
|
|
||||||
package_cthulhu-wine-access() {
|
|
||||||
pkgdesc="Wine and Proton accessibility bridge for Cthulhu"
|
|
||||||
arch=(x86_64)
|
|
||||||
depends=("cthulhu=${pkgver}" glib2 wine)
|
|
||||||
provides=(cthulhu-wine-access)
|
|
||||||
conflicts=(cthulhu-wine-access)
|
|
||||||
|
|
||||||
cd cthulhu
|
|
||||||
install -Dm755 _build/cthulhu-wine-access.exe \
|
|
||||||
"$pkgdir/usr/libexec/cthulhu/wine/cthulhu-wine-access.exe"
|
|
||||||
install -Dm755 _build/cthulhu-wine-access.exe.so \
|
|
||||||
"$pkgdir/usr/libexec/cthulhu/wine/cthulhu-wine-access.exe.so"
|
|
||||||
}
|
|
||||||
|
|
||||||
# vim:set sw=2 sts=-1 et:
|
# vim:set sw=2 sts=-1 et:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
cd "$(dirname "$0")" ; CWD=$(pwd)
|
cd "$(dirname "$0")" ; CWD=$(pwd)
|
||||||
|
|
||||||
PRGNAM=cthulhu
|
PRGNAM=cthulhu
|
||||||
VERSION=${VERSION:-2026.05.25}
|
VERSION=${VERSION:-2026.09.09}
|
||||||
BUILD=${BUILD:-1}
|
BUILD=${BUILD:-1}
|
||||||
TAG=storm
|
TAG=storm
|
||||||
PKGTYPE=txz
|
PKGTYPE=txz
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ This package requires the following packages, all available from SlackBuilds.org
|
|||||||
- gtk3
|
- gtk3
|
||||||
- liblouis
|
- liblouis
|
||||||
- libwnck3
|
- libwnck3
|
||||||
- python3-atspi
|
|
||||||
- python3-cairo
|
- python3-cairo
|
||||||
- python3-dasbus
|
- python3-dasbus
|
||||||
- python3-gobject
|
- python3-gobject
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
PRGNAM="cthulhu"
|
PRGNAM="cthulhu"
|
||||||
VERSION="2026.05.25"
|
VERSION="2026.09.09"
|
||||||
HOMEPAGE="https://git.stormux.org/storm/cthulhu"
|
HOMEPAGE="https://git.stormux.org/storm/cthulhu"
|
||||||
DOWNLOAD="https://git.stormux.org/storm/cthulhu.git"
|
DOWNLOAD="https://git.stormux.org/storm/cthulhu.git"
|
||||||
MD5SUM="SKIP"
|
MD5SUM="SKIP"
|
||||||
DOWNLOAD_x86_64=""
|
DOWNLOAD_x86_64=""
|
||||||
MD5SUM_x86_64=""
|
MD5SUM_x86_64=""
|
||||||
REQUIRES="at-spi2-core brltty gobject-introspection gstreamer gst-plugins-base gst-plugins-good gtk3 liblouis libwnck3 python3-atspi python3-cairo python3-dasbus python3-gobject python3-pluggy python3-pywayland python3-setproctitle python3-tomlkit speech-dispatcher"
|
REQUIRES="at-spi2-core brltty gobject-introspection gstreamer gst-plugins-base gst-plugins-good gtk3 liblouis libwnck3 python3-cairo python3-dasbus python3-gobject python3-pluggy python3-pywayland python3-setproctitle python3-tomlkit speech-dispatcher"
|
||||||
MAINTAINER="Storm Dragon"
|
MAINTAINER="Storm Dragon"
|
||||||
EMAIL="storm_dragon@stormux.org"
|
EMAIL="storm_dragon@stormux.org"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
cd "$(dirname "$0")" ; CWD=$(pwd)
|
cd "$(dirname "$0")" ; CWD=$(pwd)
|
||||||
|
|
||||||
PRGNAM=cthulhu
|
PRGNAM=cthulhu
|
||||||
VERSION=${VERSION:-2026.05.25}
|
VERSION=${VERSION:-2026.09.09}
|
||||||
BUILD=${BUILD:-1}
|
BUILD=${BUILD:-1}
|
||||||
TAG=storm
|
TAG=storm
|
||||||
PKGTYPE=txz
|
PKGTYPE=txz
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
project('cthulhu',
|
project('cthulhu',
|
||||||
version: '2026.07.18',
|
version: '2026.09.09',
|
||||||
meson_version: '>= 1.0.0',
|
meson_version: '>= 1.0.0',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+13
-1
@@ -59,11 +59,12 @@ fi
|
|||||||
cthulhuVersionFile="${scriptDir}/src/cthulhu/cthulhuVersion.py"
|
cthulhuVersionFile="${scriptDir}/src/cthulhu/cthulhuVersion.py"
|
||||||
mesonFile="${scriptDir}/meson.build"
|
mesonFile="${scriptDir}/meson.build"
|
||||||
pkgbuildFile="${scriptDir}/distro-packages/Arch-Linux/cthulhu/PKGBUILD"
|
pkgbuildFile="${scriptDir}/distro-packages/Arch-Linux/cthulhu/PKGBUILD"
|
||||||
|
winePkgbuildFile="${scriptDir}/distro-packages/Arch-Linux/cthulhu-wine-access/PKGBUILD"
|
||||||
slackwareBuildFile="${scriptDir}/distro-packages/Slackware/cthulhu.SlackBuild"
|
slackwareBuildFile="${scriptDir}/distro-packages/Slackware/cthulhu.SlackBuild"
|
||||||
slintBuildFile="${scriptDir}/distro-packages/Slint/cthulhu.SlackBuild"
|
slintBuildFile="${scriptDir}/distro-packages/Slint/cthulhu.SlackBuild"
|
||||||
slintInfoFile="${scriptDir}/distro-packages/Slint/cthulhu-info"
|
slintInfoFile="${scriptDir}/distro-packages/Slint/cthulhu-info"
|
||||||
|
|
||||||
for path in "$cthulhuVersionFile" "$mesonFile" "$pkgbuildFile" "$slackwareBuildFile" "$slintBuildFile" "$slintInfoFile"; do
|
for path in "$cthulhuVersionFile" "$mesonFile" "$pkgbuildFile" "$winePkgbuildFile" "$slackwareBuildFile" "$slintBuildFile" "$slintInfoFile"; do
|
||||||
if [[ ! -f "$path" ]]; then
|
if [[ ! -f "$path" ]]; then
|
||||||
echo "Error: Missing file: $path" >&2
|
echo "Error: Missing file: $path" >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -75,6 +76,8 @@ sed -i "s/^codeName = \".*\"/codeName = \"${codeNameValue}\"/" "$cthulhuVersionF
|
|||||||
sed -i "s/^ version: '.*',/ version: '${fullVersion}',/" "$mesonFile"
|
sed -i "s/^ version: '.*',/ version: '${fullVersion}',/" "$mesonFile"
|
||||||
sed -i "s/^pkgver=.*/pkgver=${pythonVersion}/" "$pkgbuildFile"
|
sed -i "s/^pkgver=.*/pkgver=${pythonVersion}/" "$pkgbuildFile"
|
||||||
sed -i "s/^pkgrel=.*/pkgrel=1/" "$pkgbuildFile"
|
sed -i "s/^pkgrel=.*/pkgrel=1/" "$pkgbuildFile"
|
||||||
|
sed -i "s/^pkgver=.*/pkgver=${pythonVersion}/" "$winePkgbuildFile"
|
||||||
|
sed -i "s/^pkgrel=.*/pkgrel=1/" "$winePkgbuildFile"
|
||||||
sed -i "s/^VERSION=\${VERSION:-.*}/VERSION=\${VERSION:-${pythonVersion}}/" "$slackwareBuildFile"
|
sed -i "s/^VERSION=\${VERSION:-.*}/VERSION=\${VERSION:-${pythonVersion}}/" "$slackwareBuildFile"
|
||||||
sed -i "s/^VERSION=\${VERSION:-.*}/VERSION=\${VERSION:-${pythonVersion}}/" "$slintBuildFile"
|
sed -i "s/^VERSION=\${VERSION:-.*}/VERSION=\${VERSION:-${pythonVersion}}/" "$slintBuildFile"
|
||||||
sed -i "s/^VERSION=\".*\"/VERSION=\"${pythonVersion}\"/" "$slintInfoFile"
|
sed -i "s/^VERSION=\".*\"/VERSION=\"${pythonVersion}\"/" "$slintInfoFile"
|
||||||
@@ -99,6 +102,14 @@ if ! rg -q "^pkgrel=1$" "$pkgbuildFile"; then
|
|||||||
echo "Error: Failed to reset pkgrel in ${pkgbuildFile}" >&2
|
echo "Error: Failed to reset pkgrel in ${pkgbuildFile}" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
if ! rg -q "^pkgver=${pythonVersion}$" "$winePkgbuildFile"; then
|
||||||
|
echo "Error: Failed to update ${winePkgbuildFile}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! rg -q "^pkgrel=1$" "$winePkgbuildFile"; then
|
||||||
|
echo "Error: Failed to reset pkgrel in ${winePkgbuildFile}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
if ! rg -q "^VERSION=\\\$\\{VERSION:-${pythonVersion}\\}$" "$slackwareBuildFile"; then
|
if ! rg -q "^VERSION=\\\$\\{VERSION:-${pythonVersion}\\}$" "$slackwareBuildFile"; then
|
||||||
echo "Error: Failed to update ${slackwareBuildFile}" >&2
|
echo "Error: Failed to update ${slackwareBuildFile}" >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -116,6 +127,7 @@ echo "Updated version to ${fullVersion} in:" \
|
|||||||
"${cthulhuVersionFile}" \
|
"${cthulhuVersionFile}" \
|
||||||
"${mesonFile}" \
|
"${mesonFile}" \
|
||||||
"${pkgbuildFile}" \
|
"${pkgbuildFile}" \
|
||||||
|
"${winePkgbuildFile}" \
|
||||||
"${slackwareBuildFile}" \
|
"${slackwareBuildFile}" \
|
||||||
"${slintBuildFile}" \
|
"${slintBuildFile}" \
|
||||||
"${slintInfoFile}"
|
"${slintInfoFile}"
|
||||||
|
|||||||
@@ -63,6 +63,15 @@ class CompositorStateAdapter:
|
|||||||
def get_snapshot(self) -> DesktopContextSnapshot:
|
def get_snapshot(self) -> DesktopContextSnapshot:
|
||||||
return self._snapshot
|
return self._snapshot
|
||||||
|
|
||||||
|
def refresh_workspace_context(self, reason: str) -> DesktopContextSnapshot:
|
||||||
|
"""Refreshes and returns the selected backend's current workspace context."""
|
||||||
|
|
||||||
|
backend = self._workspaceBackend
|
||||||
|
refreshContext = getattr(backend, "refresh_context", None)
|
||||||
|
if callable(refreshContext):
|
||||||
|
refreshContext(reason)
|
||||||
|
return self._snapshot
|
||||||
|
|
||||||
def get_session_type(self) -> str:
|
def get_session_type(self) -> str:
|
||||||
return get_session_type()
|
return get_session_type()
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,11 @@ class I3WorkspaceBackend:
|
|||||||
self._emitSignal = None
|
self._emitSignal = None
|
||||||
self._lastContext = None
|
self._lastContext = None
|
||||||
|
|
||||||
|
def refresh_context(self, reason: str) -> None:
|
||||||
|
"""Refreshes the cached i3 focus context without emitting a transition."""
|
||||||
|
|
||||||
|
self._refresh_context(reason, transition=False)
|
||||||
|
|
||||||
def _ensure_connection(self) -> bool:
|
def _ensure_connection(self) -> bool:
|
||||||
if self._connection is not None:
|
if self._connection is not None:
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -23,5 +23,5 @@
|
|||||||
# Forked from Orca screen reader.
|
# Forked from Orca screen reader.
|
||||||
# Cthulhu project: https://git.stormux.org/storm/cthulhu
|
# Cthulhu project: https://git.stormux.org/storm/cthulhu
|
||||||
|
|
||||||
version = "2026.07.30"
|
version = "2026.09.09"
|
||||||
codeName = "master"
|
codeName = ""
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ class EventManager:
|
|||||||
self._churnSuppressed: bool = cthulhu_state.pauseAtspiChurn
|
self._churnSuppressed: bool = cthulhu_state.pauseAtspiChurn
|
||||||
self._prioritizedContextToken: Optional[str] = cthulhu_state.prioritizedDesktopContextToken
|
self._prioritizedContextToken: Optional[str] = cthulhu_state.prioritizedDesktopContextToken
|
||||||
self._desktopContextConfirmedEmpty: bool = False
|
self._desktopContextConfirmedEmpty: bool = False
|
||||||
|
self._flushingStaleAtspiEvents: bool = False
|
||||||
self._relevanceBurstWindow: float = 0.15
|
self._relevanceBurstWindow: float = 0.15
|
||||||
self._relevanceBurstHistory: Dict[Tuple[str, str, str], float] = {}
|
self._relevanceBurstHistory: Dict[Tuple[str, str, str], float] = {}
|
||||||
|
|
||||||
@@ -173,6 +174,7 @@ class EventManager:
|
|||||||
|
|
||||||
debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Activating keyboard handling', True)
|
debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Activating keyboard handling', True)
|
||||||
self._inputEventManager = input_event_manager.get_manager()
|
self._inputEventManager = input_event_manager.get_manager()
|
||||||
|
self._inputEventManager.set_compositor_state_adapter(self._compositorStateAdapter)
|
||||||
self._inputEventManager.start_key_watcher()
|
self._inputEventManager.start_key_watcher()
|
||||||
cthulhu_state.device = self._inputEventManager._device
|
cthulhu_state.device = self._inputEventManager._device
|
||||||
self._keyHandlingActive = True
|
self._keyHandlingActive = True
|
||||||
@@ -276,6 +278,8 @@ class EventManager:
|
|||||||
self._compositorStateAdapter.remove_listener(self._handle_compositor_signal)
|
self._compositorStateAdapter.remove_listener(self._handle_compositor_signal)
|
||||||
|
|
||||||
self._compositorStateAdapter = adapter
|
self._compositorStateAdapter = adapter
|
||||||
|
if self._inputEventManager is not None:
|
||||||
|
self._inputEventManager.set_compositor_state_adapter(adapter)
|
||||||
if adapter is not None and hasattr(adapter, "add_listener"):
|
if adapter is not None and hasattr(adapter, "add_listener"):
|
||||||
adapter.add_listener(self._handle_compositor_signal)
|
adapter.add_listener(self._handle_compositor_signal)
|
||||||
|
|
||||||
@@ -924,32 +928,32 @@ class EventManager:
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _addToQueue(self, event: Any, asyncMode: bool) -> None:
|
def _addToQueue(self, event: Any, asyncMode: bool) -> bool:
|
||||||
debugging = debug.debugEventQueue
|
debugging = debug.debugEventQueue
|
||||||
if debugging:
|
if debugging:
|
||||||
debug.printMessage(debug.LEVEL_ALL, " acquiring lock...")
|
debug.printMessage(debug.LEVEL_ALL, " acquiring lock...")
|
||||||
self._gidleLock.acquire()
|
|
||||||
|
|
||||||
if debugging:
|
with self._gidleLock:
|
||||||
debug.printMessage(debug.LEVEL_ALL, " ...acquired")
|
effectiveAsyncMode = asyncMode or self._flushingStaleAtspiEvents
|
||||||
debug.printMessage(debug.LEVEL_ALL, " calling queue.put...")
|
if debugging:
|
||||||
debug.printMessage(debug.LEVEL_ALL, " (full=%s)" \
|
debug.printMessage(debug.LEVEL_ALL, " ...acquired")
|
||||||
% self._eventQueue.full())
|
debug.printMessage(debug.LEVEL_ALL, " calling queue.put...")
|
||||||
|
debug.printMessage(debug.LEVEL_ALL, " (full=%s)" \
|
||||||
|
% self._eventQueue.full())
|
||||||
|
|
||||||
self._eventQueue.put(event)
|
self._eventQueue.put(event)
|
||||||
if debugging:
|
if debugging:
|
||||||
debug.printMessage(debug.LEVEL_ALL, " ...put complete")
|
debug.printMessage(debug.LEVEL_ALL, " ...put complete")
|
||||||
|
|
||||||
if asyncMode and not self._gidleId:
|
if effectiveAsyncMode and not self._gidleId:
|
||||||
if self._gilSleepTime:
|
if self._gilSleepTime:
|
||||||
time.sleep(self._gilSleepTime)
|
time.sleep(self._gilSleepTime)
|
||||||
self._gidleId = GLib.idle_add(self._dequeue)
|
self._gidleId = GLib.idle_add(self._dequeue)
|
||||||
|
|
||||||
if debugging:
|
|
||||||
debug.printMessage(debug.LEVEL_ALL, " releasing lock...")
|
|
||||||
self._gidleLock.release()
|
|
||||||
if debug.debugEventQueue:
|
if debug.debugEventQueue:
|
||||||
|
debug.printMessage(debug.LEVEL_ALL, " releasing lock...")
|
||||||
debug.printMessage(debug.LEVEL_ALL, " ...released")
|
debug.printMessage(debug.LEVEL_ALL, " ...released")
|
||||||
|
return effectiveAsyncMode
|
||||||
|
|
||||||
def _queuePrintln(self, e: Any, isEnqueue: bool = True, isPrune: Optional[bool] = None) -> None:
|
def _queuePrintln(self, e: Any, isEnqueue: bool = True, isPrune: Optional[bool] = None) -> None:
|
||||||
"""Convenience method to output queue-related debugging info."""
|
"""Convenience method to output queue-related debugging info."""
|
||||||
@@ -1118,7 +1122,7 @@ class EventManager:
|
|||||||
script = cthulhu.cthulhuApp.scriptManager.get_script(AXObject.get_application(e.source), e.source)
|
script = cthulhu.cthulhuApp.scriptManager.get_script(AXObject.get_application(e.source), e.source)
|
||||||
script.eventCache[e.type] = (e, time.time())
|
script.eventCache[e.type] = (e, time.time())
|
||||||
|
|
||||||
self._addToQueue(e, asyncMode)
|
asyncMode = self._addToQueue(e, asyncMode)
|
||||||
if not asyncMode:
|
if not asyncMode:
|
||||||
self._dequeue()
|
self._dequeue()
|
||||||
|
|
||||||
@@ -1727,26 +1731,58 @@ class EventManager:
|
|||||||
def _flush_stale_atspi_events(self) -> None:
|
def _flush_stale_atspi_events(self) -> None:
|
||||||
"""Drops queued events that no longer match the compositor context."""
|
"""Drops queued events that no longer match the compositor context."""
|
||||||
|
|
||||||
self._gidleLock.acquire()
|
with self._gidleLock:
|
||||||
try:
|
self._flushingStaleAtspiEvents = True
|
||||||
originalQueue = self._eventQueue
|
originalQueue = self._eventQueue
|
||||||
newQueue: queue.Queue[Any] = queue.Queue(0)
|
self._eventQueue = queue.Queue(0)
|
||||||
|
|
||||||
|
retainedEvents: list[Any] = []
|
||||||
|
try:
|
||||||
while not originalQueue.empty():
|
while not originalQueue.empty():
|
||||||
try:
|
try:
|
||||||
event = originalQueue.get_nowait()
|
event = originalQueue.get_nowait()
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
break
|
break
|
||||||
|
|
||||||
if self._event_is_from_stale_context(event) and not self._should_preserve_during_suppression(event):
|
# Accessible property calls can dispatch nested AT-SPI events.
|
||||||
|
# Keep them outside _gidleLock so a nested enqueue cannot deadlock.
|
||||||
|
try:
|
||||||
|
isStale = self._event_is_from_stale_context(event)
|
||||||
|
shouldPreserve = (
|
||||||
|
isStale
|
||||||
|
and self._should_preserve_during_suppression(event)
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
retainedEvents.append(event)
|
||||||
|
raise
|
||||||
|
|
||||||
|
if isStale and not shouldPreserve:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
newQueue.put(event)
|
retainedEvents.append(event)
|
||||||
|
|
||||||
self._eventQueue = newQueue
|
|
||||||
if self._asyncMode and not self._eventQueue.empty() and not self._gidleId:
|
|
||||||
self._gidleId = GLib.idle_add(self._dequeue)
|
|
||||||
finally:
|
finally:
|
||||||
self._gidleLock.release()
|
while not originalQueue.empty():
|
||||||
|
try:
|
||||||
|
retainedEvents.append(originalQueue.get_nowait())
|
||||||
|
except queue.Empty:
|
||||||
|
break
|
||||||
|
|
||||||
|
with self._gidleLock:
|
||||||
|
eventsQueuedDuringFlush = self._eventQueue
|
||||||
|
mergedQueue: queue.Queue[Any] = queue.Queue(0)
|
||||||
|
for event in retainedEvents:
|
||||||
|
mergedQueue.put(event)
|
||||||
|
|
||||||
|
while not eventsQueuedDuringFlush.empty():
|
||||||
|
try:
|
||||||
|
mergedQueue.put(eventsQueuedDuringFlush.get_nowait())
|
||||||
|
except queue.Empty:
|
||||||
|
break
|
||||||
|
|
||||||
|
self._eventQueue = mergedQueue
|
||||||
|
self._flushingStaleAtspiEvents = False
|
||||||
|
if self._asyncMode and not self._eventQueue.empty() and not self._gidleId:
|
||||||
|
self._gidleId = GLib.idle_add(self._dequeue)
|
||||||
|
|
||||||
def _inFlood(self) -> bool:
|
def _inFlood(self) -> bool:
|
||||||
size = self._eventQueue.qsize()
|
size = self._eventQueue.qsize()
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ import gi
|
|||||||
gi.require_version("Atspi", "2.0")
|
gi.require_version("Atspi", "2.0")
|
||||||
from gi.repository import Atspi
|
from gi.repository import Atspi
|
||||||
from gi.repository import GLib
|
from gi.repository import GLib
|
||||||
import pyatspi
|
|
||||||
|
|
||||||
from . import debug
|
from . import debug
|
||||||
from . import focus_manager
|
from . import focus_manager
|
||||||
@@ -75,6 +74,14 @@ _X11_CONSUMABLE_MODIFIER_MASKS = (
|
|||||||
_X11_SHIFT_LOCK_MASK | _X11_NUM_LOCK_MASK,
|
_X11_SHIFT_LOCK_MASK | _X11_NUM_LOCK_MASK,
|
||||||
_X11_SHIFT_MASK | _X11_SHIFT_LOCK_MASK | _X11_NUM_LOCK_MASK,
|
_X11_SHIFT_MASK | _X11_SHIFT_LOCK_MASK | _X11_NUM_LOCK_MASK,
|
||||||
)
|
)
|
||||||
|
_X11_CONSUMABLE_EVENT_TYPES = (
|
||||||
|
(1 << int(Atspi.EventType.KEY_PRESSED_EVENT))
|
||||||
|
| (1 << int(Atspi.EventType.KEY_RELEASED_EVENT))
|
||||||
|
)
|
||||||
|
_X11_CONSUMABLE_SYNC_TYPE = (
|
||||||
|
Atspi.KeyListenerSyncType.SYNCHRONOUS
|
||||||
|
| Atspi.KeyListenerSyncType.CANCONSUME
|
||||||
|
)
|
||||||
_X11_CONSUMABLE_KEYSYMS = (
|
_X11_CONSUMABLE_KEYSYMS = (
|
||||||
"a", "b", "c", "d", "e", "f", "g", "h", "i", "k", "l", "m",
|
"a", "b", "c", "d", "e", "f", "g", "h", "i", "k", "l", "m",
|
||||||
"o", "p", "q", "r", "s", "t", "u", "v", "x", "y",
|
"o", "p", "q", "r", "s", "t", "u", "v", "x", "y",
|
||||||
@@ -110,6 +117,8 @@ class InputEventManager:
|
|||||||
self._key_pressed_id: int = 0
|
self._key_pressed_id: int = 0
|
||||||
self._key_released_id: int = 0
|
self._key_released_id: int = 0
|
||||||
self._selective_legacy_listener_active: bool = False
|
self._selective_legacy_listener_active: bool = False
|
||||||
|
self._selective_legacy_listener: Optional[Atspi.DeviceListener] = None
|
||||||
|
self._selective_legacy_registered_masks: List[int] = []
|
||||||
self._selective_legacy_key_set: List[Atspi.KeyDefinition] = []
|
self._selective_legacy_key_set: List[Atspi.KeyDefinition] = []
|
||||||
self._selective_legacy_keycodes: set[int] = set()
|
self._selective_legacy_keycodes: set[int] = set()
|
||||||
self._pointer_moved_id: int = 0
|
self._pointer_moved_id: int = 0
|
||||||
@@ -134,6 +143,12 @@ class InputEventManager:
|
|||||||
self._xtermRecoverySourceId: int = 0
|
self._xtermRecoverySourceId: int = 0
|
||||||
self._xtermRecoveryPollCount: int = 0
|
self._xtermRecoveryPollCount: int = 0
|
||||||
self._xtermHandoffHistory: List[str] = []
|
self._xtermHandoffHistory: List[str] = []
|
||||||
|
self._compositorStateAdapter: Optional[Any] = None
|
||||||
|
|
||||||
|
def set_compositor_state_adapter(self, adapter: Optional[Any]) -> None:
|
||||||
|
"""Stores the compositor adapter used to refresh XTerm handoff state."""
|
||||||
|
|
||||||
|
self._compositorStateAdapter = adapter
|
||||||
|
|
||||||
def activate_device(self) -> Atspi.Device:
|
def activate_device(self) -> Atspi.Device:
|
||||||
"""Creates and returns the AT-SPI device used by this manager."""
|
"""Creates and returns the AT-SPI device used by this manager."""
|
||||||
@@ -173,15 +188,7 @@ class InputEventManager:
|
|||||||
}
|
}
|
||||||
msg = "INPUT EVENT MANAGER: Adding selective consumable X11 key listener."
|
msg = "INPUT EVENT MANAGER: Adding selective consumable X11 key listener."
|
||||||
debug.print_message(debug.LEVEL_INFO, msg, True)
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
pyatspi.Registry.registerKeystrokeListener(
|
self._register_selective_legacy_key_listener()
|
||||||
self._process_selective_legacy_keyboard_event,
|
|
||||||
key_set=self._selective_legacy_key_set,
|
|
||||||
mask=list(_X11_CONSUMABLE_MODIFIER_MASKS),
|
|
||||||
kind=(Atspi.EventType.KEY_PRESSED_EVENT, Atspi.EventType.KEY_RELEASED_EVENT),
|
|
||||||
synchronous=True,
|
|
||||||
preemptive=True,
|
|
||||||
)
|
|
||||||
self._selective_legacy_listener_active = True
|
|
||||||
else:
|
else:
|
||||||
msg = "INPUT EVENT MANAGER: Using legacy AT-SPI key watcher."
|
msg = "INPUT EVENT MANAGER: Using legacy AT-SPI key watcher."
|
||||||
debug.print_message(debug.LEVEL_INFO, msg, True)
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
@@ -195,16 +202,7 @@ class InputEventManager:
|
|||||||
|
|
||||||
msg = "INPUT EVENT MANAGER: Stopping key watcher."
|
msg = "INPUT EVENT MANAGER: Stopping key watcher."
|
||||||
debug.print_message(debug.LEVEL_INFO, msg, True)
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
if self._selective_legacy_listener_active:
|
self._deregister_selective_legacy_key_listener()
|
||||||
pyatspi.Registry.deregisterKeystrokeListener(
|
|
||||||
self._process_selective_legacy_keyboard_event,
|
|
||||||
key_set=self._selective_legacy_key_set,
|
|
||||||
mask=list(_X11_CONSUMABLE_MODIFIER_MASKS),
|
|
||||||
kind=(Atspi.EventType.KEY_PRESSED_EVENT, Atspi.EventType.KEY_RELEASED_EVENT),
|
|
||||||
)
|
|
||||||
self._selective_legacy_listener_active = False
|
|
||||||
self._selective_legacy_key_set = []
|
|
||||||
self._selective_legacy_keycodes.clear()
|
|
||||||
if self._device is not None:
|
if self._device is not None:
|
||||||
for handlerId in (self._key_pressed_id, self._key_released_id):
|
for handlerId in (self._key_pressed_id, self._key_released_id):
|
||||||
if handlerId:
|
if handlerId:
|
||||||
@@ -847,9 +845,17 @@ class InputEventManager:
|
|||||||
|
|
||||||
snapshot = cthulhu_state.compositorSnapshot
|
snapshot = cthulhu_state.compositorSnapshot
|
||||||
usingI3Context = snapshot is not None and snapshot.backend_name == "i3-ipc"
|
usingI3Context = snapshot is not None and snapshot.backend_name == "i3-ipc"
|
||||||
|
if usingI3Context and self._lastEwmhXtermMatch is True:
|
||||||
|
adapter = self._compositorStateAdapter
|
||||||
|
refreshContext = getattr(adapter, "refresh_workspace_context", None)
|
||||||
|
if callable(refreshContext):
|
||||||
|
snapshot = refreshContext("xterm-handoff-check")
|
||||||
|
usingI3Context = snapshot is not None and snapshot.backend_name == "i3-ipc"
|
||||||
if usingI3Context and snapshot.focused_workspace_empty is True:
|
if usingI3Context and snapshot.focused_workspace_empty is True:
|
||||||
self._record_xterm_handoff_action("i3:confirmed-empty-workspace")
|
self._record_xterm_handoff_action("i3:confirmed-empty-workspace")
|
||||||
self._lastEwmhReadWindowId = None
|
self._lastEwmhReadWindowId = None
|
||||||
|
self._lastEwmhActiveWindowId = None
|
||||||
|
self._lastEwmhXtermMatch = False
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if usingI3Context and snapshot.focused_window_id is not None:
|
if usingI3Context and snapshot.focused_window_id is not None:
|
||||||
@@ -1270,7 +1276,95 @@ class InputEventManager:
|
|||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
# pylint: disable=too-many-arguments
|
||||||
# pylint: disable=too-many-positional-arguments
|
# pylint: disable=too-many-positional-arguments
|
||||||
def _process_selective_legacy_keyboard_event(self, event: Atspi.DeviceEvent) -> bool:
|
def _register_selective_legacy_key_listener(self) -> bool:
|
||||||
|
"""Registers the consumable X11 listener directly through libatspi."""
|
||||||
|
|
||||||
|
self._selective_legacy_listener = Atspi.DeviceListener.new(
|
||||||
|
self._process_selective_legacy_keyboard_event,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if self._selective_legacy_listener is None:
|
||||||
|
msg = "INPUT EVENT MANAGER: Could not create selective consumable X11 listener."
|
||||||
|
debug.print_message(debug.LEVEL_WARNING, msg, True)
|
||||||
|
self._deregister_selective_legacy_key_listener()
|
||||||
|
return False
|
||||||
|
|
||||||
|
for modifierMask in _X11_CONSUMABLE_MODIFIER_MASKS:
|
||||||
|
try:
|
||||||
|
registered = Atspi.register_keystroke_listener(
|
||||||
|
self._selective_legacy_listener,
|
||||||
|
self._selective_legacy_key_set,
|
||||||
|
modifierMask,
|
||||||
|
_X11_CONSUMABLE_EVENT_TYPES,
|
||||||
|
_X11_CONSUMABLE_SYNC_TYPE,
|
||||||
|
)
|
||||||
|
except GLib.GError as error:
|
||||||
|
msg = (
|
||||||
|
"INPUT EVENT MANAGER: Could not register selective consumable "
|
||||||
|
f"X11 listener for modifier mask {modifierMask}: {error}"
|
||||||
|
)
|
||||||
|
debug.print_message(debug.LEVEL_WARNING, msg, True)
|
||||||
|
registered = False
|
||||||
|
|
||||||
|
if not registered:
|
||||||
|
msg = (
|
||||||
|
"INPUT EVENT MANAGER: Selective consumable X11 listener registration "
|
||||||
|
f"failed for modifier mask {modifierMask}."
|
||||||
|
)
|
||||||
|
debug.print_message(debug.LEVEL_WARNING, msg, True)
|
||||||
|
self._deregister_selective_legacy_key_listener()
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._selective_legacy_registered_masks.append(modifierMask)
|
||||||
|
|
||||||
|
self._selective_legacy_listener_active = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _deregister_selective_legacy_key_listener(self) -> None:
|
||||||
|
"""Deregisters all masks associated with the consumable X11 listener."""
|
||||||
|
|
||||||
|
listener = self._selective_legacy_listener
|
||||||
|
failedMasks = []
|
||||||
|
if listener is not None:
|
||||||
|
for modifierMask in self._selective_legacy_registered_masks:
|
||||||
|
try:
|
||||||
|
deregistered = Atspi.deregister_keystroke_listener(
|
||||||
|
listener,
|
||||||
|
self._selective_legacy_key_set,
|
||||||
|
modifierMask,
|
||||||
|
_X11_CONSUMABLE_EVENT_TYPES,
|
||||||
|
)
|
||||||
|
except GLib.GError as error:
|
||||||
|
msg = (
|
||||||
|
"INPUT EVENT MANAGER: Could not deregister selective consumable "
|
||||||
|
f"X11 listener for modifier mask {modifierMask}: {error}"
|
||||||
|
)
|
||||||
|
debug.print_message(debug.LEVEL_WARNING, msg, True)
|
||||||
|
deregistered = False
|
||||||
|
|
||||||
|
if not deregistered:
|
||||||
|
failedMasks.append(modifierMask)
|
||||||
|
|
||||||
|
self._selective_legacy_registered_masks = failedMasks
|
||||||
|
if failedMasks:
|
||||||
|
self._selective_legacy_listener_active = True
|
||||||
|
msg = (
|
||||||
|
"INPUT EVENT MANAGER: Preserving selective consumable X11 listener "
|
||||||
|
f"for {len(failedMasks)} failed deregistration(s)."
|
||||||
|
)
|
||||||
|
debug.print_message(debug.LEVEL_WARNING, msg, True)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._selective_legacy_listener_active = False
|
||||||
|
self._selective_legacy_listener = None
|
||||||
|
self._selective_legacy_key_set = []
|
||||||
|
self._selective_legacy_keycodes.clear()
|
||||||
|
|
||||||
|
def _process_selective_legacy_keyboard_event(
|
||||||
|
self,
|
||||||
|
event: Atspi.DeviceEvent,
|
||||||
|
_userData: object = None,
|
||||||
|
) -> bool:
|
||||||
"""Returns an X11 consume decision for unmodified browse-navigation keys."""
|
"""Returns an X11 consume decision for unmodified browse-navigation keys."""
|
||||||
|
|
||||||
pressed = event.type == Atspi.EventType.KEY_PRESSED_EVENT
|
pressed = event.type == Atspi.EventType.KEY_PRESSED_EVENT
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import re
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class ArchPkgbuildOptionalWineTests(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls) -> None:
|
||||||
|
repository = Path(__file__).resolve().parents[1]
|
||||||
|
cls.repository = repository
|
||||||
|
versionContents = (
|
||||||
|
repository / "src" / "cthulhu" / "cthulhuVersion.py"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
versionMatch = re.search(r'(?m)^version = "([^"]+)"$', versionContents)
|
||||||
|
if versionMatch is None:
|
||||||
|
raise AssertionError("Unable to read the current Cthulhu version")
|
||||||
|
cls.releaseVersion = versionMatch.group(1)
|
||||||
|
cls.pkgbuilds = [
|
||||||
|
repository / "distro-packages" / "Arch-Linux" / name / "PKGBUILD"
|
||||||
|
for name in ("cthulhu", "cthulhu-git")
|
||||||
|
]
|
||||||
|
cls.helperPkgbuilds = [
|
||||||
|
repository / "distro-packages" / "Arch-Linux" / name / "PKGBUILD"
|
||||||
|
for name in ("cthulhu-wine-access", "cthulhu-wine-access-git")
|
||||||
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _array_contents(contents: str, arrayName: str) -> str:
|
||||||
|
return contents.split(f"{arrayName}=(", 1)[1].split(")", 1)[0]
|
||||||
|
|
||||||
|
def test_main_packages_disable_wine_helper_build(self) -> None:
|
||||||
|
for pkgbuild in self.pkgbuilds:
|
||||||
|
with self.subTest(pkgbuild=pkgbuild):
|
||||||
|
contents = pkgbuild.read_text(encoding="utf-8")
|
||||||
|
self.assertIn("-Dwine-access=disabled", contents)
|
||||||
|
|
||||||
|
def test_main_packages_do_not_depend_on_wine(self) -> None:
|
||||||
|
for pkgbuild in self.pkgbuilds:
|
||||||
|
with self.subTest(pkgbuild=pkgbuild):
|
||||||
|
contents = pkgbuild.read_text(encoding="utf-8")
|
||||||
|
dependencySection = contents.split("source=(", 1)[0]
|
||||||
|
dependencyLines = {
|
||||||
|
line.strip().strip("'")
|
||||||
|
for line in dependencySection.splitlines()
|
||||||
|
if line.strip() and not line.lstrip().startswith("#")
|
||||||
|
}
|
||||||
|
self.assertNotIn("wine", dependencyLines)
|
||||||
|
|
||||||
|
def test_main_packages_advertise_interchangeable_optional_helper(self) -> None:
|
||||||
|
helperDescription = (
|
||||||
|
"'cthulhu-wine-access: Wine, Proton, NVDA Controller, and Tolk integration'"
|
||||||
|
)
|
||||||
|
for pkgbuild in self.pkgbuilds:
|
||||||
|
with self.subTest(pkgbuild=pkgbuild):
|
||||||
|
contents = pkgbuild.read_text(encoding="utf-8")
|
||||||
|
self.assertIn(
|
||||||
|
helperDescription,
|
||||||
|
self._array_contents(contents, "optdepends_x86_64"),
|
||||||
|
)
|
||||||
|
self.assertNotIn(
|
||||||
|
"cthulhu-wine-access",
|
||||||
|
self._array_contents(contents, "optdepends"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_stable_packages_use_current_release_version(self) -> None:
|
||||||
|
for pkgbuild in (self.pkgbuilds[0], self.helperPkgbuilds[0]):
|
||||||
|
with self.subTest(pkgbuild=pkgbuild):
|
||||||
|
contents = pkgbuild.read_text(encoding="utf-8")
|
||||||
|
self.assertRegex(
|
||||||
|
contents,
|
||||||
|
rf"(?m)^pkgver={re.escape(self.releaseVersion)}$",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_release_metadata_uses_current_release_version(self) -> None:
|
||||||
|
mesonContents = (self.repository / "meson.build").read_text(encoding="utf-8")
|
||||||
|
self.assertRegex(
|
||||||
|
mesonContents,
|
||||||
|
rf"(?m)^ version: '{re.escape(self.releaseVersion)}(?:-[^']+)?',$",
|
||||||
|
)
|
||||||
|
|
||||||
|
expectedSlackwareVersion = f"VERSION=${{VERSION:-{self.releaseVersion}}}"
|
||||||
|
for relativePath in (
|
||||||
|
("distro-packages", "Slackware", "cthulhu.SlackBuild"),
|
||||||
|
("distro-packages", "Slint", "cthulhu.SlackBuild"),
|
||||||
|
):
|
||||||
|
with self.subTest(relativePath=relativePath):
|
||||||
|
contents = self.repository.joinpath(*relativePath).read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
self.assertIn(expectedSlackwareVersion, contents)
|
||||||
|
|
||||||
|
slintInfo = (
|
||||||
|
self.repository / "distro-packages" / "Slint" / "cthulhu-info"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
self.assertRegex(
|
||||||
|
slintInfo,
|
||||||
|
rf'(?m)^VERSION="{re.escape(self.releaseVersion)}"$',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_stable_helper_has_release_contract(self) -> None:
|
||||||
|
contents = self.helperPkgbuilds[0].read_text(encoding="utf-8")
|
||||||
|
self.assertRegex(contents, r"(?m)^arch=\(x86_64\)$")
|
||||||
|
|
||||||
|
def test_optional_helpers_do_not_depend_on_main_package(self) -> None:
|
||||||
|
for pkgbuild in self.helperPkgbuilds:
|
||||||
|
with self.subTest(pkgbuild=pkgbuild):
|
||||||
|
contents = pkgbuild.read_text(encoding="utf-8")
|
||||||
|
dependencies = self._array_contents(contents, "depends")
|
||||||
|
self.assertNotRegex(
|
||||||
|
dependencies,
|
||||||
|
r"(?m)^\s*['\"]?cthulhu(?:-git)?(?:[<>=].*)?['\"]?\s*$",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_git_helper_provides_stable_helper_name(self) -> None:
|
||||||
|
contents = self.helperPkgbuilds[1].read_text(encoding="utf-8")
|
||||||
|
self.assertRegex(contents, r"(?m)^provides=\(cthulhu-wine-access\)$")
|
||||||
|
self.assertRegex(contents, r"(?m)^conflicts=\(cthulhu-wine-access\)$")
|
||||||
|
|
||||||
|
def test_optional_helper_packages_build_only_the_helper(self) -> None:
|
||||||
|
for pkgbuild in self.helperPkgbuilds:
|
||||||
|
with self.subTest(pkgbuild=pkgbuild):
|
||||||
|
contents = pkgbuild.read_text(encoding="utf-8")
|
||||||
|
self.assertIn('/scripts/build-wine-access.sh"', contents)
|
||||||
|
self.assertNotIn("arch-meson", contents)
|
||||||
|
self.assertRegex(contents, r"(?m)^ wine$")
|
||||||
|
makeDependencies = self._array_contents(contents, "makedepends")
|
||||||
|
self.assertNotRegex(makeDependencies, r"(?m)^\s*meson\s*$")
|
||||||
|
|
||||||
|
def test_optional_helper_packages_disable_lto(self) -> None:
|
||||||
|
for pkgbuild in self.helperPkgbuilds:
|
||||||
|
with self.subTest(pkgbuild=pkgbuild):
|
||||||
|
contents = pkgbuild.read_text(encoding="utf-8")
|
||||||
|
self.assertRegex(contents, r"(?m)^options=\(!lto\)$")
|
||||||
|
|
||||||
|
def test_git_sources_declare_git_as_a_build_dependency(self) -> None:
|
||||||
|
for pkgbuild in self.pkgbuilds + self.helperPkgbuilds:
|
||||||
|
with self.subTest(pkgbuild=pkgbuild):
|
||||||
|
contents = pkgbuild.read_text(encoding="utf-8")
|
||||||
|
self.assertIn("git+", contents)
|
||||||
|
makeDependencies = contents.split("makedepends=(", 1)[1].split(
|
||||||
|
")", 1
|
||||||
|
)[0]
|
||||||
|
self.assertRegex(makeDependencies, r"(?m)^ git$")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -42,6 +42,7 @@ class FakeWorkspaceBackend:
|
|||||||
self.name = name
|
self.name = name
|
||||||
self.activate_calls = []
|
self.activate_calls = []
|
||||||
self.deactivate_calls = []
|
self.deactivate_calls = []
|
||||||
|
self.refresh_calls = []
|
||||||
|
|
||||||
def is_available(self, session_type: str | None = None) -> bool:
|
def is_available(self, session_type: str | None = None) -> bool:
|
||||||
return self.available
|
return self.available
|
||||||
@@ -52,6 +53,9 @@ class FakeWorkspaceBackend:
|
|||||||
def deactivate(self, emit_signal=None) -> None:
|
def deactivate(self, emit_signal=None) -> None:
|
||||||
self.deactivate_calls.append(emit_signal)
|
self.deactivate_calls.append(emit_signal)
|
||||||
|
|
||||||
|
def refresh_context(self, reason: str) -> None:
|
||||||
|
self.refresh_calls.append(reason)
|
||||||
|
|
||||||
|
|
||||||
class CompositorStateAdapterRegressionTests(unittest.TestCase):
|
class CompositorStateAdapterRegressionTests(unittest.TestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
@@ -111,6 +115,16 @@ class CompositorStateAdapterRegressionTests(unittest.TestCase):
|
|||||||
self.assertEqual(snapshot.focused_window_title, "9 devel")
|
self.assertEqual(snapshot.focused_window_title, "9 devel")
|
||||||
self.assertFalse(snapshot.focused_workspace_empty)
|
self.assertFalse(snapshot.focused_workspace_empty)
|
||||||
|
|
||||||
|
def test_refresh_workspace_context_queries_selected_backend(self) -> None:
|
||||||
|
backend = FakeWorkspaceBackend(True, "i3-ipc")
|
||||||
|
adapter = compositor_state_adapter.CompositorStateAdapter(workspace_backends=[backend])
|
||||||
|
adapter.activate()
|
||||||
|
|
||||||
|
snapshot = adapter.refresh_workspace_context("xterm-handoff-check")
|
||||||
|
|
||||||
|
self.assertEqual(backend.refresh_calls, ["xterm-handoff-check"])
|
||||||
|
self.assertIs(snapshot, adapter.get_snapshot())
|
||||||
|
|
||||||
def test_confirmed_empty_workspace_clears_stale_accessible_context(self) -> None:
|
def test_confirmed_empty_workspace_clears_stale_accessible_context(self) -> None:
|
||||||
backend = FakeWorkspaceBackend(True, "i3-ipc")
|
backend = FakeWorkspaceBackend(True, "i3-ipc")
|
||||||
adapter = compositor_state_adapter.CompositorStateAdapter(workspace_backends=[backend])
|
adapter = compositor_state_adapter.CompositorStateAdapter(workspace_backends=[backend])
|
||||||
|
|||||||
@@ -153,6 +153,26 @@ class I3WorkspaceBackendRegressionTests(unittest.TestCase):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_public_refresh_emits_updated_context_without_transition(self) -> None:
|
||||||
|
backend = compositor_state_i3.I3WorkspaceBackend(i3ipc_module=types.SimpleNamespace())
|
||||||
|
backend._active = True
|
||||||
|
backend._emitSignal = mock.Mock()
|
||||||
|
payload = {
|
||||||
|
"focused_window_id": 0x2600003,
|
||||||
|
"focused_window_title": "Browser",
|
||||||
|
"focused_workspace_empty": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
with mock.patch.object(backend, "_query_context", return_value=({"8"}, payload)):
|
||||||
|
backend.refresh_context("xterm-handoff-check")
|
||||||
|
|
||||||
|
backend._emitSignal.assert_called_once_with(
|
||||||
|
compositor_state_types.WORKSPACE_STATE_CHANGED,
|
||||||
|
{"8"},
|
||||||
|
"xterm-handoff-check",
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
|
||||||
def test_query_failure_emits_unknown_instead_of_preserving_stale_i3_context(self):
|
def test_query_failure_emits_unknown_instead_of_preserving_stale_i3_context(self):
|
||||||
backend = compositor_state_i3.I3WorkspaceBackend(i3ipc_module=types.SimpleNamespace())
|
backend = compositor_state_i3.I3WorkspaceBackend(i3ipc_module=types.SimpleNamespace())
|
||||||
backend._active = True
|
backend._active = True
|
||||||
|
|||||||
@@ -64,6 +64,16 @@ class EventManagerCompositorContextRegressionTests(unittest.TestCase):
|
|||||||
|
|
||||||
adapter.add_listener.assert_called_once_with(self.manager._handle_compositor_signal)
|
adapter.add_listener.assert_called_once_with(self.manager._handle_compositor_signal)
|
||||||
|
|
||||||
|
def test_set_compositor_state_adapter_updates_active_input_manager(self) -> None:
|
||||||
|
adapter = mock.Mock()
|
||||||
|
self.manager._inputEventManager = mock.Mock()
|
||||||
|
|
||||||
|
self.manager.set_compositor_state_adapter(adapter)
|
||||||
|
|
||||||
|
self.manager._inputEventManager.set_compositor_state_adapter.assert_called_once_with(
|
||||||
|
adapter
|
||||||
|
)
|
||||||
|
|
||||||
def test_pause_signal_updates_churn_state_and_resume_clears_it(self) -> None:
|
def test_pause_signal_updates_churn_state_and_resume_clears_it(self) -> None:
|
||||||
snapshot = compositor_state_types.DesktopContextSnapshot(session_type="wayland")
|
snapshot = compositor_state_types.DesktopContextSnapshot(session_type="wayland")
|
||||||
|
|
||||||
@@ -186,6 +196,64 @@ class EventManagerCompositorContextRegressionTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(list(self.manager._eventQueue.queue), [currentEvent])
|
self.assertEqual(list(self.manager._eventQueue.queue), [currentEvent])
|
||||||
|
|
||||||
|
def test_flush_classifies_outside_queue_lock_and_preserves_nested_event(self) -> None:
|
||||||
|
staleEvent = FakeEvent("object:children-changed:add", source="stale")
|
||||||
|
currentEvent = FakeEvent("object:children-changed:add", source="current")
|
||||||
|
nestedEvent = FakeEvent("object:state-changed:focused", source="nested", detail1=1)
|
||||||
|
self.manager._eventQueue.put(staleEvent)
|
||||||
|
self.manager._eventQueue.put(currentEvent)
|
||||||
|
self.manager._ignore = mock.Mock(return_value=False)
|
||||||
|
self.manager._prioritizeSelfHostedFocusedEvent = mock.Mock(return_value=False)
|
||||||
|
self.manager._queuePrintln = mock.Mock()
|
||||||
|
self.manager._inFlood = mock.Mock(return_value=False)
|
||||||
|
self.manager._shouldSuspendEventsFor = mock.Mock(return_value=False)
|
||||||
|
self.manager._dequeue = mock.Mock(return_value=False)
|
||||||
|
script = types.SimpleNamespace(eventCache={})
|
||||||
|
|
||||||
|
def classify_event(event) -> bool:
|
||||||
|
self.assertFalse(self.manager._gidleLock.locked())
|
||||||
|
if event is staleEvent:
|
||||||
|
self.manager._enqueue(nestedEvent)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.manager._event_is_from_stale_context = mock.Mock(side_effect=classify_event)
|
||||||
|
self.manager._should_preserve_during_suppression = mock.Mock(return_value=False)
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(event_manager.AXObject, "get_application", return_value=object()),
|
||||||
|
mock.patch.object(event_manager.GLib, "idle_add", return_value=1),
|
||||||
|
mock.patch.object(
|
||||||
|
event_manager.cthulhu.cthulhuApp.scriptManager,
|
||||||
|
"get_script",
|
||||||
|
return_value=script,
|
||||||
|
),
|
||||||
|
mock.patch.object(
|
||||||
|
event_manager.AXUtilities,
|
||||||
|
"get_application_toolkit_name",
|
||||||
|
return_value="VCL",
|
||||||
|
),
|
||||||
|
):
|
||||||
|
self.manager._flush_stale_atspi_events()
|
||||||
|
|
||||||
|
self.assertEqual(list(self.manager._eventQueue.queue), [currentEvent, nestedEvent])
|
||||||
|
self.manager._dequeue.assert_not_called()
|
||||||
|
|
||||||
|
def test_flush_restores_retained_and_unexamined_events_after_exception(self) -> None:
|
||||||
|
firstEvent = FakeEvent("object:children-changed:add", source="first")
|
||||||
|
secondEvent = FakeEvent("object:children-changed:add", source="second")
|
||||||
|
self.manager._eventQueue.put(firstEvent)
|
||||||
|
self.manager._eventQueue.put(secondEvent)
|
||||||
|
self.manager._event_is_from_stale_context = mock.Mock(
|
||||||
|
side_effect=RuntimeError("property lookup failed"),
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "property lookup failed"):
|
||||||
|
self.manager._flush_stale_atspi_events()
|
||||||
|
|
||||||
|
self.assertEqual(list(self.manager._eventQueue.queue), [firstEvent, secondEvent])
|
||||||
|
self.assertFalse(self.manager._flushingStaleAtspiEvents)
|
||||||
|
|
||||||
def test_stale_background_event_does_not_activate_script_during_suppression(self) -> None:
|
def test_stale_background_event_does_not_activate_script_during_suppression(self) -> None:
|
||||||
script = mock.Mock()
|
script = mock.Mock()
|
||||||
script.isActivatableEvent.return_value = True
|
script.isActivatableEvent.return_value = True
|
||||||
|
|||||||
@@ -118,6 +118,9 @@ class InputEventManagerKeyWatcherTests(unittest.TestCase):
|
|||||||
device = FakeDevice()
|
device = FakeDevice()
|
||||||
fakeAtspi = self._fake_atspi((2, 60, 5), device)
|
fakeAtspi = self._fake_atspi((2, 60, 5), device)
|
||||||
fakeAtspi.EventType = types.SimpleNamespace(KEY_PRESSED_EVENT=0, KEY_RELEASED_EVENT=1)
|
fakeAtspi.EventType = types.SimpleNamespace(KEY_PRESSED_EVENT=0, KEY_RELEASED_EVENT=1)
|
||||||
|
fakeAtspi.DeviceListener = types.SimpleNamespace(new=mock.Mock(return_value=object()))
|
||||||
|
fakeAtspi.register_keystroke_listener = mock.Mock(return_value=True)
|
||||||
|
fakeAtspi.deregister_keystroke_listener = mock.Mock(return_value=True)
|
||||||
fakeKeySet = [types.SimpleNamespace(keycode=38), types.SimpleNamespace(keycode=56)]
|
fakeKeySet = [types.SimpleNamespace(keycode=38), types.SimpleNamespace(keycode=56)]
|
||||||
|
|
||||||
with (
|
with (
|
||||||
@@ -128,8 +131,6 @@ class InputEventManagerKeyWatcherTests(unittest.TestCase):
|
|||||||
"_get_x11_consumable_key_set",
|
"_get_x11_consumable_key_set",
|
||||||
return_value=fakeKeySet,
|
return_value=fakeKeySet,
|
||||||
),
|
),
|
||||||
mock.patch.object(input_event_manager.pyatspi.Registry, "registerKeystrokeListener") as register,
|
|
||||||
mock.patch.object(input_event_manager.pyatspi.Registry, "deregisterKeystrokeListener") as deregister,
|
|
||||||
mock.patch.object(manager, "_initialize_xterm_handoff") as initializeXtermHandoff,
|
mock.patch.object(manager, "_initialize_xterm_handoff") as initializeXtermHandoff,
|
||||||
):
|
):
|
||||||
manager.start_key_watcher()
|
manager.start_key_watcher()
|
||||||
@@ -148,19 +149,18 @@ class InputEventManagerKeyWatcherTests(unittest.TestCase):
|
|||||||
shiftLockMask | numLockMask,
|
shiftLockMask | numLockMask,
|
||||||
shiftMask | shiftLockMask | numLockMask,
|
shiftMask | shiftLockMask | numLockMask,
|
||||||
]
|
]
|
||||||
register.assert_called_once_with(
|
listener = fakeAtspi.DeviceListener.new.return_value
|
||||||
|
fakeAtspi.DeviceListener.new.assert_called_once_with(
|
||||||
manager._process_selective_legacy_keyboard_event,
|
manager._process_selective_legacy_keyboard_event,
|
||||||
key_set=fakeKeySet,
|
None,
|
||||||
mask=expectedMasks,
|
|
||||||
kind=(0, 1),
|
|
||||||
synchronous=True,
|
|
||||||
preemptive=True,
|
|
||||||
)
|
)
|
||||||
deregister.assert_called_once_with(
|
self.assertEqual(
|
||||||
manager._process_selective_legacy_keyboard_event,
|
fakeAtspi.register_keystroke_listener.call_args_list,
|
||||||
key_set=fakeKeySet,
|
[mock.call(listener, fakeKeySet, mask, 3, 3) for mask in expectedMasks],
|
||||||
mask=expectedMasks,
|
)
|
||||||
kind=(0, 1),
|
self.assertEqual(
|
||||||
|
fakeAtspi.deregister_keystroke_listener.call_args_list,
|
||||||
|
[mock.call(listener, fakeKeySet, mask, 3) for mask in expectedMasks],
|
||||||
)
|
)
|
||||||
initializeXtermHandoff.assert_called_once_with()
|
initializeXtermHandoff.assert_called_once_with()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -171,6 +171,83 @@ class InputEventManagerKeyWatcherTests(unittest.TestCase):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_x11_consumable_listener_rolls_back_partial_registration(self) -> None:
|
||||||
|
manager = input_event_manager.InputEventManager()
|
||||||
|
device = FakeDevice()
|
||||||
|
fakeAtspi = self._fake_atspi((2, 60, 5), device)
|
||||||
|
fakeAtspi.EventType = types.SimpleNamespace(KEY_PRESSED_EVENT=0, KEY_RELEASED_EVENT=1)
|
||||||
|
fakeAtspi.DeviceListener = types.SimpleNamespace(new=mock.Mock(return_value=object()))
|
||||||
|
fakeAtspi.register_keystroke_listener = mock.Mock(
|
||||||
|
side_effect=[True, True, False],
|
||||||
|
)
|
||||||
|
fakeAtspi.deregister_keystroke_listener = mock.Mock(return_value=True)
|
||||||
|
fakeKeySet = [types.SimpleNamespace(keycode=56)]
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(input_event_manager, "Atspi", fakeAtspi),
|
||||||
|
mock.patch.dict(input_event_manager.os.environ, {"XDG_SESSION_TYPE": "x11"}),
|
||||||
|
mock.patch.object(
|
||||||
|
input_event_manager,
|
||||||
|
"_get_x11_consumable_key_set",
|
||||||
|
return_value=fakeKeySet,
|
||||||
|
),
|
||||||
|
mock.patch.object(manager, "_initialize_xterm_handoff"),
|
||||||
|
):
|
||||||
|
manager.start_key_watcher()
|
||||||
|
|
||||||
|
expectedMasks = list(input_event_manager._X11_CONSUMABLE_MODIFIER_MASKS)
|
||||||
|
listener = fakeAtspi.DeviceListener.new.return_value
|
||||||
|
self.assertEqual(
|
||||||
|
fakeAtspi.register_keystroke_listener.call_args_list,
|
||||||
|
[
|
||||||
|
mock.call(listener, fakeKeySet, expectedMasks[0], 3, 3),
|
||||||
|
mock.call(listener, fakeKeySet, expectedMasks[1], 3, 3),
|
||||||
|
mock.call(listener, fakeKeySet, expectedMasks[2], 3, 3),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
fakeAtspi.deregister_keystroke_listener.call_args_list,
|
||||||
|
[
|
||||||
|
mock.call(listener, fakeKeySet, expectedMasks[0], 3),
|
||||||
|
mock.call(listener, fakeKeySet, expectedMasks[1], 3),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.assertFalse(manager._selective_legacy_listener_active)
|
||||||
|
self.assertEqual(manager._selective_legacy_key_set, [])
|
||||||
|
self.assertEqual(manager._selective_legacy_keycodes, set())
|
||||||
|
|
||||||
|
def test_x11_consumable_listener_preserves_failed_deregistration_for_retry(self) -> None:
|
||||||
|
manager = input_event_manager.InputEventManager()
|
||||||
|
listener = object()
|
||||||
|
keySet = [types.SimpleNamespace(keycode=56)]
|
||||||
|
manager._selective_legacy_listener_active = True
|
||||||
|
manager._selective_legacy_listener = listener
|
||||||
|
manager._selective_legacy_registered_masks = [0]
|
||||||
|
manager._selective_legacy_key_set = keySet
|
||||||
|
manager._selective_legacy_keycodes = {56}
|
||||||
|
|
||||||
|
with mock.patch.object(
|
||||||
|
input_event_manager.Atspi,
|
||||||
|
"deregister_keystroke_listener",
|
||||||
|
side_effect=[False, True],
|
||||||
|
) as deregister:
|
||||||
|
manager._deregister_selective_legacy_key_listener()
|
||||||
|
|
||||||
|
self.assertTrue(manager._selective_legacy_listener_active)
|
||||||
|
self.assertIs(manager._selective_legacy_listener, listener)
|
||||||
|
self.assertEqual(manager._selective_legacy_registered_masks, [0])
|
||||||
|
self.assertEqual(manager._selective_legacy_key_set, keySet)
|
||||||
|
self.assertEqual(manager._selective_legacy_keycodes, {56})
|
||||||
|
|
||||||
|
manager._deregister_selective_legacy_key_listener()
|
||||||
|
|
||||||
|
self.assertEqual(deregister.call_count, 2)
|
||||||
|
self.assertFalse(manager._selective_legacy_listener_active)
|
||||||
|
self.assertIsNone(manager._selective_legacy_listener)
|
||||||
|
self.assertEqual(manager._selective_legacy_registered_masks, [])
|
||||||
|
self.assertEqual(manager._selective_legacy_key_set, [])
|
||||||
|
self.assertEqual(manager._selective_legacy_keycodes, set())
|
||||||
|
|
||||||
def test_x11_consumable_keycodes_include_structural_navigation_but_not_tab(self) -> None:
|
def test_x11_consumable_keycodes_include_structural_navigation_but_not_tab(self) -> None:
|
||||||
keycodes = {
|
keycodes = {
|
||||||
keysym: index + 20
|
keysym: index + 20
|
||||||
|
|||||||
@@ -658,6 +658,43 @@ class InputEventManagerX11FocusRegressionTests(unittest.TestCase):
|
|||||||
matcher.assert_called_once_with(0x2600003)
|
matcher.assert_called_once_with(0x2600003)
|
||||||
getEwmhWindowId.assert_not_called()
|
getEwmhWindowId.assert_not_called()
|
||||||
|
|
||||||
|
def test_xterm_match_refreshes_i3_context_before_reusing_cached_match(self) -> None:
|
||||||
|
manager = input_event_manager.InputEventManager()
|
||||||
|
staleSnapshot = compositor_state_types.DesktopContextSnapshot(
|
||||||
|
session_type="x11",
|
||||||
|
backend_name="i3-ipc",
|
||||||
|
focused_window_id=0x200000C,
|
||||||
|
focused_workspace_empty=False,
|
||||||
|
)
|
||||||
|
freshSnapshot = compositor_state_types.DesktopContextSnapshot(
|
||||||
|
session_type="x11",
|
||||||
|
backend_name="i3-ipc",
|
||||||
|
focused_window_id=0x2600003,
|
||||||
|
focused_workspace_empty=False,
|
||||||
|
)
|
||||||
|
adapter = mock.Mock()
|
||||||
|
|
||||||
|
def refresh_context(_reason: str) -> compositor_state_types.DesktopContextSnapshot:
|
||||||
|
input_event_manager.cthulhu_state.compositorSnapshot = freshSnapshot
|
||||||
|
return freshSnapshot
|
||||||
|
|
||||||
|
adapter.refresh_workspace_context.side_effect = refresh_context
|
||||||
|
input_event_manager.cthulhu_state.compositorSnapshot = staleSnapshot
|
||||||
|
manager._lastEwmhActiveWindowId = 0x200000C
|
||||||
|
manager._lastEwmhXtermMatch = True
|
||||||
|
|
||||||
|
manager.set_compositor_state_adapter(adapter)
|
||||||
|
with mock.patch.object(
|
||||||
|
manager,
|
||||||
|
"_x11_window_id_xterm_match",
|
||||||
|
return_value=False,
|
||||||
|
) as matcher:
|
||||||
|
result = manager._active_x11_window_xterm_match()
|
||||||
|
|
||||||
|
self.assertFalse(result)
|
||||||
|
adapter.refresh_workspace_context.assert_called_once_with("xterm-handoff-check")
|
||||||
|
matcher.assert_called_once_with(0x2600003)
|
||||||
|
|
||||||
def test_i3_exact_xid_initializes_x11_before_matching_xterm(self):
|
def test_i3_exact_xid_initializes_x11_before_matching_xterm(self):
|
||||||
manager = input_event_manager.InputEventManager()
|
manager = input_event_manager.InputEventManager()
|
||||||
snapshot = compositor_state_types.DesktopContextSnapshot(
|
snapshot = compositor_state_types.DesktopContextSnapshot(
|
||||||
@@ -696,6 +733,8 @@ class InputEventManagerX11FocusRegressionTests(unittest.TestCase):
|
|||||||
|
|
||||||
def test_confirmed_empty_i3_workspace_is_definitely_not_xterm(self):
|
def test_confirmed_empty_i3_workspace_is_definitely_not_xterm(self):
|
||||||
manager = input_event_manager.InputEventManager()
|
manager = input_event_manager.InputEventManager()
|
||||||
|
manager._lastEwmhActiveWindowId = 0x200000C
|
||||||
|
manager._lastEwmhXtermMatch = True
|
||||||
snapshot = compositor_state_types.DesktopContextSnapshot(
|
snapshot = compositor_state_types.DesktopContextSnapshot(
|
||||||
session_type="x11",
|
session_type="x11",
|
||||||
backend_name="i3-ipc",
|
backend_name="i3-ipc",
|
||||||
@@ -710,6 +749,8 @@ class InputEventManagerX11FocusRegressionTests(unittest.TestCase):
|
|||||||
result = manager._active_x11_window_xterm_match()
|
result = manager._active_x11_window_xterm_match()
|
||||||
|
|
||||||
self.assertFalse(result)
|
self.assertFalse(result)
|
||||||
|
self.assertIsNone(manager._lastEwmhActiveWindowId)
|
||||||
|
self.assertFalse(manager._lastEwmhXtermMatch)
|
||||||
getEwmhWindowId.assert_not_called()
|
getEwmhWindowId.assert_not_called()
|
||||||
matcher.assert_not_called()
|
matcher.assert_not_called()
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,18 @@ from cthulhu.wine_access_manager import PrefixProcess, WineAccessManager
|
|||||||
|
|
||||||
|
|
||||||
class WineAccessManagerTest(unittest.TestCase):
|
class WineAccessManagerTest(unittest.TestCase):
|
||||||
|
@mock.patch.object(WineAccessManager, "_find_helper", return_value=None)
|
||||||
|
@mock.patch.object(WineAccessManager, "discover_prefixes")
|
||||||
|
def test_poll_without_installed_helper_is_a_no_op(
|
||||||
|
self, discover_prefixes, _find_helper
|
||||||
|
):
|
||||||
|
manager = WineAccessManager(helperPath=None)
|
||||||
|
|
||||||
|
self.assertIsNone(manager.helperPath)
|
||||||
|
self.assertTrue(manager.poll())
|
||||||
|
discover_prefixes.assert_not_called()
|
||||||
|
self.assertEqual(manager.prefixes, {})
|
||||||
|
|
||||||
def test_prefix_prefers_explicit_wineprefix(self):
|
def test_prefix_prefers_explicit_wineprefix(self):
|
||||||
environment = {
|
environment = {
|
||||||
"WINEPREFIX": "/tmp/custom-prefix",
|
"WINEPREFIX": "/tmp/custom-prefix",
|
||||||
|
|||||||
Reference in New Issue
Block a user