Try to make Cthulhu python 3.9 compatible. Fixed keybinding and notifications bugs.
This commit is contained in:
@@ -225,7 +225,7 @@ subdir('PluginName')
|
|||||||
- Community: IRC #stormux on irc.stormux.org
|
- Community: IRC #stormux on irc.stormux.org
|
||||||
|
|
||||||
### Key Dependencies
|
### Key Dependencies
|
||||||
- Python 3.10+, pygobject-3.0, pluggy, gtk+-3.0
|
- Python 3.9+, pygobject-3.0, pluggy, gtk+-3.0
|
||||||
- AT-SPI2, ATK for accessibility
|
- AT-SPI2, ATK for accessibility
|
||||||
- Optional: BrlTTY/BrlAPI (braille), Speech Dispatcher, liblouis, GStreamer
|
- Optional: BrlTTY/BrlAPI (braille), Speech Dispatcher, liblouis, GStreamer
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ toolkit, OpenOffice/LibreOffice, Gecko, WebKitGtk, and KDE Qt toolkit.
|
|||||||
|
|
||||||
### Core Requirements
|
### Core Requirements
|
||||||
|
|
||||||
* **Python 3.10+** - Python platform
|
* **Python 3.9+** - Python platform
|
||||||
* **pygobject-3.0** - Python bindings for the GObject library
|
* **pygobject-3.0** - Python bindings for the GObject library
|
||||||
* **gtk+-3.0** - GTK+ toolkit (minimal usage for AT-SPI integration)
|
* **gtk+-3.0** - GTK+ toolkit (minimal usage for AT-SPI integration)
|
||||||
* **AT-SPI2** - Assistive Technology Service Provider Interface
|
* **AT-SPI2** - Assistive Technology Service Provider Interface
|
||||||
|
|||||||
+22
-35
@@ -6,85 +6,72 @@
|
|||||||
|
|
||||||
set -e # Exit on error
|
set -e # Exit on error
|
||||||
|
|
||||||
# Colors for output (only if stdout is a terminal)
|
echo "Cthulhu Meson Local Build Script"
|
||||||
if [[ -t 1 ]]; then
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
else
|
|
||||||
RED=''
|
|
||||||
GREEN=''
|
|
||||||
YELLOW=''
|
|
||||||
BLUE=''
|
|
||||||
NC=''
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "${BLUE}Cthulhu Meson Local Build Script${NC}"
|
|
||||||
echo "Building and installing Cthulhu to ~/.local"
|
echo "Building and installing Cthulhu to ~/.local"
|
||||||
echo "================================================"
|
echo "================================================"
|
||||||
|
|
||||||
# Check if we're in the right directory
|
# Check if we're in the right directory
|
||||||
if [[ ! -f "meson.build" ]] || [[ ! -f "src/cthulhu/cthulhu.py" ]]; then
|
if [[ ! -f "meson.build" ]] || [[ ! -f "src/cthulhu/cthulhu.py" ]]; then
|
||||||
echo -e "${RED}Error: This script must be run from the Cthulhu source directory${NC}"
|
echo "Error: This script must be run from the Cthulhu source directory"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check for required dependencies
|
# Check for required dependencies
|
||||||
echo -e "${YELLOW}Checking dependencies...${NC}"
|
echo "Checking dependencies..."
|
||||||
for cmd in meson ninja python3; do
|
for cmd in meson ninja python3; do
|
||||||
if ! command -v "$cmd" &> /dev/null; then
|
if ! command -v "$cmd" &> /dev/null; then
|
||||||
echo -e "${RED}Error: $cmd is not installed${NC}"
|
echo "Error: $cmd is not installed"
|
||||||
echo "Please install: meson ninja python"
|
echo "Please install: meson ninja python"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Check for optional dependencies
|
# Check for optional dependencies
|
||||||
missing_optional=()
|
missingOptional=()
|
||||||
for module in gi speech; do
|
if ! python3 -c "import gi" 2>/dev/null; then
|
||||||
if ! python3 -c "import $module" 2>/dev/null; then
|
missingOptional+=("python-gi")
|
||||||
missing_optional+=("python-$module")
|
fi
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ ${#missing_optional[@]} -gt 0 ]]; then
|
if [[ ${#missingOptional[@]} -gt 0 ]]; then
|
||||||
echo -e "${YELLOW}Warning: Optional dependencies missing: ${missing_optional[*]}${NC}"
|
echo "Warning: Optional dependencies missing: ${missingOptional[*]}"
|
||||||
echo "Cthulhu may not function properly without these."
|
echo "Cthulhu may not function properly without these."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Clean any cached bytecode
|
||||||
|
echo "Removing __pycache__ directories..."
|
||||||
|
find . -type d -name "__pycache__" -prune -exec rm -rf {} +
|
||||||
|
|
||||||
# Clean any existing build directory
|
# Clean any existing build directory
|
||||||
if [[ -d "_build" ]]; then
|
if [[ -d "_build" ]]; then
|
||||||
echo -e "${YELLOW}Removing existing build directory...${NC}"
|
echo "Removing existing build directory..."
|
||||||
rm -rf _build
|
rm -rf _build
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Setup Meson build
|
# Setup Meson build
|
||||||
echo -e "${YELLOW}Setting up Meson build...${NC}"
|
echo "Setting up Meson build..."
|
||||||
meson setup _build --prefix="$HOME/.local" --buildtype=debugoptimized
|
meson setup _build --prefix="$HOME/.local" --buildtype=debugoptimized
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
echo -e "${YELLOW}Building Cthulhu...${NC}"
|
echo "Building Cthulhu..."
|
||||||
meson compile -C _build
|
meson compile -C _build
|
||||||
|
|
||||||
# Install
|
# Install
|
||||||
echo -e "${YELLOW}Installing Cthulhu to ~/.local...${NC}"
|
echo "Installing Cthulhu to ~/.local..."
|
||||||
meson install -C _build
|
meson install -C _build
|
||||||
|
|
||||||
# Update desktop database and icon cache
|
# Update desktop database and icon cache
|
||||||
if command -v update-desktop-database &> /dev/null; then
|
if command -v update-desktop-database &> /dev/null; then
|
||||||
echo -e "${YELLOW}Updating desktop database...${NC}"
|
echo "Updating desktop database..."
|
||||||
update-desktop-database "$HOME/.local/share/applications" 2>/dev/null || true
|
update-desktop-database "$HOME/.local/share/applications" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if command -v gtk-update-icon-cache &> /dev/null; then
|
if command -v gtk-update-icon-cache &> /dev/null; then
|
||||||
echo -e "${YELLOW}Updating icon cache...${NC}"
|
echo "Updating icon cache..."
|
||||||
gtk-update-icon-cache -f -t "$HOME/.local/share/icons/hicolor" 2>/dev/null || true
|
gtk-update-icon-cache -f -t "$HOME/.local/share/icons/hicolor" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo -e "${GREEN}Build completed successfully!${NC}"
|
echo "Build completed successfully!"
|
||||||
echo
|
echo
|
||||||
echo "To run Cthulhu:"
|
echo "To run Cthulhu:"
|
||||||
echo " ~/.local/bin/cthulhu"
|
echo " ~/.local/bin/cthulhu"
|
||||||
@@ -93,4 +80,4 @@ echo "To run Cthulhu setup:"
|
|||||||
echo " ~/.local/bin/cthulhu -s"
|
echo " ~/.local/bin/cthulhu -s"
|
||||||
echo
|
echo
|
||||||
echo "Build artifacts are in: _build/"
|
echo "Build artifacts are in: _build/"
|
||||||
echo -e "${BLUE}To clean build artifacts, run: ${YELLOW}rm -rf _build${NC}"
|
echo "To clean build artifacts, run: rm -rf _build"
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ project('cthulhu',
|
|||||||
python = import('python')
|
python = import('python')
|
||||||
i18n = import('i18n')
|
i18n = import('i18n')
|
||||||
|
|
||||||
python_minimum_version = '3.10'
|
python_minimum_version = '3.9'
|
||||||
python3 = python.find_installation('python3', required: true)
|
python3 = python.find_installation('python3', required: true)
|
||||||
if not python3.language_version().version_compare(f'>= @python_minimum_version@')
|
if not python3.language_version().version_compare(f'>= @python_minimum_version@')
|
||||||
error(f'Python @python_minimum_version@ or newer is required.')
|
error(f'Python @python_minimum_version@ or newer is required.')
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ name = "cthulhu"
|
|||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
description = "Fork of the Orca screen reader"
|
description = "Fork of the Orca screen reader"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.9"
|
||||||
license = { text = "LGPL-2.1-or-later" }
|
license = { text = "LGPL-2.1-or-later" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pygobject>=3.18",
|
"pygobject>=3.18",
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ __copyright__ = "Copyright (c) 2023 Igalia, S.L."
|
|||||||
__license__ = "LGPL"
|
__license__ = "LGPL"
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Union
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ class ActionList(Gtk.Window):
|
|||||||
|
|
||||||
GLib.idle_add(self._presenter._clear_gui_and_restore_focus)
|
GLib.idle_add(self._presenter._clear_gui_and_restore_focus)
|
||||||
|
|
||||||
def populate_actions(self, actions: dict[str, str] | list[str]) -> None:
|
def populate_actions(self, actions: Union[dict[str, str], list[str]]) -> None:
|
||||||
"""Populates the list with accessible actions."""
|
"""Populates the list with accessible actions."""
|
||||||
|
|
||||||
if isinstance(actions, dict):
|
if isinstance(actions, dict):
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ __date__ = "$Date$"
|
|||||||
__copyright__ = "Copyright (c) 2023 Igalia, S.L."
|
__copyright__ = "Copyright (c) 2023 Igalia, S.L."
|
||||||
__license__ = "LGPL"
|
__license__ = "LGPL"
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
@@ -48,15 +49,15 @@ class AXCollection:
|
|||||||
# pylint: disable=R0913,R0914
|
# pylint: disable=R0913,R0914
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_match_rule(
|
def create_match_rule(
|
||||||
states: list[str] | None = None,
|
states: Optional[list[str]] = None,
|
||||||
state_match_type: Atspi.CollectionMatchType = Atspi.CollectionMatchType.ALL,
|
state_match_type: Atspi.CollectionMatchType = Atspi.CollectionMatchType.ALL,
|
||||||
attributes: list[str] | None = None,
|
attributes: Optional[list[str]] = None,
|
||||||
attribute_match_type: Atspi.CollectionMatchType = Atspi.CollectionMatchType.ALL,
|
attribute_match_type: Atspi.CollectionMatchType = Atspi.CollectionMatchType.ALL,
|
||||||
roles: list[str] | None = None,
|
roles: Optional[list[str]] = None,
|
||||||
role_match_type: Atspi.CollectionMatchType = Atspi.CollectionMatchType.ALL,
|
role_match_type: Atspi.CollectionMatchType = Atspi.CollectionMatchType.ALL,
|
||||||
interfaces: list[str] | None = None,
|
interfaces: Optional[list[str]] = None,
|
||||||
interface_match_type: Atspi.CollectionMatchType = Atspi.CollectionMatchType.ALL,
|
interface_match_type: Atspi.CollectionMatchType = Atspi.CollectionMatchType.ALL,
|
||||||
invert: bool = False) -> Atspi.MatchRule | None:
|
invert: bool = False) -> Optional[Atspi.MatchRule]:
|
||||||
"""Creates a match rule based on the supplied criteria."""
|
"""Creates a match rule based on the supplied criteria."""
|
||||||
|
|
||||||
if states is None:
|
if states is None:
|
||||||
@@ -136,7 +137,7 @@ class AXCollection:
|
|||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
rule: Atspi.MatchRule,
|
rule: Atspi.MatchRule,
|
||||||
order: Atspi.CollectionSortOrder = Atspi.CollectionSortOrder.CANONICAL
|
order: Atspi.CollectionSortOrder = Atspi.CollectionSortOrder.CANONICAL
|
||||||
) -> Atspi.Accessible | None:
|
) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the first object matching the specified rule."""
|
"""Returns the first object matching the specified rule."""
|
||||||
|
|
||||||
if not AXObject.supports_collection(obj):
|
if not AXObject.supports_collection(obj):
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ __copyright__ = "Copyright (c) 2024 Igalia, S.L." \
|
|||||||
"Copyright (c) 2024 GNOME Foundation Inc."
|
"Copyright (c) 2024 GNOME Foundation Inc."
|
||||||
__license__ = "LGPL"
|
__license__ = "LGPL"
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
import functools
|
import functools
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
@@ -285,7 +286,7 @@ class AXComponent:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _find_descendant_at_point(
|
def _find_descendant_at_point(
|
||||||
obj: Atspi.Accessible, x: int, y: int
|
obj: Atspi.Accessible, x: int, y: int
|
||||||
) -> Atspi.Accessible | None:
|
) -> Optional[Atspi.Accessible]:
|
||||||
"""Checks each child to see if it has a descendant at the specified point."""
|
"""Checks each child to see if it has a descendant at the specified point."""
|
||||||
|
|
||||||
for child in AXObject.iter_children(obj):
|
for child in AXObject.iter_children(obj):
|
||||||
@@ -297,7 +298,7 @@ class AXComponent:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_object_at_point(obj: Atspi.Accessible, x: int, y: int) -> Atspi.Accessible | None:
|
def _get_object_at_point(obj: Atspi.Accessible, x: int, y: int) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the child (or descendant?) of obj at the specified point."""
|
"""Returns the child (or descendant?) of obj at the specified point."""
|
||||||
|
|
||||||
if not AXObject.supports_component(obj):
|
if not AXObject.supports_component(obj):
|
||||||
@@ -317,7 +318,7 @@ class AXComponent:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_descendant_at_point(
|
def _get_descendant_at_point(
|
||||||
obj: Atspi.Accessible, x: int, y: int
|
obj: Atspi.Accessible, x: int, y: int
|
||||||
) -> Atspi.Accessible | None:
|
) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the deepest descendant of obj at the specified point."""
|
"""Returns the deepest descendant of obj at the specified point."""
|
||||||
|
|
||||||
child = AXComponent._get_object_at_point(obj, x, y)
|
child = AXComponent._get_object_at_point(obj, x, y)
|
||||||
@@ -338,7 +339,7 @@ class AXComponent:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_descendant_at_point(
|
def get_descendant_at_point(
|
||||||
obj: Atspi.Accessible, x: int, y: int
|
obj: Atspi.Accessible, x: int, y: int
|
||||||
) -> Atspi.Accessible | None:
|
) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the deepest descendant of obj at the specified point."""
|
"""Returns the deepest descendant of obj at the specified point."""
|
||||||
|
|
||||||
result = AXComponent._get_descendant_at_point(obj, x, y)
|
result = AXComponent._get_descendant_at_point(obj, x, y)
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ __copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." \
|
|||||||
"Copyright (c) 2018-2023 Igalia, S.L."
|
"Copyright (c) 2018-2023 Igalia, S.L."
|
||||||
__license__ = "LGPL"
|
__license__ = "LGPL"
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
import gi
|
import gi
|
||||||
gi.require_version("Atspi", "2.0")
|
gi.require_version("Atspi", "2.0")
|
||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
@@ -60,8 +61,8 @@ class AXEventSynthesizer:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _is_scrolled_off_screen(
|
def _is_scrolled_off_screen(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
offset: int | None = None,
|
offset: Optional[int] = None,
|
||||||
ancestor: Atspi.Accessible | None = None
|
ancestor: Optional[Atspi.Accessible] = None
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Returns true if obj, or the caret offset therein, is scrolled off-screen."""
|
"""Returns true if obj, or the caret offset therein, is scrolled off-screen."""
|
||||||
|
|
||||||
@@ -119,7 +120,7 @@ class AXEventSynthesizer:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _mouse_event_on_character(obj: Atspi.Accessible, offset: int | None, event: str) -> bool:
|
def _mouse_event_on_character(obj: Atspi.Accessible, offset: Optional[int], event: str) -> bool:
|
||||||
"""Performs the specified mouse event on the current character in obj."""
|
"""Performs the specified mouse event on the current character in obj."""
|
||||||
|
|
||||||
if offset is None:
|
if offset is None:
|
||||||
@@ -164,7 +165,7 @@ class AXEventSynthesizer:
|
|||||||
return AXEventSynthesizer._generate_mouse_event(obj, relative_x, relative_y, event)
|
return AXEventSynthesizer._generate_mouse_event(obj, relative_x, relative_y, event)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def route_to_character(obj: Atspi.Accessible, offset: int | None = None) -> bool:
|
def route_to_character(obj: Atspi.Accessible, offset: Optional[int] = None) -> bool:
|
||||||
"""Routes the pointer to the current character in obj."""
|
"""Routes the pointer to the current character in obj."""
|
||||||
|
|
||||||
tokens = [f"AXEventSynthesizer: Attempting to route to offset {offset} in", obj]
|
tokens = [f"AXEventSynthesizer: Attempting to route to offset {offset} in", obj]
|
||||||
@@ -181,7 +182,7 @@ class AXEventSynthesizer:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def click_character(
|
def click_character(
|
||||||
obj: Atspi.Accessible, offset: int | None = None, button: int = 1
|
obj: Atspi.Accessible, offset: Optional[int] = None, button: int = 1
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Single click on the current character in obj using the specified button."""
|
"""Single click on the current character in obj using the specified button."""
|
||||||
|
|
||||||
@@ -198,7 +199,7 @@ class AXEventSynthesizer:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _scroll_to_location(
|
def _scroll_to_location(
|
||||||
obj: Atspi.Accessible, location: Atspi.ScrollType,
|
obj: Atspi.Accessible, location: Atspi.ScrollType,
|
||||||
start_offset: int | None = None, end_offset: int | None = None
|
start_offset: Optional[int] = None, end_offset: Optional[int] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Attempts to scroll to the specified location."""
|
"""Attempts to scroll to the specified location."""
|
||||||
|
|
||||||
@@ -220,7 +221,7 @@ class AXEventSynthesizer:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _scroll_to_point(
|
def _scroll_to_point(
|
||||||
obj: Atspi.Accessible, x_coord: int, y_coord: int,
|
obj: Atspi.Accessible, x_coord: int, y_coord: int,
|
||||||
start_offset: int | None = None, end_offset: int | None = None
|
start_offset: Optional[int] = None, end_offset: Optional[int] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Attempts to scroll obj to the specified point."""
|
"""Attempts to scroll obj to the specified point."""
|
||||||
|
|
||||||
@@ -242,8 +243,8 @@ class AXEventSynthesizer:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def scroll_into_view(
|
def scroll_into_view(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
start_offset: int | None = None,
|
start_offset: Optional[int] = None,
|
||||||
end_offset: int | None = None
|
end_offset: Optional[int] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Attempts to scroll obj into view."""
|
"""Attempts to scroll obj into view."""
|
||||||
|
|
||||||
@@ -253,8 +254,8 @@ class AXEventSynthesizer:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def scroll_to_center(
|
def scroll_to_center(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
start_offset: int | None = None,
|
start_offset: Optional[int] = None,
|
||||||
end_offset: int | None = None
|
end_offset: Optional[int] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Attempts to scroll obj to the center of its window."""
|
"""Attempts to scroll obj to the center of its window."""
|
||||||
|
|
||||||
@@ -272,8 +273,8 @@ class AXEventSynthesizer:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def scroll_to_top_edge(
|
def scroll_to_top_edge(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
start_offset: int | None = None,
|
start_offset: Optional[int] = None,
|
||||||
end_offset: int | None = None
|
end_offset: Optional[int] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Attempts to scroll obj to the top edge."""
|
"""Attempts to scroll obj to the top edge."""
|
||||||
|
|
||||||
@@ -283,8 +284,8 @@ class AXEventSynthesizer:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def scroll_to_top_left(
|
def scroll_to_top_left(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
start_offset: int | None = None,
|
start_offset: Optional[int] = None,
|
||||||
end_offset: int | None = None
|
end_offset: Optional[int] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Attempts to scroll obj to the top left."""
|
"""Attempts to scroll obj to the top left."""
|
||||||
|
|
||||||
@@ -294,8 +295,8 @@ class AXEventSynthesizer:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def scroll_to_left_edge(
|
def scroll_to_left_edge(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
start_offset: int | None = None,
|
start_offset: Optional[int] = None,
|
||||||
end_offset: int | None = None
|
end_offset: Optional[int] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Attempts to scroll obj to the left edge."""
|
"""Attempts to scroll obj to the left edge."""
|
||||||
|
|
||||||
@@ -305,8 +306,8 @@ class AXEventSynthesizer:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def scroll_to_bottom_edge(
|
def scroll_to_bottom_edge(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
start_offset: int | None = None,
|
start_offset: Optional[int] = None,
|
||||||
end_offset: int | None = None
|
end_offset: Optional[int] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Attempts to scroll obj to the bottom edge."""
|
"""Attempts to scroll obj to the bottom edge."""
|
||||||
|
|
||||||
@@ -316,8 +317,8 @@ class AXEventSynthesizer:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def scroll_to_bottom_right(
|
def scroll_to_bottom_right(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
start_offset: int | None = None,
|
start_offset: Optional[int] = None,
|
||||||
end_offset: int | None = None
|
end_offset: Optional[int] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Attempts to scroll obj to the bottom right."""
|
"""Attempts to scroll obj to the bottom right."""
|
||||||
|
|
||||||
@@ -327,8 +328,8 @@ class AXEventSynthesizer:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def scroll_to_right_edge(
|
def scroll_to_right_edge(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
start_offset: int | None = None,
|
start_offset: Optional[int] = None,
|
||||||
end_offset: int | None = None
|
end_offset: Optional[int] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Attempts to scroll obj to the right edge."""
|
"""Attempts to scroll obj to the right edge."""
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ __date__ = "$Date$"
|
|||||||
__copyright__ = "Copyright (c) 2024 Igalia, S.L."
|
__copyright__ = "Copyright (c) 2024 Igalia, S.L."
|
||||||
__license__ = "LGPL"
|
__license__ = "LGPL"
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
@@ -66,7 +67,7 @@ class AXHypertext:
|
|||||||
return count
|
return count
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_link_at_index(obj: Atspi.Accessible, index: int) -> Atspi.Hyperlink | None:
|
def _get_link_at_index(obj: Atspi.Accessible, index: int) -> Optional[Atspi.Hyperlink]:
|
||||||
"""Returns the hyperlink object at the specified index."""
|
"""Returns the hyperlink object at the specified index."""
|
||||||
|
|
||||||
if not AXObject.supports_hypertext(obj):
|
if not AXObject.supports_hypertext(obj):
|
||||||
@@ -202,7 +203,7 @@ class AXHypertext:
|
|||||||
return basename
|
return basename
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_child_at_offset(obj: Atspi.Accessible, offset: int) -> Atspi.Accessible | None:
|
def find_child_at_offset(obj: Atspi.Accessible, offset: int) -> Optional[Atspi.Accessible]:
|
||||||
"""Attempts to correct for off-by-one brokenness in implementations"""
|
"""Attempts to correct for off-by-one brokenness in implementations"""
|
||||||
|
|
||||||
if child := AXHypertext.get_child_at_offset(obj, offset):
|
if child := AXHypertext.get_child_at_offset(obj, offset):
|
||||||
@@ -227,7 +228,7 @@ class AXHypertext:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_child_at_offset(obj: Atspi.Accessible, offset: int) -> Atspi.Accessible | None:
|
def get_child_at_offset(obj: Atspi.Accessible, offset: int) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the embedded-object child of obj at the specified offset."""
|
"""Returns the embedded-object child of obj at the specified offset."""
|
||||||
|
|
||||||
if not AXObject.supports_hypertext(obj):
|
if not AXObject.supports_hypertext(obj):
|
||||||
|
|||||||
+21
-21
@@ -34,7 +34,7 @@ __license__ = "LGPL"
|
|||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from typing import Callable, Generator
|
from typing import Callable, Generator, Optional
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
gi.require_version("Atspi", "2.0")
|
gi.require_version("Atspi", "2.0")
|
||||||
@@ -105,7 +105,7 @@ class AXObject:
|
|||||||
return name.lower()
|
return name.lower()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_application(obj: Atspi.Accessible) -> Atspi.Accessible | None:
|
def get_application(obj: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the accessible application associated with obj."""
|
"""Returns the accessible application associated with obj."""
|
||||||
|
|
||||||
if obj is None:
|
if obj is None:
|
||||||
@@ -313,7 +313,7 @@ class AXObject:
|
|||||||
return iface is not None
|
return iface is not None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_real_app_and_window_for(obj: Atspi.Accessible, app: Atspi.Accessible | None = None):
|
def find_real_app_and_window_for(obj: Atspi.Accessible, app: Optional[Atspi.Accessible] = None):
|
||||||
"""Work around for window events coming from mutter-x11-frames."""
|
"""Work around for window events coming from mutter-x11-frames."""
|
||||||
|
|
||||||
if app is None:
|
if app is None:
|
||||||
@@ -545,7 +545,7 @@ class AXObject:
|
|||||||
return index
|
return index
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_parent(obj: Atspi.Accessible) -> Atspi.Accessible | None:
|
def get_parent(obj: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the accessible parent of obj. See also get_parent_checked."""
|
"""Returns the accessible parent of obj. See also get_parent_checked."""
|
||||||
|
|
||||||
if not AXObject.is_valid(obj):
|
if not AXObject.is_valid(obj):
|
||||||
@@ -571,7 +571,7 @@ class AXObject:
|
|||||||
return parent
|
return parent
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_parent_checked(obj: Atspi.Accessible) -> Atspi.Accessible | None:
|
def get_parent_checked(obj: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the parent of obj, doing checks for tree validity"""
|
"""Returns the parent of obj, doing checks for tree validity"""
|
||||||
|
|
||||||
if not AXObject.is_valid(obj):
|
if not AXObject.is_valid(obj):
|
||||||
@@ -619,7 +619,7 @@ class AXObject:
|
|||||||
def get_common_ancestor(
|
def get_common_ancestor(
|
||||||
obj1: Atspi.Accessible,
|
obj1: Atspi.Accessible,
|
||||||
obj2: Atspi.Accessible
|
obj2: Atspi.Accessible
|
||||||
) -> Atspi.Accessible | None:
|
) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the common ancestor of obj1 and obj2."""
|
"""Returns the common ancestor of obj1 and obj2."""
|
||||||
|
|
||||||
tokens = ["AXObject: Looking for common ancestor of", obj1, "and", obj2]
|
tokens = ["AXObject: Looking for common ancestor of", obj1, "and", obj2]
|
||||||
@@ -647,7 +647,7 @@ class AXObject:
|
|||||||
def find_ancestor_inclusive(
|
def find_ancestor_inclusive(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
pred: Callable[[Atspi.Accessible], bool]
|
pred: Callable[[Atspi.Accessible], bool]
|
||||||
) -> Atspi.Accessible | None:
|
) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns obj, or the ancestor of obj, for which the function pred is true"""
|
"""Returns obj, or the ancestor of obj, for which the function pred is true"""
|
||||||
|
|
||||||
if pred(obj):
|
if pred(obj):
|
||||||
@@ -659,7 +659,7 @@ class AXObject:
|
|||||||
def find_ancestor(
|
def find_ancestor(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
pred: Callable[[Atspi.Accessible], bool]
|
pred: Callable[[Atspi.Accessible], bool]
|
||||||
) -> Atspi.Accessible | None:
|
) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the ancestor of obj if the function pred is true"""
|
"""Returns the ancestor of obj if the function pred is true"""
|
||||||
|
|
||||||
if not AXObject.is_valid(obj):
|
if not AXObject.is_valid(obj):
|
||||||
@@ -703,7 +703,7 @@ class AXObject:
|
|||||||
return AXObject.find_ancestor(obj, lambda x: x == ancestor) is not None
|
return AXObject.find_ancestor(obj, lambda x: x == ancestor) is not None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_child(obj: Atspi.Accessible, index: int) -> Atspi.Accessible | None:
|
def get_child(obj: Atspi.Accessible, index: int) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the nth child of obj. See also get_child_checked."""
|
"""Returns the nth child of obj. See also get_child_checked."""
|
||||||
|
|
||||||
if not AXObject.is_valid(obj):
|
if not AXObject.is_valid(obj):
|
||||||
@@ -736,7 +736,7 @@ class AXObject:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_child_checked(
|
def get_child_checked(
|
||||||
obj: Atspi.Accessible, index: int
|
obj: Atspi.Accessible, index: int
|
||||||
) -> Atspi.Accessible | None:
|
) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the nth child of obj, doing checks for tree validity"""
|
"""Returns the nth child of obj, doing checks for tree validity"""
|
||||||
|
|
||||||
if not AXObject.is_valid(obj):
|
if not AXObject.is_valid(obj):
|
||||||
@@ -757,7 +757,7 @@ class AXObject:
|
|||||||
def get_active_descendant_checked(
|
def get_active_descendant_checked(
|
||||||
container: Atspi.Accessible,
|
container: Atspi.Accessible,
|
||||||
reported_child: Atspi.Accessible
|
reported_child: Atspi.Accessible
|
||||||
) -> Atspi.Accessible | None:
|
) -> Optional[Atspi.Accessible]:
|
||||||
"""Checks the reported active descendant and return the real/valid one."""
|
"""Checks the reported active descendant and return the real/valid one."""
|
||||||
|
|
||||||
if not AXObject.has_state(container, Atspi.StateType.MANAGES_DESCENDANTS):
|
if not AXObject.has_state(container, Atspi.StateType.MANAGES_DESCENDANTS):
|
||||||
@@ -784,7 +784,7 @@ class AXObject:
|
|||||||
def _find_descendant(
|
def _find_descendant(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
pred: Callable[[Atspi.Accessible], bool]
|
pred: Callable[[Atspi.Accessible], bool]
|
||||||
) -> Atspi.Accessible | None:
|
) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the descendant of obj if the function pred is true"""
|
"""Returns the descendant of obj if the function pred is true"""
|
||||||
|
|
||||||
if not AXObject.is_valid(obj):
|
if not AXObject.is_valid(obj):
|
||||||
@@ -806,7 +806,7 @@ class AXObject:
|
|||||||
def find_descendant(
|
def find_descendant(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
pred: Callable[[Atspi.Accessible], bool]
|
pred: Callable[[Atspi.Accessible], bool]
|
||||||
) -> Atspi.Accessible | None:
|
) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the descendant of obj if the function pred is true"""
|
"""Returns the descendant of obj if the function pred is true"""
|
||||||
|
|
||||||
start = time.time()
|
start = time.time()
|
||||||
@@ -816,7 +816,7 @@ class AXObject:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_deepest_descendant(obj: Atspi.Accessible) -> Atspi.Accessible | None:
|
def find_deepest_descendant(obj: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the deepest descendant of obj"""
|
"""Returns the deepest descendant of obj"""
|
||||||
|
|
||||||
if not AXObject.is_valid(obj):
|
if not AXObject.is_valid(obj):
|
||||||
@@ -831,8 +831,8 @@ class AXObject:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _find_all_descendants(
|
def _find_all_descendants(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
include_if: Callable[[Atspi.Accessible], bool] | None,
|
include_if: Optional[Callable[[Atspi.Accessible], bool]],
|
||||||
exclude_if: Callable[[Atspi.Accessible], bool] | None,
|
exclude_if: Optional[Callable[[Atspi.Accessible], bool]],
|
||||||
matches: list[Atspi.Accessible]
|
matches: list[Atspi.Accessible]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Returns all descendants which match the specified inclusion and exclusion"""
|
"""Returns all descendants which match the specified inclusion and exclusion"""
|
||||||
@@ -852,8 +852,8 @@ class AXObject:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def find_all_descendants(
|
def find_all_descendants(
|
||||||
root: Atspi.Accessible,
|
root: Atspi.Accessible,
|
||||||
include_if: Callable[[Atspi.Accessible], bool] | None = None,
|
include_if: Optional[Callable[[Atspi.Accessible], bool]] = None,
|
||||||
exclude_if: Callable[[Atspi.Accessible], bool] | None = None
|
exclude_if: Optional[Callable[[Atspi.Accessible], bool]] = None
|
||||||
) -> list[Atspi.Accessible]:
|
) -> list[Atspi.Accessible]:
|
||||||
"""Returns all descendants which match the specified inclusion and exclusion"""
|
"""Returns all descendants which match the specified inclusion and exclusion"""
|
||||||
|
|
||||||
@@ -1044,7 +1044,7 @@ class AXObject:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def iter_children(
|
def iter_children(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
pred: Callable[[Atspi.Accessible], bool] | None = None
|
pred: Optional[Callable[[Atspi.Accessible], bool]] = None
|
||||||
) -> Generator[Atspi.Accessible, None, None]:
|
) -> Generator[Atspi.Accessible, None, None]:
|
||||||
"""Generator to iterate through obj's children. If the function pred is
|
"""Generator to iterate through obj's children. If the function pred is
|
||||||
specified, children for which pred is False will be skipped."""
|
specified, children for which pred is False will be skipped."""
|
||||||
@@ -1068,7 +1068,7 @@ class AXObject:
|
|||||||
yield child
|
yield child
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_previous_sibling(obj: Atspi.Accessible) -> Atspi.Accessible | None:
|
def get_previous_sibling(obj: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the previous sibling of obj, based on child indices"""
|
"""Returns the previous sibling of obj, based on child indices"""
|
||||||
|
|
||||||
if not AXObject.is_valid(obj):
|
if not AXObject.is_valid(obj):
|
||||||
@@ -1091,7 +1091,7 @@ class AXObject:
|
|||||||
return sibling
|
return sibling
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_next_sibling(obj: Atspi.Accessible) -> Atspi.Accessible | None:
|
def get_next_sibling(obj: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the next sibling of obj, based on child indices"""
|
"""Returns the next sibling of obj, based on child indices"""
|
||||||
|
|
||||||
if not AXObject.is_valid(obj):
|
if not AXObject.is_valid(obj):
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ __date__ = "$Date$"
|
|||||||
__copyright__ = "Copyright (c) 2023 Igalia, S.L."
|
__copyright__ = "Copyright (c) 2023 Igalia, S.L."
|
||||||
__license__ = "LGPL"
|
__license__ = "LGPL"
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
import gi
|
import gi
|
||||||
gi.require_version("Atspi", "2.0")
|
gi.require_version("Atspi", "2.0")
|
||||||
from gi.repository import Atspi
|
from gi.repository import Atspi
|
||||||
@@ -60,7 +61,7 @@ class AXSelection:
|
|||||||
return count
|
return count
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_selected_child(obj: Atspi.Accessible, index: int) -> Atspi.Accessible | None:
|
def get_selected_child(obj: Atspi.Accessible, index: int) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the nth selected child of obj."""
|
"""Returns the nth selected child of obj."""
|
||||||
|
|
||||||
n_children = AXSelection.get_selected_child_count(obj)
|
n_children = AXSelection.get_selected_child_count(obj)
|
||||||
|
|||||||
+27
-27
@@ -35,7 +35,7 @@ __license__ = "LGPL"
|
|||||||
|
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from typing import Generator
|
from typing import Generator, Optional
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
gi.require_version("Atspi", "2.0")
|
gi.require_version("Atspi", "2.0")
|
||||||
@@ -62,11 +62,11 @@ class AXTable:
|
|||||||
PHYSICAL_SPANS_FROM_TABLE: dict[int, tuple[int, int]] = {}
|
PHYSICAL_SPANS_FROM_TABLE: dict[int, tuple[int, int]] = {}
|
||||||
PHYSICAL_COLUMN_COUNT: dict[int, int] = {}
|
PHYSICAL_COLUMN_COUNT: dict[int, int] = {}
|
||||||
PHYSICAL_ROW_COUNT: dict[int, int] = {}
|
PHYSICAL_ROW_COUNT: dict[int, int] = {}
|
||||||
PRESENTABLE_COORDINATES: dict[int, tuple[str | None, str | None]] = {}
|
PRESENTABLE_COORDINATES: dict[int, tuple[Optional[str], Optional[str]]] = {}
|
||||||
PRESENTABLE_COORDINATES_LABELS: dict[int, str] = {}
|
PRESENTABLE_COORDINATES_LABELS: dict[int, str] = {}
|
||||||
PRESENTABLE_SPANS: dict[int, tuple[str | None, str | None]] = {}
|
PRESENTABLE_SPANS: dict[int, tuple[Optional[str], Optional[str]]] = {}
|
||||||
PRESENTABLE_COLUMN_COUNT: dict[int, int | None] = {}
|
PRESENTABLE_COLUMN_COUNT: dict[int, Optional[int]] = {}
|
||||||
PRESENTABLE_ROW_COUNT: dict[int, int | None] = {}
|
PRESENTABLE_ROW_COUNT: dict[int, Optional[int]] = {}
|
||||||
COLUMN_HEADERS_FOR_CELL: dict[int, list[Atspi.Accessible]] = {}
|
COLUMN_HEADERS_FOR_CELL: dict[int, list[Atspi.Accessible]] = {}
|
||||||
ROW_HEADERS_FOR_CELL: dict[int, list[Atspi.Accessible]] = {}
|
ROW_HEADERS_FOR_CELL: dict[int, list[Atspi.Accessible]] = {}
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ class AXTable:
|
|||||||
AXTable._clear_all_dictionaries(reason)
|
AXTable._clear_all_dictionaries(reason)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_caption(table: Atspi.Accessible) -> Atspi.Accessible | None:
|
def get_caption(table: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the accessible object containing the caption of table."""
|
"""Returns the accessible object containing the caption of table."""
|
||||||
|
|
||||||
if not AXObject.supports_table(table):
|
if not AXObject.supports_table(table):
|
||||||
@@ -171,7 +171,7 @@ class AXTable:
|
|||||||
return count
|
return count
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_column_count_from_attribute(table: Atspi.Accessible) -> int | None:
|
def _get_column_count_from_attribute(table: Atspi.Accessible) -> Optional[int]:
|
||||||
"""Returns the value of the 'colcount' object attribute or None if not found."""
|
"""Returns the value of the 'colcount' object attribute or None if not found."""
|
||||||
|
|
||||||
if hash(table) in AXTable.PRESENTABLE_COLUMN_COUNT:
|
if hash(table) in AXTable.PRESENTABLE_COLUMN_COUNT:
|
||||||
@@ -217,7 +217,7 @@ class AXTable:
|
|||||||
return count
|
return count
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_row_count_from_attribute(table: Atspi.Accessible) -> int | None:
|
def _get_row_count_from_attribute(table: Atspi.Accessible) -> Optional[int]:
|
||||||
"""Returns the value of the 'rowcount' object attribute or None if not found."""
|
"""Returns the value of the 'rowcount' object attribute or None if not found."""
|
||||||
|
|
||||||
if hash(table) in AXTable.PRESENTABLE_ROW_COUNT:
|
if hash(table) in AXTable.PRESENTABLE_ROW_COUNT:
|
||||||
@@ -344,7 +344,7 @@ class AXTable:
|
|||||||
return AXTable.get_selected_column_count(table) == cols
|
return AXTable.get_selected_column_count(table) == cols
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_cell_at(table: Atspi.Accessible, row: int, column: int) -> Atspi.Accessible | None:
|
def get_cell_at(table: Atspi.Accessible, row: int, column: int) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the cell at the 0-indexed row and column."""
|
"""Returns the cell at the 0-indexed row and column."""
|
||||||
|
|
||||||
if not AXObject.supports_table(table):
|
if not AXObject.supports_table(table):
|
||||||
@@ -405,7 +405,7 @@ class AXTable:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_cell_spans_from_attribute(
|
def _get_cell_spans_from_attribute(
|
||||||
cell: Atspi.Accessible
|
cell: Atspi.Accessible
|
||||||
) -> tuple[str | None, str | None]:
|
) -> tuple[Optional[str], Optional[str]]:
|
||||||
"""Returns the row and column spans exposed via object attribute, or None, None."""
|
"""Returns the row and column spans exposed via object attribute, or None, None."""
|
||||||
|
|
||||||
if hash(cell) in AXTable.PRESENTABLE_SPANS:
|
if hash(cell) in AXTable.PRESENTABLE_SPANS:
|
||||||
@@ -606,7 +606,7 @@ class AXTable:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_new_row_headers(
|
def get_new_row_headers(
|
||||||
cell: Atspi.Accessible,
|
cell: Atspi.Accessible,
|
||||||
old_cell: Atspi.Accessible | None
|
old_cell: Optional[Atspi.Accessible]
|
||||||
) -> list[Atspi.Accessible]:
|
) -> list[Atspi.Accessible]:
|
||||||
"""Returns row headers of cell that are not also headers of old_cell. """
|
"""Returns row headers of cell that are not also headers of old_cell. """
|
||||||
|
|
||||||
@@ -623,7 +623,7 @@ class AXTable:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_new_column_headers(
|
def get_new_column_headers(
|
||||||
cell: Atspi.Accessible,
|
cell: Atspi.Accessible,
|
||||||
old_cell: Atspi.Accessible | None
|
old_cell: Optional[Atspi.Accessible]
|
||||||
) -> list[Atspi.Accessible]:
|
) -> list[Atspi.Accessible]:
|
||||||
"""Returns column headers of cell that are not also headers of old_cell. """
|
"""Returns column headers of cell that are not also headers of old_cell. """
|
||||||
|
|
||||||
@@ -880,7 +880,7 @@ class AXTable:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_cell_coordinates_from_attribute(
|
def _get_cell_coordinates_from_attribute(
|
||||||
cell: Atspi.Accessible
|
cell: Atspi.Accessible
|
||||||
) -> tuple[str | None, str | None]:
|
) -> tuple[Optional[str], Optional[str]]:
|
||||||
"""Returns the 1-based indices for cell exposed via object attribute, or None, None."""
|
"""Returns the 1-based indices for cell exposed via object attribute, or None, None."""
|
||||||
|
|
||||||
if cell is None:
|
if cell is None:
|
||||||
@@ -941,7 +941,7 @@ class AXTable:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_table(obj: Atspi.Accessible) -> Atspi.Accessible | None:
|
def get_table(obj: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns obj if it is a table, otherwise returns the ancestor table of obj."""
|
"""Returns obj if it is a table, otherwise returns the ancestor table of obj."""
|
||||||
|
|
||||||
if obj is None:
|
if obj is None:
|
||||||
@@ -981,21 +981,21 @@ class AXTable:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_first_cell(table: Atspi.Accessible) -> Atspi.Accessible | None:
|
def get_first_cell(table: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the first cell in table."""
|
"""Returns the first cell in table."""
|
||||||
|
|
||||||
row, col = 0, 0
|
row, col = 0, 0
|
||||||
return AXTable.get_cell_at(table, row, col)
|
return AXTable.get_cell_at(table, row, col)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_last_cell(table: Atspi.Accessible) -> Atspi.Accessible | None:
|
def get_last_cell(table: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the last cell in table."""
|
"""Returns the last cell in table."""
|
||||||
|
|
||||||
row, col = AXTable.get_row_count(table) - 1, AXTable.get_column_count(table) - 1
|
row, col = AXTable.get_row_count(table) - 1, AXTable.get_column_count(table) - 1
|
||||||
return AXTable.get_cell_at(table, row, col)
|
return AXTable.get_cell_at(table, row, col)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_cell_above(cell: Atspi.Accessible) -> Atspi.Accessible | None:
|
def get_cell_above(cell: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the cell above cell in table."""
|
"""Returns the cell above cell in table."""
|
||||||
|
|
||||||
row, col = AXTable.get_cell_coordinates(cell, prefer_attribute=False)
|
row, col = AXTable.get_cell_coordinates(cell, prefer_attribute=False)
|
||||||
@@ -1003,7 +1003,7 @@ class AXTable:
|
|||||||
return AXTable.get_cell_at(AXTable.get_table(cell), row, col)
|
return AXTable.get_cell_at(AXTable.get_table(cell), row, col)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_cell_below(cell: Atspi.Accessible) -> Atspi.Accessible | None:
|
def get_cell_below(cell: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the cell below cell in table."""
|
"""Returns the cell below cell in table."""
|
||||||
|
|
||||||
row, col = AXTable.get_cell_coordinates(cell, prefer_attribute=False)
|
row, col = AXTable.get_cell_coordinates(cell, prefer_attribute=False)
|
||||||
@@ -1011,7 +1011,7 @@ class AXTable:
|
|||||||
return AXTable.get_cell_at(AXTable.get_table(cell), row, col)
|
return AXTable.get_cell_at(AXTable.get_table(cell), row, col)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_cell_on_left(cell: Atspi.Accessible) -> Atspi.Accessible | None:
|
def get_cell_on_left(cell: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the cell to the left."""
|
"""Returns the cell to the left."""
|
||||||
|
|
||||||
row, col = AXTable.get_cell_coordinates(cell, prefer_attribute=False)
|
row, col = AXTable.get_cell_coordinates(cell, prefer_attribute=False)
|
||||||
@@ -1019,7 +1019,7 @@ class AXTable:
|
|||||||
return AXTable.get_cell_at(AXTable.get_table(cell), row, col)
|
return AXTable.get_cell_at(AXTable.get_table(cell), row, col)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_cell_on_right(cell: Atspi.Accessible) -> Atspi.Accessible | None:
|
def get_cell_on_right(cell: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the cell to the right."""
|
"""Returns the cell to the right."""
|
||||||
|
|
||||||
row, col = AXTable.get_cell_coordinates(cell, prefer_attribute=False)
|
row, col = AXTable.get_cell_coordinates(cell, prefer_attribute=False)
|
||||||
@@ -1027,14 +1027,14 @@ class AXTable:
|
|||||||
return AXTable.get_cell_at(AXTable.get_table(cell), row, col)
|
return AXTable.get_cell_at(AXTable.get_table(cell), row, col)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_start_of_row(cell: Atspi.Accessible) -> Atspi.Accessible | None:
|
def get_start_of_row(cell: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the cell at the start of cell's row."""
|
"""Returns the cell at the start of cell's row."""
|
||||||
|
|
||||||
row = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[0]
|
row = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[0]
|
||||||
return AXTable.get_cell_at(AXTable.get_table(cell), row, 0)
|
return AXTable.get_cell_at(AXTable.get_table(cell), row, 0)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_end_of_row(cell: Atspi.Accessible) -> Atspi.Accessible | None:
|
def get_end_of_row(cell: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the cell at the end of cell's row."""
|
"""Returns the cell at the end of cell's row."""
|
||||||
|
|
||||||
row = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[0]
|
row = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[0]
|
||||||
@@ -1043,14 +1043,14 @@ class AXTable:
|
|||||||
return AXTable.get_cell_at(AXTable.get_table(cell), row, col)
|
return AXTable.get_cell_at(AXTable.get_table(cell), row, col)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_top_of_column(cell: Atspi.Accessible) -> Atspi.Accessible | None:
|
def get_top_of_column(cell: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the cell at the top of cell's column."""
|
"""Returns the cell at the top of cell's column."""
|
||||||
|
|
||||||
col = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[1]
|
col = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[1]
|
||||||
return AXTable.get_cell_at(AXTable.get_table(cell), 0, col)
|
return AXTable.get_cell_at(AXTable.get_table(cell), 0, col)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_bottom_of_column(cell: Atspi.Accessible) -> Atspi.Accessible | None:
|
def get_bottom_of_column(cell: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the cell at the bottom of cell's column."""
|
"""Returns the cell at the bottom of cell's column."""
|
||||||
|
|
||||||
col = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[1]
|
col = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[1]
|
||||||
@@ -1059,7 +1059,7 @@ class AXTable:
|
|||||||
return AXTable.get_cell_at(AXTable.get_table(cell), row, col)
|
return AXTable.get_cell_at(AXTable.get_table(cell), row, col)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_cell_formula(cell: Atspi.Accessible) -> str | None:
|
def get_cell_formula(cell: Atspi.Accessible) -> Optional[str]:
|
||||||
"""Returns the formula associated with this cell."""
|
"""Returns the formula associated with this cell."""
|
||||||
|
|
||||||
attrs = AXObject.get_attributes_dict(cell, use_cache=False)
|
attrs = AXObject.get_attributes_dict(cell, use_cache=False)
|
||||||
@@ -1192,7 +1192,7 @@ class AXTable:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_dynamic_row_header(cell: Atspi.Accessible) -> Atspi.Accessible | None:
|
def get_dynamic_row_header(cell: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the user-set row header for cell."""
|
"""Returns the user-set row header for cell."""
|
||||||
|
|
||||||
table = AXTable.get_table(cell)
|
table = AXTable.get_table(cell)
|
||||||
@@ -1207,7 +1207,7 @@ class AXTable:
|
|||||||
return AXTable.get_cell_at(table, cell_row, headers_column)
|
return AXTable.get_cell_at(table, cell_row, headers_column)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_dynamic_column_header(cell: Atspi.Accessible) -> Atspi.Accessible | None:
|
def get_dynamic_column_header(cell: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the user-set column header for cell."""
|
"""Returns the user-set column header for cell."""
|
||||||
|
|
||||||
table = AXTable.get_table(cell)
|
table = AXTable.get_table(cell)
|
||||||
|
|||||||
+32
-32
@@ -44,7 +44,7 @@ __license__ = "LGPL"
|
|||||||
import enum
|
import enum
|
||||||
import locale
|
import locale
|
||||||
import re
|
import re
|
||||||
from typing import Generator
|
from typing import Generator, Optional
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
gi.require_version("Atspi", "2.0")
|
gi.require_version("Atspi", "2.0")
|
||||||
@@ -104,7 +104,7 @@ class AXTextAttribute(enum.Enum):
|
|||||||
WRITING_MODE = ("writing-mode", False)
|
WRITING_MODE = ("writing-mode", False)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_string(cls, string: str) -> "AXTextAttribute" | None:
|
def from_string(cls, string: str) -> Optional['AXTextAttribute']:
|
||||||
"""Returns the AXTextAttribute for the specified string."""
|
"""Returns the AXTextAttribute for the specified string."""
|
||||||
|
|
||||||
for attribute in cls:
|
for attribute in cls:
|
||||||
@@ -114,7 +114,7 @@ class AXTextAttribute(enum.Enum):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_localized_string(cls, string: str) -> "AXTextAttribute" | None:
|
def from_localized_string(cls, string: str) -> Optional['AXTextAttribute']:
|
||||||
"""Returns the AXTextAttribute for the specified localized string."""
|
"""Returns the AXTextAttribute for the specified localized string."""
|
||||||
|
|
||||||
for attribute in cls:
|
for attribute in cls:
|
||||||
@@ -192,7 +192,7 @@ class AXText:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_character_at_offset(
|
def get_character_at_offset(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
offset: int | None = None
|
offset: Optional[int] = None
|
||||||
) -> tuple[str, int, int]:
|
) -> tuple[str, int, int]:
|
||||||
"""Returns the (character, start, end) for the current or specified offset."""
|
"""Returns the (character, start, end) for the current or specified offset."""
|
||||||
|
|
||||||
@@ -239,7 +239,7 @@ class AXText:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_next_character(
|
def get_next_character(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
offset: int | None = None
|
offset: Optional[int] = None
|
||||||
) -> tuple[str, int, int]:
|
) -> tuple[str, int, int]:
|
||||||
"""Returns the next (character, start, end) for the current or specified offset."""
|
"""Returns the next (character, start, end) for the current or specified offset."""
|
||||||
|
|
||||||
@@ -264,7 +264,7 @@ class AXText:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_previous_character(
|
def get_previous_character(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
offset: int | None = None
|
offset: Optional[int] = None
|
||||||
) -> tuple[str, int, int]:
|
) -> tuple[str, int, int]:
|
||||||
"""Returns the previous (character, start, end) for the current or specified offset."""
|
"""Returns the previous (character, start, end) for the current or specified offset."""
|
||||||
|
|
||||||
@@ -291,7 +291,7 @@ class AXText:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def iter_character(
|
def iter_character(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
offset: int | None = None
|
offset: Optional[int] = None
|
||||||
) -> Generator[tuple[str, int, int], None, None]:
|
) -> Generator[tuple[str, int, int], None, None]:
|
||||||
"""Generator to iterate by character in obj starting with the character at offset."""
|
"""Generator to iterate by character in obj starting with the character at offset."""
|
||||||
|
|
||||||
@@ -312,7 +312,7 @@ class AXText:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_word_at_offset(
|
def get_word_at_offset(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
offset: int | None = None
|
offset: Optional[int] = None
|
||||||
) -> tuple[str, int, int]:
|
) -> tuple[str, int, int]:
|
||||||
"""Returns the (word, start, end) for the current or specified offset."""
|
"""Returns the (word, start, end) for the current or specified offset."""
|
||||||
|
|
||||||
@@ -354,7 +354,7 @@ class AXText:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_next_word(
|
def get_next_word(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
offset: int | None = None
|
offset: Optional[int] = None
|
||||||
) -> tuple[str, int, int]:
|
) -> tuple[str, int, int]:
|
||||||
"""Returns the next (word, start, end) for the current or specified offset."""
|
"""Returns the next (word, start, end) for the current or specified offset."""
|
||||||
|
|
||||||
@@ -379,7 +379,7 @@ class AXText:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_previous_word(
|
def get_previous_word(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
offset: int | None = None
|
offset: Optional[int] = None
|
||||||
) -> tuple[str, int, int]:
|
) -> tuple[str, int, int]:
|
||||||
"""Returns the previous (word, start, end) for the current or specified offset."""
|
"""Returns the previous (word, start, end) for the current or specified offset."""
|
||||||
|
|
||||||
@@ -406,7 +406,7 @@ class AXText:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def iter_word(
|
def iter_word(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
offset: int | None = None
|
offset: Optional[int] = None
|
||||||
) -> Generator[tuple[str, int, int], None, None]:
|
) -> Generator[tuple[str, int, int], None, None]:
|
||||||
"""Generator to iterate by word in obj starting with the word at offset."""
|
"""Generator to iterate by word in obj starting with the word at offset."""
|
||||||
|
|
||||||
@@ -427,7 +427,7 @@ class AXText:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_line_at_offset(
|
def get_line_at_offset(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
offset: int | None = None
|
offset: Optional[int] = None
|
||||||
) -> tuple[str, int, int]:
|
) -> tuple[str, int, int]:
|
||||||
"""Returns the (line, start, end) for the current or specified offset."""
|
"""Returns the (line, start, end) for the current or specified offset."""
|
||||||
|
|
||||||
@@ -495,7 +495,7 @@ class AXText:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_next_line(
|
def get_next_line(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
offset: int | None = None
|
offset: Optional[int] = None
|
||||||
) -> tuple[str, int, int]:
|
) -> tuple[str, int, int]:
|
||||||
"""Returns the next (line, start, end) for the current or specified offset."""
|
"""Returns the next (line, start, end) for the current or specified offset."""
|
||||||
|
|
||||||
@@ -520,7 +520,7 @@ class AXText:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_previous_line(
|
def get_previous_line(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
offset: int | None = None
|
offset: Optional[int] = None
|
||||||
) -> tuple[str, int, int]:
|
) -> tuple[str, int, int]:
|
||||||
"""Returns the previous (line, start, end) for the current or specified offset."""
|
"""Returns the previous (line, start, end) for the current or specified offset."""
|
||||||
|
|
||||||
@@ -549,7 +549,7 @@ class AXText:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def iter_line(
|
def iter_line(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
offset: int | None = None
|
offset: Optional[int] = None
|
||||||
) -> Generator[tuple[str, int, int], None, None]:
|
) -> Generator[tuple[str, int, int], None, None]:
|
||||||
"""Generator to iterate by line in obj starting with the line at offset."""
|
"""Generator to iterate by line in obj starting with the line at offset."""
|
||||||
|
|
||||||
@@ -606,7 +606,7 @@ class AXText:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_sentence_at_offset_fallback(
|
def _get_sentence_at_offset_fallback(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
offset: int | None = None
|
offset: Optional[int] = None
|
||||||
) -> tuple[str, int, int]:
|
) -> tuple[str, int, int]:
|
||||||
"""Fallback sentence detection for broken implementations."""
|
"""Fallback sentence detection for broken implementations."""
|
||||||
|
|
||||||
@@ -633,7 +633,7 @@ class AXText:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_sentence_at_offset(
|
def get_sentence_at_offset(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
offset: int | None = None
|
offset: Optional[int] = None
|
||||||
) -> tuple[str, int, int]:
|
) -> tuple[str, int, int]:
|
||||||
"""Returns the (sentence, start, end) for the current or specified offset."""
|
"""Returns the (sentence, start, end) for the current or specified offset."""
|
||||||
|
|
||||||
@@ -682,7 +682,7 @@ class AXText:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_next_sentence(
|
def get_next_sentence(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
offset: int | None = None
|
offset: Optional[int] = None
|
||||||
) -> tuple[str, int, int]:
|
) -> tuple[str, int, int]:
|
||||||
"""Returns the next (sentence, start, end) for the current or specified offset."""
|
"""Returns the next (sentence, start, end) for the current or specified offset."""
|
||||||
|
|
||||||
@@ -707,7 +707,7 @@ class AXText:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_previous_sentence(
|
def get_previous_sentence(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
offset: int | None = None
|
offset: Optional[int] = None
|
||||||
) -> tuple[str, int, int]:
|
) -> tuple[str, int, int]:
|
||||||
"""Returns the previous (sentence, start, end) for the current or specified offset."""
|
"""Returns the previous (sentence, start, end) for the current or specified offset."""
|
||||||
|
|
||||||
@@ -734,7 +734,7 @@ class AXText:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def iter_sentence(
|
def iter_sentence(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
offset: int | None = None
|
offset: Optional[int] = None
|
||||||
) -> Generator[tuple[str, int, int], None, None]:
|
) -> Generator[tuple[str, int, int], None, None]:
|
||||||
"""Generator to iterate by sentence in obj starting with the sentence at offset."""
|
"""Generator to iterate by sentence in obj starting with the sentence at offset."""
|
||||||
|
|
||||||
@@ -759,7 +759,7 @@ class AXText:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_paragraph_at_offset(
|
def get_paragraph_at_offset(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
offset: int | None = None
|
offset: Optional[int] = None
|
||||||
) -> tuple[str, int, int]:
|
) -> tuple[str, int, int]:
|
||||||
"""Returns the (paragraph, start, end) for the current or specified offset."""
|
"""Returns the (paragraph, start, end) for the current or specified offset."""
|
||||||
|
|
||||||
@@ -801,7 +801,7 @@ class AXText:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_next_paragraph(
|
def get_next_paragraph(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
offset: int | None = None
|
offset: Optional[int] = None
|
||||||
) -> tuple[str, int, int]:
|
) -> tuple[str, int, int]:
|
||||||
"""Returns the next (paragraph, start, end) for the current or specified offset."""
|
"""Returns the next (paragraph, start, end) for the current or specified offset."""
|
||||||
|
|
||||||
@@ -826,7 +826,7 @@ class AXText:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_previous_paragraph(
|
def get_previous_paragraph(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
offset: int | None = None
|
offset: Optional[int] = None
|
||||||
) -> tuple[str, int, int]:
|
) -> tuple[str, int, int]:
|
||||||
"""Returns the previous (paragraph, start, end) for the current or specified offset."""
|
"""Returns the previous (paragraph, start, end) for the current or specified offset."""
|
||||||
|
|
||||||
@@ -852,7 +852,7 @@ class AXText:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def iter_paragraph(
|
def iter_paragraph(
|
||||||
obj: Atspi.Accessible, offset: int | None = None
|
obj: Atspi.Accessible, offset: Optional[int] = None
|
||||||
) -> Generator[tuple[str, int, int], None, None]:
|
) -> Generator[tuple[str, int, int], None, None]:
|
||||||
"""Generator to iterate by paragraph in obj starting with the paragraph at offset."""
|
"""Generator to iterate by paragraph in obj starting with the paragraph at offset."""
|
||||||
|
|
||||||
@@ -1206,7 +1206,7 @@ class AXText:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_text_attributes_at_offset(
|
def get_text_attributes_at_offset(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
offset: int | None = None
|
offset: Optional[int] = None
|
||||||
) -> tuple[dict[str, str], int, int]:
|
) -> tuple[dict[str, str], int, int]:
|
||||||
"""Returns a (dict, start, end) tuple for attributes at offset in obj."""
|
"""Returns a (dict, start, end) tuple for attributes at offset in obj."""
|
||||||
|
|
||||||
@@ -1303,7 +1303,7 @@ class AXText:
|
|||||||
return offset
|
return offset
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_character_rect(obj: Atspi.Accessible, offset: int | None = None) -> Atspi.Rect:
|
def get_character_rect(obj: Atspi.Accessible, offset: Optional[int] = None) -> Atspi.Rect:
|
||||||
"""Returns the Atspi rect of the character at the specified offset in obj."""
|
"""Returns the Atspi rect of the character at the specified offset in obj."""
|
||||||
|
|
||||||
if not AXObject.supports_text(obj):
|
if not AXObject.supports_text(obj):
|
||||||
@@ -1487,7 +1487,7 @@ class AXText:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def string_has_spelling_error(obj: Atspi.Accessible, offset: int | None = None) -> bool:
|
def string_has_spelling_error(obj: Atspi.Accessible, offset: Optional[int] = None) -> bool:
|
||||||
"""Returns True if the text attributes indicate a spelling error."""
|
"""Returns True if the text attributes indicate a spelling error."""
|
||||||
|
|
||||||
attributes = AXText.get_text_attributes_at_offset(obj, offset)[0]
|
attributes = AXText.get_text_attributes_at_offset(obj, offset)[0]
|
||||||
@@ -1502,7 +1502,7 @@ class AXText:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def string_has_grammar_error(obj: Atspi.Accessible, offset: int | None = None) -> bool:
|
def string_has_grammar_error(obj: Atspi.Accessible, offset: Optional[int] = None) -> bool:
|
||||||
"""Returns True if the text attributes indicate a grammar error."""
|
"""Returns True if the text attributes indicate a grammar error."""
|
||||||
|
|
||||||
attributes = AXText.get_text_attributes_at_offset(obj, offset)[0]
|
attributes = AXText.get_text_attributes_at_offset(obj, offset)[0]
|
||||||
@@ -1552,8 +1552,8 @@ class AXText:
|
|||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
x: int,
|
x: int,
|
||||||
y: int,
|
y: int,
|
||||||
start_offset: int | None = None,
|
start_offset: Optional[int] = None,
|
||||||
end_offset: int | None = None
|
end_offset: Optional[int] = None
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Attempts to scroll obj to the specified point."""
|
"""Attempts to scroll obj to the specified point."""
|
||||||
|
|
||||||
@@ -1583,8 +1583,8 @@ class AXText:
|
|||||||
def scroll_substring_to_location(
|
def scroll_substring_to_location(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
location: Atspi.ScrollType,
|
location: Atspi.ScrollType,
|
||||||
start_offset: int | None = None,
|
start_offset: Optional[int] = None,
|
||||||
end_offset: int | None = None
|
end_offset: Optional[int] = None
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Attempts to scroll the substring to the specified Atspi.ScrollType location."""
|
"""Attempts to scroll the substring to the specified Atspi.ScrollType location."""
|
||||||
|
|
||||||
|
|||||||
+13
-12
@@ -34,6 +34,7 @@ __date__ = "$Date$"
|
|||||||
__copyright__ = "Copyright (c) 2023-2025 Igalia, S.L."
|
__copyright__ = "Copyright (c) 2023-2025 Igalia, S.L."
|
||||||
__license__ = "LGPL"
|
__license__ = "LGPL"
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
import queue
|
import queue
|
||||||
@@ -97,7 +98,7 @@ class AXUtilities:
|
|||||||
AXUtilities.IS_LAYOUT_ONLY.clear()
|
AXUtilities.IS_LAYOUT_ONLY.clear()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def clear_all_cache_now(obj: Atspi.Accessible | None = None, reason: str = "") -> None:
|
def clear_all_cache_now(obj: Optional[Atspi.Accessible] = None, reason: str = "") -> None:
|
||||||
"""Clears all cached information immediately."""
|
"""Clears all cached information immediately."""
|
||||||
|
|
||||||
AXUtilities._clear_all_dictionaries(reason)
|
AXUtilities._clear_all_dictionaries(reason)
|
||||||
@@ -152,7 +153,7 @@ class AXUtilities:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_active_window() -> Atspi.Accessible | None:
|
def find_active_window() -> Optional[Atspi.Accessible]:
|
||||||
"""Tries to locate the active window; may or may not succeed."""
|
"""Tries to locate the active window; may or may not succeed."""
|
||||||
|
|
||||||
candidates = []
|
candidates = []
|
||||||
@@ -192,7 +193,7 @@ class AXUtilities:
|
|||||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||||
return filtered[0]
|
return filtered[0]
|
||||||
|
|
||||||
guess: Atspi.Accessible | None = None
|
guess: Optional[Atspi.Accessible] = None
|
||||||
if filtered:
|
if filtered:
|
||||||
tokens = ["AXUtilities: Still have multiple active windows:", filtered]
|
tokens = ["AXUtilities: Still have multiple active windows:", filtered]
|
||||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||||
@@ -266,7 +267,7 @@ class AXUtilities:
|
|||||||
return AXObject.find_all_descendants(obj, is_match)
|
return AXObject.find_all_descendants(obj, is_match)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_default_button(obj: Atspi.Accessible) -> Atspi.Accessible | None:
|
def get_default_button(obj: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the default button descendant of obj"""
|
"""Returns the default button descendant of obj"""
|
||||||
|
|
||||||
result = None
|
result = None
|
||||||
@@ -278,7 +279,7 @@ class AXUtilities:
|
|||||||
return AXObject.find_descendant(obj, AXUtilitiesRole.is_default_button)
|
return AXObject.find_descendant(obj, AXUtilitiesRole.is_default_button)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_focused_object(obj: Atspi.Accessible) -> Atspi.Accessible | None:
|
def get_focused_object(obj: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the focused descendant of obj"""
|
"""Returns the focused descendant of obj"""
|
||||||
|
|
||||||
result = None
|
result = None
|
||||||
@@ -290,7 +291,7 @@ class AXUtilities:
|
|||||||
return AXObject.find_descendant(obj, AXUtilitiesState.is_focused)
|
return AXObject.find_descendant(obj, AXUtilitiesState.is_focused)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_info_bar(obj: Atspi.Accessible) -> Atspi.Accessible | None:
|
def get_info_bar(obj: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the info bar descendant of obj"""
|
"""Returns the info bar descendant of obj"""
|
||||||
|
|
||||||
result = None
|
result = None
|
||||||
@@ -302,7 +303,7 @@ class AXUtilities:
|
|||||||
return AXObject.find_descendant(obj, AXUtilitiesRole.is_info_bar)
|
return AXObject.find_descendant(obj, AXUtilitiesRole.is_info_bar)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_status_bar(obj: Atspi.Accessible) -> Atspi.Accessible | None:
|
def get_status_bar(obj: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the status bar descendant of obj"""
|
"""Returns the status bar descendant of obj"""
|
||||||
|
|
||||||
result = None
|
result = None
|
||||||
@@ -748,7 +749,7 @@ class AXUtilities:
|
|||||||
return len(ancestors)
|
return len(ancestors)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_next_object(obj: Atspi.Accessible) -> Atspi.Accessible | None:
|
def get_next_object(obj: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the next object (depth first, unless there's a flows-to relation)"""
|
"""Returns the next object (depth first, unless there's a flows-to relation)"""
|
||||||
|
|
||||||
if not AXObject.is_valid(obj):
|
if not AXObject.is_valid(obj):
|
||||||
@@ -778,7 +779,7 @@ class AXUtilities:
|
|||||||
return next_object
|
return next_object
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_previous_object(obj: Atspi.Accessible) -> Atspi.Accessible | None:
|
def get_previous_object(obj: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the previous object (depth first, unless there's a flows-from relation)"""
|
"""Returns the previous object (depth first, unless there's a flows-from relation)"""
|
||||||
|
|
||||||
if not AXObject.is_valid(obj):
|
if not AXObject.is_valid(obj):
|
||||||
@@ -810,7 +811,7 @@ class AXUtilities:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def is_on_screen(
|
def is_on_screen(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
bounding_box: Atspi.Rect | None = None
|
bounding_box: Optional[Atspi.Rect] = None
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Returns true if obj should be treated as being on screen."""
|
"""Returns true if obj should be treated as being on screen."""
|
||||||
|
|
||||||
@@ -887,7 +888,7 @@ class AXUtilities:
|
|||||||
def _get_on_screen_objects(
|
def _get_on_screen_objects(
|
||||||
root: Atspi.Accessible,
|
root: Atspi.Accessible,
|
||||||
cancellation_event: threading.Event,
|
cancellation_event: threading.Event,
|
||||||
bounding_box: Atspi.Rect | None = None
|
bounding_box: Optional[Atspi.Rect] = None
|
||||||
) -> list:
|
) -> list:
|
||||||
|
|
||||||
tokens = ["AXUtilities: Getting on-screen objects in", root, f"({hex(id(root))})"]
|
tokens = ["AXUtilities: Getting on-screen objects in", root, f"({hex(id(root))})"]
|
||||||
@@ -939,7 +940,7 @@ class AXUtilities:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_on_screen_objects(
|
def get_on_screen_objects(
|
||||||
root: Atspi.Accessible,
|
root: Atspi.Accessible,
|
||||||
bounding_box: Atspi.Rect | None = None,
|
bounding_box: Optional[Atspi.Rect] = None,
|
||||||
timeout: float = 5.0
|
timeout: float = 5.0
|
||||||
) -> list:
|
) -> list:
|
||||||
"""Returns a list of onscreen objects in the given root."""
|
"""Returns a list of onscreen objects in the given root."""
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ __copyright__ = "Copyright (c) 2023-2024 Igalia, S.L." \
|
|||||||
"Copyright (c) 2024 GNOME Foundation Inc."
|
"Copyright (c) 2024 GNOME Foundation Inc."
|
||||||
__license__ = "LGPL"
|
__license__ = "LGPL"
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
@@ -82,7 +83,7 @@ class AXUtilitiesApplication:
|
|||||||
return list(AXObject.iter_children(desktop, pred))
|
return list(AXObject.iter_children(desktop, pred))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_application(obj: Atspi.Accessible) -> Atspi.Accessible | None:
|
def get_application(obj: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the accessible application associated with obj"""
|
"""Returns the accessible application associated with obj"""
|
||||||
|
|
||||||
if obj is None:
|
if obj is None:
|
||||||
@@ -131,7 +132,7 @@ class AXUtilitiesApplication:
|
|||||||
return version
|
return version
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_application_with_pid(pid: int) -> Atspi.Accessible | None:
|
def get_application_with_pid(pid: int) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the accessible application with the specified pid"""
|
"""Returns the accessible application with the specified pid"""
|
||||||
|
|
||||||
applications = AXUtilitiesApplication.get_all_applications()
|
applications = AXUtilitiesApplication.get_all_applications()
|
||||||
@@ -144,7 +145,7 @@ class AXUtilitiesApplication:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_desktop() -> Atspi.Accessible | None:
|
def get_desktop() -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the accessible desktop"""
|
"""Returns the accessible desktop"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,7 @@ __copyright__ = "Copyright (c) 2024 Igalia, S.L." \
|
|||||||
"Copyright (c) 2024 GNOME Foundation Inc."
|
"Copyright (c) 2024 GNOME Foundation Inc."
|
||||||
__license__ = "LGPL"
|
__license__ = "LGPL"
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@@ -118,7 +119,7 @@ class AXUtilitiesRelation:
|
|||||||
def _get_relation(
|
def _get_relation(
|
||||||
obj: Atspi.Accessible,
|
obj: Atspi.Accessible,
|
||||||
relation_type: Atspi.RelationType
|
relation_type: Atspi.RelationType
|
||||||
) -> Atspi.Relation | None:
|
) -> Optional[Atspi.Relation]:
|
||||||
"""Returns the specified Atspi.Relation for obj"""
|
"""Returns the specified Atspi.Relation for obj"""
|
||||||
|
|
||||||
for relation in AXUtilitiesRelation.get_relations(obj):
|
for relation in AXUtilitiesRelation.get_relations(obj):
|
||||||
|
|||||||
+242
-241
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,7 @@ __copyright__ = "Copyright (c) 2024 Igalia, S.L." \
|
|||||||
"Copyright (c) 2024 GNOME Foundation Inc."
|
"Copyright (c) 2024 GNOME Foundation Inc."
|
||||||
__license__ = "LGPL"
|
__license__ = "LGPL"
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@@ -149,7 +150,7 @@ class AXValue:
|
|||||||
return f"{current:.{decimal_places}f}"
|
return f"{current:.{decimal_places}f}"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_value_as_percent(obj: Atspi.Accessible) -> int | None:
|
def get_value_as_percent(obj: Atspi.Accessible) -> Optional[int]:
|
||||||
"""Returns the current value as a percent, or None if that is not applicable."""
|
"""Returns the current value as a percent, or None if that is not applicable."""
|
||||||
|
|
||||||
if not AXObject.supports_value(obj):
|
if not AXObject.supports_value(obj):
|
||||||
|
|||||||
@@ -3571,9 +3571,8 @@
|
|||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="hexpand">True</property>
|
<property name="hexpand">True</property>
|
||||||
<items>
|
<items>
|
||||||
<item translatable="yes">Claude Code (Enhanced)</item>
|
<item translatable="yes">Claude Code (CLI)</item>
|
||||||
<item translatable="yes">Claude (Anthropic)</item>
|
<item translatable="yes">Codex (CLI)</item>
|
||||||
<item translatable="yes">ChatGPT (OpenAI)</item>
|
|
||||||
<item translatable="yes">Gemini (Google)</item>
|
<item translatable="yes">Gemini (Google)</item>
|
||||||
<item translatable="yes">Ollama (Local - Free)</item>
|
<item translatable="yes">Ollama (Local - Free)</item>
|
||||||
</items>
|
</items>
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ from . import cthulhu_state
|
|||||||
from . import settings
|
from . import settings
|
||||||
from . import settings_manager
|
from . import settings_manager
|
||||||
from . import input_event
|
from . import input_event
|
||||||
|
from . import input_event_manager
|
||||||
from . import keybindings
|
from . import keybindings
|
||||||
from . import pronunciation_dict
|
from . import pronunciation_dict
|
||||||
from . import braille
|
from . import braille
|
||||||
@@ -64,6 +65,7 @@ from . import speech
|
|||||||
from . import speechserver
|
from . import speechserver
|
||||||
from . import text_attribute_names
|
from . import text_attribute_names
|
||||||
from . import sound_theme_manager
|
from . import sound_theme_manager
|
||||||
|
from . import script_manager
|
||||||
from .ax_object import AXObject
|
from .ax_object import AXObject
|
||||||
|
|
||||||
_settingsManager = settings_manager.getManager()
|
_settingsManager = settings_manager.getManager()
|
||||||
@@ -1874,10 +1876,12 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
|||||||
# Set provider combo
|
# Set provider combo
|
||||||
provider = prefs.get("aiProvider", settings.aiProvider)
|
provider = prefs.get("aiProvider", settings.aiProvider)
|
||||||
providerIndex = 0 # Default to Claude Code
|
providerIndex = 0 # Default to Claude Code
|
||||||
if provider == settings.AI_PROVIDER_GEMINI:
|
if provider == settings.AI_PROVIDER_CODEX:
|
||||||
providerIndex = 1
|
providerIndex = 1
|
||||||
elif provider == settings.AI_PROVIDER_OLLAMA:
|
elif provider == settings.AI_PROVIDER_GEMINI:
|
||||||
providerIndex = 2
|
providerIndex = 2
|
||||||
|
elif provider == settings.AI_PROVIDER_OLLAMA:
|
||||||
|
providerIndex = 3
|
||||||
self.aiProviderCombo.set_active(providerIndex)
|
self.aiProviderCombo.set_active(providerIndex)
|
||||||
|
|
||||||
# Set API key file
|
# Set API key file
|
||||||
@@ -1947,6 +1951,8 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
|||||||
# Update labels based on provider
|
# Update labels based on provider
|
||||||
if provider == settings.AI_PROVIDER_CLAUDE_CODE:
|
if provider == settings.AI_PROVIDER_CLAUDE_CODE:
|
||||||
self.aiApiKeyEntry.set_placeholder_text("No API key needed - uses Claude Code CLI")
|
self.aiApiKeyEntry.set_placeholder_text("No API key needed - uses Claude Code CLI")
|
||||||
|
elif provider == settings.AI_PROVIDER_CODEX:
|
||||||
|
self.aiApiKeyEntry.set_placeholder_text("No API key needed - uses Codex CLI")
|
||||||
elif provider == settings.AI_PROVIDER_OLLAMA:
|
elif provider == settings.AI_PROVIDER_OLLAMA:
|
||||||
self.aiApiKeyEntry.set_placeholder_text("No API key needed - uses local Ollama")
|
self.aiApiKeyEntry.set_placeholder_text("No API key needed - uses local Ollama")
|
||||||
else:
|
else:
|
||||||
@@ -3065,6 +3071,14 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
|||||||
|
|
||||||
self._presentMessage(messages.KB_ENTER_NEW_KEY)
|
self._presentMessage(messages.KB_ENTER_NEW_KEY)
|
||||||
cthulhu_state.capturingKeys = True
|
cthulhu_state.capturingKeys = True
|
||||||
|
try:
|
||||||
|
script_manager.get_manager().get_active_script().removeKeyGrabs()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
input_event_manager.get_manager().unmap_all_modifiers()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
editable.connect('key-press-event', self.kbKeyPressed)
|
editable.connect('key-press-event', self.kbKeyPressed)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -3073,6 +3087,10 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
|||||||
|
|
||||||
cthulhu_state.capturingKeys = False
|
cthulhu_state.capturingKeys = False
|
||||||
self._capturedKey = []
|
self._capturedKey = []
|
||||||
|
try:
|
||||||
|
script_manager.get_manager().get_active_script().refreshKeyGrabs()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return
|
return
|
||||||
|
|
||||||
def _processKeyCaptured(self, keyPressedEvent):
|
def _processKeyCaptured(self, keyPressedEvent):
|
||||||
@@ -3090,9 +3108,17 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
|||||||
entries = entries_for_keycode[-1]
|
entries = entries_for_keycode[-1]
|
||||||
eventString = Gdk.keyval_name(entries[0])
|
eventString = Gdk.keyval_name(entries[0])
|
||||||
eventState = keyPressedEvent.state
|
eventState = keyPressedEvent.state
|
||||||
|
eventKeyvalName = Gdk.keyval_name(keyPressedEvent.keyval)
|
||||||
|
|
||||||
cthulhuMods = settings.cthulhuModifierKeys
|
cthulhuMods = settings.cthulhuModifierKeys
|
||||||
if eventString in cthulhuMods:
|
if eventKeyvalName in cthulhuMods:
|
||||||
|
eventString = eventKeyvalName
|
||||||
|
self._capturedKey = ['', keybindings.CTHULHU_MODIFIER_MASK, 0]
|
||||||
|
return False
|
||||||
|
if eventKeyvalName == "KP_0" \
|
||||||
|
and "KP_Insert" in cthulhuMods \
|
||||||
|
and eventState & Gdk.ModifierType.SHIFT_MASK:
|
||||||
|
eventString = "KP_Insert"
|
||||||
self._capturedKey = ['', keybindings.CTHULHU_MODIFIER_MASK, 0]
|
self._capturedKey = ['', keybindings.CTHULHU_MODIFIER_MASK, 0]
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -3180,6 +3206,10 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
|||||||
|
|
||||||
cthulhu_state.capturingKeys = False
|
cthulhu_state.capturingKeys = False
|
||||||
self._capturedKey = []
|
self._capturedKey = []
|
||||||
|
try:
|
||||||
|
script_manager.get_manager().get_active_script().refreshKeyGrabs()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
myiter = treeModel.get_iter_from_string(path)
|
myiter = treeModel.get_iter_from_string(path)
|
||||||
try:
|
try:
|
||||||
originalBinding = treeModel.get_value(myiter, text)
|
originalBinding = treeModel.get_value(myiter, text)
|
||||||
@@ -3793,7 +3823,12 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
|||||||
|
|
||||||
def aiProviderChanged(self, widget):
|
def aiProviderChanged(self, widget):
|
||||||
"""AI Provider combo box changed handler"""
|
"""AI Provider combo box changed handler"""
|
||||||
providers = [settings.AI_PROVIDER_CLAUDE_CODE, settings.AI_PROVIDER_GEMINI, settings.AI_PROVIDER_OLLAMA]
|
providers = [
|
||||||
|
settings.AI_PROVIDER_CLAUDE_CODE,
|
||||||
|
settings.AI_PROVIDER_CODEX,
|
||||||
|
settings.AI_PROVIDER_GEMINI,
|
||||||
|
settings.AI_PROVIDER_OLLAMA
|
||||||
|
]
|
||||||
activeIndex = widget.get_active()
|
activeIndex = widget.get_active()
|
||||||
if 0 <= activeIndex < len(providers):
|
if 0 <= activeIndex < len(providers):
|
||||||
provider = providers[activeIndex]
|
provider = providers[activeIndex]
|
||||||
@@ -3970,4 +4005,3 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
|||||||
"""OCR copy to clipboard checkbox toggled handler"""
|
"""OCR copy to clipboard checkbox toggled handler"""
|
||||||
self.prefsDict["ocrCopyToClipboard"] = widget.get_active()
|
self.prefsDict["ocrCopyToClipboard"] = widget.get_active()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ __license__ = "LGPL"
|
|||||||
|
|
||||||
import enum
|
import enum
|
||||||
import inspect
|
import inspect
|
||||||
from typing import Callable
|
from typing import Callable, Optional
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from dasbus.connection import SessionMessageBus
|
from dasbus.connection import SessionMessageBus
|
||||||
@@ -156,7 +156,7 @@ class _HandlerInfo:
|
|||||||
description: str,
|
description: str,
|
||||||
action: Callable[..., bool],
|
action: Callable[..., bool],
|
||||||
handler_type: 'HandlerType' = HandlerType.COMMAND,
|
handler_type: 'HandlerType' = HandlerType.COMMAND,
|
||||||
parameters: list[tuple[str, str]] | None = None
|
parameters: Optional[list[tuple[str, str]]] = None
|
||||||
):
|
):
|
||||||
self.python_function_name: str = python_function_name
|
self.python_function_name: str = python_function_name
|
||||||
self.description: str = description
|
self.description: str = description
|
||||||
@@ -548,10 +548,10 @@ class CthulhuRemoteController:
|
|||||||
OBJECT_PATH = "/org/stormux/Cthulhu/Service"
|
OBJECT_PATH = "/org/stormux/Cthulhu/Service"
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._dbus_service_interface: CthulhuDBusServiceInterface | None = None
|
self._dbus_service_interface: Optional[CthulhuDBusServiceInterface] = None
|
||||||
self._is_running: bool = False
|
self._is_running: bool = False
|
||||||
self._bus: SessionMessageBus | None = None
|
self._bus: Optional[SessionMessageBus] = None
|
||||||
self._event_loop: EventLoop | None = None
|
self._event_loop: Optional[EventLoop] = None
|
||||||
self._pending_registrations: dict[str, object] = {}
|
self._pending_registrations: dict[str, object] = {}
|
||||||
self._total_commands: int = 0
|
self._total_commands: int = 0
|
||||||
self._total_getters: int = 0
|
self._total_getters: int = 0
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ __copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." \
|
|||||||
"Copyright (c) 2016-2023 Igalia, S.L."
|
"Copyright (c) 2016-2023 Igalia, S.L."
|
||||||
__license__ = "LGPL"
|
__license__ = "LGPL"
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
gi.require_version("Atspi", "2.0")
|
gi.require_version("Atspi", "2.0")
|
||||||
@@ -75,13 +75,13 @@ class FocusManager:
|
|||||||
"""Manages the focused object, window, etc."""
|
"""Manages the focused object, window, etc."""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._window: Atspi.Accessible | None = cthulhu_state.activeWindow
|
self._window: Optional[Atspi.Accessible] = cthulhu_state.activeWindow
|
||||||
self._focus: Atspi.Accessible | None = cthulhu_state.locusOfFocus
|
self._focus: Optional[Atspi.Accessible] = cthulhu_state.locusOfFocus
|
||||||
self._object_of_interest: Atspi.Accessible | None = cthulhu_state.objOfInterest
|
self._object_of_interest: Optional[Atspi.Accessible] = cthulhu_state.objOfInterest
|
||||||
self._active_mode: str | None = cthulhu_state.activeMode
|
self._active_mode: Optional[str] = cthulhu_state.activeMode
|
||||||
self._last_cell_coordinates: tuple[int, int] = (-1, -1)
|
self._last_cell_coordinates: tuple[int, int] = (-1, -1)
|
||||||
self._last_cursor_position: tuple[Atspi.Accessible | None, int] = (None, -1)
|
self._last_cursor_position: tuple[Optional[Atspi.Accessible], int] = (None, -1)
|
||||||
self._penultimate_cursor_position: tuple[Atspi.Accessible | None, int] = (None, -1)
|
self._penultimate_cursor_position: tuple[Optional[Atspi.Accessible], int] = (None, -1)
|
||||||
|
|
||||||
msg = "FOCUS MANAGER: Registering D-Bus commands."
|
msg = "FOCUS MANAGER: Registering D-Bus commands."
|
||||||
debug.print_message(debug.LEVEL_INFO, msg, True)
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
@@ -104,7 +104,7 @@ class FocusManager:
|
|||||||
cthulhu_state.objOfInterest = None
|
cthulhu_state.objOfInterest = None
|
||||||
cthulhu_state.activeMode = None
|
cthulhu_state.activeMode = None
|
||||||
|
|
||||||
def find_focused_object(self) -> Atspi.Accessible | None:
|
def find_focused_object(self) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the focused object in the active window."""
|
"""Returns the focused object in the active window."""
|
||||||
|
|
||||||
result = _get_ax_utilities().get_focused_object(self._window)
|
result = _get_ax_utilities().get_focused_object(self._window)
|
||||||
@@ -147,9 +147,9 @@ class FocusManager:
|
|||||||
|
|
||||||
def emit_region_changed(
|
def emit_region_changed(
|
||||||
self, obj: Atspi.Accessible,
|
self, obj: Atspi.Accessible,
|
||||||
start_offset: int | None = None,
|
start_offset: Optional[int] = None,
|
||||||
end_offset: int | None = None,
|
end_offset: Optional[int] = None,
|
||||||
mode: str | None = None
|
mode: Optional[str] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Notifies interested clients that the current region of interest has changed."""
|
"""Notifies interested clients that the current region of interest has changed."""
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ class FocusManager:
|
|||||||
|
|
||||||
def get_active_mode_and_object_of_interest(
|
def get_active_mode_and_object_of_interest(
|
||||||
self
|
self
|
||||||
) -> tuple[str | None, Atspi.Accessible | None]:
|
) -> tuple[Optional[str], Optional[Atspi.Accessible]]:
|
||||||
"""Returns the current mode and associated object of interest"""
|
"""Returns the current mode and associated object of interest"""
|
||||||
|
|
||||||
tokens = ["FOCUS MANAGER: Active mode:", self._active_mode,
|
tokens = ["FOCUS MANAGER: Active mode:", self._active_mode,
|
||||||
@@ -200,7 +200,7 @@ class FocusManager:
|
|||||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||||
return self._active_mode, self._object_of_interest
|
return self._active_mode, self._object_of_interest
|
||||||
|
|
||||||
def get_penultimate_cursor_position(self) -> tuple[Atspi.Accessible | None, int]:
|
def get_penultimate_cursor_position(self) -> tuple[Optional[Atspi.Accessible], int]:
|
||||||
"""Returns the penultimate cursor position as a tuple of (object, offset)."""
|
"""Returns the penultimate cursor position as a tuple of (object, offset)."""
|
||||||
|
|
||||||
obj, offset = self._penultimate_cursor_position
|
obj, offset = self._penultimate_cursor_position
|
||||||
@@ -208,7 +208,7 @@ class FocusManager:
|
|||||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||||
return obj, offset
|
return obj, offset
|
||||||
|
|
||||||
def get_last_cursor_position(self) -> tuple[Atspi.Accessible | None, int]:
|
def get_last_cursor_position(self) -> tuple[Optional[Atspi.Accessible], int]:
|
||||||
"""Returns the last cursor position as a tuple of (object, offset)."""
|
"""Returns the last cursor position as a tuple of (object, offset)."""
|
||||||
|
|
||||||
obj, offset = self._last_cursor_position
|
obj, offset = self._last_cursor_position
|
||||||
@@ -216,7 +216,7 @@ class FocusManager:
|
|||||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||||
return obj, offset
|
return obj, offset
|
||||||
|
|
||||||
def set_last_cursor_position(self, obj: Atspi.Accessible | None, offset: int) -> None:
|
def set_last_cursor_position(self, obj: Optional[Atspi.Accessible], offset: int) -> None:
|
||||||
"""Sets the last cursor position as a tuple of (object, offset)."""
|
"""Sets the last cursor position as a tuple of (object, offset)."""
|
||||||
|
|
||||||
tokens = ["FOCUS MANAGER: Setting last cursor position to", obj, offset]
|
tokens = ["FOCUS MANAGER: Setting last cursor position to", obj, offset]
|
||||||
@@ -239,7 +239,7 @@ class FocusManager:
|
|||||||
debug.print_message(debug.LEVEL_INFO, msg, True)
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
self._last_cell_coordinates = row, column
|
self._last_cell_coordinates = row, column
|
||||||
|
|
||||||
def get_locus_of_focus(self) -> Atspi.Accessible | None:
|
def get_locus_of_focus(self) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the current locus of focus (i.e. the object with visual focus)."""
|
"""Returns the current locus of focus (i.e. the object with visual focus)."""
|
||||||
|
|
||||||
tokens = ["FOCUS MANAGER: Locus of focus is", self._focus]
|
tokens = ["FOCUS MANAGER: Locus of focus is", self._focus]
|
||||||
@@ -248,8 +248,8 @@ class FocusManager:
|
|||||||
|
|
||||||
def set_locus_of_focus(
|
def set_locus_of_focus(
|
||||||
self,
|
self,
|
||||||
event: Atspi.Event | None,
|
event: Optional[Atspi.Event],
|
||||||
obj: Atspi.Accessible | None,
|
obj: Optional[Atspi.Accessible],
|
||||||
notify_script: bool = True,
|
notify_script: bool = True,
|
||||||
force: bool = False
|
force: bool = False
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -340,7 +340,7 @@ class FocusManager:
|
|||||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||||
return is_active
|
return is_active
|
||||||
|
|
||||||
def get_active_window(self) -> Atspi.Accessible | None:
|
def get_active_window(self) -> Optional[Atspi.Accessible]:
|
||||||
"""Returns the currently-active window (i.e. without searching or verifying)."""
|
"""Returns the currently-active window (i.e. without searching or verifying)."""
|
||||||
|
|
||||||
tokens = ["FOCUS MANAGER: Active window is", self._window]
|
tokens = ["FOCUS MANAGER: Active window is", self._window]
|
||||||
@@ -349,8 +349,8 @@ class FocusManager:
|
|||||||
|
|
||||||
def set_active_window(
|
def set_active_window(
|
||||||
self,
|
self,
|
||||||
frame: Atspi.Accessible | None,
|
frame: Optional[Atspi.Accessible],
|
||||||
app: Atspi.Accessible | None = None,
|
app: Optional[Atspi.Accessible] = None,
|
||||||
set_window_as_focus: bool = False,
|
set_window_as_focus: bool = False,
|
||||||
notify_script: bool = False
|
notify_script: bool = False
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -390,7 +390,7 @@ class FocusManager:
|
|||||||
def toggle_presentation_mode(
|
def toggle_presentation_mode(
|
||||||
self,
|
self,
|
||||||
script: default.Script,
|
script: default.Script,
|
||||||
event: InputEvent | None = None,
|
event: Optional[InputEvent] = None,
|
||||||
notify_user: bool = True
|
notify_user: bool = True
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Switches between browse mode and focus mode (web content only)."""
|
"""Switches between browse mode and focus mode (web content only)."""
|
||||||
@@ -401,7 +401,7 @@ class FocusManager:
|
|||||||
def toggle_layout_mode(
|
def toggle_layout_mode(
|
||||||
self,
|
self,
|
||||||
script: default.Script,
|
script: default.Script,
|
||||||
event: InputEvent | None = None,
|
event: Optional[InputEvent] = None,
|
||||||
notify_user: bool = True
|
notify_user: bool = True
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Switches between object mode and layout mode for line presentation (web content only)."""
|
"""Switches between object mode and layout mode for line presentation (web content only)."""
|
||||||
@@ -412,7 +412,7 @@ class FocusManager:
|
|||||||
def enable_sticky_browse_mode(
|
def enable_sticky_browse_mode(
|
||||||
self,
|
self,
|
||||||
script: default.Script,
|
script: default.Script,
|
||||||
event: InputEvent | None = None,
|
event: Optional[InputEvent] = None,
|
||||||
notify_user: bool = True
|
notify_user: bool = True
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Enables sticky browse mode (web content only)."""
|
"""Enables sticky browse mode (web content only)."""
|
||||||
@@ -423,7 +423,7 @@ class FocusManager:
|
|||||||
def enable_sticky_focus_mode(
|
def enable_sticky_focus_mode(
|
||||||
self,
|
self,
|
||||||
script: default.Script,
|
script: default.Script,
|
||||||
event: InputEvent | None = None,
|
event: Optional[InputEvent] = None,
|
||||||
notify_user: bool = True
|
notify_user: bool = True
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Enables sticky focus mode (web content only)."""
|
"""Enables sticky focus mode (web content only)."""
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ __copyright__ = "Copyright (c) 2024 Igalia, S.L." \
|
|||||||
"Copyright (c) 2024 GNOME Foundation Inc."
|
"Copyright (c) 2024 GNOME Foundation Inc."
|
||||||
__license__ = "LGPL"
|
__license__ = "LGPL"
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
gi.require_version("Atspi", "2.0")
|
gi.require_version("Atspi", "2.0")
|
||||||
@@ -50,6 +50,7 @@ from . import focus_manager
|
|||||||
from . import input_event
|
from . import input_event
|
||||||
from . import script_manager
|
from . import script_manager
|
||||||
from . import settings
|
from . import settings
|
||||||
|
from . import cthulhu_state
|
||||||
from .ax_object import AXObject
|
from .ax_object import AXObject
|
||||||
from .ax_utilities import AXUtilities
|
from .ax_utilities import AXUtilities
|
||||||
|
|
||||||
@@ -60,9 +61,9 @@ class InputEventManager:
|
|||||||
"""Provides utilities for managing input events."""
|
"""Provides utilities for managing input events."""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._last_input_event: input_event.InputEvent | None = None
|
self._last_input_event: Optional[input_event.InputEvent] = None
|
||||||
self._last_non_modifier_key_event: input_event.KeyboardEvent | None = None
|
self._last_non_modifier_key_event: Optional[input_event.KeyboardEvent] = None
|
||||||
self._device: Atspi.Device | None = None
|
self._device: Optional[Atspi.Device] = None
|
||||||
self._mapped_keycodes: list[int] = []
|
self._mapped_keycodes: list[int] = []
|
||||||
self._mapped_keysyms: list[int] = []
|
self._mapped_keysyms: list[int] = []
|
||||||
self._grabbed_bindings: dict[int, keybindings.KeyBinding] = {}
|
self._grabbed_bindings: dict[int, keybindings.KeyBinding] = {}
|
||||||
@@ -267,6 +268,10 @@ class InputEventManager:
|
|||||||
msg = "INPUT EVENT MANAGER: Keyboard event processing is paused."
|
msg = "INPUT EVENT MANAGER: Keyboard event processing is paused."
|
||||||
debug.print_message(debug.LEVEL_INFO, msg, True)
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
return False
|
return False
|
||||||
|
if cthulhu_state.capturingKeys:
|
||||||
|
msg = "INPUT EVENT MANAGER: Capturing keys; ignoring keyboard event."
|
||||||
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
|
return False
|
||||||
|
|
||||||
event = input_event.KeyboardEvent(pressed, keycode, keysym, modifiers, text)
|
event = input_event.KeyboardEvent(pressed, keycode, keysym, modifiers, text)
|
||||||
if event in [self._last_input_event, self._last_non_modifier_key_event]:
|
if event in [self._last_input_event, self._last_non_modifier_key_event]:
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ import gi
|
|||||||
gi.require_version('Gtk', '3.0')
|
gi.require_version('Gtk', '3.0')
|
||||||
from gi.repository import GObject
|
from gi.repository import GObject
|
||||||
from gi.repository import Gtk
|
from gi.repository import Gtk
|
||||||
|
from gi.repository import Gdk
|
||||||
|
from gi.repository import Gdk
|
||||||
|
|
||||||
from . import cmdnames
|
from . import cmdnames
|
||||||
from . import debug
|
from . import debug
|
||||||
@@ -275,16 +277,20 @@ class NotificationPresenter:
|
|||||||
class NotificationListGUI:
|
class NotificationListGUI:
|
||||||
"""The dialog containing the notifications list."""
|
"""The dialog containing the notifications list."""
|
||||||
|
|
||||||
|
RESPONSE_COPY = 1
|
||||||
|
|
||||||
def __init__(self, script, title, column_headers, rows):
|
def __init__(self, script, title, column_headers, rows):
|
||||||
self._script = script
|
self._script = script
|
||||||
self._model = None
|
self._model = None
|
||||||
|
self._tree = None
|
||||||
self._gui = self._create_dialog(title, column_headers, rows)
|
self._gui = self._create_dialog(title, column_headers, rows)
|
||||||
|
|
||||||
def _create_dialog(self, title, column_headers, rows):
|
def _create_dialog(self, title, column_headers, rows):
|
||||||
dialog = Gtk.Dialog(title,
|
dialog = Gtk.Dialog(title,
|
||||||
None,
|
None,
|
||||||
Gtk.DialogFlags.MODAL,
|
Gtk.DialogFlags.MODAL,
|
||||||
(Gtk.STOCK_CLEAR, Gtk.ResponseType.APPLY,
|
(Gtk.STOCK_COPY, self.RESPONSE_COPY,
|
||||||
|
Gtk.STOCK_CLEAR, Gtk.ResponseType.APPLY,
|
||||||
Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE))
|
Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE))
|
||||||
dialog.set_default_size(600, 400)
|
dialog.set_default_size(600, 400)
|
||||||
|
|
||||||
@@ -315,6 +321,11 @@ class NotificationListGUI:
|
|||||||
self._model.set_value(row_iter, i, cell)
|
self._model.set_value(row_iter, i, cell)
|
||||||
|
|
||||||
tree.set_model(self._model)
|
tree.set_model(self._model)
|
||||||
|
selection = tree.get_selection()
|
||||||
|
selection.set_mode(Gtk.SelectionMode.SINGLE)
|
||||||
|
if self._model.iter_n_children(None) > 0:
|
||||||
|
selection.select_path(0)
|
||||||
|
self._tree = tree
|
||||||
dialog.connect("response", self.on_response)
|
dialog.connect("response", self.on_response)
|
||||||
return dialog
|
return dialog
|
||||||
|
|
||||||
@@ -325,6 +336,10 @@ class NotificationListGUI:
|
|||||||
self._gui.destroy()
|
self._gui.destroy()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if response == self.RESPONSE_COPY:
|
||||||
|
self._copy_selected_notification()
|
||||||
|
return
|
||||||
|
|
||||||
if response == Gtk.ResponseType.APPLY and self._model is not None:
|
if response == Gtk.ResponseType.APPLY and self._model is not None:
|
||||||
self._model.clear()
|
self._model.clear()
|
||||||
getPresenter().clear_list()
|
getPresenter().clear_list()
|
||||||
@@ -336,10 +351,39 @@ class NotificationListGUI:
|
|||||||
"""Shows the notifications list dialog."""
|
"""Shows the notifications list dialog."""
|
||||||
|
|
||||||
self._gui.show_all()
|
self._gui.show_all()
|
||||||
time_stamp = cthulhu_state.lastInputEvent.timestamp
|
time_stamp = Gtk.get_current_event_time()
|
||||||
if time_stamp == 0:
|
if not time_stamp or time_stamp > 0xFFFFFFFF:
|
||||||
time_stamp = Gtk.get_current_event_time()
|
time_stamp = Gdk.CURRENT_TIME
|
||||||
self._gui.present_with_time(time_stamp)
|
self._gui.present_with_time(int(time_stamp))
|
||||||
|
|
||||||
|
def _copy_selected_notification(self):
|
||||||
|
if self._model is None or self._tree is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
selection = self._tree.get_selection()
|
||||||
|
model, paths = selection.get_selected_rows()
|
||||||
|
if not paths and self._model.iter_n_children(None) > 0:
|
||||||
|
selection.select_path(0)
|
||||||
|
model, paths = selection.get_selected_rows()
|
||||||
|
|
||||||
|
if not paths:
|
||||||
|
return
|
||||||
|
|
||||||
|
iter_for_path = model.get_iter(paths[0])
|
||||||
|
if iter_for_path is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
message = model.get_value(iter_for_path, 0)
|
||||||
|
timestamp = model.get_value(iter_for_path, 1)
|
||||||
|
if timestamp:
|
||||||
|
text = f"{message}\t{timestamp}"
|
||||||
|
else:
|
||||||
|
text = f"{message}"
|
||||||
|
|
||||||
|
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
||||||
|
clipboard.set_text(text, -1)
|
||||||
|
clipboard.store()
|
||||||
|
self._script.presentMessage(messages.CLIPBOARD_COPIED_FULL)
|
||||||
|
|
||||||
_presenter = None
|
_presenter = None
|
||||||
def getPresenter():
|
def getPresenter():
|
||||||
|
|||||||
@@ -59,9 +59,9 @@ class Plugin:
|
|||||||
self.plugin_info = plugin_info
|
self.plugin_info = plugin_info
|
||||||
if plugin_info:
|
if plugin_info:
|
||||||
self.module_name = getattr(plugin_info, 'module_name', '')
|
self.module_name = getattr(plugin_info, 'module_name', '')
|
||||||
self.name = getattr(plugin_info, 'name', '')
|
self.name = plugin_info.get_name()
|
||||||
self.version = getattr(plugin_info, 'version', '')
|
self.version = plugin_info.get_version()
|
||||||
self.description = getattr(plugin_info, 'description', '')
|
self.description = plugin_info.get_description()
|
||||||
|
|
||||||
@cthulhu_hookimpl
|
@cthulhu_hookimpl
|
||||||
def activate(self, plugin=None):
|
def activate(self, plugin=None):
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import os
|
|||||||
import inspect
|
import inspect
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import logging
|
import logging
|
||||||
|
import configparser
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
|
||||||
# Import pluggy if available
|
# Import pluggy if available
|
||||||
@@ -29,6 +30,12 @@ logger = logging.getLogger(__name__)
|
|||||||
if PLUGIN_DEBUG:
|
if PLUGIN_DEBUG:
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
_manager = None
|
||||||
|
|
||||||
|
def getManager():
|
||||||
|
"""Return the shared PluginSystemManager instance."""
|
||||||
|
return _manager
|
||||||
|
|
||||||
class PluginType(IntEnum):
|
class PluginType(IntEnum):
|
||||||
"""Types of plugins we support."""
|
"""Types of plugins we support."""
|
||||||
SYSTEM = 1
|
SYSTEM = 1
|
||||||
@@ -78,8 +85,10 @@ class PluginSystemManager:
|
|||||||
"""Cthulhu Plugin Manager using pluggy."""
|
"""Cthulhu Plugin Manager using pluggy."""
|
||||||
|
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
|
global _manager
|
||||||
self.app = app
|
self.app = app
|
||||||
logger.info("Initializing PluginSystemManager")
|
logger.info("Initializing PluginSystemManager")
|
||||||
|
_manager = self
|
||||||
|
|
||||||
# Initialize plugin manager
|
# Initialize plugin manager
|
||||||
if PLUGGY_AVAILABLE:
|
if PLUGGY_AVAILABLE:
|
||||||
@@ -378,15 +387,38 @@ class PluginSystemManager:
|
|||||||
|
|
||||||
if os.path.isfile(metadata_file):
|
if os.path.isfile(metadata_file):
|
||||||
try:
|
try:
|
||||||
with open(metadata_file, 'r') as f:
|
with open(metadata_file, 'r', encoding='utf-8') as f:
|
||||||
for line in f:
|
contents = f.read()
|
||||||
line = line.strip()
|
|
||||||
if not line or line.startswith('#'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if '=' in line:
|
has_section_header = False
|
||||||
key, value = line.split('=', 1)
|
for line in contents.splitlines():
|
||||||
metadata[key.strip()] = value.strip()
|
stripped = line.strip()
|
||||||
|
if not stripped or stripped.startswith('#') or stripped.startswith(';'):
|
||||||
|
continue
|
||||||
|
if stripped.startswith('[') and stripped.endswith(']'):
|
||||||
|
has_section_header = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if has_section_header:
|
||||||
|
parser = configparser.ConfigParser()
|
||||||
|
try:
|
||||||
|
parser.read_string(contents)
|
||||||
|
if parser.sections():
|
||||||
|
for section in parser.sections():
|
||||||
|
for key, value in parser[section].items():
|
||||||
|
metadata[key.strip().lower()] = value.strip()
|
||||||
|
return metadata
|
||||||
|
except configparser.Error as e:
|
||||||
|
logger.warning(f"Plugin metadata INI parse failed for {metadata_file}: {e}")
|
||||||
|
|
||||||
|
for line in contents.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith('#') or line.startswith(';') or line.startswith('['):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if '=' in line:
|
||||||
|
key, value = line.split('=', 1)
|
||||||
|
metadata[key.strip().lower()] = value.strip()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error loading plugin metadata: {e}")
|
logger.error(f"Error loading plugin metadata: {e}")
|
||||||
|
|
||||||
|
|||||||
@@ -334,6 +334,172 @@ Keep descriptions informative and well-structured."""
|
|||||||
return base_prompt
|
return base_prompt
|
||||||
|
|
||||||
|
|
||||||
|
class CodexProvider(AIProvider):
|
||||||
|
"""Codex CLI provider - uses installed Codex CLI."""
|
||||||
|
|
||||||
|
def __init__(self, codex_path=None, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.codex_path = codex_path or self._resolve_codex_path()
|
||||||
|
|
||||||
|
def describe_screen(self, screenshot_data, accessibility_data):
|
||||||
|
"""Generate a description using Codex CLI."""
|
||||||
|
try:
|
||||||
|
prompt = self._build_prompt("describe", None, accessibility_data)
|
||||||
|
return self._call_codex(prompt, screenshot_data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Codex describe error: {e}")
|
||||||
|
return f"Error getting screen description: {e}"
|
||||||
|
|
||||||
|
def answer_question(self, question, screenshot_data, accessibility_data):
|
||||||
|
"""Answer a question using Codex CLI."""
|
||||||
|
try:
|
||||||
|
prompt = self._build_prompt("question", question, accessibility_data)
|
||||||
|
return self._call_codex(prompt, screenshot_data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Codex question error: {e}")
|
||||||
|
return f"Error answering question: {e}"
|
||||||
|
|
||||||
|
def suggest_actions(self, request, screenshot_data, accessibility_data):
|
||||||
|
"""Suggest actions using Codex CLI."""
|
||||||
|
try:
|
||||||
|
prompt = self._build_prompt("action", request, accessibility_data)
|
||||||
|
return self._call_codex(prompt, screenshot_data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Codex action error: {e}")
|
||||||
|
return f"Error suggesting actions: {e}"
|
||||||
|
|
||||||
|
def analyze_images(self, user_question, screenshot_data, accessibility_data):
|
||||||
|
"""Analyze images visible on screen using Codex CLI."""
|
||||||
|
try:
|
||||||
|
prompt = self._build_prompt("image", user_question, accessibility_data)
|
||||||
|
return self._call_codex(prompt, screenshot_data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Codex image analysis error: {e}")
|
||||||
|
return f"Error analyzing images: {e}"
|
||||||
|
|
||||||
|
def _build_prompt(self, task_type, user_input, accessibility_data):
|
||||||
|
"""Build the complete prompt for Codex CLI."""
|
||||||
|
system_prompt = self._prepare_system_prompt(task_type)
|
||||||
|
|
||||||
|
if task_type == "image":
|
||||||
|
if user_input == "ANALYZE_SINGLE_IMAGE_FILE":
|
||||||
|
prompt = (
|
||||||
|
f"{system_prompt}\n\nAnalyze and describe the single image file provided. "
|
||||||
|
"Focus on visual content only - describe what you see in the image: objects, "
|
||||||
|
"people, scenery, colors, text, composition, and any other visual details."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
prompt = f"{system_prompt}\n\nCurrent screen context (focus on images):\n"
|
||||||
|
if user_input:
|
||||||
|
prompt += f"User question about images: {user_input}\n\n"
|
||||||
|
prompt += "Analyze and describe any images visible on this screen. Focus on visual content, not UI elements."
|
||||||
|
else:
|
||||||
|
prompt = (
|
||||||
|
f"{system_prompt}\n\nCurrent accessibility information:\n"
|
||||||
|
f"```json\n{json.dumps(accessibility_data, indent=2)}\n```\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
if task_type == "describe":
|
||||||
|
prompt += "Please describe what's on this screen."
|
||||||
|
elif task_type == "question":
|
||||||
|
prompt += f"User question: {user_input}"
|
||||||
|
elif task_type == "action":
|
||||||
|
prompt += f"User wants to: {user_input}\n\nProvide the action analysis in the required format."
|
||||||
|
|
||||||
|
return prompt
|
||||||
|
|
||||||
|
def _resolve_codex_path(self):
|
||||||
|
import shutil
|
||||||
|
import os
|
||||||
|
|
||||||
|
codex_path = shutil.which('codex')
|
||||||
|
if not codex_path and os.path.isfile('/usr/bin/codex'):
|
||||||
|
codex_path = '/usr/bin/codex'
|
||||||
|
return codex_path
|
||||||
|
|
||||||
|
def _call_codex(self, prompt, screenshot_data):
|
||||||
|
"""Call Codex CLI with the prompt and optional image."""
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
import base64
|
||||||
|
|
||||||
|
if not self.codex_path:
|
||||||
|
return "Codex CLI not found"
|
||||||
|
|
||||||
|
output_path = None
|
||||||
|
image_path = None
|
||||||
|
try:
|
||||||
|
# Write screenshot to a temp file if available
|
||||||
|
if screenshot_data:
|
||||||
|
image_format = screenshot_data.get('format', 'png')
|
||||||
|
suffix = f".{image_format}"
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp_file:
|
||||||
|
image_data = base64.b64decode(screenshot_data['data'])
|
||||||
|
temp_file.write(image_data)
|
||||||
|
image_path = temp_file.name
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False) as output_file:
|
||||||
|
output_path = output_file.name
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
self.codex_path,
|
||||||
|
'exec',
|
||||||
|
'--skip-git-repo-check',
|
||||||
|
'--color',
|
||||||
|
'never',
|
||||||
|
'--output-last-message',
|
||||||
|
output_path,
|
||||||
|
'-'
|
||||||
|
]
|
||||||
|
|
||||||
|
if image_path:
|
||||||
|
cmd.extend(['-i', image_path])
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
input=prompt,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=120
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
error_msg = result.stderr.strip() or result.stdout.strip() or "Codex CLI error"
|
||||||
|
logger.error(error_msg)
|
||||||
|
return f"Codex CLI error: {error_msg}"
|
||||||
|
|
||||||
|
response_text = ""
|
||||||
|
if output_path and os.path.exists(output_path):
|
||||||
|
with open(output_path, 'r') as f:
|
||||||
|
response_text = f.read().strip()
|
||||||
|
|
||||||
|
if not response_text:
|
||||||
|
response_text = result.stdout.strip()
|
||||||
|
|
||||||
|
return response_text or "No response from Codex"
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
error_msg = "Codex CLI timed out"
|
||||||
|
logger.error(error_msg)
|
||||||
|
return error_msg
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Error calling Codex CLI: {e}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
return error_msg
|
||||||
|
finally:
|
||||||
|
if image_path and os.path.exists(image_path):
|
||||||
|
try:
|
||||||
|
os.unlink(image_path)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if output_path and os.path.exists(output_path):
|
||||||
|
try:
|
||||||
|
os.unlink(output_path)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class OllamaProvider(AIProvider):
|
class OllamaProvider(AIProvider):
|
||||||
"""Ollama local AI provider."""
|
"""Ollama local AI provider."""
|
||||||
|
|
||||||
@@ -563,9 +729,11 @@ def create_provider(provider_type, **kwargs):
|
|||||||
"""Factory function to create AI providers."""
|
"""Factory function to create AI providers."""
|
||||||
if provider_type == "claude_code":
|
if provider_type == "claude_code":
|
||||||
return ClaudeCodeProvider(**kwargs)
|
return ClaudeCodeProvider(**kwargs)
|
||||||
|
elif provider_type == "codex":
|
||||||
|
return CodexProvider(**kwargs)
|
||||||
elif provider_type == "ollama":
|
elif provider_type == "ollama":
|
||||||
return OllamaProvider(**kwargs)
|
return OllamaProvider(**kwargs)
|
||||||
elif provider_type == "gemini":
|
elif provider_type == "gemini":
|
||||||
return GeminiProvider(**kwargs)
|
return GeminiProvider(**kwargs)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown provider type: {provider_type}")
|
raise ValueError(f"Unknown provider type: {provider_type}")
|
||||||
|
|||||||
@@ -204,6 +204,11 @@ class AIAssistant(Plugin):
|
|||||||
result = self._check_claude_code_availability()
|
result = self._check_claude_code_availability()
|
||||||
logger.info(f"Claude Code availability check result: {result}")
|
logger.info(f"Claude Code availability check result: {result}")
|
||||||
return result
|
return result
|
||||||
|
elif self._provider_type == settings.AI_PROVIDER_CODEX:
|
||||||
|
logger.info("Checking Codex CLI availability")
|
||||||
|
result = self._check_codex_availability()
|
||||||
|
logger.info(f"Codex CLI availability check result: {result}")
|
||||||
|
return result
|
||||||
elif self._provider_type == settings.AI_PROVIDER_GEMINI:
|
elif self._provider_type == settings.AI_PROVIDER_GEMINI:
|
||||||
logger.info("Checking Gemini API key")
|
logger.info("Checking Gemini API key")
|
||||||
if not self._api_key:
|
if not self._api_key:
|
||||||
@@ -258,12 +263,43 @@ class AIAssistant(Plugin):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Claude Code CLI not available: {e}")
|
logger.warning(f"Claude Code CLI not available: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _check_codex_availability(self):
|
||||||
|
"""Check if Codex CLI is available."""
|
||||||
|
try:
|
||||||
|
import subprocess
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
codex_path = shutil.which('codex')
|
||||||
|
if not codex_path and os.path.isfile('/usr/bin/codex'):
|
||||||
|
codex_path = '/usr/bin/codex'
|
||||||
|
|
||||||
|
if not codex_path:
|
||||||
|
logger.warning("Codex CLI not found in PATH")
|
||||||
|
return False
|
||||||
|
|
||||||
|
result = subprocess.run([codex_path, '--version'],
|
||||||
|
capture_output=True, text=True, timeout=5)
|
||||||
|
if result.returncode == 0:
|
||||||
|
logger.info("Codex CLI is available")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.warning(f"Codex CLI not responding: {result.stderr}")
|
||||||
|
return False
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
logger.warning("Codex CLI timeout")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Codex CLI not available: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
def _initialize_ai_provider(self):
|
def _initialize_ai_provider(self):
|
||||||
"""Initialize the AI provider based on settings."""
|
"""Initialize the AI provider based on settings."""
|
||||||
try:
|
try:
|
||||||
if self._provider_type == settings.AI_PROVIDER_CLAUDE_CODE:
|
if self._provider_type == settings.AI_PROVIDER_CLAUDE_CODE:
|
||||||
self._ai_provider = create_provider("claude_code")
|
self._ai_provider = create_provider("claude_code")
|
||||||
|
elif self._provider_type == settings.AI_PROVIDER_CODEX:
|
||||||
|
self._ai_provider = create_provider("codex")
|
||||||
elif self._provider_type == settings.AI_PROVIDER_OLLAMA:
|
elif self._provider_type == settings.AI_PROVIDER_OLLAMA:
|
||||||
self._ai_provider = create_provider("ollama", model=self._ollama_model, base_url=self._ollama_endpoint)
|
self._ai_provider = create_provider("ollama", model=self._ollama_model, base_url=self._ollama_endpoint)
|
||||||
elif self._provider_type == settings.AI_PROVIDER_GEMINI:
|
elif self._provider_type == settings.AI_PROVIDER_GEMINI:
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ ocrdesktop_python_sources = files([
|
|||||||
|
|
||||||
python3.install_sources(
|
python3.install_sources(
|
||||||
ocrdesktop_python_sources,
|
ocrdesktop_python_sources,
|
||||||
subdir: 'cthulhu/plugins/OCRDesktop'
|
subdir: 'cthulhu/plugins/OCR'
|
||||||
)
|
)
|
||||||
|
|
||||||
install_data(
|
install_data(
|
||||||
'plugin.info',
|
'plugin.info',
|
||||||
install_dir: python3.get_install_dir() / 'cthulhu' / 'plugins' / 'OCRDesktop'
|
install_dir: python3.get_install_dir() / 'cthulhu' / 'plugins' / 'OCR'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ class PluginManager(Plugin):
|
|||||||
checkbox.connect("toggled", self._on_plugin_toggled, plugin_name)
|
checkbox.connect("toggled", self._on_plugin_toggled, plugin_name)
|
||||||
|
|
||||||
# Create plugin info label
|
# Create plugin info label
|
||||||
info_text = f"<b>{plugin_name}</b>"
|
info_text = f"<b>{plugin_info.get('name', plugin_name)}</b>"
|
||||||
if plugin_info.get('description'):
|
if plugin_info.get('description'):
|
||||||
info_text += f"\n{plugin_info['description']}"
|
info_text += f"\n{plugin_info['description']}"
|
||||||
if plugin_info.get('version'):
|
if plugin_info.get('version'):
|
||||||
@@ -257,17 +257,17 @@ class PluginManager(Plugin):
|
|||||||
from cthulhu import plugin_system_manager
|
from cthulhu import plugin_system_manager
|
||||||
|
|
||||||
# Use existing plugin manager to get plugins
|
# Use existing plugin manager to get plugins
|
||||||
if hasattr(plugin_system_manager, '_manager') and plugin_system_manager._manager:
|
manager = plugin_system_manager.getManager()
|
||||||
manager = plugin_system_manager._manager
|
if manager:
|
||||||
manager.rescanPlugins()
|
manager.rescanPlugins()
|
||||||
|
|
||||||
for plugin_info in manager.plugins:
|
for plugin_info in manager.plugins:
|
||||||
plugin_name = plugin_info.get_module_name()
|
plugin_name = plugin_info.get_module_name()
|
||||||
plugins[plugin_name] = {
|
plugins[plugin_name] = {
|
||||||
'name': plugin_name,
|
'name': plugin_info.get_name(),
|
||||||
'description': getattr(plugin_info, 'description', ''),
|
'description': plugin_info.get_description(),
|
||||||
'version': getattr(plugin_info, 'version', ''),
|
'version': plugin_info.get_version(),
|
||||||
'path': getattr(plugin_info, 'module_dir', '')
|
'path': plugin_info.get_module_dir()
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
# Fallback: manually scan plugin directories
|
# Fallback: manually scan plugin directories
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ CHAT_SPEAK_FOCUSED_CHANNEL = 2
|
|||||||
|
|
||||||
# AI Assistant constants - simplified to providers that don't need complex API key management
|
# AI Assistant constants - simplified to providers that don't need complex API key management
|
||||||
AI_PROVIDER_CLAUDE_CODE = "claude_code"
|
AI_PROVIDER_CLAUDE_CODE = "claude_code"
|
||||||
|
AI_PROVIDER_CODEX = "codex"
|
||||||
AI_PROVIDER_GEMINI = "gemini"
|
AI_PROVIDER_GEMINI = "gemini"
|
||||||
AI_PROVIDER_OLLAMA = "ollama"
|
AI_PROVIDER_OLLAMA = "ollama"
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ falls off the end, it may be added again later.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
import threading
|
import threading
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
|
||||||
@@ -76,7 +77,7 @@ def is_capture_paused() -> bool:
|
|||||||
return _pauseCount > 0
|
return _pauseCount > 0
|
||||||
|
|
||||||
|
|
||||||
def add(text: str | None, source: str = "") -> bool:
|
def add(text: Optional[str], source: str = "") -> bool:
|
||||||
"""Add text to speech history if it's not already present.
|
"""Add text to speech history if it's not already present.
|
||||||
|
|
||||||
Returns True if the item was added; False otherwise.
|
Returns True if the item was added; False otherwise.
|
||||||
@@ -141,7 +142,7 @@ def get_items() -> list[str]:
|
|||||||
return list(_historyItems)
|
return list(_historyItems)
|
||||||
|
|
||||||
|
|
||||||
def remove(text: str | None) -> bool:
|
def remove(text: Optional[str]) -> bool:
|
||||||
"""Remove an item from the history (if present)."""
|
"""Remove an item from the history (if present)."""
|
||||||
if text is None:
|
if text is None:
|
||||||
return False
|
return False
|
||||||
|
|||||||
Reference in New Issue
Block a user