A few more web tweaks. Updated version stuff, made a small script to update all version strings.

This commit is contained in:
Storm Dragon
2025-12-27 18:49:01 -05:00
parent 71776ad24c
commit 88a88574ac
7 changed files with 164 additions and 82 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
# Maintainer: Storm Dragon <storm_dragon@stormux.org> # Maintainer: Storm Dragon <storm_dragon@stormux.org>
pkgname=cthulhu pkgname=cthulhu
pkgver=2025.12.09 pkgver=2025.12.27
pkgrel=1 pkgrel=1
pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca" pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca"
url="https://git.stormux.org/storm/cthulhu" url="https://git.stormux.org/storm/cthulhu"
+1 -1
View File
@@ -1,5 +1,5 @@
project('cthulhu', project('cthulhu',
version: '2025.12.09', version: '2025.12.27',
meson_version: '>= 1.0.0', meson_version: '>= 1.0.0',
) )
Executable
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
set -euo pipefail
scriptDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
newVersion="${1:-}"
if [[ -z "$newVersion" ]]; then
newVersion="$(date +%Y.%m.%d)"
fi
if [[ ! "$newVersion" =~ ^[0-9]{4}\.[0-9]{2}\.[0-9]{2}$ ]]; then
echo "Usage: $(basename "$0") [YYYY.MM.DD]" >&2
echo "Error: version must match YYYY.MM.DD" >&2
exit 1
fi
cthulhuVersionFile="${scriptDir}/src/cthulhu/cthulhuVersion.py"
mesonFile="${scriptDir}/meson.build"
pkgbuildFile="${scriptDir}/distro-packages/Arch-Linux/PKGBUILD"
for path in "$cthulhuVersionFile" "$mesonFile" "$pkgbuildFile"; do
if [[ ! -f "$path" ]]; then
echo "Error: Missing file: $path" >&2
exit 1
fi
done
sed -i "s/^version = \".*\"/version = \"${newVersion}\"/" "$cthulhuVersionFile"
sed -i "s/^ version: '.*',/ version: '${newVersion}',/" "$mesonFile"
sed -i "s/^pkgver=.*/pkgver=${newVersion}/" "$pkgbuildFile"
sed -i "s/^pkgrel=.*/pkgrel=1/" "$pkgbuildFile"
if ! rg -q "^version = \"${newVersion}\"" "$cthulhuVersionFile"; then
echo "Error: Failed to update ${cthulhuVersionFile}" >&2
exit 1
fi
if ! rg -q "^ version: '${newVersion}'," "$mesonFile"; then
echo "Error: Failed to update ${mesonFile}" >&2
exit 1
fi
if ! rg -q "^pkgver=${newVersion}$" "$pkgbuildFile"; then
echo "Error: Failed to update ${pkgbuildFile}" >&2
exit 1
fi
if ! rg -q "^pkgrel=1$" "$pkgbuildFile"; then
echo "Error: Failed to reset pkgrel in ${pkgbuildFile}" >&2
exit 1
fi
echo "Updated version to ${newVersion} in:" \
"${cthulhuVersionFile}" \
"${mesonFile}" \
"${pkgbuildFile}"
+78 -27
View File
@@ -43,12 +43,13 @@ gi.require_version("Gtk", "3.0")
from gi.repository import Gdk, GLib, Gtk from gi.repository import Gdk, GLib, Gtk
from . import cmdnames from . import cmdnames
from . import dbus_service
from . import debug from . import debug
from . import focus_manager
from . import guilabels from . import guilabels
from . import input_event from . import input_event
from . import keybindings from . import keybindings
from . import messages from . import messages
from . import cthulhu
from . import cthulhu_state from . import cthulhu_state
from . import script_manager from . import script_manager
from .ax_object import AXObject from .ax_object import AXObject
@@ -122,15 +123,20 @@ class ActionList(Gtk.Window):
def populate_actions(self, actions: list[str]) -> None: def populate_actions(self, actions: list[str]) -> None:
"""Populates the list with accessible actions.""" """Populates the list with accessible actions."""
self._actions = actions if isinstance(actions, dict):
items = list(actions.items())
else:
items = [(action, action) for action in actions]
self._actions = [name for name, _label in items]
# Clear existing items # Clear existing items
for child in self._listbox.get_children(): for child in self._listbox.get_children():
self._listbox.remove(child) self._listbox.remove(child)
# Add actions to list # Add actions to list
for action in actions: for _action, label_text in items:
label = Gtk.Label(label=action) label = Gtk.Label(label=label_text)
label.set_xalign(0.0) # Left align label.set_xalign(0.0) # Left align
label.set_margin_left(10) label.set_margin_left(10)
label.set_margin_right(10) label.set_margin_right(10)
@@ -163,6 +169,11 @@ class ActionPresenter:
self._handlers = self.get_handlers(True) self._handlers = self.get_handlers(True)
# _bindings will be initialized lazily in get_bindings() # _bindings will be initialized lazily in get_bindings()
msg = "ACTION PRESENTER: Registering D-Bus commands."
debug.printMessage(debug.LEVEL_INFO, msg, True)
controller = dbus_service.get_remote_controller()
controller.register_decorated_module("ActionPresenter", self)
def get_handlers(self, refresh: bool = False) -> dict: def get_handlers(self, refresh: bool = False) -> dict:
"""Returns a dictionary of input event handlers.""" """Returns a dictionary of input event handlers."""
@@ -226,9 +237,28 @@ class ActionPresenter:
script = script_manager.get_manager().get_script(app, self._obj) script = script_manager.get_manager().get_script(app, self._obj)
script_manager.get_manager().set_active_script(script, reason) script_manager.get_manager().set_active_script(script, reason)
# Update Cthulhu state manager = focus_manager.get_manager()
cthulhu_state.activeWindow = self._window manager.clear_state(reason)
cthulhu_state.locusOfFocus = self._obj manager.set_active_window(self._window)
manager.set_locus_of_focus(None, self._obj)
def _present_message(self, script, full_message, brief_message=None, notify_user=True) -> None:
"""Presents a message using the provided script or the active script."""
if not notify_user:
return
if script is not None:
script.presentMessage(full_message, brief_message)
return
active_script = cthulhu_state.activeScript
if active_script is not None:
active_script.presentMessage(full_message, brief_message)
return
msg = "ACTION PRESENTER: Unable to present message (no script)."
debug.printMessage(debug.LEVEL_INFO, msg, True)
def _clear_gui_and_restore_focus(self) -> None: def _clear_gui_and_restore_focus(self) -> None:
"""Clears the GUI reference and then restores focus.""" """Clears the GUI reference and then restores focus."""
@@ -252,7 +282,7 @@ class ActionPresenter:
# Use idle_add for asynchronous destruction to allow action to complete # Use idle_add for asynchronous destruction to allow action to complete
GLib.idle_add(self._gui.destroy) GLib.idle_add(self._gui.destroy)
def present_with_time(self, obj, start_time: float) -> bool: def present_with_time(self, obj, start_time: float, script=None, notify_user=True) -> bool:
"""Presents accessible actions for the given object with timing.""" """Presents accessible actions for the given object with timing."""
try: try:
@@ -264,28 +294,36 @@ class ActionPresenter:
return False return False
if obj is None: if obj is None:
debug.printMessage(debug.LEVEL_INFO, "ACTION PRESENTER: obj is None, using locusOfFocus", True) msg = "ACTION PRESENTER: No object found, presenting LOCATION_NOT_FOUND"
obj = cthulhu_state.locusOfFocus debug.printMessage(debug.LEVEL_INFO, msg, True)
full_message = messages.LOCATION_NOT_FOUND_FULL
if obj is None: brief_message = messages.LOCATION_NOT_FOUND_BRIEF
debug.printMessage(debug.LEVEL_INFO, "ACTION PRESENTER: No object found, presenting NO_ACCESSIBLE_ACTIONS", True) self._present_message(script, full_message, brief_message, notify_user)
full_message = messages.NO_ACCESSIBLE_ACTIONS return True
cthulhu.presentMessage(full_message)
return False
debug.printMessage(debug.LEVEL_INFO, f"ACTION PRESENTER: Getting actions for object: {obj}", True) debug.printMessage(debug.LEVEL_INFO, f"ACTION PRESENTER: Getting actions for object: {obj}", True)
actions = AXObject.get_action_names(obj) actions = {}
debug.printMessage(debug.LEVEL_INFO, f"ACTION PRESENTER: Found {len(actions) if actions else 0} actions: {actions}", True) for i in range(AXObject.get_n_actions(obj)):
name = AXObject.get_action_name(obj, i)
if not name:
continue
localized_name = AXObject.get_action_localized_name(obj, i)
description = AXObject.get_action_description(obj, i)
actions[name] = localized_name or description or name
debug.printMessage(debug.LEVEL_INFO, f"ACTION PRESENTER: Found {len(actions)} actions: {actions}", True)
if not actions: if not actions:
debug.printMessage(debug.LEVEL_INFO, "ACTION PRESENTER: No actions found, presenting NO_ACCESSIBLE_ACTIONS", True) msg = "ACTION PRESENTER: No actions found, presenting NO_ACTIONS_FOUND_ON"
full_message = messages.NO_ACCESSIBLE_ACTIONS debug.printMessage(debug.LEVEL_INFO, msg, True)
cthulhu.presentMessage(full_message) name = AXObject.get_name(obj) or AXUtilities.get_localized_role_name(obj)
return False full_message = messages.NO_ACTIONS_FOUND_ON % name
self._present_message(script, full_message, notify_user=notify_user)
return True
debug.printMessage(debug.LEVEL_INFO, "ACTION PRESENTER: Creating GUI", True) debug.printMessage(debug.LEVEL_INFO, "ACTION PRESENTER: Creating GUI", True)
self._obj = obj self._obj = obj
self._window = cthulhu_state.activeWindow self._window = focus_manager.get_manager().get_active_window()
self._gui = ActionList(self) self._gui = ActionList(self)
self._gui.populate_actions(actions) self._gui.populate_actions(actions)
@@ -298,7 +336,13 @@ class ActionPresenter:
debug.printMessage(debug.LEVEL_INFO, f"ACTION PRESENTER: Traceback: {traceback.format_exc()}", True) debug.printMessage(debug.LEVEL_INFO, f"ACTION PRESENTER: Traceback: {traceback.format_exc()}", True)
return False return False
def show_actions_list(self, script: script.Script = None, input_event: input_event.InputEvent = None) -> bool: @dbus_service.command
def show_actions_list(
self,
script: script.Script = None,
input_event: input_event.InputEvent = None,
notify_user: bool = True
) -> bool:
"""Shows the accessible actions list.""" """Shows the accessible actions list."""
try: try:
@@ -307,11 +351,18 @@ class ActionPresenter:
start_time = time.time() start_time = time.time()
# Get object from input event if available, otherwise let present_with_time handle it obj = None
obj = input_event.get_object() if input_event else None if input_event is not None:
obj = input_event.get_object()
debug.printMessage(debug.LEVEL_INFO, f"ACTION PRESENTER: Object from input_event: {obj}", True) debug.printMessage(debug.LEVEL_INFO, f"ACTION PRESENTER: Object from input_event: {obj}", True)
result = self.present_with_time(obj, start_time) if obj is None:
manager = focus_manager.get_manager()
_mode, obj = manager.get_active_mode_and_object_of_interest()
if obj is None:
obj = manager.get_locus_of_focus()
result = self.present_with_time(obj, start_time, script, notify_user)
debug.printMessage(debug.LEVEL_INFO, f"ACTION PRESENTER: show_actions_list returning: {result}", True) debug.printMessage(debug.LEVEL_INFO, f"ACTION PRESENTER: show_actions_list returning: {result}", True)
return result return result
except Exception as e: except Exception as e:
+1 -1
View File
@@ -23,5 +23,5 @@
# Forked from Orca screen reader. # Forked from Orca screen reader.
# Cthulhu project: https://git.stormux.org/storm/cthulhu # Cthulhu project: https://git.stormux.org/storm/cthulhu
version = "2025.12.09" version = "2025.12.27"
codeName = "testing" codeName = "testing"
+21 -47
View File
@@ -31,11 +31,9 @@ __date__ = "$Date$"
__copyright__ = "Copyright (C) 2011-2013 Igalia, S.L." __copyright__ = "Copyright (C) 2011-2013 Igalia, S.L."
__license__ = "LGPL" __license__ = "LGPL"
import gi
gi.require_version("Atspi", "2.0")
from gi.repository import Atspi
from . import debug from . import debug
from .ax_component import AXComponent
from .ax_hypertext import AXHypertext
from .ax_object import AXObject from .ax_object import AXObject
from .ax_table import AXTable from .ax_table import AXTable
from .ax_text import AXText from .ax_text import AXText
@@ -80,7 +78,9 @@ class LabelInference:
result, objects = self.inferFromTextLeft(obj) result, objects = self.inferFromTextLeft(obj)
debug.printMessage(debug.LEVEL_INFO, f"LABEL INFERENCE: Text Left: '{result}'", True) debug.printMessage(debug.LEVEL_INFO, f"LABEL INFERENCE: Text Left: '{result}'", True)
if not result or self._preferRight(obj): if not result or self._preferRight(obj):
result, objects = self.inferFromTextRight(obj) or result rightResult = self.inferFromTextRight(obj)
if rightResult[0] is not None:
result, objects = rightResult
debug.printMessage(debug.LEVEL_INFO, f"LABEL INFERENCE: Text Right: '{result}'", True) debug.printMessage(debug.LEVEL_INFO, f"LABEL INFERENCE: Text Right: '{result}'", True)
if not result: if not result:
result, objects = self.inferFromTable(obj) result, objects = self.inferFromTable(obj)
@@ -153,17 +153,12 @@ class LabelInference:
return False return False
def isMatch(x): def isMatch(x):
return x is not None \ return AXUtilities.is_web_element(x) and not AXUtilities.is_link(x)
and not self._script.utilities.isStaticTextLeaf(x) \
and not AXUtilities.is_link(x)
children = [child for child in AXObject.iter_children(obj, isMatch)] children = list(AXObject.iter_children(obj, isMatch))
if len(children) > 1: if len(children) > 1:
return False return False
if not AXObject.supports_text(obj):
return True
string = AXText.get_all_text(obj).strip() string = AXText.get_all_text(obj).strip()
if string.count(self._script.EMBEDDED_OBJECT_CHARACTER) > 1: if string.count(self._script.EMBEDDED_OBJECT_CHARACTER) > 1:
return False return False
@@ -191,19 +186,7 @@ class LabelInference:
if rv is not None: if rv is not None:
return rv return rv
widgetRoles = [Atspi.Role.CHECK_BOX, isWidget = AXUtilities.is_widget(obj) or AXUtilities.is_menu_related(obj)
Atspi.Role.RADIO_BUTTON,
Atspi.Role.TOGGLE_BUTTON,
Atspi.Role.COMBO_BOX,
Atspi.Role.LIST,
Atspi.Role.LIST_BOX,
Atspi.Role.MENU,
Atspi.Role.MENU_ITEM,
Atspi.Role.ENTRY,
Atspi.Role.PASSWORD_TEXT,
Atspi.Role.PUSH_BUTTON]
isWidget = AXObject.get_role(obj) in widgetRoles
if not isWidget and AXUtilities.is_editable(obj): if not isWidget and AXUtilities.is_editable(obj):
isWidget = True isWidget = True
@@ -223,30 +206,15 @@ class LabelInference:
return rv return rv
extents = 0, 0, 0, 0 extents = 0, 0, 0, 0
text = self._script.utilities.queryNonEmptyText(obj) if AXObject.supports_text(obj) and not AXUtilities.is_text_input(obj):
if text and not AXUtilities.is_text_input(obj):
if endOffset == -1: if endOffset == -1:
endOffset = AXText.get_character_count(obj) endOffset = AXText.get_character_count(obj)
rect = AXText.get_range_rect(obj, startOffset, endOffset)
try: extents = rect.x, rect.y, rect.width, rect.height
extents = Atspi.Text.get_range_extents(
obj, startOffset, endOffset, Atspi.CoordType.SCREEN)
except Exception:
tokens = ["LABEL INFERENCE: Exception getting character count for", obj]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return extents
if not (extents[2] and extents[3]): if not (extents[2] and extents[3]):
if not AXObject.supports_component(obj): rect = AXComponent.get_rect(obj)
tokens = ["LABEL INFERENCE:", obj, "does not support the component interface"] extents = rect.x, rect.y, rect.width, rect.height
debug.printTokens(debug.LEVEL_INFO, tokens, True)
else:
try:
ext = Atspi.Component.get_extents(obj, Atspi.CoordType.SCREEN)
extents = ext.x, ext.y, ext.width, ext.height
except Exception:
tokens = ["LABEL INFERENCE: Exception getting extents for", obj]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
self._extentsCache[(hash(obj), startOffset, endOffset)] = extents self._extentsCache[(hash(obj), startOffset, endOffset)] = extents
return extents return extents
@@ -278,10 +246,12 @@ class LabelInference:
key = hash(obj) key = hash(obj)
if self._isWidget(obj): if self._isWidget(obj):
start, end = self._script.utilities.getHyperlinkRange(obj) start = AXHypertext.get_link_start_offset(obj)
obj = AXObject.get_parent(obj) obj = AXObject.get_parent(obj)
rv = self._script.utilities.getLineContentsAtOffset(obj, start, True, False) rv = self._script.utilities.getLineContentsAtOffset(obj, start, True, False)
if rv is None:
rv = []
self._lineCache[key] = rv self._lineCache[key] = rv
return rv return rv
@@ -408,6 +378,10 @@ class LabelInference:
if self._cannotLabel(prevObj): if self._cannotLabel(prevObj):
return None, [] return None, []
if string.endswith("\n"):
string = string[:-1]
end -= 1
if string.strip(): if string.strip():
x, y, width, height = self._getExtents(prevObj, start, end) x, y, width, height = self._getExtents(prevObj, start, end)
objX, objY, objWidth, objHeight = self._getExtents(obj) objX, objY, objWidth, objHeight = self._getExtents(obj)
@@ -528,7 +502,7 @@ class LabelInference:
cellLeft = cellRight = cellAbove = cellBelow = None cellLeft = cellRight = cellAbove = cellBelow = None
gridrow = AXObject.find_ancestor(cell, self._isRow) gridrow = AXObject.find_ancestor(cell, self._isRow)
rowindex, colindex = self._script.utilities.coordinatesForCell(cell) rowindex, colindex = AXTable.get_cell_coordinates(cell, prefer_attribute=False)
if colindex > -1: if colindex > -1:
cellLeft = self._getCellFromTable(grid, rowindex, colindex - 1) cellLeft = self._getCellFromTable(grid, rowindex, colindex - 1)
cellRight = self._getCellFromTable(grid, rowindex, colindex + 1) cellRight = self._getCellFromTable(grid, rowindex, colindex + 1)
+5 -1
View File
@@ -3547,7 +3547,11 @@ class Utilities(script_utilities.Utilities):
if rv and not AXObject.get_name(obj) and AXObject.supports_text(obj): if rv and not AXObject.get_name(obj) and AXObject.supports_text(obj):
string = AXText.get_all_text(obj) string = AXText.get_all_text(obj)
if not string.strip(): if not string.replace("\ufffc", ""):
tokens = ["WEB:", obj, "is not clickable: its text is just EOCs"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
rv = False
elif not string.strip():
rv = not (AXUtilities.is_static(obj) or AXUtilities.is_link(obj)) rv = not (AXUtilities.is_static(obj) or AXUtilities.is_link(obj))
self._isClickableElement[hash(obj)] = rv self._isClickableElement[hash(obj)] = rv