Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 191317eb1f | |||
| fcd10069ae | |||
| ed80fb0ea7 | |||
| ddea36c211 |
+5
-3
@@ -74,9 +74,11 @@ debug.log
|
||||
|
||||
# Package artifacts
|
||||
*.pkg.tar.zst
|
||||
distro-packages/*/cthulhu/
|
||||
distro-packages/*/cthulhu-git/
|
||||
distro-packages/*/pkg/
|
||||
distro-packages/**/.SRCINFO
|
||||
distro-packages/**/*.log
|
||||
distro-packages/*/*/cthulhu/
|
||||
distro-packages/*/*/cthulhu-git/
|
||||
distro-packages/*/*/pkg/
|
||||
|
||||
# Generated makefiles (should not be committed)
|
||||
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.
|
||||
- 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
|
||||
- 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
|
||||
|
||||
pkgbase=cthulhu-git
|
||||
pkgname=(cthulhu-git cthulhu-wine-access-git)
|
||||
pkgname=cthulhu-git
|
||||
_pkgname=cthulhu
|
||||
pkgver=2026.05.25.r423.g0576b6f
|
||||
pkgrel=2
|
||||
pkgver=2026.07.18.r465.g050ae25
|
||||
pkgrel=1
|
||||
pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca"
|
||||
url="https://git.stormux.org/storm/cthulhu"
|
||||
arch=(x86_64 aarch64 armv7h)
|
||||
@@ -65,18 +65,14 @@ optdepends=(
|
||||
'python-pillow: Image processing for OCR'
|
||||
'python-pytesseract: Python wrapper for Tesseract OCR engine'
|
||||
'tesseract: OCR engine for text recognition'
|
||||
)
|
||||
optdepends_x86_64=(
|
||||
|
||||
# The helper is x86_64-only, but this package is architecture-independent.
|
||||
'cthulhu-wine-access-git: Wine, Proton, NVDA Controller, and Tolk integration'
|
||||
)
|
||||
makedepends=(
|
||||
git
|
||||
meson
|
||||
)
|
||||
makedepends_x86_64=(
|
||||
cmake
|
||||
wine
|
||||
)
|
||||
source=("${_pkgname}::git+https://git.stormux.org/storm/${_pkgname}.git#branch=master")
|
||||
sha512sums=('SKIP')
|
||||
|
||||
@@ -96,35 +92,19 @@ pkgver() {
|
||||
|
||||
build() {
|
||||
cd "${_pkgname}"
|
||||
arch-meson _build
|
||||
arch-meson _build -Dwine-access=disabled
|
||||
meson compile -C _build
|
||||
}
|
||||
|
||||
package_cthulhu-git() {
|
||||
package() {
|
||||
arch=(any)
|
||||
provides=("cthulhu=${pkgver}")
|
||||
conflicts=(cthulhu)
|
||||
cd "${_pkgname}"
|
||||
meson install -C _build --destdir "$pkgdir"
|
||||
|
||||
rm -rf "$pkgdir/usr/libexec/cthulhu/wine"
|
||||
|
||||
# Remove icon cache - it will be generated by post-install hooks
|
||||
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:
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
|
||||
# shellcheck shell=bash disable=SC2034,SC2154,SC2164
|
||||
|
||||
pkgname=cthulhu-wine-access-git
|
||||
_pkgname=cthulhu
|
||||
pkgver=2026.07.18.r465.g050ae25
|
||||
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-git=${pkgver}"
|
||||
glib2
|
||||
wine
|
||||
)
|
||||
makedepends=(
|
||||
cmake
|
||||
git
|
||||
meson
|
||||
)
|
||||
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() {
|
||||
cd "${_pkgname}"
|
||||
arch-meson _build -Dwine-access=enabled
|
||||
meson compile -C _build
|
||||
}
|
||||
|
||||
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:
|
||||
@@ -2,8 +2,8 @@
|
||||
# shellcheck shell=bash disable=SC2034,SC2154,SC2164
|
||||
|
||||
pkgbase=cthulhu
|
||||
pkgname=(cthulhu cthulhu-wine-access)
|
||||
pkgver=2026.07.18
|
||||
pkgname=cthulhu
|
||||
pkgver=2026.05.25
|
||||
pkgrel=1
|
||||
pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca"
|
||||
url="https://git.stormux.org/storm/cthulhu"
|
||||
@@ -63,17 +63,10 @@ optdepends=(
|
||||
'python-pytesseract: Python wrapper for Tesseract OCR engine'
|
||||
'tesseract: OCR engine for text recognition'
|
||||
)
|
||||
optdepends_x86_64=(
|
||||
'cthulhu-wine-access: Wine, Proton, NVDA Controller, and Tolk integration'
|
||||
)
|
||||
makedepends=(
|
||||
git
|
||||
meson
|
||||
)
|
||||
makedepends_x86_64=(
|
||||
cmake
|
||||
wine
|
||||
)
|
||||
source=("git+https://git.stormux.org/storm/cthulhu.git#tag=${pkgver}")
|
||||
sha512sums=('SKIP')
|
||||
|
||||
@@ -83,31 +76,15 @@ build() {
|
||||
meson compile -C _build
|
||||
}
|
||||
|
||||
package_cthulhu() {
|
||||
package() {
|
||||
arch=(any)
|
||||
provides=("cthulhu=${pkgver}")
|
||||
conflicts=(cthulhu)
|
||||
cd cthulhu
|
||||
meson install -C _build --destdir "$pkgdir"
|
||||
|
||||
rm -rf "$pkgdir/usr/libexec/cthulhu/wine"
|
||||
|
||||
# Remove icon cache - it will be generated by post-install hooks
|
||||
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:
|
||||
|
||||
@@ -63,6 +63,15 @@ class CompositorStateAdapter:
|
||||
def get_snapshot(self) -> DesktopContextSnapshot:
|
||||
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:
|
||||
return get_session_type()
|
||||
|
||||
|
||||
@@ -92,6 +92,11 @@ class I3WorkspaceBackend:
|
||||
self._emitSignal = 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:
|
||||
if self._connection is not None:
|
||||
return True
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
# Forked from Orca screen reader.
|
||||
# Cthulhu project: https://git.stormux.org/storm/cthulhu
|
||||
|
||||
version = "2026.07.30"
|
||||
codeName = "master"
|
||||
version = "2026.07.19"
|
||||
codeName = "wine-access"
|
||||
|
||||
@@ -115,6 +115,7 @@ class EventManager:
|
||||
self._churnSuppressed: bool = cthulhu_state.pauseAtspiChurn
|
||||
self._prioritizedContextToken: Optional[str] = cthulhu_state.prioritizedDesktopContextToken
|
||||
self._desktopContextConfirmedEmpty: bool = False
|
||||
self._flushingStaleAtspiEvents: bool = False
|
||||
self._relevanceBurstWindow: float = 0.15
|
||||
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)
|
||||
self._inputEventManager = input_event_manager.get_manager()
|
||||
self._inputEventManager.set_compositor_state_adapter(self._compositorStateAdapter)
|
||||
self._inputEventManager.start_key_watcher()
|
||||
cthulhu_state.device = self._inputEventManager._device
|
||||
self._keyHandlingActive = True
|
||||
@@ -276,6 +278,8 @@ class EventManager:
|
||||
self._compositorStateAdapter.remove_listener(self._handle_compositor_signal)
|
||||
|
||||
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"):
|
||||
adapter.add_listener(self._handle_compositor_signal)
|
||||
|
||||
@@ -924,12 +928,13 @@ class EventManager:
|
||||
|
||||
return False
|
||||
|
||||
def _addToQueue(self, event: Any, asyncMode: bool) -> None:
|
||||
def _addToQueue(self, event: Any, asyncMode: bool) -> bool:
|
||||
debugging = debug.debugEventQueue
|
||||
if debugging:
|
||||
debug.printMessage(debug.LEVEL_ALL, " acquiring lock...")
|
||||
self._gidleLock.acquire()
|
||||
|
||||
with self._gidleLock:
|
||||
effectiveAsyncMode = asyncMode or self._flushingStaleAtspiEvents
|
||||
if debugging:
|
||||
debug.printMessage(debug.LEVEL_ALL, " ...acquired")
|
||||
debug.printMessage(debug.LEVEL_ALL, " calling queue.put...")
|
||||
@@ -940,16 +945,15 @@ class EventManager:
|
||||
if debugging:
|
||||
debug.printMessage(debug.LEVEL_ALL, " ...put complete")
|
||||
|
||||
if asyncMode and not self._gidleId:
|
||||
if effectiveAsyncMode and not self._gidleId:
|
||||
if self._gilSleepTime:
|
||||
time.sleep(self._gilSleepTime)
|
||||
self._gidleId = GLib.idle_add(self._dequeue)
|
||||
|
||||
if debugging:
|
||||
debug.printMessage(debug.LEVEL_ALL, " releasing lock...")
|
||||
self._gidleLock.release()
|
||||
if debug.debugEventQueue:
|
||||
debug.printMessage(debug.LEVEL_ALL, " releasing lock...")
|
||||
debug.printMessage(debug.LEVEL_ALL, " ...released")
|
||||
return effectiveAsyncMode
|
||||
|
||||
def _queuePrintln(self, e: Any, isEnqueue: bool = True, isPrune: Optional[bool] = None) -> None:
|
||||
"""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.eventCache[e.type] = (e, time.time())
|
||||
|
||||
self._addToQueue(e, asyncMode)
|
||||
asyncMode = self._addToQueue(e, asyncMode)
|
||||
if not asyncMode:
|
||||
self._dequeue()
|
||||
|
||||
@@ -1727,26 +1731,58 @@ class EventManager:
|
||||
def _flush_stale_atspi_events(self) -> None:
|
||||
"""Drops queued events that no longer match the compositor context."""
|
||||
|
||||
self._gidleLock.acquire()
|
||||
try:
|
||||
with self._gidleLock:
|
||||
self._flushingStaleAtspiEvents = True
|
||||
originalQueue = self._eventQueue
|
||||
newQueue: queue.Queue[Any] = queue.Queue(0)
|
||||
self._eventQueue = queue.Queue(0)
|
||||
|
||||
retainedEvents: list[Any] = []
|
||||
try:
|
||||
while not originalQueue.empty():
|
||||
try:
|
||||
event = originalQueue.get_nowait()
|
||||
except queue.Empty:
|
||||
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
|
||||
|
||||
newQueue.put(event)
|
||||
retainedEvents.append(event)
|
||||
finally:
|
||||
while not originalQueue.empty():
|
||||
try:
|
||||
retainedEvents.append(originalQueue.get_nowait())
|
||||
except queue.Empty:
|
||||
break
|
||||
|
||||
self._eventQueue = newQueue
|
||||
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)
|
||||
finally:
|
||||
self._gidleLock.release()
|
||||
|
||||
def _inFlood(self) -> bool:
|
||||
size = self._eventQueue.qsize()
|
||||
|
||||
@@ -134,6 +134,12 @@ class InputEventManager:
|
||||
self._xtermRecoverySourceId: int = 0
|
||||
self._xtermRecoveryPollCount: int = 0
|
||||
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:
|
||||
"""Creates and returns the AT-SPI device used by this manager."""
|
||||
@@ -847,9 +853,17 @@ class InputEventManager:
|
||||
|
||||
snapshot = cthulhu_state.compositorSnapshot
|
||||
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:
|
||||
self._record_xterm_handoff_action("i3:confirmed-empty-workspace")
|
||||
self._lastEwmhReadWindowId = None
|
||||
self._lastEwmhActiveWindowId = None
|
||||
self._lastEwmhXtermMatch = False
|
||||
return False
|
||||
|
||||
if usingI3Context and snapshot.focused_window_id is not None:
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class ArchPkgbuildOptionalWineTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
repository = Path(__file__).resolve().parents[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-git",)
|
||||
]
|
||||
|
||||
def test_main_packages_disable_wine_helper_build(self):
|
||||
stableContents = self.pkgbuilds[0].read_text(encoding="utf-8")
|
||||
self.assertNotIn("wine-access", stableContents)
|
||||
|
||||
gitContents = self.pkgbuilds[1].read_text(encoding="utf-8")
|
||||
self.assertIn("-Dwine-access=disabled", gitContents)
|
||||
|
||||
def test_main_packages_do_not_depend_on_wine(self):
|
||||
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_matching_optional_helper(self):
|
||||
stableContents = self.pkgbuilds[0].read_text(encoding="utf-8")
|
||||
self.assertNotIn("cthulhu-wine-access", stableContents)
|
||||
|
||||
gitContents = self.pkgbuilds[1].read_text(encoding="utf-8")
|
||||
self.assertIn(
|
||||
"'cthulhu-wine-access-git: Wine, Proton, NVDA Controller, and Tolk integration'",
|
||||
gitContents,
|
||||
)
|
||||
self.assertNotIn("optdepends_x86_64", gitContents)
|
||||
|
||||
def test_stable_packages_use_current_release_version(self):
|
||||
contents = self.pkgbuilds[0].read_text(encoding="utf-8")
|
||||
self.assertRegex(contents, r"(?m)^pkgver=2026\.05\.25$")
|
||||
|
||||
def test_optional_helper_packages_own_the_wine_dependency(self):
|
||||
for pkgbuild in self.helperPkgbuilds:
|
||||
with self.subTest(pkgbuild=pkgbuild):
|
||||
contents = pkgbuild.read_text(encoding="utf-8")
|
||||
self.assertIn("-Dwine-access=enabled", contents)
|
||||
self.assertRegex(contents, r"(?m)^ wine$")
|
||||
|
||||
def test_optional_helper_packages_disable_lto(self):
|
||||
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):
|
||||
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.activate_calls = []
|
||||
self.deactivate_calls = []
|
||||
self.refresh_calls = []
|
||||
|
||||
def is_available(self, session_type: str | None = None) -> bool:
|
||||
return self.available
|
||||
@@ -52,6 +53,9 @@ class FakeWorkspaceBackend:
|
||||
def deactivate(self, emit_signal=None) -> None:
|
||||
self.deactivate_calls.append(emit_signal)
|
||||
|
||||
def refresh_context(self, reason: str) -> None:
|
||||
self.refresh_calls.append(reason)
|
||||
|
||||
|
||||
class CompositorStateAdapterRegressionTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
@@ -111,6 +115,16 @@ class CompositorStateAdapterRegressionTests(unittest.TestCase):
|
||||
self.assertEqual(snapshot.focused_window_title, "9 devel")
|
||||
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:
|
||||
backend = FakeWorkspaceBackend(True, "i3-ipc")
|
||||
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):
|
||||
backend = compositor_state_i3.I3WorkspaceBackend(i3ipc_module=types.SimpleNamespace())
|
||||
backend._active = True
|
||||
|
||||
@@ -64,6 +64,16 @@ class EventManagerCompositorContextRegressionTests(unittest.TestCase):
|
||||
|
||||
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:
|
||||
snapshot = compositor_state_types.DesktopContextSnapshot(session_type="wayland")
|
||||
|
||||
@@ -186,6 +196,64 @@ class EventManagerCompositorContextRegressionTests(unittest.TestCase):
|
||||
|
||||
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:
|
||||
script = mock.Mock()
|
||||
script.isActivatableEvent.return_value = True
|
||||
|
||||
@@ -658,6 +658,43 @@ class InputEventManagerX11FocusRegressionTests(unittest.TestCase):
|
||||
matcher.assert_called_once_with(0x2600003)
|
||||
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):
|
||||
manager = input_event_manager.InputEventManager()
|
||||
snapshot = compositor_state_types.DesktopContextSnapshot(
|
||||
@@ -696,6 +733,8 @@ class InputEventManagerX11FocusRegressionTests(unittest.TestCase):
|
||||
|
||||
def test_confirmed_empty_i3_workspace_is_definitely_not_xterm(self):
|
||||
manager = input_event_manager.InputEventManager()
|
||||
manager._lastEwmhActiveWindowId = 0x200000C
|
||||
manager._lastEwmhXtermMatch = True
|
||||
snapshot = compositor_state_types.DesktopContextSnapshot(
|
||||
session_type="x11",
|
||||
backend_name="i3-ipc",
|
||||
@@ -710,6 +749,8 @@ class InputEventManagerX11FocusRegressionTests(unittest.TestCase):
|
||||
result = manager._active_x11_window_xterm_match()
|
||||
|
||||
self.assertFalse(result)
|
||||
self.assertIsNone(manager._lastEwmhActiveWindowId)
|
||||
self.assertFalse(manager._lastEwmhXtermMatch)
|
||||
getEwmhWindowId.assert_not_called()
|
||||
matcher.assert_not_called()
|
||||
|
||||
|
||||
@@ -5,6 +5,18 @@ from cthulhu.wine_access_manager import PrefixProcess, WineAccessManager
|
||||
|
||||
|
||||
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):
|
||||
environment = {
|
||||
"WINEPREFIX": "/tmp/custom-prefix",
|
||||
|
||||
Reference in New Issue
Block a user