Clean lint surface and drop PyWayland backend

Remove the direct PyWayland workspace backend and local protocol wrapper from the Orca 50 rebase branch.

Keep the null/external workspace signal path, clear E402/F401 for this branch, and remove the Arch python-pywayland dependency.
This commit is contained in:
2026-04-11 21:17:21 -04:00
parent 09c03ad06a
commit 642fe6da66
73 changed files with 114 additions and 787 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ depends=(
python-gobject python-gobject
python-cairo python-cairo
gtk3 gtk3
python-pywayland
# Audio and speech # Audio and speech
speech-dispatcher speech-dispatcher
gstreamer gstreamer
-1
View File
@@ -62,7 +62,6 @@ optional_modules = {
'speechd': 'speech output', 'speechd': 'speech output',
'dasbus': 'D-Bus remote controller', 'dasbus': 'D-Bus remote controller',
'psutil': 'system information commands', 'psutil': 'system information commands',
'pywayland': 'Wayland shared workspace backend',
'gi.repository.Wnck': 'mouse review', 'gi.repository.Wnck': 'mouse review',
'pdf2image': 'PDF processing for OCR', 'pdf2image': 'PDF processing for OCR',
'scipy': 'Scientific computing for OCR analysis', 'scipy': 'Scientific computing for OCR analysis',
+6 -5
View File
@@ -20,14 +20,15 @@ dependencies = [
"louis; extra == 'braille'" "louis; extra == 'braille'"
] ]
[project.optional-dependencies]
wayland = [
"pywayland"
]
[project.scripts] [project.scripts]
cthulhu = "cthulhu.cthulhu:main" cthulhu = "cthulhu.cthulhu:main"
[tool.ruff.lint.per-file-ignores]
"src/cthulhu.py" = ["E402"]
"src/cthulhu/*.py" = ["E402"]
"src/cthulhu/**/*.py" = ["E402"]
"tests/*.py" = ["E402"]
[tool.hatch.version] [tool.hatch.version]
path = "src/cthulhu/__init__.py" path = "src/cthulhu/__init__.py"
-1
View File
@@ -88,7 +88,6 @@ setup_paths()
from cthulhu import debug from cthulhu import debug
from cthulhu import messages from cthulhu import messages
from cthulhu import settings
from cthulhu.ax_object import AXObject from cthulhu.ax_object import AXObject
from cthulhu.ax_utilities import AXUtilities from cthulhu.ax_utilities import AXUtilities
from cthulhu.cthulhu_platform import version, revision from cthulhu.cthulhu_platform import version, revision
-1
View File
@@ -35,7 +35,6 @@ from . import cmdnames
from . import keybindings from . import keybindings
from . import input_event from . import input_event
from . import messages from . import messages
from . import settings_manager
from .ax_object import AXObject from .ax_object import AXObject
_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager _settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager
-2
View File
@@ -50,10 +50,8 @@ from gi.repository import GLib
from . import brltablenames from . import brltablenames
from . import cmdnames from . import cmdnames
from . import debug from . import debug
from . import logger
from . import cthulhu_state from . import cthulhu_state
from . import settings from . import settings
from . import settings_manager
from .ax_event_synthesizer import AXEventSynthesizer from .ax_event_synthesizer import AXEventSynthesizer
from .ax_object import AXObject from .ax_object import AXObject
-1
View File
@@ -46,7 +46,6 @@ from . import object_properties
from . import role_keys from . import role_keys
from . import cthulhu_state from . import cthulhu_state
from . import settings from . import settings
from . import settings_manager
from .ax_object import AXObject from .ax_object import AXObject
from .ax_text import AXText from .ax_text import AXText
from .ax_utilities import AXUtilities from .ax_utilities import AXUtilities
-1
View File
@@ -40,7 +40,6 @@ from . import keybindings
from . import messages from . import messages
from . import cthulhu_state from . import cthulhu_state
from . import settings from . import settings
from . import settings_manager
from .ax_object import AXObject from .ax_object import AXObject
from .ax_utilities import AXUtilities from .ax_utilities import AXUtilities
+18 -2
View File
@@ -29,10 +29,27 @@ from .compositor_state_types import (
RESUME_ATSPI_CHURN, RESUME_ATSPI_CHURN,
WORKSPACE_STATE_CHANGED, WORKSPACE_STATE_CHANGED,
) )
from .compositor_state_wayland import NullWorkspaceBackend, WaylandSharedProtocolsBackend
from .wnck_support import get_session_type from .wnck_support import get_session_type
class NullWorkspaceBackend:
"""No-op backend used when no real workspace backend is available."""
name = "null"
def __init__(self) -> None:
self._emitSignal = None
def is_available(self, session_type: str | None = None) -> bool:
return True
def activate(self, emit_signal: Any = None) -> None:
self._emitSignal = emit_signal
def deactivate(self, emit_signal: Any = None) -> None:
self._emitSignal = None
class CompositorStateAdapter: class CompositorStateAdapter:
"""Normalizes compositor state and desktop-focus context changes.""" """Normalizes compositor state and desktop-focus context changes."""
@@ -42,7 +59,6 @@ class CompositorStateAdapter:
) -> None: ) -> None:
if workspace_backends is None: if workspace_backends is None:
workspace_backends = [ workspace_backends = [
WaylandSharedProtocolsBackend(),
NullWorkspaceBackend(), NullWorkspaceBackend(),
] ]
self._workspaceBackends = list(workspace_backends) self._workspaceBackends = list(workspace_backends)
-365
View File
@@ -1,365 +0,0 @@
#!/usr/bin/env python3
#
# Copyright (c) 2026 Stormux
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
"""Wayland workspace backends for normalized compositor state tracking."""
from __future__ import annotations
import os
from collections.abc import Mapping, Sequence
from typing import Any
from gi.repository import GLib
from . import debug
from .compositor_state_types import (
DESKTOP_TRANSITION_FINISHED,
DESKTOP_TRANSITION_STARTED,
WORKSPACE_STATE_CHANGED,
)
from .wayland_protocols import ext_workspace_v1
from .wnck_support import get_session_type
class NullWorkspaceBackend:
"""No-op backend used when no real workspace backend is available."""
name = "null"
def __init__(self) -> None:
self._emitSignal = None
def is_available(self, session_type: str | None = None) -> bool:
return True
def activate(self, emit_signal: Any = None) -> None:
self._emitSignal = emit_signal
def deactivate(self, emit_signal: Any = None) -> None:
self._emitSignal = None
class WaylandSharedProtocolsBackend:
"""Runtime-optional backend for the ext-workspace shared Wayland protocol."""
name = "wayland-shared-protocols"
def __init__(
self,
*,
environment: Mapping[str, str] | None = None,
protocols: Any = None,
) -> None:
self._environment = environment if environment is not None else os.environ
self._protocols = protocols or ext_workspace_v1
self._emitSignal = None
self._display = None
self._registry = None
self._workspaceManager = None
self._workspaceStates: dict[Any, dict[str, Any]] = {}
self._batchDirty = False
self._transitionPending = False
self._dispatchSourceId = 0
def is_available(self, session_type: str | None = None) -> bool:
effectiveSessionType = (session_type or get_session_type()).strip().lower()
return (
effectiveSessionType == "wayland"
and bool((self._environment.get("WAYLAND_DISPLAY") or "").strip())
and bool(self._protocols.has_runtime_support())
)
def activate(self, emit_signal: Any = None) -> None:
self.deactivate()
self._emitSignal = emit_signal
if emit_signal is None or not self.is_available():
return
displayClass = self._protocols.get_display_class()
if displayClass is None:
return
try:
self._display = displayClass()
connect = getattr(self._display, "connect", None)
if callable(connect):
connect()
get_registry = getattr(self._display, "get_registry", None)
if not callable(get_registry):
raise RuntimeError("Wayland display missing registry access")
self._registry = get_registry()
self._bind_listener(self._registry, "global", self._handle_registry_global)
self._bind_listener(self._registry, "global_remove", self._handle_registry_global_remove)
self._roundtrip()
self._install_dispatch_watch()
except Exception as error:
msg = f"COMPOSITOR STATE: Wayland backend activation failed: {error}"
debug.printMessage(debug.LEVEL_WARNING, msg, True)
self.deactivate()
def deactivate(self, emit_signal: Any = None) -> None:
self._workspaceStates = {}
self._batchDirty = False
self._transitionPending = False
self._remove_dispatch_watch()
self._safe_close_proxy(self._workspaceManager)
self._safe_close_proxy(self._registry)
self._workspaceManager = None
self._registry = None
if self._display is not None:
disconnect = getattr(self._display, "disconnect", None)
if callable(disconnect):
try:
disconnect()
except Exception:
pass
self._display = None
self._emitSignal = None
def _bind_listener(self, target: Any, event_name: str, callback: Any) -> bool:
dispatcher = getattr(target, "dispatcher", None)
if dispatcher is not None:
try:
dispatcher[event_name] = callback
return True
except Exception:
pass
add_listener = getattr(target, "add_listener", None)
if callable(add_listener):
try:
add_listener(**{event_name: callback})
return True
except TypeError:
try:
add_listener(event_name, callback)
return True
except TypeError:
pass
attributeName = f"on_{event_name}"
if hasattr(target, attributeName):
setattr(target, attributeName, callback)
return True
return False
def _roundtrip(self) -> None:
if self._display is None:
return
for methodName in ("roundtrip", "dispatch", "dispatch_pending"):
method = getattr(self._display, methodName, None)
if callable(method):
method()
break
def _install_dispatch_watch(self) -> None:
if self._display is None or self._dispatchSourceId:
return
getFd = getattr(self._display, "get_fd", None)
if not callable(getFd):
return
try:
displayFd = int(getFd())
except Exception:
return
if displayFd < 0:
return
self._dispatchSourceId = GLib.io_add_watch(
displayFd,
GLib.PRIORITY_DEFAULT,
GLib.IO_IN | GLib.IO_ERR | GLib.IO_HUP,
self._dispatch_display_events,
)
def _remove_dispatch_watch(self) -> None:
if not self._dispatchSourceId:
return
try:
GLib.source_remove(self._dispatchSourceId)
except Exception:
pass
self._dispatchSourceId = 0
def _dispatch_display_events(self, _fd: int, condition: Any) -> bool:
if condition & (GLib.IO_ERR | GLib.IO_HUP):
self.deactivate()
return False
dispatch = getattr(self._display, "dispatch", None)
if not callable(dispatch):
self.deactivate()
return False
try:
while True:
dispatched = dispatch(block=False)
if not dispatched:
break
except TypeError:
try:
dispatch()
except Exception:
self.deactivate()
return False
except Exception:
self.deactivate()
return False
return True
def _safe_close_proxy(self, proxy: Any) -> None:
if proxy is None:
return
for methodName in ("destroy", "release"):
method = getattr(proxy, methodName, None)
if callable(method):
try:
method()
except Exception:
pass
return
def _handle_registry_global(self, *args: Any) -> None:
if self._workspaceManager is not None:
return
globalName, interfaceName, interfaceVersion = self._parse_registry_global_args(args)
if interfaceName != self._protocols.INTERFACE_NAME:
return
registry = self._registry
if registry is None:
return
bindVersion = min(interfaceVersion, self._protocols.INTERFACE_VERSION)
try:
self._workspaceManager = self._protocols.bind_workspace_manager(
registry,
globalName,
bindVersion,
)
except Exception:
self._workspaceManager = None
if self._workspaceManager is None:
return
self._bind_listener(self._workspaceManager, "workspace", self._handle_workspace_manager_workspace)
self._bind_listener(self._workspaceManager, "done", self._handle_workspace_manager_done)
self._bind_listener(self._workspaceManager, "finished", self._handle_workspace_manager_finished)
def _handle_registry_global_remove(self, *args: Any) -> None:
return
def _handle_workspace_manager_workspace(self, *args: Any) -> None:
workspaceHandle = args[-1] if args else None
if workspaceHandle is None or workspaceHandle in self._workspaceStates:
return
self._workspaceStates[workspaceHandle] = {"active": False}
self._bind_listener(workspaceHandle, "id", self._handle_workspace_id)
self._bind_listener(workspaceHandle, "state", self._handle_workspace_state)
self._bind_listener(workspaceHandle, "removed", self._handle_workspace_removed)
def _handle_workspace_id(self, workspaceHandle: Any, workspaceId: str, *_args: Any) -> None:
workspaceState = self._workspaceStates.setdefault(workspaceHandle, {"active": False})
workspaceState["workspace_id"] = (workspaceId or "").strip()
self._batchDirty = True
def _handle_workspace_state(self, workspaceHandle: Any, stateValue: Any, value: Any = None, *_args: Any) -> None:
workspaceState = self._workspaceStates.setdefault(workspaceHandle, {"active": False})
workspaceState["active"] = self._workspace_is_active(stateValue, value)
self._batchDirty = True
self._emit_transition_started("workspace-state-update")
def _handle_workspace_removed(self, workspaceHandle: Any, *_args: Any) -> None:
self._workspaceStates.pop(workspaceHandle, None)
self._batchDirty = True
self._emit_transition_started("workspace-removed")
def _handle_workspace_manager_done(self, *_args: Any) -> None:
if not self._batchDirty:
return
activeWorkspaceIds = self._active_workspace_ids()
self._emit_signal(WORKSPACE_STATE_CHANGED, activeWorkspaceIds, "workspace-batch-done")
self._emit_signal(DESKTOP_TRANSITION_FINISHED, activeWorkspaceIds, "workspace-batch-done")
self._batchDirty = False
self._transitionPending = False
def _handle_workspace_manager_finished(self, *_args: Any) -> None:
self._workspaceManager = None
def _workspace_is_active(self, stateValue: Any, value: Any = None) -> bool:
if isinstance(stateValue, str):
return stateValue.strip().lower() == "active" and bool(value)
if isinstance(stateValue, Mapping):
return bool(stateValue.get("active"))
if isinstance(stateValue, Sequence) and not isinstance(stateValue, (str, bytes, bytearray)):
return self._protocols.ACTIVE_STATE_VALUE in stateValue
return bool(stateValue)
def _active_workspace_ids(self) -> set[str]:
workspaceIds = set()
for workspaceHandle, workspaceState in self._workspaceStates.items():
if not workspaceState.get("active"):
continue
workspaceId = (workspaceState.get("workspace_id") or "").strip()
if not workspaceId:
workspaceId = self._workspace_id(workspaceHandle)
workspaceIds.add(workspaceId)
return workspaceIds
def _workspace_id(self, workspaceHandle: Any) -> str:
return f"workspace:{id(workspaceHandle)}"
def _emit_transition_started(self, reason: str) -> None:
if self._transitionPending:
return
self._transitionPending = True
self._emit_signal(DESKTOP_TRANSITION_STARTED, self._active_workspace_ids(), reason)
def _emit_signal(self, signalType: str, workspaceIds: set[str], reason: str) -> None:
if callable(self._emitSignal):
self._emitSignal(signalType, workspaceIds, reason)
return
def _parse_registry_global_args(self, args: Sequence[Any]) -> tuple[int, str, int]:
if len(args) >= 4:
_, globalName, interfaceName, interfaceVersion = args[-4:]
elif len(args) >= 3:
globalName, interfaceName, interfaceVersion = args[-3:]
else:
return (-1, "", 0)
try:
parsedName = int(globalName)
except (TypeError, ValueError):
parsedName = -1
try:
parsedVersion = int(interfaceVersion)
except (TypeError, ValueError):
parsedVersion = self._protocols.INTERFACE_VERSION
return parsedName, str(interfaceName or ""), parsedVersion
+1 -7
View File
@@ -36,26 +36,20 @@ __copyright__ = "Copyright (c) 2004-2009 Sun Microsystems Inc." \
__license__ = "LGPL" __license__ = "LGPL"
import faulthandler import faulthandler
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional
from . import dbus_service from . import dbus_service
if TYPE_CHECKING: if TYPE_CHECKING:
from types import FrameType from types import FrameType
from gi.repository.Gio import Settings as GSettings from gi.repository.Gio import Settings as GSettings
from gi.repository import Gtk
from .settings_manager import SettingsManager from .settings_manager import SettingsManager
from .script_manager import ScriptManager from .script_manager import ScriptManager
from .plugin_system_manager import PluginSystemManager from .plugin_system_manager import PluginSystemManager
from .input_event import InputEvent
from .input_event_manager import InputEventManager
from .event_manager import EventManager from .event_manager import EventManager
from .signal_manager import SignalManager from .signal_manager import SignalManager
from .dynamic_api_manager import DynamicApiManager from .dynamic_api_manager import DynamicApiManager
from .speech import Speech
from .braille import Braille
from .script import Script
class APIHelper: class APIHelper:
"""Helper class for plugin API interactions, including keybindings.""" """Helper class for plugin API interactions, including keybindings."""
+1 -2
View File
@@ -33,13 +33,12 @@ __copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." \
__license__ = "LGPL" __license__ = "LGPL"
import time import time
from typing import Optional, Dict, Callable, Any from typing import Optional, Dict, Any
from . import cthulhu # Need access to cthulhuApp from . import cthulhu # Need access to cthulhuApp
from . import cmdnames from . import cmdnames
from . import input_event from . import input_event
from . import keybindings from . import keybindings
from . import settings_manager
_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager _settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager
-2
View File
@@ -23,8 +23,6 @@
# 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
import gi
from gi.repository import GObject
from cthulhu import resource_manager from cthulhu import resource_manager
-1
View File
@@ -51,7 +51,6 @@ from . import messages
from . import object_properties from . import object_properties
from . import role_keys from . import role_keys
from . import settings from . import settings
from . import settings_manager
from .ax_object import AXObject from .ax_object import AXObject
from .ax_text import AXText from .ax_text import AXText
from .ax_utilities import AXUtilities from .ax_utilities import AXUtilities
-1
View File
@@ -11,7 +11,6 @@
from __future__ import annotations from __future__ import annotations
import os
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
+1 -2
View File
@@ -45,12 +45,11 @@ from typing import TYPE_CHECKING
from . import debug from . import debug
from . import settings from . import settings
from . import cthulhu_state
from .cthulhu_i18n import _ from .cthulhu_i18n import _
if TYPE_CHECKING: if TYPE_CHECKING:
from .input_event import KeyboardEvent, InputEventHandler pass
_keysymsCache = {} _keysymsCache = {}
_keycodeCache = {} _keycodeCache = {}
-1
View File
@@ -53,7 +53,6 @@ from . import keybindings
from . import messages from . import messages
from . import cthulhu_state from . import cthulhu_state
from . import settings from . import settings
from . import settings_manager
from .ax_object import AXObject from .ax_object import AXObject
-2
View File
@@ -25,7 +25,6 @@
import gi import gi
gi.require_version("Atspi", "2.0") gi.require_version("Atspi", "2.0")
from gi.repository import Atspi
import bisect import bisect
import copy import copy
@@ -40,7 +39,6 @@ from . import keybindings
from . import messages from . import messages
from . import input_event from . import input_event
from . import cthulhu_state from . import cthulhu_state
from . import settings_manager
from .ax_collection import AXCollection from .ax_collection import AXCollection
from .ax_object import AXObject from .ax_object import AXObject
from .ax_text import AXText from .ax_text import AXText
-2
View File
@@ -50,7 +50,6 @@ cthulhu_python_sources = files([
'command_manager.py', 'command_manager.py',
'compositor_state_adapter.py', 'compositor_state_adapter.py',
'compositor_state_types.py', 'compositor_state_types.py',
'compositor_state_wayland.py',
'common_keyboardmap.py', 'common_keyboardmap.py',
'cthulhuVersion.py', 'cthulhuVersion.py',
'cthulhu_modifier_manager.py', 'cthulhu_modifier_manager.py',
@@ -219,5 +218,4 @@ install_data(
# Subdirectories # Subdirectories
subdir('backends') subdir('backends')
subdir('scripts') subdir('scripts')
subdir('wayland_protocols')
subdir('plugins') subdir('plugins')
-2
View File
@@ -54,8 +54,6 @@ from . import input_event_manager
from . import messages from . import messages
from . import cthulhu from . import cthulhu
from . import cthulhu_state from . import cthulhu_state
from . import script_manager
from . import settings_manager
from .ax_component import AXComponent from .ax_component import AXComponent
from .ax_object import AXObject from .ax_object import AXObject
from .ax_text import AXText from .ax_text import AXText
-1
View File
@@ -8,7 +8,6 @@
"""Base class for Cthulhu plugins using pluggy.""" """Base class for Cthulhu plugins using pluggy."""
import os
import logging import logging
import pluggy import pluggy
+1 -1
View File
@@ -19,4 +19,4 @@
"""AI Assistant plugin package.""" """AI Assistant plugin package."""
from .plugin import AIAssistant from .plugin import AIAssistant as AIAssistant
@@ -238,8 +238,6 @@ class ClaudeCodeProvider(AIProvider):
def _call_claude_code(self, prompt, image_path=None): def _call_claude_code(self, prompt, image_path=None):
"""Call Claude Code CLI with the prompt and optional image.""" """Call Claude Code CLI with the prompt and optional image."""
import subprocess import subprocess
import tempfile
import os
try: try:
# Build the command # Build the command
+1 -4
View File
@@ -11,16 +11,14 @@
import logging import logging
import os import os
import json
import base64 import base64
from io import BytesIO
import gi import gi
gi.require_version('Gdk', '3.0') gi.require_version('Gdk', '3.0')
gi.require_version('GdkPixbuf', '2.0') gi.require_version('GdkPixbuf', '2.0')
gi.require_version('Atspi', '2.0') gi.require_version('Atspi', '2.0')
gi.require_version('Gtk', '3.0') gi.require_version('Gtk', '3.0')
from gi.repository import Gdk, GdkPixbuf, Atspi, Gtk from gi.repository import Gdk, Atspi, Gtk
from cthulhu.plugin import Plugin, cthulhu_hookimpl from cthulhu.plugin import Plugin, cthulhu_hookimpl
from cthulhu import settings from cthulhu import settings
@@ -29,7 +27,6 @@ from cthulhu import cthulhu_state
from cthulhu import ax_object from cthulhu import ax_object
from cthulhu.ax_text import AXText from cthulhu.ax_text import AXText
from cthulhu.ax_value import AXValue from cthulhu.ax_value import AXValue
from cthulhu import ax_utilities
from cthulhu.ax_utilities_state import AXUtilitiesState from cthulhu.ax_utilities_state import AXUtilitiesState
from cthulhu.plugins.AIAssistant.ai_providers import create_provider from cthulhu.plugins.AIAssistant.ai_providers import create_provider
-1
View File
@@ -10,7 +10,6 @@
"""Bye Cthulhu plugin for Cthulhu.""" """Bye Cthulhu plugin for Cthulhu."""
import logging import logging
import time
from cthulhu.plugin import Plugin, cthulhu_hookimpl from cthulhu.plugin import Plugin, cthulhu_hookimpl
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -21,7 +21,6 @@
"""Hello Cthulhu plugin for Cthulhu.""" """Hello Cthulhu plugin for Cthulhu."""
import logging import logging
import weakref
from cthulhu.plugin import Plugin, cthulhu_hookimpl from cthulhu.plugin import Plugin, cthulhu_hookimpl
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
+1 -1
View File
@@ -20,4 +20,4 @@
"""OCRDesktop plugin package.""" """OCRDesktop plugin package."""
from .plugin import OCRDesktop from .plugin import OCRDesktop as OCRDesktop
+11 -27
View File
@@ -10,15 +10,11 @@
"""OCRDesktop plugin for Cthulhu screen reader.""" """OCRDesktop plugin for Cthulhu screen reader."""
import importlib.util
import logging import logging
import os
import sys
import locale import locale
import time import time
import re import re
import tempfile
import threading
from mimetypes import MimeTypes
import gi import gi
gi.require_version('Atspi', '2.0') gi.require_version('Atspi', '2.0')
@@ -33,6 +29,11 @@ from cthulhu.wnck_support import load_wnck
# Note: Removed complex beep system - simple announcements work perfectly! # Note: Removed complex beep system - simple announcements work perfectly!
def _has_module(module_name: str) -> bool:
return importlib.util.find_spec(module_name) is not None
# PIL # PIL
try: try:
from PIL import Image from PIL import Image
@@ -42,34 +43,16 @@ except ImportError:
PIL_AVAILABLE = False PIL_AVAILABLE = False
# pytesseract # pytesseract
try: PYTESSERACT_AVAILABLE = _has_module("pytesseract")
import pytesseract
from pytesseract import Output
PYTESSERACT_AVAILABLE = True
except ImportError:
PYTESSERACT_AVAILABLE = False
# pdf2image # pdf2image
try: PDF2IMAGE_AVAILABLE = _has_module("pdf2image")
from pdf2image import convert_from_path
PDF2IMAGE_AVAILABLE = True
except ImportError:
PDF2IMAGE_AVAILABLE = False
# scipy # scipy
try: SCIPY_AVAILABLE = _has_module("scipy")
from scipy.spatial import KDTree
SCIPY_AVAILABLE = True
except ImportError:
SCIPY_AVAILABLE = False
# webcolors # webcolors
try: WEBCOLORS_AVAILABLE = _has_module("webcolors")
from webcolors import CSS3_HEX_TO_NAMES
from webcolors import hex_to_rgb
WEBCOLORS_AVAILABLE = True
except ImportError:
WEBCOLORS_AVAILABLE = False
# GTK/GDK # GTK/GDK
try: try:
@@ -664,6 +647,7 @@ class OCRDesktop(Plugin):
modifiedImg = self._transformImg(img) modifiedImg = self._transformImg(img)
try: try:
# Extract coordinate data using image_to_data # Extract coordinate data using image_to_data
import pytesseract
from pytesseract import Output from pytesseract import Output
OCRWords = pytesseract.image_to_data(modifiedImg, output_type=Output.DICT, OCRWords = pytesseract.image_to_data(modifiedImg, output_type=Output.DICT,
lang=self._languageCode, config='--psm 4') lang=self._languageCode, config='--psm 4')
@@ -12,7 +12,6 @@
import logging import logging
import os import os
import configparser import configparser
from pathlib import Path
import gi import gi
gi.require_version('Gtk', '3.0') gi.require_version('Gtk', '3.0')
-1
View File
@@ -28,7 +28,6 @@ to have their speech output go through Cthulhu for both speech and braille suppo
import logging import logging
import select import select
import socket import socket
import threading
from threading import Thread, Lock from threading import Thread, Lock
from cthulhu.plugin import Plugin, cthulhu_hookimpl from cthulhu.plugin import Plugin, cthulhu_hookimpl
from cthulhu import debug from cthulhu import debug
@@ -22,12 +22,9 @@
import glob import glob
import os import os
import importlib.util
import random import random
import string import string
import _thread
import logging import logging
from subprocess import Popen, PIPE
import gettext import gettext
from cthulhu.plugin import Plugin, cthulhu_hookimpl from cthulhu.plugin import Plugin, cthulhu_hookimpl
@@ -1,2 +1 @@
from .plugin import SpeechHistory from .plugin import SpeechHistory as SpeechHistory
-1
View File
@@ -24,7 +24,6 @@ import os
import socket import socket
import select import select
import logging import logging
import threading
from threading import Thread, Lock from threading import Thread, Lock
from cthulhu.plugin import Plugin, cthulhu_hookimpl from cthulhu.plugin import Plugin, cthulhu_hookimpl
+1 -2
View File
@@ -43,7 +43,7 @@ import math
import re import re
import time import time
from difflib import SequenceMatcher from difflib import SequenceMatcher
from typing import Any, Callable, Generator, Optional, TYPE_CHECKING from typing import Any, Callable, Optional, TYPE_CHECKING
gi.require_version("Atspi", "2.0") gi.require_version("Atspi", "2.0")
from gi.repository import Atspi from gi.repository import Atspi
@@ -63,7 +63,6 @@ from . import cthulhu_state
from . import object_properties from . import object_properties
from . import pronunciation_dict from . import pronunciation_dict
from . import settings from . import settings
from . import settings_manager
from . import text_attribute_names from . import text_attribute_names
from .ax_document_selection import AXDocumentSelection from .ax_document_selection import AXDocumentSelection
from .ax_object import AXObject from .ax_object import AXObject
+1 -1
View File
@@ -23,4 +23,4 @@
# 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
from .script import Script from .script import Script as Script
+1 -1
View File
@@ -23,4 +23,4 @@
# 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
from .script import Script from .script import Script as Script
+1 -1
View File
@@ -23,4 +23,4 @@
# 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
from .script import Script from .script import Script as Script
+1 -1
View File
@@ -23,4 +23,4 @@
# 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
from .script import Script from .script import Script as Script
@@ -23,4 +23,4 @@
# 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
from .script import Script from .script import Script as Script
@@ -34,7 +34,6 @@ __license__ = "LGPL"
import cthulhu.messages as messages import cthulhu.messages as messages
import cthulhu.scripts.default as default import cthulhu.scripts.default as default
import cthulhu.settings as settings import cthulhu.settings as settings
import cthulhu.settings_manager as settings_manager
from cthulhu.ax_object import AXObject from cthulhu.ax_object import AXObject
from cthulhu.ax_value import AXValue from cthulhu.ax_value import AXValue
+1 -1
View File
@@ -23,4 +23,4 @@
# 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
from .script import Script from .script import Script as Script
@@ -23,4 +23,4 @@
# 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
from .script import Script from .script import Script as Script
+1 -1
View File
@@ -23,4 +23,4 @@
# 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
from .script import Script from .script import Script as Script
@@ -38,7 +38,6 @@ from gi.repository import Atspi
import cthulhu.braille as braille import cthulhu.braille as braille
import cthulhu.braille_generator as braille_generator import cthulhu.braille_generator as braille_generator
import cthulhu.object_properties as object_properties import cthulhu.object_properties as object_properties
import cthulhu.settings_manager as settings_manager
from cthulhu.ax_object import AXObject from cthulhu.ax_object import AXObject
from cthulhu.ax_utilities import AXUtilities from cthulhu.ax_utilities import AXUtilities
@@ -40,7 +40,6 @@ import cthulhu.role_keys as role_keys
import cthulhu.settings_manager as settings_manager import cthulhu.settings_manager as settings_manager
import cthulhu.speech_generator as speech_generator import cthulhu.speech_generator as speech_generator
from cthulhu.ax_object import AXObject from cthulhu.ax_object import AXObject
from cthulhu.ax_text import AXText
from cthulhu.ax_utilities import AXUtilities from cthulhu.ax_utilities import AXUtilities
_settingsManager = settings_manager.getManager() _settingsManager = settings_manager.getManager()
@@ -1 +1 @@
from .script import Script from .script import Script as Script
+4 -5
View File
@@ -23,8 +23,7 @@
# 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
from .braille_generator import BrailleGenerator from .braille_generator import BrailleGenerator as BrailleGenerator
from .script import Script from .script import Script as Script
from .speech_generator import SpeechGenerator from .speech_generator import SpeechGenerator as SpeechGenerator
from .script_utilities import Utilities from .script_utilities import Utilities as Utilities
@@ -31,7 +31,6 @@ __license__ = "LGPL"
import gi import gi
gi.require_version("Atspi", "2.0") gi.require_version("Atspi", "2.0")
from gi.repository import Atspi
import re import re
@@ -40,7 +39,6 @@ from cthulhu import cthulhu
from cthulhu import keybindings from cthulhu import keybindings
from cthulhu import cthulhu_state from cthulhu import cthulhu_state
from cthulhu import script_utilities from cthulhu import script_utilities
from cthulhu import settings_manager
from cthulhu.ax_object import AXObject from cthulhu.ax_object import AXObject
from cthulhu.ax_text import AXText from cthulhu.ax_text import AXText
from cthulhu.ax_utilities import AXUtilities from cthulhu.ax_utilities import AXUtilities
@@ -23,4 +23,4 @@
# 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
from .script import Script from .script import Script as Script
@@ -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
from .script import Script from .script import Script as Script
from .script_utilities import Utilities from .script_utilities import Utilities as Utilities
@@ -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
from .script import Script from .script import Script as Script
from .speech_generator import SpeechGenerator from .speech_generator import SpeechGenerator as SpeechGenerator
+1 -1
View File
@@ -23,4 +23,4 @@
# 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
from .script import Script from .script import Script as Script
@@ -23,7 +23,7 @@
# 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
from .script import Script from .script import Script as Script
from .speech_generator import SpeechGenerator from .speech_generator import SpeechGenerator as SpeechGenerator
from .braille_generator import BrailleGenerator from .braille_generator import BrailleGenerator as BrailleGenerator
from .script_utilities import Utilities from .script_utilities import Utilities as Utilities
@@ -23,4 +23,4 @@
# 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
from .script import Script from .script import Script as Script
+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
from .script import Script from .script import Script as Script
from .script_utilities import Utilities from .script_utilities import Utilities as Utilities
@@ -46,8 +46,6 @@ from cthulhu import messages
from cthulhu import cthulhu from cthulhu import cthulhu
from cthulhu import cthulhu_state from cthulhu import cthulhu_state
from cthulhu import script_utilities from cthulhu import script_utilities
from cthulhu import script_manager
from cthulhu import settings_manager
from cthulhu.ax_collection import AXCollection from cthulhu.ax_collection import AXCollection
from cthulhu.ax_component import AXComponent from cthulhu.ax_component import AXComponent
from cthulhu.ax_document import AXDocument from cthulhu.ax_document import AXDocument
@@ -36,7 +36,6 @@ gi.require_version("Atspi", "2.0")
from gi.repository import Atspi from gi.repository import Atspi
from cthulhu import cthulhu from cthulhu import cthulhu
from cthulhu import settings_manager
from cthulhu import sound_generator from cthulhu import sound_generator
_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager _settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager
@@ -44,8 +44,6 @@ from cthulhu import object_properties
from cthulhu import role_keys from cthulhu import role_keys
from cthulhu import cthulhu_state from cthulhu import cthulhu_state
from cthulhu import settings from cthulhu import settings
from cthulhu import settings_manager
from cthulhu import sound_theme_manager
from cthulhu import speech_generator from cthulhu import speech_generator
from cthulhu.ax_object import AXObject from cthulhu.ax_object import AXObject
from cthulhu.ax_text import AXText from cthulhu.ax_text import AXText
-1
View File
@@ -23,7 +23,6 @@
# 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
import gi
from gi.repository import GObject from gi.repository import GObject
from typing import Optional, Any, Callable, Tuple from typing import Optional, Any, Callable, Tuple
-1
View File
@@ -39,7 +39,6 @@ import os
from . import generator from . import generator
from . import role_keys from . import role_keys
from . import settings_manager
from .ax_object import AXObject from .ax_object import AXObject
from .ax_utilities import AXUtilities from .ax_utilities import AXUtilities
from .sound import Icon, Tone from .sound import Icon, Tone
+1 -2
View File
@@ -36,10 +36,9 @@ __license__ = "LGPL"
import importlib import importlib
import time import time
from typing import TYPE_CHECKING, Optional, List, Dict, Any, Union, Callable from typing import TYPE_CHECKING, Optional, List, Any, Union, Callable
from . import debug from . import debug
from . import logger
from . import settings from . import settings
from . import speech_generator from . import speech_generator
from . import sound from . import sound
@@ -40,10 +40,8 @@ from . import input_event
from . import keybindings from . import keybindings
from . import messages from . import messages
from . import cthulhu_state from . import cthulhu_state
from . import script_manager
from . import speechserver from . import speechserver
from . import settings from . import settings
from . import settings_manager
from . import speech from . import speech
# Removed global _settings_manager # Removed global _settings_manager
-1
View File
@@ -52,7 +52,6 @@ from . import messages
from . import object_properties from . import object_properties
from . import role_keys from . import role_keys
from . import settings from . import settings
from . import settings_manager
from . import sound_theme_manager from . import sound_theme_manager
from . import speech from . import speech
from . import text_attribute_names from . import text_attribute_names
-1
View File
@@ -45,7 +45,6 @@ from . import speechserver
from . import settings from . import settings
from . import cthulhu_state from . import cthulhu_state
from . import punctuation_settings from . import punctuation_settings
from . import settings_manager
from .acss import ACSS from .acss import ACSS
_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager _settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager
-2
View File
@@ -33,7 +33,6 @@ __license__ = "LGPL"
import gi import gi
gi.require_version("Atspi", "2.0") gi.require_version("Atspi", "2.0")
from gi.repository import Atspi
import re import re
@@ -42,7 +41,6 @@ from cthulhu import guilabels
from cthulhu import messages from cthulhu import messages
from cthulhu import object_properties from cthulhu import object_properties
from cthulhu import cthulhu_state from cthulhu import cthulhu_state
from cthulhu import settings_manager
from cthulhu.ax_object import AXObject from cthulhu.ax_object import AXObject
from cthulhu.ax_text import AXText from cthulhu.ax_text import AXText
from cthulhu.ax_utilities import AXUtilities from cthulhu.ax_utilities import AXUtilities
-2
View File
@@ -38,7 +38,6 @@ from gi.repository import Atspi
from . import cmdnames from . import cmdnames
from . import cthulhu from . import cthulhu
from . import dbus_service
from . import debug from . import debug
from . import guilabels from . import guilabels
from . import input_event from . import input_event
@@ -48,7 +47,6 @@ from . import object_properties
from . import cthulhu_gui_navlist from . import cthulhu_gui_navlist
from . import cthulhu_state from . import cthulhu_state
from . import settings from . import settings
from . import settings_manager
from .ax_collection import AXCollection from .ax_collection import AXCollection
from .ax_event_synthesizer import AXEventSynthesizer from .ax_event_synthesizer import AXEventSynthesizer
from .ax_object import AXObject from .ax_object import AXObject
+1 -2
View File
@@ -23,8 +23,7 @@
# 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
import gi, os, locale, gettext import locale, gettext
from gi.repository import GObject
import gettext import gettext
from cthulhu import cthulhu_i18n from cthulhu import cthulhu_i18n
+1 -4
View File
@@ -23,11 +23,8 @@
# 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
import gi, os, locale, gettext import os
from gi.repository import GObject
import gettext
from cthulhu import cthulhu_i18n
from cthulhu import translation_context from cthulhu import translation_context
class TranslationManager(): class TranslationManager():
-14
View File
@@ -1,14 +0,0 @@
#!/usr/bin/env python3
#
# Copyright (c) 2026 Stormux
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
"""Runtime-optional Wayland protocol compatibility wrappers."""
from . import ext_workspace_v1
__all__ = ["ext_workspace_v1"]
@@ -1,130 +0,0 @@
#!/usr/bin/env python3
#
# Copyright (c) 2026 Stormux
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
"""Local ext-workspace protocol wrapper built on top of base pywayland."""
from __future__ import annotations
import importlib
from types import ModuleType
from typing import Any
INTERFACE_NAME = "ext_workspace_manager_v1"
INTERFACE_VERSION = 1
ACTIVE_STATE_VALUE = 1
WORKSPACE_HANDLE_INTERFACE_NAME = "ext_workspace_handle_v1"
def _load_module(module_names: list[str]) -> ModuleType | None:
for moduleName in module_names:
try:
return importlib.import_module(moduleName)
except ImportError:
continue
return None
def _first_attribute(module: ModuleType | None, attribute_names: list[str]) -> Any:
if module is None:
return None
for attributeName in attribute_names:
if hasattr(module, attributeName):
return getattr(module, attributeName)
return None
_pywaylandClientModule = _load_module(
[
"pywayland.client",
"pywayland.client.display",
]
)
Display = _first_attribute(_pywaylandClientModule, ["Display"])
class _LocalProtocolProxy:
"""Minimal local proxy descriptor with a dispatcher-compatible shape."""
interface_name = ""
version = 1
event_names: tuple[str, ...] = ()
def __init__(self, *args: Any, **kwargs: Any) -> None:
try:
super().__init__(*args, **kwargs)
except Exception:
pass
self.dispatcher = getattr(self, "dispatcher", {}) or {}
class ExtWorkspaceManagerV1(_LocalProtocolProxy):
interface_name = INTERFACE_NAME
version = INTERFACE_VERSION
event_names = ("workspace", "done", "finished")
class ExtWorkspaceHandleV1(_LocalProtocolProxy):
interface_name = WORKSPACE_HANDLE_INTERFACE_NAME
version = INTERFACE_VERSION
event_names = ("name", "coordinates", "state", "removed")
def get_display_class() -> Any:
return Display
def has_runtime_support() -> bool:
return Display is not None
def bind_workspace_manager(registry: Any, global_name: int, version: int) -> Any:
bind = getattr(registry, "bind", None)
if not callable(bind):
return None
for bindArgs in (
(global_name, ExtWorkspaceManagerV1, version),
(global_name, version, ExtWorkspaceManagerV1),
(global_name, INTERFACE_NAME, version),
(global_name, version, INTERFACE_NAME),
):
try:
proxy = bind(*bindArgs)
return _ensure_dispatcher(proxy)
except TypeError:
continue
return None
def _ensure_dispatcher(proxy: Any) -> Any:
if proxy is None:
return None
if getattr(proxy, "dispatcher", None) is None:
try:
proxy.dispatcher = {}
except Exception:
pass
return proxy
__all__ = [
"ACTIVE_STATE_VALUE",
"Display",
"ExtWorkspaceHandleV1",
"ExtWorkspaceManagerV1",
"INTERFACE_NAME",
"INTERFACE_VERSION",
"WORKSPACE_HANDLE_INTERFACE_NAME",
"bind_workspace_manager",
"get_display_class",
"has_runtime_support",
]
@@ -1,9 +0,0 @@
wayland_protocol_python_sources = files([
'__init__.py',
'ext_workspace_v1.py',
])
python3.install_sources(
wayland_protocol_python_sources,
subdir: 'cthulhu/wayland_protocols'
)
@@ -1,6 +1,4 @@
import importlib
import sys import sys
import types
import unittest import unittest
from pathlib import Path from pathlib import Path
from unittest import mock from unittest import mock
@@ -10,7 +8,6 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from cthulhu import cthulhu_state from cthulhu import cthulhu_state
from cthulhu import compositor_state_adapter from cthulhu import compositor_state_adapter
from cthulhu import compositor_state_types from cthulhu import compositor_state_types
from cthulhu import compositor_state_wayland
class FakeWorkspaceBackend: class FakeWorkspaceBackend:
@@ -52,7 +49,7 @@ class CompositorStateAdapterRegressionTests(unittest.TestCase):
self.assertTrue(callable(selectedBackend.activate_calls[0])) self.assertTrue(callable(selectedBackend.activate_calls[0]))
self.assertEqual(adapter.get_snapshot().backend_name, "selected") self.assertEqual(adapter.get_snapshot().backend_name, "selected")
def test_default_workspace_backends_include_wayland_then_null_backend(self) -> None: def test_default_workspace_backends_include_null_backend(self) -> None:
adapter = compositor_state_adapter.CompositorStateAdapter() adapter = compositor_state_adapter.CompositorStateAdapter()
backend_types = [type(backend) for backend in adapter._workspaceBackends] backend_types = [type(backend) for backend in adapter._workspaceBackends]
@@ -60,117 +57,15 @@ class CompositorStateAdapterRegressionTests(unittest.TestCase):
self.assertEqual( self.assertEqual(
backend_types, backend_types,
[ [
compositor_state_wayland.WaylandSharedProtocolsBackend, compositor_state_adapter.NullWorkspaceBackend,
compositor_state_wayland.NullWorkspaceBackend,
], ],
) )
def test_wayland_backend_is_unavailable_without_wayland_session(self) -> None: def test_injected_workspace_backend_is_selected_when_available(self) -> None:
backend = compositor_state_wayland.WaylandSharedProtocolsBackend()
with mock.patch.dict(compositor_state_wayland.os.environ, {}, clear=True):
self.assertFalse(backend.is_available("x11"))
self.assertFalse(backend.is_available("wayland"))
def test_wayland_backend_installs_dispatch_watch_and_processes_runtime_events(self) -> None:
fakeDisplay = mock.Mock()
fakeRegistry = mock.Mock()
fakeDisplay.connect = mock.Mock()
fakeDisplay.get_registry = mock.Mock(return_value=fakeRegistry)
fakeDisplay.get_fd = mock.Mock(return_value=17)
fakeDisplay.roundtrip = mock.Mock()
fakeDisplay.dispatch = mock.Mock(side_effect=[1, 0])
fakeProtocols = mock.Mock()
fakeProtocols.INTERFACE_NAME = "ext_workspace_manager_v1"
fakeProtocols.INTERFACE_VERSION = 1
fakeProtocols.ACTIVE_STATE_VALUE = 1
fakeProtocols.has_runtime_support.return_value = True
fakeProtocols.get_display_class.return_value = mock.Mock(return_value=fakeDisplay)
emitSignal = mock.Mock()
backend = compositor_state_wayland.WaylandSharedProtocolsBackend(
environment={"WAYLAND_DISPLAY": "wayland-0"},
protocols=fakeProtocols,
)
with (
mock.patch.object(compositor_state_wayland, "get_session_type", return_value="wayland"),
mock.patch.object(compositor_state_wayland.GLib, "io_add_watch", return_value=41) as ioAddWatch,
mock.patch.object(compositor_state_wayland.GLib, "source_remove") as sourceRemove,
):
backend.activate(emitSignal)
ioAddWatch.assert_called_once()
watchCallback = ioAddWatch.call_args.args[3]
self.assertTrue(watchCallback(17, compositor_state_wayland.GLib.IO_IN))
self.assertEqual(fakeDisplay.dispatch.call_count, 2)
backend.deactivate()
sourceRemove.assert_called_once_with(41)
def test_wayland_backend_logs_activation_failure_reason(self) -> None:
fakeDisplay = mock.Mock()
fakeDisplay.connect.side_effect = ValueError("Unable to connect to display")
fakeProtocols = mock.Mock()
fakeProtocols.has_runtime_support.return_value = True
fakeProtocols.get_display_class.return_value = mock.Mock(return_value=fakeDisplay)
backend = compositor_state_wayland.WaylandSharedProtocolsBackend(
environment={"WAYLAND_DISPLAY": "wayland-0"},
protocols=fakeProtocols,
)
with (
mock.patch.object(compositor_state_wayland, "get_session_type", return_value="wayland"),
mock.patch.object(compositor_state_wayland.debug, "printMessage") as printMessage,
):
backend.activate(mock.Mock())
printMessage.assert_any_call(
compositor_state_wayland.debug.LEVEL_WARNING,
mock.ANY,
True,
)
self.assertIn("Unable to connect to display", printMessage.call_args_list[-1].args[1])
self.assertIsNone(backend._display)
def test_local_ext_workspace_wrapper_supports_base_pywayland_without_distro_protocol_module(self) -> None:
from cthulhu.wayland_protocols import ext_workspace_v1
fakeClientModule = types.ModuleType("pywayland.client")
class FakeDisplay:
pass
fakeClientModule.Display = FakeDisplay
def fake_import(moduleName: str, package=None):
if moduleName in ("pywayland.client", "pywayland.client.display"):
return fakeClientModule
raise ImportError(moduleName)
try:
with mock.patch("importlib.import_module", side_effect=fake_import):
importlib.reload(ext_workspace_v1)
self.assertTrue(ext_workspace_v1.has_runtime_support())
self.assertIs(ext_workspace_v1.get_display_class(), FakeDisplay)
self.assertIsNotNone(ext_workspace_v1.ExtWorkspaceManagerV1)
self.assertIsNotNone(ext_workspace_v1.ExtWorkspaceHandleV1)
self.assertEqual(
ext_workspace_v1.ExtWorkspaceManagerV1.interface_name,
"ext_workspace_manager_v1",
)
self.assertEqual(
ext_workspace_v1.ExtWorkspaceHandleV1.interface_name,
"ext_workspace_handle_v1",
)
finally:
importlib.reload(ext_workspace_v1)
def test_wayland_backend_is_selected_when_available(self) -> None:
adapter = compositor_state_adapter.CompositorStateAdapter() adapter = compositor_state_adapter.CompositorStateAdapter()
selected_backend = mock.Mock(name="selected-backend") selected_backend = mock.Mock(name="selected-backend")
fallback_backend = mock.Mock(name="fallback-backend") fallback_backend = mock.Mock(name="fallback-backend")
selected_backend.name = "wayland-shared-protocols" selected_backend.name = "external-workspace-signals"
selected_backend.is_available.return_value = True selected_backend.is_available.return_value = True
fallback_backend.name = "null" fallback_backend.name = "null"
fallback_backend.is_available.return_value = True fallback_backend.is_available.return_value = True
@@ -181,7 +76,7 @@ class CompositorStateAdapterRegressionTests(unittest.TestCase):
selected_backend.activate.assert_called_once() selected_backend.activate.assert_called_once()
fallback_backend.activate.assert_not_called() fallback_backend.activate.assert_not_called()
self.assertTrue(callable(selected_backend.activate.call_args.args[0])) self.assertTrue(callable(selected_backend.activate.call_args.args[0]))
self.assertEqual(adapter.get_snapshot().backend_name, "wayland-shared-protocols") self.assertEqual(adapter.get_snapshot().backend_name, "external-workspace-signals")
def test_activate_is_idempotent_and_deactivates_previous_backend(self) -> None: def test_activate_is_idempotent_and_deactivates_previous_backend(self) -> None:
backend = FakeWorkspaceBackend(True, "selected") backend = FakeWorkspaceBackend(True, "selected")
@@ -245,24 +140,31 @@ class CompositorStateAdapterRegressionTests(unittest.TestCase):
self.assertEqual(snapshot.active_window_token, "4242:Terminal") self.assertEqual(snapshot.active_window_token, "4242:Terminal")
self.assertEqual(cthulhu_state.compositorSnapshot.active_window_token, "4242:Terminal") self.assertEqual(cthulhu_state.compositorSnapshot.active_window_token, "4242:Terminal")
def test_workspace_backend_normalizes_initial_done_and_handoff(self) -> None: def test_external_workspace_signal_normalizes_transition_and_handoff(self) -> None:
adapter = compositor_state_adapter.CompositorStateAdapter(workspace_backends=[]) adapter = compositor_state_adapter.CompositorStateAdapter(workspace_backends=[])
events = [] events = []
adapter.add_listener(events.append) adapter.add_listener(events.append)
workspace = compositor_state_wayland.WaylandSharedProtocolsBackend()
first_workspace = mock.Mock()
second_workspace = mock.Mock()
first_workspace_token = f"workspace:{id(first_workspace)}"
adapter._snapshot = compositor_state_types.DesktopContextSnapshot( adapter._snapshot = compositor_state_types.DesktopContextSnapshot(
session_type="wayland", session_type="wayland",
backend_name="wayland-shared-protocols", backend_name="external-workspace-signals",
) )
with mock.patch.object(workspace, "is_available", return_value=False): adapter._handle_workspace_signal(
workspace.activate(adapter._handle_workspace_signal) compositor_state_types.DESKTOP_TRANSITION_STARTED,
workspace._handle_workspace_state(first_workspace, "active", True) ["workspace-1"],
workspace._handle_workspace_manager_done() "transition-started",
)
adapter._handle_workspace_signal(
compositor_state_types.WORKSPACE_STATE_CHANGED,
["workspace-1"],
"workspace-changed",
)
adapter._handle_workspace_signal(
compositor_state_types.DESKTOP_TRANSITION_FINISHED,
["workspace-1"],
"transition-finished",
)
self.assertEqual( self.assertEqual(
[event.type for event in events], [event.type for event in events],
@@ -275,18 +177,29 @@ class CompositorStateAdapterRegressionTests(unittest.TestCase):
compositor_state_types.FLUSH_STALE_ATSPI_EVENTS, compositor_state_types.FLUSH_STALE_ATSPI_EVENTS,
], ],
) )
self.assertEqual(events[0].snapshot.active_workspace_ids, frozenset({first_workspace_token})) self.assertEqual(events[0].snapshot.active_workspace_ids, frozenset({"workspace-1"}))
self.assertTrue(events[0].snapshot.workspace_transition_pending) self.assertTrue(events[0].snapshot.workspace_transition_pending)
self.assertEqual(events[2].snapshot.active_workspace_ids, frozenset({first_workspace_token})) self.assertEqual(events[2].snapshot.active_workspace_ids, frozenset({"workspace-1"}))
self.assertFalse(adapter.get_snapshot().workspace_transition_pending) self.assertFalse(adapter.get_snapshot().workspace_transition_pending)
self.assertEqual(adapter.get_snapshot().active_workspace_ids, frozenset({first_workspace_token})) self.assertEqual(adapter.get_snapshot().active_workspace_ids, frozenset({"workspace-1"}))
self.assertEqual(cthulhu_state.compositorSnapshot.active_workspace_ids, frozenset({first_workspace_token})) self.assertEqual(cthulhu_state.compositorSnapshot.active_workspace_ids, frozenset({"workspace-1"}))
events.clear() events.clear()
workspace._handle_workspace_id(second_workspace, "ws-2") adapter._handle_workspace_signal(
workspace._handle_workspace_state(first_workspace, "active", False) compositor_state_types.DESKTOP_TRANSITION_STARTED,
workspace._handle_workspace_state(second_workspace, "active", True) [],
workspace._handle_workspace_manager_done() "transition-started",
)
adapter._handle_workspace_signal(
compositor_state_types.WORKSPACE_STATE_CHANGED,
["ws-2"],
"workspace-changed",
)
adapter._handle_workspace_signal(
compositor_state_types.DESKTOP_TRANSITION_FINISHED,
["ws-2"],
"transition-finished",
)
event_types = [event.type for event in events] event_types = [event.type for event in events]
@@ -7,7 +7,6 @@ from unittest import mock
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from cthulhu import cthulhu
from cthulhu import settings from cthulhu import settings
from cthulhu import cthulhu_gui_prefs from cthulhu import cthulhu_gui_prefs
from cthulhu import settings_manager from cthulhu import settings_manager