Try to make Cthulhu python 3.9 compatible. Fixed keybinding and notifications bugs.

This commit is contained in:
Storm Dragon
2025-12-29 05:17:33 -05:00
parent fee5800220
commit 87786e9c72
34 changed files with 1003 additions and 686 deletions
+2 -2
View File
@@ -34,7 +34,7 @@ __copyright__ = "Copyright (c) 2023 Igalia, S.L."
__license__ = "LGPL"
import time
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Union
import gi
@@ -103,7 +103,7 @@ class ActionList(Gtk.Window):
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."""
if isinstance(actions, dict):
+7 -6
View File
@@ -29,6 +29,7 @@ __date__ = "$Date$"
__copyright__ = "Copyright (c) 2023 Igalia, S.L."
__license__ = "LGPL"
from typing import Optional
import time
import gi
@@ -48,15 +49,15 @@ class AXCollection:
# pylint: disable=R0913,R0914
@staticmethod
def create_match_rule(
states: list[str] | None = None,
states: Optional[list[str]] = None,
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,
roles: list[str] | None = None,
roles: Optional[list[str]] = None,
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,
invert: bool = False) -> Atspi.MatchRule | None:
invert: bool = False) -> Optional[Atspi.MatchRule]:
"""Creates a match rule based on the supplied criteria."""
if states is None:
@@ -136,7 +137,7 @@ class AXCollection:
obj: Atspi.Accessible,
rule: Atspi.MatchRule,
order: Atspi.CollectionSortOrder = Atspi.CollectionSortOrder.CANONICAL
) -> Atspi.Accessible | None:
) -> Optional[Atspi.Accessible]:
"""Returns the first object matching the specified rule."""
if not AXObject.supports_collection(obj):
+5 -4
View File
@@ -34,6 +34,7 @@ __copyright__ = "Copyright (c) 2024 Igalia, S.L." \
"Copyright (c) 2024 GNOME Foundation Inc."
__license__ = "LGPL"
from typing import Optional
import functools
import gi
@@ -285,7 +286,7 @@ class AXComponent:
@staticmethod
def _find_descendant_at_point(
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."""
for child in AXObject.iter_children(obj):
@@ -297,7 +298,7 @@ class AXComponent:
return None
@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."""
if not AXObject.supports_component(obj):
@@ -317,7 +318,7 @@ class AXComponent:
@staticmethod
def _get_descendant_at_point(
obj: Atspi.Accessible, x: int, y: int
) -> Atspi.Accessible | None:
) -> Optional[Atspi.Accessible]:
"""Returns the deepest descendant of obj at the specified point."""
child = AXComponent._get_object_at_point(obj, x, y)
@@ -338,7 +339,7 @@ class AXComponent:
@staticmethod
def get_descendant_at_point(
obj: Atspi.Accessible, x: int, y: int
) -> Atspi.Accessible | None:
) -> Optional[Atspi.Accessible]:
"""Returns the deepest descendant of obj at the specified point."""
result = AXComponent._get_descendant_at_point(obj, x, y)
+24 -23
View File
@@ -34,6 +34,7 @@ __copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." \
"Copyright (c) 2018-2023 Igalia, S.L."
__license__ = "LGPL"
from typing import Optional
import gi
gi.require_version("Atspi", "2.0")
gi.require_version("Gtk", "3.0")
@@ -60,8 +61,8 @@ class AXEventSynthesizer:
@staticmethod
def _is_scrolled_off_screen(
obj: Atspi.Accessible,
offset: int | None = None,
ancestor: Atspi.Accessible | None = None
offset: Optional[int] = None,
ancestor: Optional[Atspi.Accessible] = None
) -> bool:
"""Returns true if obj, or the caret offset therein, is scrolled off-screen."""
@@ -119,7 +120,7 @@ class AXEventSynthesizer:
return True
@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."""
if offset is None:
@@ -164,7 +165,7 @@ class AXEventSynthesizer:
return AXEventSynthesizer._generate_mouse_event(obj, relative_x, relative_y, event)
@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."""
tokens = [f"AXEventSynthesizer: Attempting to route to offset {offset} in", obj]
@@ -181,7 +182,7 @@ class AXEventSynthesizer:
@staticmethod
def click_character(
obj: Atspi.Accessible, offset: int | None = None, button: int = 1
obj: Atspi.Accessible, offset: Optional[int] = None, button: int = 1
) -> bool:
"""Single click on the current character in obj using the specified button."""
@@ -198,7 +199,7 @@ class AXEventSynthesizer:
@staticmethod
def _scroll_to_location(
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:
"""Attempts to scroll to the specified location."""
@@ -220,7 +221,7 @@ class AXEventSynthesizer:
@staticmethod
def _scroll_to_point(
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:
"""Attempts to scroll obj to the specified point."""
@@ -242,8 +243,8 @@ class AXEventSynthesizer:
@staticmethod
def scroll_into_view(
obj: Atspi.Accessible,
start_offset: int | None = None,
end_offset: int | None = None
start_offset: Optional[int] = None,
end_offset: Optional[int] = None
) -> None:
"""Attempts to scroll obj into view."""
@@ -253,8 +254,8 @@ class AXEventSynthesizer:
@staticmethod
def scroll_to_center(
obj: Atspi.Accessible,
start_offset: int | None = None,
end_offset: int | None = None
start_offset: Optional[int] = None,
end_offset: Optional[int] = None
) -> None:
"""Attempts to scroll obj to the center of its window."""
@@ -272,8 +273,8 @@ class AXEventSynthesizer:
@staticmethod
def scroll_to_top_edge(
obj: Atspi.Accessible,
start_offset: int | None = None,
end_offset: int | None = None
start_offset: Optional[int] = None,
end_offset: Optional[int] = None
) -> None:
"""Attempts to scroll obj to the top edge."""
@@ -283,8 +284,8 @@ class AXEventSynthesizer:
@staticmethod
def scroll_to_top_left(
obj: Atspi.Accessible,
start_offset: int | None = None,
end_offset: int | None = None
start_offset: Optional[int] = None,
end_offset: Optional[int] = None
) -> None:
"""Attempts to scroll obj to the top left."""
@@ -294,8 +295,8 @@ class AXEventSynthesizer:
@staticmethod
def scroll_to_left_edge(
obj: Atspi.Accessible,
start_offset: int | None = None,
end_offset: int | None = None
start_offset: Optional[int] = None,
end_offset: Optional[int] = None
) -> None:
"""Attempts to scroll obj to the left edge."""
@@ -305,8 +306,8 @@ class AXEventSynthesizer:
@staticmethod
def scroll_to_bottom_edge(
obj: Atspi.Accessible,
start_offset: int | None = None,
end_offset: int | None = None
start_offset: Optional[int] = None,
end_offset: Optional[int] = None
) -> None:
"""Attempts to scroll obj to the bottom edge."""
@@ -316,8 +317,8 @@ class AXEventSynthesizer:
@staticmethod
def scroll_to_bottom_right(
obj: Atspi.Accessible,
start_offset: int | None = None,
end_offset: int | None = None
start_offset: Optional[int] = None,
end_offset: Optional[int] = None
) -> None:
"""Attempts to scroll obj to the bottom right."""
@@ -327,8 +328,8 @@ class AXEventSynthesizer:
@staticmethod
def scroll_to_right_edge(
obj: Atspi.Accessible,
start_offset: int | None = None,
end_offset: int | None = None
start_offset: Optional[int] = None,
end_offset: Optional[int] = None
) -> None:
"""Attempts to scroll obj to the right edge."""
+4 -3
View File
@@ -32,6 +32,7 @@ __date__ = "$Date$"
__copyright__ = "Copyright (c) 2024 Igalia, S.L."
__license__ = "LGPL"
from typing import Optional
import os
import re
from urllib.parse import urlparse
@@ -66,7 +67,7 @@ class AXHypertext:
return count
@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."""
if not AXObject.supports_hypertext(obj):
@@ -202,7 +203,7 @@ class AXHypertext:
return basename
@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"""
if child := AXHypertext.get_child_at_offset(obj, offset):
@@ -227,7 +228,7 @@ class AXHypertext:
return None
@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."""
if not AXObject.supports_hypertext(obj):
+21 -21
View File
@@ -34,7 +34,7 @@ __license__ = "LGPL"
import re
import threading
import time
from typing import Callable, Generator
from typing import Callable, Generator, Optional
import gi
gi.require_version("Atspi", "2.0")
@@ -105,7 +105,7 @@ class AXObject:
return name.lower()
@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."""
if obj is None:
@@ -313,7 +313,7 @@ class AXObject:
return iface is not None
@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."""
if app is None:
@@ -545,7 +545,7 @@ class AXObject:
return index
@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."""
if not AXObject.is_valid(obj):
@@ -571,7 +571,7 @@ class AXObject:
return parent
@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"""
if not AXObject.is_valid(obj):
@@ -619,7 +619,7 @@ class AXObject:
def get_common_ancestor(
obj1: Atspi.Accessible,
obj2: Atspi.Accessible
) -> Atspi.Accessible | None:
) -> Optional[Atspi.Accessible]:
"""Returns the 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(
obj: Atspi.Accessible,
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"""
if pred(obj):
@@ -659,7 +659,7 @@ class AXObject:
def find_ancestor(
obj: Atspi.Accessible,
pred: Callable[[Atspi.Accessible], bool]
) -> Atspi.Accessible | None:
) -> Optional[Atspi.Accessible]:
"""Returns the ancestor of obj if the function pred is true"""
if not AXObject.is_valid(obj):
@@ -703,7 +703,7 @@ class AXObject:
return AXObject.find_ancestor(obj, lambda x: x == ancestor) is not None
@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."""
if not AXObject.is_valid(obj):
@@ -736,7 +736,7 @@ class AXObject:
@staticmethod
def get_child_checked(
obj: Atspi.Accessible, index: int
) -> Atspi.Accessible | None:
) -> Optional[Atspi.Accessible]:
"""Returns the nth child of obj, doing checks for tree validity"""
if not AXObject.is_valid(obj):
@@ -757,7 +757,7 @@ class AXObject:
def get_active_descendant_checked(
container: Atspi.Accessible,
reported_child: Atspi.Accessible
) -> Atspi.Accessible | None:
) -> Optional[Atspi.Accessible]:
"""Checks the reported active descendant and return the real/valid one."""
if not AXObject.has_state(container, Atspi.StateType.MANAGES_DESCENDANTS):
@@ -784,7 +784,7 @@ class AXObject:
def _find_descendant(
obj: Atspi.Accessible,
pred: Callable[[Atspi.Accessible], bool]
) -> Atspi.Accessible | None:
) -> Optional[Atspi.Accessible]:
"""Returns the descendant of obj if the function pred is true"""
if not AXObject.is_valid(obj):
@@ -806,7 +806,7 @@ class AXObject:
def find_descendant(
obj: Atspi.Accessible,
pred: Callable[[Atspi.Accessible], bool]
) -> Atspi.Accessible | None:
) -> Optional[Atspi.Accessible]:
"""Returns the descendant of obj if the function pred is true"""
start = time.time()
@@ -816,7 +816,7 @@ class AXObject:
return result
@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"""
if not AXObject.is_valid(obj):
@@ -831,8 +831,8 @@ class AXObject:
@staticmethod
def _find_all_descendants(
obj: Atspi.Accessible,
include_if: Callable[[Atspi.Accessible], bool] | None,
exclude_if: Callable[[Atspi.Accessible], bool] | None,
include_if: Optional[Callable[[Atspi.Accessible], bool]],
exclude_if: Optional[Callable[[Atspi.Accessible], bool]],
matches: list[Atspi.Accessible]
) -> None:
"""Returns all descendants which match the specified inclusion and exclusion"""
@@ -852,8 +852,8 @@ class AXObject:
@staticmethod
def find_all_descendants(
root: Atspi.Accessible,
include_if: Callable[[Atspi.Accessible], bool] | None = None,
exclude_if: Callable[[Atspi.Accessible], bool] | None = None
include_if: Optional[Callable[[Atspi.Accessible], bool]] = None,
exclude_if: Optional[Callable[[Atspi.Accessible], bool]] = None
) -> list[Atspi.Accessible]:
"""Returns all descendants which match the specified inclusion and exclusion"""
@@ -1044,7 +1044,7 @@ class AXObject:
@staticmethod
def iter_children(
obj: Atspi.Accessible,
pred: Callable[[Atspi.Accessible], bool] | None = None
pred: Optional[Callable[[Atspi.Accessible], bool]] = None
) -> Generator[Atspi.Accessible, None, None]:
"""Generator to iterate through obj's children. If the function pred is
specified, children for which pred is False will be skipped."""
@@ -1068,7 +1068,7 @@ class AXObject:
yield child
@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"""
if not AXObject.is_valid(obj):
@@ -1091,7 +1091,7 @@ class AXObject:
return sibling
@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"""
if not AXObject.is_valid(obj):
+2 -1
View File
@@ -28,6 +28,7 @@ __date__ = "$Date$"
__copyright__ = "Copyright (c) 2023 Igalia, S.L."
__license__ = "LGPL"
from typing import Optional
import gi
gi.require_version("Atspi", "2.0")
from gi.repository import Atspi
@@ -60,7 +61,7 @@ class AXSelection:
return count
@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."""
n_children = AXSelection.get_selected_child_count(obj)
+27 -27
View File
@@ -35,7 +35,7 @@ __license__ = "LGPL"
import threading
import time
from typing import Generator
from typing import Generator, Optional
import gi
gi.require_version("Atspi", "2.0")
@@ -62,11 +62,11 @@ class AXTable:
PHYSICAL_SPANS_FROM_TABLE: dict[int, tuple[int, int]] = {}
PHYSICAL_COLUMN_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_SPANS: dict[int, tuple[str | None, str | None]] = {}
PRESENTABLE_COLUMN_COUNT: dict[int, int | None] = {}
PRESENTABLE_ROW_COUNT: dict[int, int | None] = {}
PRESENTABLE_SPANS: dict[int, tuple[Optional[str], Optional[str]]] = {}
PRESENTABLE_COLUMN_COUNT: dict[int, Optional[int]] = {}
PRESENTABLE_ROW_COUNT: dict[int, Optional[int]] = {}
COLUMN_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)
@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."""
if not AXObject.supports_table(table):
@@ -171,7 +171,7 @@ class AXTable:
return count
@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."""
if hash(table) in AXTable.PRESENTABLE_COLUMN_COUNT:
@@ -217,7 +217,7 @@ class AXTable:
return count
@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."""
if hash(table) in AXTable.PRESENTABLE_ROW_COUNT:
@@ -344,7 +344,7 @@ class AXTable:
return AXTable.get_selected_column_count(table) == cols
@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."""
if not AXObject.supports_table(table):
@@ -405,7 +405,7 @@ class AXTable:
@staticmethod
def _get_cell_spans_from_attribute(
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."""
if hash(cell) in AXTable.PRESENTABLE_SPANS:
@@ -606,7 +606,7 @@ class AXTable:
@staticmethod
def get_new_row_headers(
cell: Atspi.Accessible,
old_cell: Atspi.Accessible | None
old_cell: Optional[Atspi.Accessible]
) -> list[Atspi.Accessible]:
"""Returns row headers of cell that are not also headers of old_cell. """
@@ -623,7 +623,7 @@ class AXTable:
@staticmethod
def get_new_column_headers(
cell: Atspi.Accessible,
old_cell: Atspi.Accessible | None
old_cell: Optional[Atspi.Accessible]
) -> list[Atspi.Accessible]:
"""Returns column headers of cell that are not also headers of old_cell. """
@@ -880,7 +880,7 @@ class AXTable:
@staticmethod
def _get_cell_coordinates_from_attribute(
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."""
if cell is None:
@@ -941,7 +941,7 @@ class AXTable:
return result
@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."""
if obj is None:
@@ -981,21 +981,21 @@ class AXTable:
return result
@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."""
row, col = 0, 0
return AXTable.get_cell_at(table, row, col)
@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."""
row, col = AXTable.get_row_count(table) - 1, AXTable.get_column_count(table) - 1
return AXTable.get_cell_at(table, row, col)
@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."""
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)
@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."""
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)
@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."""
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)
@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."""
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)
@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."""
row = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[0]
return AXTable.get_cell_at(AXTable.get_table(cell), row, 0)
@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."""
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)
@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."""
col = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[1]
return AXTable.get_cell_at(AXTable.get_table(cell), 0, col)
@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."""
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)
@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."""
attrs = AXObject.get_attributes_dict(cell, use_cache=False)
@@ -1192,7 +1192,7 @@ class AXTable:
return result
@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."""
table = AXTable.get_table(cell)
@@ -1207,7 +1207,7 @@ class AXTable:
return AXTable.get_cell_at(table, cell_row, headers_column)
@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."""
table = AXTable.get_table(cell)
+32 -32
View File
@@ -44,7 +44,7 @@ __license__ = "LGPL"
import enum
import locale
import re
from typing import Generator
from typing import Generator, Optional
import gi
gi.require_version("Atspi", "2.0")
@@ -104,7 +104,7 @@ class AXTextAttribute(enum.Enum):
WRITING_MODE = ("writing-mode", False)
@classmethod
def from_string(cls, string: str) -> "AXTextAttribute" | None:
def from_string(cls, string: str) -> Optional['AXTextAttribute']:
"""Returns the AXTextAttribute for the specified string."""
for attribute in cls:
@@ -114,7 +114,7 @@ class AXTextAttribute(enum.Enum):
return None
@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."""
for attribute in cls:
@@ -192,7 +192,7 @@ class AXText:
@staticmethod
def get_character_at_offset(
obj: Atspi.Accessible,
offset: int | None = None
offset: Optional[int] = None
) -> tuple[str, int, int]:
"""Returns the (character, start, end) for the current or specified offset."""
@@ -239,7 +239,7 @@ class AXText:
@staticmethod
def get_next_character(
obj: Atspi.Accessible,
offset: int | None = None
offset: Optional[int] = None
) -> tuple[str, int, int]:
"""Returns the next (character, start, end) for the current or specified offset."""
@@ -264,7 +264,7 @@ class AXText:
@staticmethod
def get_previous_character(
obj: Atspi.Accessible,
offset: int | None = None
offset: Optional[int] = None
) -> tuple[str, int, int]:
"""Returns the previous (character, start, end) for the current or specified offset."""
@@ -291,7 +291,7 @@ class AXText:
@staticmethod
def iter_character(
obj: Atspi.Accessible,
offset: int | None = None
offset: Optional[int] = None
) -> Generator[tuple[str, int, int], None, None]:
"""Generator to iterate by character in obj starting with the character at offset."""
@@ -312,7 +312,7 @@ class AXText:
@staticmethod
def get_word_at_offset(
obj: Atspi.Accessible,
offset: int | None = None
offset: Optional[int] = None
) -> tuple[str, int, int]:
"""Returns the (word, start, end) for the current or specified offset."""
@@ -354,7 +354,7 @@ class AXText:
@staticmethod
def get_next_word(
obj: Atspi.Accessible,
offset: int | None = None
offset: Optional[int] = None
) -> tuple[str, int, int]:
"""Returns the next (word, start, end) for the current or specified offset."""
@@ -379,7 +379,7 @@ class AXText:
@staticmethod
def get_previous_word(
obj: Atspi.Accessible,
offset: int | None = None
offset: Optional[int] = None
) -> tuple[str, int, int]:
"""Returns the previous (word, start, end) for the current or specified offset."""
@@ -406,7 +406,7 @@ class AXText:
@staticmethod
def iter_word(
obj: Atspi.Accessible,
offset: int | None = None
offset: Optional[int] = None
) -> Generator[tuple[str, int, int], None, None]:
"""Generator to iterate by word in obj starting with the word at offset."""
@@ -427,7 +427,7 @@ class AXText:
@staticmethod
def get_line_at_offset(
obj: Atspi.Accessible,
offset: int | None = None
offset: Optional[int] = None
) -> tuple[str, int, int]:
"""Returns the (line, start, end) for the current or specified offset."""
@@ -495,7 +495,7 @@ class AXText:
@staticmethod
def get_next_line(
obj: Atspi.Accessible,
offset: int | None = None
offset: Optional[int] = None
) -> tuple[str, int, int]:
"""Returns the next (line, start, end) for the current or specified offset."""
@@ -520,7 +520,7 @@ class AXText:
@staticmethod
def get_previous_line(
obj: Atspi.Accessible,
offset: int | None = None
offset: Optional[int] = None
) -> tuple[str, int, int]:
"""Returns the previous (line, start, end) for the current or specified offset."""
@@ -549,7 +549,7 @@ class AXText:
@staticmethod
def iter_line(
obj: Atspi.Accessible,
offset: int | None = None
offset: Optional[int] = None
) -> Generator[tuple[str, int, int], None, None]:
"""Generator to iterate by line in obj starting with the line at offset."""
@@ -606,7 +606,7 @@ class AXText:
@staticmethod
def _get_sentence_at_offset_fallback(
obj: Atspi.Accessible,
offset: int | None = None
offset: Optional[int] = None
) -> tuple[str, int, int]:
"""Fallback sentence detection for broken implementations."""
@@ -633,7 +633,7 @@ class AXText:
@staticmethod
def get_sentence_at_offset(
obj: Atspi.Accessible,
offset: int | None = None
offset: Optional[int] = None
) -> tuple[str, int, int]:
"""Returns the (sentence, start, end) for the current or specified offset."""
@@ -682,7 +682,7 @@ class AXText:
@staticmethod
def get_next_sentence(
obj: Atspi.Accessible,
offset: int | None = None
offset: Optional[int] = None
) -> tuple[str, int, int]:
"""Returns the next (sentence, start, end) for the current or specified offset."""
@@ -707,7 +707,7 @@ class AXText:
@staticmethod
def get_previous_sentence(
obj: Atspi.Accessible,
offset: int | None = None
offset: Optional[int] = None
) -> tuple[str, int, int]:
"""Returns the previous (sentence, start, end) for the current or specified offset."""
@@ -734,7 +734,7 @@ class AXText:
@staticmethod
def iter_sentence(
obj: Atspi.Accessible,
offset: int | None = None
offset: Optional[int] = None
) -> Generator[tuple[str, int, int], None, None]:
"""Generator to iterate by sentence in obj starting with the sentence at offset."""
@@ -759,7 +759,7 @@ class AXText:
@staticmethod
def get_paragraph_at_offset(
obj: Atspi.Accessible,
offset: int | None = None
offset: Optional[int] = None
) -> tuple[str, int, int]:
"""Returns the (paragraph, start, end) for the current or specified offset."""
@@ -801,7 +801,7 @@ class AXText:
@staticmethod
def get_next_paragraph(
obj: Atspi.Accessible,
offset: int | None = None
offset: Optional[int] = None
) -> tuple[str, int, int]:
"""Returns the next (paragraph, start, end) for the current or specified offset."""
@@ -826,7 +826,7 @@ class AXText:
@staticmethod
def get_previous_paragraph(
obj: Atspi.Accessible,
offset: int | None = None
offset: Optional[int] = None
) -> tuple[str, int, int]:
"""Returns the previous (paragraph, start, end) for the current or specified offset."""
@@ -852,7 +852,7 @@ class AXText:
@staticmethod
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 to iterate by paragraph in obj starting with the paragraph at offset."""
@@ -1206,7 +1206,7 @@ class AXText:
@staticmethod
def get_text_attributes_at_offset(
obj: Atspi.Accessible,
offset: int | None = None
offset: Optional[int] = None
) -> tuple[dict[str, str], int, int]:
"""Returns a (dict, start, end) tuple for attributes at offset in obj."""
@@ -1303,7 +1303,7 @@ class AXText:
return offset
@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."""
if not AXObject.supports_text(obj):
@@ -1487,7 +1487,7 @@ class AXText:
return result
@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."""
attributes = AXText.get_text_attributes_at_offset(obj, offset)[0]
@@ -1502,7 +1502,7 @@ class AXText:
return False
@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."""
attributes = AXText.get_text_attributes_at_offset(obj, offset)[0]
@@ -1552,8 +1552,8 @@ class AXText:
obj: Atspi.Accessible,
x: int,
y: int,
start_offset: int | None = None,
end_offset: int | None = None
start_offset: Optional[int] = None,
end_offset: Optional[int] = None
) -> bool:
"""Attempts to scroll obj to the specified point."""
@@ -1583,8 +1583,8 @@ class AXText:
def scroll_substring_to_location(
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
) -> bool:
"""Attempts to scroll the substring to the specified Atspi.ScrollType location."""
+13 -12
View File
@@ -34,6 +34,7 @@ __date__ = "$Date$"
__copyright__ = "Copyright (c) 2023-2025 Igalia, S.L."
__license__ = "LGPL"
from typing import Optional
import functools
import inspect
import queue
@@ -97,7 +98,7 @@ class AXUtilities:
AXUtilities.IS_LAYOUT_ONLY.clear()
@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."""
AXUtilities._clear_all_dictionaries(reason)
@@ -152,7 +153,7 @@ class AXUtilities:
return True
@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."""
candidates = []
@@ -192,7 +193,7 @@ class AXUtilities:
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
return filtered[0]
guess: Atspi.Accessible | None = None
guess: Optional[Atspi.Accessible] = None
if filtered:
tokens = ["AXUtilities: Still have multiple active windows:", filtered]
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
@@ -266,7 +267,7 @@ class AXUtilities:
return AXObject.find_all_descendants(obj, is_match)
@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"""
result = None
@@ -278,7 +279,7 @@ class AXUtilities:
return AXObject.find_descendant(obj, AXUtilitiesRole.is_default_button)
@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"""
result = None
@@ -290,7 +291,7 @@ class AXUtilities:
return AXObject.find_descendant(obj, AXUtilitiesState.is_focused)
@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"""
result = None
@@ -302,7 +303,7 @@ class AXUtilities:
return AXObject.find_descendant(obj, AXUtilitiesRole.is_info_bar)
@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"""
result = None
@@ -748,7 +749,7 @@ class AXUtilities:
return len(ancestors)
@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)"""
if not AXObject.is_valid(obj):
@@ -778,7 +779,7 @@ class AXUtilities:
return next_object
@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)"""
if not AXObject.is_valid(obj):
@@ -810,7 +811,7 @@ class AXUtilities:
@staticmethod
def is_on_screen(
obj: Atspi.Accessible,
bounding_box: Atspi.Rect | None = None
bounding_box: Optional[Atspi.Rect] = None
) -> bool:
"""Returns true if obj should be treated as being on screen."""
@@ -887,7 +888,7 @@ class AXUtilities:
def _get_on_screen_objects(
root: Atspi.Accessible,
cancellation_event: threading.Event,
bounding_box: Atspi.Rect | None = None
bounding_box: Optional[Atspi.Rect] = None
) -> list:
tokens = ["AXUtilities: Getting on-screen objects in", root, f"({hex(id(root))})"]
@@ -939,7 +940,7 @@ class AXUtilities:
@staticmethod
def get_on_screen_objects(
root: Atspi.Accessible,
bounding_box: Atspi.Rect | None = None,
bounding_box: Optional[Atspi.Rect] = None,
timeout: float = 5.0
) -> list:
"""Returns a list of onscreen objects in the given root."""
+4 -3
View File
@@ -30,6 +30,7 @@ __copyright__ = "Copyright (c) 2023-2024 Igalia, S.L." \
"Copyright (c) 2024 GNOME Foundation Inc."
__license__ = "LGPL"
from typing import Optional
import subprocess
import gi
@@ -82,7 +83,7 @@ class AXUtilitiesApplication:
return list(AXObject.iter_children(desktop, pred))
@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"""
if obj is None:
@@ -131,7 +132,7 @@ class AXUtilitiesApplication:
return version
@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"""
applications = AXUtilitiesApplication.get_all_applications()
@@ -144,7 +145,7 @@ class AXUtilitiesApplication:
return None
@staticmethod
def get_desktop() -> Atspi.Accessible | None:
def get_desktop() -> Optional[Atspi.Accessible]:
"""Returns the accessible desktop"""
try:
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -35,6 +35,7 @@ __copyright__ = "Copyright (c) 2024 Igalia, S.L." \
"Copyright (c) 2024 GNOME Foundation Inc."
__license__ = "LGPL"
from typing import Optional
import threading
import time
@@ -118,7 +119,7 @@ class AXUtilitiesRelation:
def _get_relation(
obj: Atspi.Accessible,
relation_type: Atspi.RelationType
) -> Atspi.Relation | None:
) -> Optional[Atspi.Relation]:
"""Returns the specified Atspi.Relation for obj"""
for relation in AXUtilitiesRelation.get_relations(obj):
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -34,6 +34,7 @@ __copyright__ = "Copyright (c) 2024 Igalia, S.L." \
"Copyright (c) 2024 GNOME Foundation Inc."
__license__ = "LGPL"
from typing import Optional
import threading
import time
@@ -149,7 +150,7 @@ class AXValue:
return f"{current:.{decimal_places}f}"
@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."""
if not AXObject.supports_value(obj):
+2 -3
View File
@@ -3571,9 +3571,8 @@
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<items>
<item translatable="yes">Claude Code (Enhanced)</item>
<item translatable="yes">Claude (Anthropic)</item>
<item translatable="yes">ChatGPT (OpenAI)</item>
<item translatable="yes">Claude Code (CLI)</item>
<item translatable="yes">Codex (CLI)</item>
<item translatable="yes">Gemini (Google)</item>
<item translatable="yes">Ollama (Local - Free)</item>
</items>
+39 -5
View File
@@ -57,6 +57,7 @@ from . import cthulhu_state
from . import settings
from . import settings_manager
from . import input_event
from . import input_event_manager
from . import keybindings
from . import pronunciation_dict
from . import braille
@@ -64,6 +65,7 @@ from . import speech
from . import speechserver
from . import text_attribute_names
from . import sound_theme_manager
from . import script_manager
from .ax_object import AXObject
_settingsManager = settings_manager.getManager()
@@ -1874,10 +1876,12 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
# Set provider combo
provider = prefs.get("aiProvider", settings.aiProvider)
providerIndex = 0 # Default to Claude Code
if provider == settings.AI_PROVIDER_GEMINI:
if provider == settings.AI_PROVIDER_CODEX:
providerIndex = 1
elif provider == settings.AI_PROVIDER_OLLAMA:
elif provider == settings.AI_PROVIDER_GEMINI:
providerIndex = 2
elif provider == settings.AI_PROVIDER_OLLAMA:
providerIndex = 3
self.aiProviderCombo.set_active(providerIndex)
# Set API key file
@@ -1947,6 +1951,8 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
# Update labels based on provider
if provider == settings.AI_PROVIDER_CLAUDE_CODE:
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:
self.aiApiKeyEntry.set_placeholder_text("No API key needed - uses local Ollama")
else:
@@ -3065,6 +3071,14 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self._presentMessage(messages.KB_ENTER_NEW_KEY)
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)
return
@@ -3073,6 +3087,10 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
cthulhu_state.capturingKeys = False
self._capturedKey = []
try:
script_manager.get_manager().get_active_script().refreshKeyGrabs()
except Exception:
pass
return
def _processKeyCaptured(self, keyPressedEvent):
@@ -3090,9 +3108,17 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
entries = entries_for_keycode[-1]
eventString = Gdk.keyval_name(entries[0])
eventState = keyPressedEvent.state
eventKeyvalName = Gdk.keyval_name(keyPressedEvent.keyval)
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]
return False
@@ -3180,6 +3206,10 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
cthulhu_state.capturingKeys = False
self._capturedKey = []
try:
script_manager.get_manager().get_active_script().refreshKeyGrabs()
except Exception:
pass
myiter = treeModel.get_iter_from_string(path)
try:
originalBinding = treeModel.get_value(myiter, text)
@@ -3793,7 +3823,12 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
def aiProviderChanged(self, widget):
"""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()
if 0 <= activeIndex < len(providers):
provider = providers[activeIndex]
@@ -3970,4 +4005,3 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
"""OCR copy to clipboard checkbox toggled handler"""
self.prefsDict["ocrCopyToClipboard"] = widget.get_active()
+5 -5
View File
@@ -29,7 +29,7 @@ __license__ = "LGPL"
import enum
import inspect
from typing import Callable
from typing import Callable, Optional
try:
from dasbus.connection import SessionMessageBus
@@ -156,7 +156,7 @@ class _HandlerInfo:
description: str,
action: Callable[..., bool],
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.description: str = description
@@ -548,10 +548,10 @@ class CthulhuRemoteController:
OBJECT_PATH = "/org/stormux/Cthulhu/Service"
def __init__(self) -> None:
self._dbus_service_interface: CthulhuDBusServiceInterface | None = None
self._dbus_service_interface: Optional[CthulhuDBusServiceInterface] = None
self._is_running: bool = False
self._bus: SessionMessageBus | None = None
self._event_loop: EventLoop | None = None
self._bus: Optional[SessionMessageBus] = None
self._event_loop: Optional[EventLoop] = None
self._pending_registrations: dict[str, object] = {}
self._total_commands: int = 0
self._total_getters: int = 0
+25 -25
View File
@@ -40,7 +40,7 @@ __copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." \
"Copyright (c) 2016-2023 Igalia, S.L."
__license__ = "LGPL"
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Optional
import gi
gi.require_version("Atspi", "2.0")
@@ -75,13 +75,13 @@ class FocusManager:
"""Manages the focused object, window, etc."""
def __init__(self) -> None:
self._window: Atspi.Accessible | None = cthulhu_state.activeWindow
self._focus: Atspi.Accessible | None = cthulhu_state.locusOfFocus
self._object_of_interest: Atspi.Accessible | None = cthulhu_state.objOfInterest
self._active_mode: str | None = cthulhu_state.activeMode
self._window: Optional[Atspi.Accessible] = cthulhu_state.activeWindow
self._focus: Optional[Atspi.Accessible] = cthulhu_state.locusOfFocus
self._object_of_interest: Optional[Atspi.Accessible] = cthulhu_state.objOfInterest
self._active_mode: Optional[str] = cthulhu_state.activeMode
self._last_cell_coordinates: tuple[int, int] = (-1, -1)
self._last_cursor_position: tuple[Atspi.Accessible | None, int] = (None, -1)
self._penultimate_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[Optional[Atspi.Accessible], int] = (None, -1)
msg = "FOCUS MANAGER: Registering D-Bus commands."
debug.print_message(debug.LEVEL_INFO, msg, True)
@@ -104,7 +104,7 @@ class FocusManager:
cthulhu_state.objOfInterest = 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."""
result = _get_ax_utilities().get_focused_object(self._window)
@@ -147,9 +147,9 @@ class FocusManager:
def emit_region_changed(
self, obj: Atspi.Accessible,
start_offset: int | None = None,
end_offset: int | None = None,
mode: str | None = None
start_offset: Optional[int] = None,
end_offset: Optional[int] = None,
mode: Optional[str] = None
) -> None:
"""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(
self
) -> tuple[str | None, Atspi.Accessible | None]:
) -> tuple[Optional[str], Optional[Atspi.Accessible]]:
"""Returns the current mode and associated object of interest"""
tokens = ["FOCUS MANAGER: Active mode:", self._active_mode,
@@ -200,7 +200,7 @@ class FocusManager:
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
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)."""
obj, offset = self._penultimate_cursor_position
@@ -208,7 +208,7 @@ class FocusManager:
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
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)."""
obj, offset = self._last_cursor_position
@@ -216,7 +216,7 @@ class FocusManager:
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
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)."""
tokens = ["FOCUS MANAGER: Setting last cursor position to", obj, offset]
@@ -239,7 +239,7 @@ class FocusManager:
debug.print_message(debug.LEVEL_INFO, msg, True)
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)."""
tokens = ["FOCUS MANAGER: Locus of focus is", self._focus]
@@ -248,8 +248,8 @@ class FocusManager:
def set_locus_of_focus(
self,
event: Atspi.Event | None,
obj: Atspi.Accessible | None,
event: Optional[Atspi.Event],
obj: Optional[Atspi.Accessible],
notify_script: bool = True,
force: bool = False
) -> None:
@@ -340,7 +340,7 @@ class FocusManager:
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
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)."""
tokens = ["FOCUS MANAGER: Active window is", self._window]
@@ -349,8 +349,8 @@ class FocusManager:
def set_active_window(
self,
frame: Atspi.Accessible | None,
app: Atspi.Accessible | None = None,
frame: Optional[Atspi.Accessible],
app: Optional[Atspi.Accessible] = None,
set_window_as_focus: bool = False,
notify_script: bool = False
) -> None:
@@ -390,7 +390,7 @@ class FocusManager:
def toggle_presentation_mode(
self,
script: default.Script,
event: InputEvent | None = None,
event: Optional[InputEvent] = None,
notify_user: bool = True
) -> bool:
"""Switches between browse mode and focus mode (web content only)."""
@@ -401,7 +401,7 @@ class FocusManager:
def toggle_layout_mode(
self,
script: default.Script,
event: InputEvent | None = None,
event: Optional[InputEvent] = None,
notify_user: bool = True
) -> bool:
"""Switches between object mode and layout mode for line presentation (web content only)."""
@@ -412,7 +412,7 @@ class FocusManager:
def enable_sticky_browse_mode(
self,
script: default.Script,
event: InputEvent | None = None,
event: Optional[InputEvent] = None,
notify_user: bool = True
) -> bool:
"""Enables sticky browse mode (web content only)."""
@@ -423,7 +423,7 @@ class FocusManager:
def enable_sticky_focus_mode(
self,
script: default.Script,
event: InputEvent | None = None,
event: Optional[InputEvent] = None,
notify_user: bool = True
) -> bool:
"""Enables sticky focus mode (web content only)."""
+9 -4
View File
@@ -39,7 +39,7 @@ __copyright__ = "Copyright (c) 2024 Igalia, S.L." \
"Copyright (c) 2024 GNOME Foundation Inc."
__license__ = "LGPL"
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Optional
import gi
gi.require_version("Atspi", "2.0")
@@ -50,6 +50,7 @@ from . import focus_manager
from . import input_event
from . import script_manager
from . import settings
from . import cthulhu_state
from .ax_object import AXObject
from .ax_utilities import AXUtilities
@@ -60,9 +61,9 @@ class InputEventManager:
"""Provides utilities for managing input events."""
def __init__(self) -> None:
self._last_input_event: input_event.InputEvent | None = None
self._last_non_modifier_key_event: input_event.KeyboardEvent | None = None
self._device: Atspi.Device | None = None
self._last_input_event: Optional[input_event.InputEvent] = None
self._last_non_modifier_key_event: Optional[input_event.KeyboardEvent] = None
self._device: Optional[Atspi.Device] = None
self._mapped_keycodes: list[int] = []
self._mapped_keysyms: list[int] = []
self._grabbed_bindings: dict[int, keybindings.KeyBinding] = {}
@@ -267,6 +268,10 @@ class InputEventManager:
msg = "INPUT EVENT MANAGER: Keyboard event processing is paused."
debug.print_message(debug.LEVEL_INFO, msg, True)
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)
if event in [self._last_input_event, self._last_non_modifier_key_event]:
+49 -5
View File
@@ -38,6 +38,8 @@ import gi
gi.require_version('Gtk', '3.0')
from gi.repository import GObject
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import Gdk
from . import cmdnames
from . import debug
@@ -275,16 +277,20 @@ class NotificationPresenter:
class NotificationListGUI:
"""The dialog containing the notifications list."""
RESPONSE_COPY = 1
def __init__(self, script, title, column_headers, rows):
self._script = script
self._model = None
self._tree = None
self._gui = self._create_dialog(title, column_headers, rows)
def _create_dialog(self, title, column_headers, rows):
dialog = Gtk.Dialog(title,
None,
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))
dialog.set_default_size(600, 400)
@@ -315,6 +321,11 @@ class NotificationListGUI:
self._model.set_value(row_iter, i, cell)
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)
return dialog
@@ -325,6 +336,10 @@ class NotificationListGUI:
self._gui.destroy()
return
if response == self.RESPONSE_COPY:
self._copy_selected_notification()
return
if response == Gtk.ResponseType.APPLY and self._model is not None:
self._model.clear()
getPresenter().clear_list()
@@ -336,10 +351,39 @@ class NotificationListGUI:
"""Shows the notifications list dialog."""
self._gui.show_all()
time_stamp = cthulhu_state.lastInputEvent.timestamp
if time_stamp == 0:
time_stamp = Gtk.get_current_event_time()
self._gui.present_with_time(time_stamp)
time_stamp = Gtk.get_current_event_time()
if not time_stamp or time_stamp > 0xFFFFFFFF:
time_stamp = Gdk.CURRENT_TIME
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
def getPresenter():
+3 -3
View File
@@ -59,9 +59,9 @@ class Plugin:
self.plugin_info = plugin_info
if plugin_info:
self.module_name = getattr(plugin_info, 'module_name', '')
self.name = getattr(plugin_info, 'name', '')
self.version = getattr(plugin_info, 'version', '')
self.description = getattr(plugin_info, 'description', '')
self.name = plugin_info.get_name()
self.version = plugin_info.get_version()
self.description = plugin_info.get_description()
@cthulhu_hookimpl
def activate(self, plugin=None):
+40 -8
View File
@@ -12,6 +12,7 @@ import os
import inspect
import importlib.util
import logging
import configparser
from enum import IntEnum
# Import pluggy if available
@@ -29,6 +30,12 @@ logger = logging.getLogger(__name__)
if PLUGIN_DEBUG:
logger.setLevel(logging.DEBUG)
_manager = None
def getManager():
"""Return the shared PluginSystemManager instance."""
return _manager
class PluginType(IntEnum):
"""Types of plugins we support."""
SYSTEM = 1
@@ -78,8 +85,10 @@ class PluginSystemManager:
"""Cthulhu Plugin Manager using pluggy."""
def __init__(self, app):
global _manager
self.app = app
logger.info("Initializing PluginSystemManager")
_manager = self
# Initialize plugin manager
if PLUGGY_AVAILABLE:
@@ -378,15 +387,38 @@ class PluginSystemManager:
if os.path.isfile(metadata_file):
try:
with open(metadata_file, 'r') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
with open(metadata_file, 'r', encoding='utf-8') as f:
contents = f.read()
if '=' in line:
key, value = line.split('=', 1)
metadata[key.strip()] = value.strip()
has_section_header = False
for line in contents.splitlines():
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:
logger.error(f"Error loading plugin metadata: {e}")
+169 -1
View File
@@ -334,6 +334,172 @@ Keep descriptions informative and well-structured."""
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):
"""Ollama local AI provider."""
@@ -563,9 +729,11 @@ def create_provider(provider_type, **kwargs):
"""Factory function to create AI providers."""
if provider_type == "claude_code":
return ClaudeCodeProvider(**kwargs)
elif provider_type == "codex":
return CodexProvider(**kwargs)
elif provider_type == "ollama":
return OllamaProvider(**kwargs)
elif provider_type == "gemini":
return GeminiProvider(**kwargs)
else:
raise ValueError(f"Unknown provider type: {provider_type}")
raise ValueError(f"Unknown provider type: {provider_type}")
+36
View File
@@ -204,6 +204,11 @@ class AIAssistant(Plugin):
result = self._check_claude_code_availability()
logger.info(f"Claude Code availability check result: {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:
logger.info("Checking Gemini API key")
if not self._api_key:
@@ -258,12 +263,43 @@ class AIAssistant(Plugin):
except Exception as e:
logger.warning(f"Claude Code CLI not available: {e}")
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):
"""Initialize the AI provider based on settings."""
try:
if self._provider_type == settings.AI_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:
self._ai_provider = create_provider("ollama", model=self._ollama_model, base_url=self._ollama_endpoint)
elif self._provider_type == settings.AI_PROVIDER_GEMINI:
+3 -3
View File
@@ -5,10 +5,10 @@ ocrdesktop_python_sources = files([
python3.install_sources(
ocrdesktop_python_sources,
subdir: 'cthulhu/plugins/OCRDesktop'
subdir: 'cthulhu/plugins/OCR'
)
install_data(
'plugin.info',
install_dir: python3.get_install_dir() / 'cthulhu' / 'plugins' / 'OCRDesktop'
)
install_dir: python3.get_install_dir() / 'cthulhu' / 'plugins' / 'OCR'
)
+7 -7
View File
@@ -223,7 +223,7 @@ class PluginManager(Plugin):
checkbox.connect("toggled", self._on_plugin_toggled, plugin_name)
# 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'):
info_text += f"\n{plugin_info['description']}"
if plugin_info.get('version'):
@@ -257,17 +257,17 @@ class PluginManager(Plugin):
from cthulhu import plugin_system_manager
# Use existing plugin manager to get plugins
if hasattr(plugin_system_manager, '_manager') and plugin_system_manager._manager:
manager = plugin_system_manager._manager
manager = plugin_system_manager.getManager()
if manager:
manager.rescanPlugins()
for plugin_info in manager.plugins:
plugin_name = plugin_info.get_module_name()
plugins[plugin_name] = {
'name': plugin_name,
'description': getattr(plugin_info, 'description', ''),
'version': getattr(plugin_info, 'version', ''),
'path': getattr(plugin_info, 'module_dir', '')
'name': plugin_info.get_name(),
'description': plugin_info.get_description(),
'version': plugin_info.get_version(),
'path': plugin_info.get_module_dir()
}
else:
# Fallback: manually scan plugin directories
+1
View File
@@ -210,6 +210,7 @@ CHAT_SPEAK_FOCUSED_CHANNEL = 2
# AI Assistant constants - simplified to providers that don't need complex API key management
AI_PROVIDER_CLAUDE_CODE = "claude_code"
AI_PROVIDER_CODEX = "codex"
AI_PROVIDER_GEMINI = "gemini"
AI_PROVIDER_OLLAMA = "ollama"
+3 -2
View File
@@ -17,6 +17,7 @@ falls off the end, it may be added again later.
from __future__ import annotations
from typing import Optional
import threading
from collections import deque
@@ -76,7 +77,7 @@ def is_capture_paused() -> bool:
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.
Returns True if the item was added; False otherwise.
@@ -141,7 +142,7 @@ def get_items() -> list[str]:
return list(_historyItems)
def remove(text: str | None) -> bool:
def remove(text: Optional[str]) -> bool:
"""Remove an item from the history (if present)."""
if text is None:
return False