1 Commits
Author SHA1 Message Date
Storm Dragon ae255f27cb Remove python-atspi dependency 2026-08-12 19:17:50 -04:00
14 changed files with 221 additions and 179 deletions
+1 -2
View File
@@ -73,13 +73,12 @@ debug.log
*.pot *.pot
# Package artifacts # Package artifacts
*.pkg.tar.* *.pkg.tar.zst
distro-packages/**/.SRCINFO distro-packages/**/.SRCINFO
distro-packages/**/*.log distro-packages/**/*.log
distro-packages/*/*/cthulhu/ distro-packages/*/*/cthulhu/
distro-packages/*/*/cthulhu-git/ distro-packages/*/*/cthulhu-git/
distro-packages/*/*/pkg/ distro-packages/*/*/pkg/
distro-packages/*/*/src/
# Generated makefiles (should not be committed) # Generated makefiles (should not be committed)
Makefile Makefile
@@ -65,8 +65,7 @@ optdepends=(
'python-pillow: Image processing for OCR' 'python-pillow: Image processing for OCR'
'python-pytesseract: Python wrapper for Tesseract OCR engine' 'python-pytesseract: Python wrapper for Tesseract OCR engine'
'tesseract: OCR engine for text recognition' 'tesseract: OCR engine for text recognition'
)
optdepends_x86_64=(
# The helper is x86_64-only, but this package is architecture-independent. # The helper is x86_64-only, but this package is architecture-independent.
'cthulhu-wine-access-git: Wine, Proton, NVDA Controller, and Tolk integration' 'cthulhu-wine-access-git: Wine, Proton, NVDA Controller, and Tolk integration'
) )
@@ -1,39 +0,0 @@
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
# shellcheck shell=bash disable=SC2034,SC2154,SC2164
pkgname=cthulhu-wine-access
pkgver=2026.08.06
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=(
"cthulhu=${pkgver}"
glib2
wine
)
makedepends=(
cmake
git
meson
)
source=("git+https://git.stormux.org/storm/cthulhu.git#tag=${pkgver}")
sha512sums=('SKIP')
options=(!lto)
build() {
cd cthulhu
arch-meson _build -Dwine-access=enabled
meson compile -C _build
}
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 -5
View File
@@ -3,7 +3,7 @@
pkgbase=cthulhu pkgbase=cthulhu
pkgname=cthulhu pkgname=cthulhu
pkgver=2026.08.06 pkgver=2026.05.25
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"
@@ -63,9 +63,6 @@ optdepends=(
'python-pytesseract: Python wrapper for Tesseract OCR engine' 'python-pytesseract: Python wrapper for Tesseract OCR engine'
'tesseract: OCR engine for text recognition' 'tesseract: OCR engine for text recognition'
) )
optdepends_x86_64=(
'cthulhu-wine-access: Wine, Proton, NVDA Controller, and Tolk integration'
)
makedepends=( makedepends=(
git git
meson meson
@@ -75,7 +72,7 @@ sha512sums=('SKIP')
build() { build() {
cd cthulhu cd cthulhu
arch-meson _build -Dwine-access=disabled arch-meson _build
meson compile -C _build meson compile -C _build
} }
+1 -1
View File
@@ -6,7 +6,7 @@
cd "$(dirname "$0")" ; CWD=$(pwd) cd "$(dirname "$0")" ; CWD=$(pwd)
PRGNAM=cthulhu PRGNAM=cthulhu
VERSION=${VERSION:-2026.08.06} VERSION=${VERSION:-2026.05.25}
BUILD=${BUILD:-1} BUILD=${BUILD:-1}
TAG=storm TAG=storm
PKGTYPE=txz PKGTYPE=txz
-1
View File
@@ -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
+2 -2
View File
@@ -1,10 +1,10 @@
PRGNAM="cthulhu" PRGNAM="cthulhu"
VERSION="2026.08.06" VERSION="2026.05.25"
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"
+1 -1
View File
@@ -6,7 +6,7 @@
cd "$(dirname "$0")" ; CWD=$(pwd) cd "$(dirname "$0")" ; CWD=$(pwd)
PRGNAM=cthulhu PRGNAM=cthulhu
VERSION=${VERSION:-2026.08.06} VERSION=${VERSION:-2026.05.25}
BUILD=${BUILD:-1} BUILD=${BUILD:-1}
TAG=storm TAG=storm
PKGTYPE=txz PKGTYPE=txz
+1 -1
View File
@@ -1,5 +1,5 @@
project('cthulhu', project('cthulhu',
version: '2026.08.06', version: '2026.07.18',
meson_version: '>= 1.0.0', meson_version: '>= 1.0.0',
) )
+1 -13
View File
@@ -59,12 +59,11 @@ 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" "$winePkgbuildFile" "$slackwareBuildFile" "$slintBuildFile" "$slintInfoFile"; do for path in "$cthulhuVersionFile" "$mesonFile" "$pkgbuildFile" "$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
@@ -76,8 +75,6 @@ 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"
@@ -102,14 +99,6 @@ 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
@@ -127,7 +116,6 @@ echo "Updated version to ${fullVersion} in:" \
"${cthulhuVersionFile}" \ "${cthulhuVersionFile}" \
"${mesonFile}" \ "${mesonFile}" \
"${pkgbuildFile}" \ "${pkgbuildFile}" \
"${winePkgbuildFile}" \
"${slackwareBuildFile}" \ "${slackwareBuildFile}" \
"${slintBuildFile}" \ "${slintBuildFile}" \
"${slintInfoFile}" "${slintInfoFile}"
+2 -2
View File
@@ -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.08.06" version = "2026.07.19"
codeName = "master" codeName = "wine-access"
+101 -21
View File
@@ -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
@@ -179,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)
@@ -201,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:
@@ -1284,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
+18 -76
View File
@@ -1,40 +1,28 @@
import re
import unittest import unittest
from pathlib import Path from pathlib import Path
class ArchPkgbuildOptionalWineTests(unittest.TestCase): class ArchPkgbuildOptionalWineTests(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls) -> None: def setUpClass(cls):
repository = Path(__file__).resolve().parents[1] 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 = [ cls.pkgbuilds = [
repository / "distro-packages" / "Arch-Linux" / name / "PKGBUILD" repository / "distro-packages" / "Arch-Linux" / name / "PKGBUILD"
for name in ("cthulhu", "cthulhu-git") for name in ("cthulhu", "cthulhu-git")
] ]
cls.helperPkgbuilds = [ cls.helperPkgbuilds = [
repository / "distro-packages" / "Arch-Linux" / name / "PKGBUILD" repository / "distro-packages" / "Arch-Linux" / name / "PKGBUILD"
for name in ("cthulhu-wine-access", "cthulhu-wine-access-git") for name in ("cthulhu-wine-access-git",)
] ]
@staticmethod def test_main_packages_disable_wine_helper_build(self):
def _array_contents(contents: str, arrayName: str) -> str: stableContents = self.pkgbuilds[0].read_text(encoding="utf-8")
return contents.split(f"{arrayName}=(", 1)[1].split(")", 1)[0] self.assertNotIn("wine-access", stableContents)
def test_main_packages_disable_wine_helper_build(self) -> None: gitContents = self.pkgbuilds[1].read_text(encoding="utf-8")
for pkgbuild in self.pkgbuilds: self.assertIn("-Dwine-access=disabled", gitContents)
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: def test_main_packages_do_not_depend_on_wine(self):
for pkgbuild in self.pkgbuilds: for pkgbuild in self.pkgbuilds:
with self.subTest(pkgbuild=pkgbuild): with self.subTest(pkgbuild=pkgbuild):
contents = pkgbuild.read_text(encoding="utf-8") contents = pkgbuild.read_text(encoding="utf-8")
@@ -46,81 +34,35 @@ class ArchPkgbuildOptionalWineTests(unittest.TestCase):
} }
self.assertNotIn("wine", dependencyLines) self.assertNotIn("wine", dependencyLines)
def test_main_packages_advertise_matching_optional_helper(self) -> None: def test_main_packages_advertise_matching_optional_helper(self):
stableContents = self.pkgbuilds[0].read_text(encoding="utf-8") stableContents = self.pkgbuilds[0].read_text(encoding="utf-8")
self.assertIn( self.assertNotIn("cthulhu-wine-access", stableContents)
"'cthulhu-wine-access: Wine, Proton, NVDA Controller, and Tolk integration'",
self._array_contents(stableContents, "optdepends_x86_64"),
)
self.assertNotIn(
"cthulhu-wine-access",
self._array_contents(stableContents, "optdepends"),
)
gitContents = self.pkgbuilds[1].read_text(encoding="utf-8") gitContents = self.pkgbuilds[1].read_text(encoding="utf-8")
self.assertIn( self.assertIn(
"'cthulhu-wine-access-git: Wine, Proton, NVDA Controller, and Tolk integration'", "'cthulhu-wine-access-git: Wine, Proton, NVDA Controller, and Tolk integration'",
self._array_contents(gitContents, "optdepends_x86_64"), gitContents,
)
self.assertNotIn(
"cthulhu-wine-access-git",
self._array_contents(gitContents, "optdepends"),
) )
self.assertNotIn("optdepends_x86_64", gitContents)
def test_stable_packages_use_current_release_version(self) -> None: def test_stable_packages_use_current_release_version(self):
for pkgbuild in (self.pkgbuilds[0], self.helperPkgbuilds[0]): contents = self.pkgbuilds[0].read_text(encoding="utf-8")
with self.subTest(pkgbuild=pkgbuild): self.assertRegex(contents, r"(?m)^pkgver=2026\.05\.25$")
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: def test_optional_helper_packages_own_the_wine_dependency(self):
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\)$")
self.assertRegex(contents, r'(?m)^ "cthulhu=\$\{pkgver\}"$')
def test_optional_helper_packages_own_the_wine_dependency(self) -> None:
for pkgbuild in self.helperPkgbuilds: for pkgbuild in self.helperPkgbuilds:
with self.subTest(pkgbuild=pkgbuild): with self.subTest(pkgbuild=pkgbuild):
contents = pkgbuild.read_text(encoding="utf-8") contents = pkgbuild.read_text(encoding="utf-8")
self.assertIn("-Dwine-access=enabled", contents) self.assertIn("-Dwine-access=enabled", contents)
self.assertRegex(contents, r"(?m)^ wine$") self.assertRegex(contents, r"(?m)^ wine$")
def test_optional_helper_packages_disable_lto(self) -> None: def test_optional_helper_packages_disable_lto(self):
for pkgbuild in self.helperPkgbuilds: for pkgbuild in self.helperPkgbuilds:
with self.subTest(pkgbuild=pkgbuild): with self.subTest(pkgbuild=pkgbuild):
contents = pkgbuild.read_text(encoding="utf-8") contents = pkgbuild.read_text(encoding="utf-8")
self.assertRegex(contents, r"(?m)^options=\(!lto\)$") self.assertRegex(contents, r"(?m)^options=\(!lto\)$")
def test_git_sources_declare_git_as_a_build_dependency(self) -> None: def test_git_sources_declare_git_as_a_build_dependency(self):
for pkgbuild in self.pkgbuilds + self.helperPkgbuilds: for pkgbuild in self.pkgbuilds + self.helperPkgbuilds:
with self.subTest(pkgbuild=pkgbuild): with self.subTest(pkgbuild=pkgbuild):
contents = pkgbuild.read_text(encoding="utf-8") contents = pkgbuild.read_text(encoding="utf-8")
@@ -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