From e2cbcb0ac4799ffe58f186d2bc6d6fba2409fbeb Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Thu, 1 Jan 2026 02:59:23 -0500 Subject: [PATCH 1/5] Attempt to fix sounds for nonstandard paths. --- sounds/meson.build | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/sounds/meson.build b/sounds/meson.build index 8b8419a..0d1b8b0 100644 --- a/sounds/meson.build +++ b/sounds/meson.build @@ -1,7 +1,26 @@ # Install sound theme files # Themes are installed to: {datadir}/cthulhu/sounds/{theme_name}/ -install_subdir( - 'default', - install_dir: get_option('datadir') / 'cthulhu' / 'sounds' +# Use explicit install_data for better compatibility across Meson versions +default_theme_sounds = files( + 'default/browse_mode.wav', + 'default/button.wav', + 'default/checkbox_checked.wav', + 'default/checkbox_mixed.wav', + 'default/checkbox_unchecked.wav', + 'default/combobox.wav', + 'default/focus_mode.wav', + 'default/radiobutton_checked.wav', + 'default/radiobutton_unchecked.wav', + 'default/start.wav', + 'default/stop.wav', + 'default/switch_off.wav', + 'default/switch_on.wav', + 'default/togglebutton_checked.wav', + 'default/togglebutton_unchecked.wav', +) + +install_data( + default_theme_sounds, + install_dir: get_option('datadir') / 'cthulhu' / 'sounds' / 'default' ) From afdd812f2f439d61746aae43225f1816d0996374 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Thu, 1 Jan 2026 03:40:39 -0500 Subject: [PATCH 2/5] Updated sound fallback paths to include nonstandard installation paths. --- src/cthulhu/sound_theme_manager.py | 76 ++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/src/cthulhu/sound_theme_manager.py b/src/cthulhu/sound_theme_manager.py index 0759cc1..912f0f5 100644 --- a/src/cthulhu/sound_theme_manager.py +++ b/src/cthulhu/sound_theme_manager.py @@ -99,14 +99,59 @@ class SoundThemeManager: self._systemSoundsDir = None self._userSoundsDir = None + def _deriveDatadirFromModulePath(self): + """Derive the datadir from where this module is actually installed. + + If module is at /usr/local/lib/python3.x/site-packages/cthulhu/, + then datadir should be /usr/local/share. + """ + try: + modulePath = os.path.dirname(os.path.abspath(__file__)) + # Walk up looking for lib/python* pattern + currentPath = modulePath + for _ in range(10): # Limit iterations to avoid infinite loop + parent = os.path.dirname(currentPath) + if parent == currentPath: + break + dirName = os.path.basename(currentPath) + # Check if we're in a lib directory + if dirName == 'lib' or dirName.startswith('lib64'): + # Parent of lib is the prefix + prefix = parent + datadir = os.path.join(prefix, 'share') + if os.path.isdir(datadir): + return datadir + currentPath = parent + except Exception: + pass + return None + def getSystemSoundsDir(self): - """Get system sounds directory from platform settings.""" + """Get system sounds directory from platform settings. + + First tries cthulhu_platform.datadir, then derives from module location. + """ if self._systemSoundsDir is None: + datadir = None + + # First try the configured datadir from cthulhu_platform try: from . import cthulhu_platform - datadir = getattr(cthulhu_platform, 'datadir', '/usr/share') + datadir = getattr(cthulhu_platform, 'datadir', None) except ImportError: + pass + + # If datadir points to user home, it's likely wrong - try to derive it + if datadir and (datadir.startswith(os.path.expanduser('~')) or + '/home/' in datadir): + derivedDatadir = self._deriveDatadirFromModulePath() + if derivedDatadir: + datadir = derivedDatadir + + # Fallback to /usr/share if nothing else works + if not datadir: datadir = '/usr/share' + self._systemSoundsDir = os.path.join(datadir, 'cthulhu', 'sounds') return self._systemSoundsDir @@ -123,10 +168,20 @@ class SoundThemeManager: Returns list of theme names (folder names) from both system and user dirs. User themes with same name as system themes will override system themes. + Also checks standard fallback paths. """ themes = set() - for baseDir in [self.getSystemSoundsDir(), self.getUserSoundsDir()]: + # Check configured paths plus standard fallback locations + # /usr/share is standard, /usr/local/share is non-standard + searchDirs = [ + self.getUserSoundsDir(), + self.getSystemSoundsDir(), + '/usr/share/cthulhu/sounds', + '/usr/local/share/cthulhu/sounds', + ] + + for baseDir in searchDirs: if os.path.isdir(baseDir): try: for entry in os.listdir(baseDir): @@ -143,15 +198,30 @@ class SoundThemeManager: """Get the path to a theme directory. User themes take precedence over system themes. + Checks multiple standard locations as fallbacks. """ + # Check user directory first userPath = os.path.join(self.getUserSoundsDir(), themeName) if os.path.isdir(userPath): return userPath + # Check the configured system directory systemPath = os.path.join(self.getSystemSoundsDir(), themeName) if os.path.isdir(systemPath): return systemPath + # Fallback: check standard system paths in case cthulhu_platform.datadir + # doesn't match where sounds are actually installed + # Check /usr/share first (standard), then /usr/local/share (non-standard) + fallbackPaths = [ + '/usr/share/cthulhu/sounds', + '/usr/local/share/cthulhu/sounds', + ] + for fallbackDir in fallbackPaths: + fallbackPath = os.path.join(fallbackDir, themeName) + if os.path.isdir(fallbackPath): + return fallbackPath + return None def getSoundPath(self, themeName, soundName): From 921ffc4145c18fda9a83e96534847a3da31d0b0d Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 2 Jan 2026 00:37:52 -0500 Subject: [PATCH 3/5] Steam improvements. I can't say it's perfect by any means, but much much improved. --- src/cthulhu/event_manager.py | 49 ++- src/cthulhu/script_manager.py | 6 + src/cthulhu/scripts/apps/__init__.py | 1 + src/cthulhu/scripts/apps/meson.build | 1 + .../scripts/apps/steamwebhelper/__init__.py | 1 + .../scripts/apps/steamwebhelper/meson.build | 9 + .../scripts/apps/steamwebhelper/script.py | 305 ++++++++++++++++++ 7 files changed, 369 insertions(+), 3 deletions(-) create mode 100644 src/cthulhu/scripts/apps/steamwebhelper/__init__.py create mode 100644 src/cthulhu/scripts/apps/steamwebhelper/meson.build create mode 100644 src/cthulhu/scripts/apps/steamwebhelper/script.py diff --git a/src/cthulhu/event_manager.py b/src/cthulhu/event_manager.py index 35fc16a..f9a6624 100644 --- a/src/cthulhu/event_manager.py +++ b/src/cthulhu/event_manager.py @@ -60,6 +60,7 @@ class EventManager: self._active = False self._enqueueCount = 0 self._dequeueCount = 0 + self._cmdlineCache = {} self._eventQueue = queue.Queue(0) self._gidleId = 0 self._gidleLock = threading.Lock() @@ -175,6 +176,43 @@ class EventManager: return False + def _getAppCmdline(self, app): + pid = AXObject.get_process_id(app) + if pid == -1: + return "" + + cmdline = self._cmdlineCache.get(pid) + if cmdline is None: + cmdline = debug.getCmdline(pid) + self._cmdlineCache[pid] = cmdline + return cmdline + + def _isSteamApp(self, app): + name = AXObject.get_name(app) + if not name: + nameLower = "" + else: + nameLower = name.lower() + + if "steamwebhelper" in nameLower or nameLower in ("steam", "steam web helper"): + return True + + cmdline = self._getAppCmdline(app) + return "steamwebhelper" in cmdline + + def _isSteamNotificationEvent(self, event): + for obj in (event.any_data, event.source): + if not isinstance(obj, Atspi.Accessible): + continue + if AXUtilities.is_notification(obj) or AXUtilities.is_alert(obj): + return True + liveAttr = AXObject.get_attribute(obj, 'live') + containerLive = AXObject.get_attribute(obj, 'container-live') + if liveAttr in ('assertive', 'polite') or containerLive in ('assertive', 'polite'): + return True + + return False + def _ignore(self, event): """Returns True if this event should be ignored.""" @@ -241,9 +279,14 @@ class EventManager: debug.printMessage(debug.LEVEL_INFO, msg, True) return True if script.app != app: - msg = 'EVENT MANAGER: Ignoring because event is not from active app' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return True + # Allow Steam notifications from inactive apps. + if self._isSteamApp(app) and self._isSteamNotificationEvent(event): + msg = 'EVENT MANAGER: Allowing Steam notification from inactive app' + debug.printMessage(debug.LEVEL_INFO, msg, True) + else: + msg = 'EVENT MANAGER: Ignoring because event is not from active app' + debug.printMessage(debug.LEVEL_INFO, msg, True) + return True if event.type.startswith('object:text-changed') \ and self.EMBEDDED_OBJECT_CHARACTER in event.any_data \ diff --git a/src/cthulhu/script_manager.py b/src/cthulhu/script_manager.py index f21abd3..47e9602 100644 --- a/src/cthulhu/script_manager.py +++ b/src/cthulhu/script_manager.py @@ -113,6 +113,12 @@ class ScriptManager: debug.printMessage(debug.LEVEL_INFO, msg, True) return None + pid = AXObject.get_process_id(app) + if pid != -1: + cmdline = debug.getCmdline(pid) + if "steamwebhelper" in cmdline: + return "steamwebhelper" + altNames = list(self._appNames.keys()) if name.endswith(".py") or name.endswith(".bin"): name = name.split('.')[0] diff --git a/src/cthulhu/scripts/apps/__init__.py b/src/cthulhu/scripts/apps/__init__.py index 1d73da5..8333088 100644 --- a/src/cthulhu/scripts/apps/__init__.py +++ b/src/cthulhu/scripts/apps/__init__.py @@ -40,5 +40,6 @@ __all__ = ['Banshee', 'soffice', 'SeaMonkey', 'smuxi-frontend-gnome', + 'steamwebhelper', 'Thunderbird', 'xfwm4'] diff --git a/src/cthulhu/scripts/apps/meson.build b/src/cthulhu/scripts/apps/meson.build index a9ecfe6..91991cd 100644 --- a/src/cthulhu/scripts/apps/meson.build +++ b/src/cthulhu/scripts/apps/meson.build @@ -25,4 +25,5 @@ subdir('notify-osd') subdir('pidgin') subdir('soffice') subdir('smuxi-frontend-gnome') +subdir('steamwebhelper') subdir('xfwm4') \ No newline at end of file diff --git a/src/cthulhu/scripts/apps/steamwebhelper/__init__.py b/src/cthulhu/scripts/apps/steamwebhelper/__init__.py new file mode 100644 index 0000000..585c964 --- /dev/null +++ b/src/cthulhu/scripts/apps/steamwebhelper/__init__.py @@ -0,0 +1 @@ +from .script import Script diff --git a/src/cthulhu/scripts/apps/steamwebhelper/meson.build b/src/cthulhu/scripts/apps/steamwebhelper/meson.build new file mode 100644 index 0000000..2ae9c2d --- /dev/null +++ b/src/cthulhu/scripts/apps/steamwebhelper/meson.build @@ -0,0 +1,9 @@ +steamwebhelper_python_sources = files([ + '__init__.py', + 'script.py', +]) + +python3.install_sources( + steamwebhelper_python_sources, + subdir: 'cthulhu/scripts/apps/steamwebhelper' +) diff --git a/src/cthulhu/scripts/apps/steamwebhelper/script.py b/src/cthulhu/scripts/apps/steamwebhelper/script.py new file mode 100644 index 0000000..b5314d8 --- /dev/null +++ b/src/cthulhu/scripts/apps/steamwebhelper/script.py @@ -0,0 +1,305 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 Stormux +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., Franklin Street, Fifth Floor, +# Boston MA 02110-1301 USA. +# +# Cthulhu project: https://git.stormux.org/storm/cthulhu + +"""Custom script for Steam Web Helper (Steam client UI).""" + +__id__ = "$Id$" +__version__ = "$Revision$" +__date__ = "$Date$" +__copyright__ = "Copyright (c) 2024 Stormux" +__license__ = "LGPL" + +import time + +from cthulhu import debug +from cthulhu import settings +from cthulhu import cthulhu_state +from cthulhu import settings_manager +from cthulhu import structural_navigation +from cthulhu.ax_object import AXObject +from cthulhu.ax_utilities import AXUtilities +from cthulhu.ax_utilities_relation import AXUtilitiesRelation +from cthulhu.ax_utilities_role import AXUtilitiesRole +from cthulhu.scripts.toolkits import Chromium + +settingsManager = settings_manager.getManager() + + +class SteamStructuralNavigation(structural_navigation.StructuralNavigation): + def _headingGetter(self, document, arg=None): + def isUsefulHeading(candidate): + if not self._script.utilities.inDocumentContent(candidate): + return False + + text = self._script.utilities.displayedText(candidate) or AXObject.get_name(candidate) + if not text: + return False + + return not text.strip().isdigit() + + if arg is None: + return super()._headingGetter(document, arg) + + headings = super()._headingGetter(document, None) + headings = [heading for heading in headings if isUsefulHeading(heading)] + return [heading for heading in headings if self._script.utilities.headingLevel(heading) == arg] + + +class Script(Chromium.Script): + """Script for Steam Web Helper with background notification support.""" + + def __init__(self, app): + """Creates a new script for the Steam Web Helper application. + + Arguments: + - app: the application to create a script for. + """ + super().__init__(app) + # CRITICAL: Enable background event processing for notifications + # This allows Steam notifications to be spoken even when Steam + # is not the focused application. + self.presentIfInactive = True + self._lastSteamNotification = ("", 0.0) + + def onShowingChanged(self, event): + """Callback for object:state-changed:showing accessibility events.""" + + # Check if this is a Steam notification/alert becoming visible + if event.detail1 and self._isSteamNotification(event.source): + self._presentSteamNotification(event.source) + return + + # Fall through to Chromium/web handling + super().onShowingChanged(event) + + def onChildrenAdded(self, event): + """Callback for object:children-changed:add accessibility events.""" + + # Check if a notification element was added + if self._isSteamNotification(event.any_data): + self._presentSteamNotification(event.any_data) + return + + super().onChildrenAdded(event) + + def getStructuralNavigation(self): + types = self.getEnabledStructuralNavigationTypes() + enable = settingsManager.getSetting('structuralNavigationEnabled') + return SteamStructuralNavigation(self, types, enable) + + def onFocusedChanged(self, event): + """Callback for object:state-changed:focused accessibility events.""" + + if event.detail1 and AXUtilities.is_document_web(event.source): + prevDocument = self.utilities.getDocumentForObject(cthulhu_state.locusOfFocus) + if prevDocument and AXUtilities.is_embedded(prevDocument) \ + and self.utilities.inDocumentContent(cthulhu_state.locusOfFocus): + msg = "STEAM: Ignoring focus event for top-level document while in embedded content" + debug.printMessage(debug.LEVEL_INFO, msg, True) + return True + + if event.detail1 \ + and AXUtilitiesRole.is_button(event.source) \ + and cthulhu_state.locusOfFocus in AXUtilitiesRelation.get_is_labelled_by(event.source): + msg = "STEAM: Ignoring focus event for labelled button" + debug.printMessage(debug.LEVEL_INFO, msg, True) + return True + + return super().onFocusedChanged(event) + + def onTextInserted(self, event): + """Callback for object:text-changed:insert accessibility events.""" + + if self._presentSteamLiveRegionText(event): + return + + super().onTextInserted(event) + + def _isSteamNotification(self, obj): + """Detect if object is a Steam notification. + + Arguments: + - obj: the accessible object to check + + Returns True if obj appears to be a notification element. + """ + if obj is None: + return False + + # Check for NOTIFICATION or ALERT roles + if AXUtilities.is_notification(obj) or AXUtilities.is_alert(obj): + msg = "STEAM: Found notification/alert role" + debug.printMessage(debug.LEVEL_INFO, msg, True) + return True + + # Check for live region with assertive/polite politeness + # (Steam may use ARIA live regions for toasts) + liveAttr = AXObject.get_attribute(obj, 'live') + containerLive = AXObject.get_attribute(obj, 'container-live') + if liveAttr in ['assertive', 'polite'] or containerLive in ['assertive', 'polite']: + politeness = liveAttr or containerLive + msg = f"STEAM: Found live region with politeness: {politeness}" + debug.printMessage(debug.LEVEL_INFO, msg, True) + return True + + def isNotificationCandidate(candidate): + if AXUtilities.is_notification(candidate) or AXUtilities.is_alert(candidate): + return True + candidateLive = AXObject.get_attribute(candidate, 'live') + candidateContainerLive = AXObject.get_attribute(candidate, 'container-live') + return candidateLive in ['assertive', 'polite'] or candidateContainerLive in ['assertive', 'polite'] + + ancestor = AXObject.find_ancestor(obj, isNotificationCandidate) + if ancestor: + msg = "STEAM: Found notification ancestor" + debug.printMessage(debug.LEVEL_INFO, msg, True) + return True + + return False + + def _presentSteamNotification(self, obj): + """Speak and save the notification. + + Arguments: + - obj: the notification accessible object + """ + text = self._getNotificationText(obj) + if not text: + msg = "STEAM: Notification had no text content" + debug.printMessage(debug.LEVEL_INFO, msg, True) + return + + self._presentSteamNotificationText(text, obj) + + def _presentSteamLiveRegionText(self, event): + if not isinstance(event.any_data, str): + return False + + text = event.any_data.strip() + if not text: + return False + + if not self._isSteamLiveRegion(event.source): + return False + + msg = f"STEAM: Live region text inserted: {text}" + debug.printMessage(debug.LEVEL_INFO, msg, True) + self._presentSteamNotificationText(text, event.source) + return True + + def _isSteamLiveRegion(self, obj): + if not obj: + return False + + liveAttr = AXObject.get_attribute(obj, 'live') + containerLive = AXObject.get_attribute(obj, 'container-live') + if liveAttr in ['assertive', 'polite'] or containerLive in ['assertive', 'polite']: + return True + + def isLiveRegion(candidate): + candidateLive = AXObject.get_attribute(candidate, 'live') + candidateContainerLive = AXObject.get_attribute(candidate, 'container-live') + return candidateLive in ['assertive', 'polite'] \ + or candidateContainerLive in ['assertive', 'polite'] + + return AXObject.find_ancestor(obj, isLiveRegion) is not None + + def _presentSteamNotificationText(self, text, obj): + if self._isDuplicateSteamNotification(text): + msg = "STEAM: Suppressing duplicate notification" + debug.printMessage(debug.LEVEL_INFO, msg, True) + return + + notificationMsg = f"Steam: {text}" + + # Speak the notification + voice = self.speechGenerator.voice(obj=obj, string=notificationMsg) + self.speakMessage(notificationMsg, voice=voice) + + # Display on braille + self.displayBrailleMessage(notificationMsg, flashTime=settings.brailleFlashTime) + + # Save to notification history + self.notificationPresenter.save_notification(notificationMsg) + + msg = f"STEAM: Presented notification: {text}" + debug.printMessage(debug.LEVEL_INFO, msg, True) + + def _isDuplicateSteamNotification(self, text): + lastText, lastTime = self._lastSteamNotification + now = time.monotonic() + if text == lastText and (now - lastTime) < 1.0: + return True + + self._lastSteamNotification = (text, now) + return False + + def _getNotificationText(self, obj): + """Extract text from notification element. + + Arguments: + - obj: the notification accessible object + + Returns the text content of the notification, or empty string if none found. + """ + def getText(candidate): + name = AXObject.get_name(candidate) + if name: + return name + return self.utilities.displayedText(candidate) + + # Try getting name directly + text = getText(obj) + if text: + return text + + # Try getting all text from labels/text elements + texts = [] + for label in AXUtilities.find_all_labels(obj): + text = getText(label) + if text: + texts.append(text) + + if texts: + return ' '.join(texts) + + def isNamedButton(candidate): + if not AXUtilitiesRole.is_button(candidate): + return False + return bool(getText(candidate)) + + descendantButton = AXObject.find_descendant(obj, isNamedButton) + if descendantButton: + return getText(descendantButton) + + ancestorButton = AXObject.find_ancestor(obj, isNamedButton) + if ancestorButton: + return getText(ancestorButton) + + def hasReadableText(candidate): + return bool(getText(candidate)) + + descendantText = AXObject.find_descendant(obj, hasReadableText) + if descendantText: + return getText(descendantText) + + # Fall back to displayed text of the object itself + return "" From 733f5eee69a0f7cc87dbb9d3308309f71aaf981e Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 2 Jan 2026 08:14:47 -0500 Subject: [PATCH 4/5] Improve focus detection on initial page loading. This should make structural navigation much more reliable on inital page load. --- src/cthulhu/scripts/web/script_utilities.py | 5 +++++ src/cthulhu/structural_navigation.py | 24 +++++++++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/cthulhu/scripts/web/script_utilities.py b/src/cthulhu/scripts/web/script_utilities.py index 3ab47ae..8826ed4 100644 --- a/src/cthulhu/scripts/web/script_utilities.py +++ b/src/cthulhu/scripts/web/script_utilities.py @@ -330,6 +330,11 @@ class Utilities(script_utilities.Utilities): if len(documents) == 1: document = documents[0] + if focusDoc and focusDoc != document and documentHasUri(focusDoc): + tokens = ["WEB: Single showing document differs; using focus-based document:", + focusDoc] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + return focusDoc if documentHasUri(document): return document diff --git a/src/cthulhu/structural_navigation.py b/src/cthulhu/structural_navigation.py index eeb009b..e48a812 100644 --- a/src/cthulhu/structural_navigation.py +++ b/src/cthulhu/structural_navigation.py @@ -37,6 +37,7 @@ gi.require_version("Atspi", "2.0") from gi.repository import Atspi from . import cmdnames +from . import cthulhu from . import dbus_service from . import debug from . import guilabels @@ -1175,23 +1176,38 @@ class StructuralNavigation: objPath = AXObject.get_path(obj) objRole = AXObject.get_role(obj) if objRole == Atspi.Role.INVALID: - return obj, characterOffset + return None, characterOffset self._script.utilities.setCaretPosition(obj, characterOffset) AXObject.clear_cache(obj) - if not AXUtilities.is_defunct(obj): + if not AXUtilities.is_defunct(obj) and not self._script.utilities.isZombie(obj): return obj, characterOffset tokens = ["STRUCTURAL NAVIGATION:", obj, "became defunct after setting caret position"] debug.printTokens(debug.LEVEL_INFO, tokens, True) replicant = self._script.utilities.get_objectFromPath(objPath) - if replicant and AXObject.get_role(replicant) == objRole: + if replicant and AXObject.get_role(replicant) == objRole \ + and not self._script.utilities.isZombie(replicant): tokens = ["STRUCTURAL NAVIGATION: Updating obj to replicant", replicant] debug.printTokens(debug.LEVEL_INFO, tokens, True) obj = replicant + return obj, characterOffset - return obj, characterOffset + document = getattr(self._script.utilities, "documentFrame", None) + searchForCaretContext = getattr(self._script.utilities, "searchForCaretContext", None) + if document and searchForCaretContext: + documentFrame = document() + if documentFrame and not self._script.utilities.isZombie(documentFrame): + fallbackObj, fallbackOffset = searchForCaretContext(documentFrame) + if fallbackObj and not self._script.utilities.isZombie(fallbackObj): + tokens = ["STRUCTURAL NAVIGATION: Recovering focus to", fallbackObj] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + cthulhu.setLocusOfFocus(None, fallbackObj, notifyScript=False) + self._script.utilities.setCaretContext( + fallbackObj, fallbackOffset, documentFrame) + + return None, characterOffset def _presentLine(self, obj, offset): """Presents the first line of the object to the user. From 5b446000b88f7ed04bd0c7c0e65c7352de495701 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 2 Jan 2026 08:58:21 -0500 Subject: [PATCH 5/5] Notification keybindings added for the list and for last notification. Also bound flat review copy and append to clipboard as well as the show contents window. Fixed bug with using shift along with the cthulhu modifier in keybinding assignments. --- README.md | 1 - distro-packages/Slint/README | 1 - distro-packages/Slint/cthulhu-info | 2 +- meson.build | 8 -------- src/cthulhu/cthulhu-setup.ui | 22 +++++++++++----------- src/cthulhu/cthulhu_gui_prefs.py | 17 +++++++++++++++++ src/cthulhu/flat_review_presenter.py | 24 ++++++++++++------------ src/cthulhu/notification_presenter.py | 8 ++++---- 8 files changed, 45 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 95faf18..70ee6ee 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,6 @@ toolkit, OpenOffice/LibreOffice, Gecko, WebKitGtk, and KDE Qt toolkit. * **pluggy** - Plugin and hook calling mechanisms for Python * **python-dasbus** - D-Bus remote control interface (optional) -* **libpeas** - Plugin loader library ### Audio and Speech diff --git a/distro-packages/Slint/README b/distro-packages/Slint/README index 811924e..1825ead 100644 --- a/distro-packages/Slint/README +++ b/distro-packages/Slint/README @@ -19,7 +19,6 @@ This package requires the following packages, all available from SlackBuilds.org - gst-plugins-good - gtk3 - liblouis -- libpeas - libwnck3 - python3-atspi - python3-cairo diff --git a/distro-packages/Slint/cthulhu-info b/distro-packages/Slint/cthulhu-info index 2cef0ec..7f30753 100644 --- a/distro-packages/Slint/cthulhu-info +++ b/distro-packages/Slint/cthulhu-info @@ -5,6 +5,6 @@ DOWNLOAD="https://git.stormux.org/storm/cthulhu.git" MD5SUM="SKIP" DOWNLOAD_x86_64="" MD5SUM_x86_64="" -REQUIRES="at-spi2-core brltty gobject-introspection gsettings-desktop-schemas gstreamer gst-plugins-base gst-plugins-good gtk3 liblouis libpeas libwnck3 python3-atspi python3-cairo python3-gobject python3-setproctitle speech-dispatcher" +REQUIRES="at-spi2-core brltty gobject-introspection gsettings-desktop-schemas gstreamer gst-plugins-base gst-plugins-good gtk3 liblouis libwnck3 python3-atspi python3-cairo python3-gobject python3-setproctitle speech-dispatcher" MAINTAINER="Storm Dragon" EMAIL="storm_dragon@stormux.org" diff --git a/meson.build b/meson.build index 0dedee2..6f09605 100644 --- a/meson.build +++ b/meson.build @@ -89,14 +89,6 @@ else summary += {'sound support': 'no (missing gstreamer)'} endif -# Check for libpeas -libpeas_dep = dependency('libpeas-1.0', required: false) -if libpeas_dep.found() - summary += {'plugin loading': 'yes (found libpeas)'} -else - summary += {'plugin loading': 'no (missing libpeas)'} -endif - # Integration with session startup i18n.merge_file( input: 'cthulhu-autostart.desktop.in', diff --git a/src/cthulhu/cthulhu-setup.ui b/src/cthulhu/cthulhu-setup.ui index 34abfcd..f69e8c3 100644 --- a/src/cthulhu/cthulhu-setup.ui +++ b/src/cthulhu/cthulhu-setup.ui @@ -3776,6 +3776,17 @@ 8 + + + True + False + AI Assistant + + + 8 + False + + True @@ -3951,17 +3962,6 @@ 9 - - - True - False - AI Assistant - - - 8 - False - - True diff --git a/src/cthulhu/cthulhu_gui_prefs.py b/src/cthulhu/cthulhu_gui_prefs.py index f2dec26..09814ef 100644 --- a/src/cthulhu/cthulhu_gui_prefs.py +++ b/src/cthulhu/cthulhu_gui_prefs.py @@ -3088,9 +3088,26 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): input_event_manager.get_manager().unmap_all_modifiers() except Exception: pass + self._unmapCthulhuModifiersForCapture() editable.connect('key-press-event', self.kbKeyPressed) return + def _unmapCthulhuModifiersForCapture(self): + """Unmap Cthulhu modifier keys so they can be captured as bindings.""" + device = cthulhu_state.device + if device is None: + return + + for modifierKey in settings.cthulhuModifierKeys: + keycode = keybindings.getKeycode(modifierKey) + if keycode == 0 and modifierKey == "Shift_Lock": + keycode = keybindings.getKeycode("Caps_Lock") + if keycode: + try: + device.unmap_modifier(keycode) + except Exception: + pass + def editingCanceledKey(self, editable): """Stops user input of a Key for a selected key binding""" diff --git a/src/cthulhu/flat_review_presenter.py b/src/cthulhu/flat_review_presenter.py index ea29ac1..e1b3338 100644 --- a/src/cthulhu/flat_review_presenter.py +++ b/src/cthulhu/flat_review_presenter.py @@ -479,23 +479,23 @@ class FlatReviewPresenter: bindings.add( keybindings.KeyBinding( - "", + "f", keybindings.defaultModifierMask, - keybindings.NO_MODIFIER_MASK, + keybindings.CTHULHU_MODIFIER_MASK | keybindings.SHIFT_MODIFIER_MASK, self._handlers.get("showContentsHandler"))) bindings.add( keybindings.KeyBinding( - "", + "c", keybindings.defaultModifierMask, - keybindings.NO_MODIFIER_MASK, + keybindings.CTHULHU_CTRL_MODIFIER_MASK, self._handlers.get("flatReviewCopyHandler"))) bindings.add( keybindings.KeyBinding( - "", + "c", keybindings.defaultModifierMask, - keybindings.NO_MODIFIER_MASK, + keybindings.CTHULHU_CTRL_MODIFIER_MASK | keybindings.SHIFT_MODIFIER_MASK, self._handlers.get("flatReviewAppendHandler"))) bindings.add( @@ -685,23 +685,23 @@ class FlatReviewPresenter: bindings.add( keybindings.KeyBinding( - "", + "f", keybindings.defaultModifierMask, - keybindings.NO_MODIFIER_MASK, + keybindings.CTHULHU_MODIFIER_MASK | keybindings.SHIFT_MODIFIER_MASK, self._handlers.get("showContentsHandler"))) bindings.add( keybindings.KeyBinding( - "", + "c", keybindings.defaultModifierMask, - keybindings.NO_MODIFIER_MASK, + keybindings.CTHULHU_CTRL_MODIFIER_MASK, self._handlers.get("flatReviewCopyHandler"))) bindings.add( keybindings.KeyBinding( - "", + "c", keybindings.defaultModifierMask, - keybindings.NO_MODIFIER_MASK, + keybindings.CTHULHU_CTRL_MODIFIER_MASK | keybindings.SHIFT_MODIFIER_MASK, self._handlers.get("flatReviewAppendHandler"))) bindings.add( diff --git a/src/cthulhu/notification_presenter.py b/src/cthulhu/notification_presenter.py index af6e614..2e8e22f 100644 --- a/src/cthulhu/notification_presenter.py +++ b/src/cthulhu/notification_presenter.py @@ -126,9 +126,9 @@ class NotificationPresenter: bindings.add( keybindings.KeyBinding( - "", + "n", keybindings.defaultModifierMask, - keybindings.NO_MODIFIER_MASK, + keybindings.CTHULHU_MODIFIER_MASK, self._handlers.get("present_last_notification"))) bindings.add( @@ -147,9 +147,9 @@ class NotificationPresenter: bindings.add( keybindings.KeyBinding( - "", + "n", keybindings.defaultModifierMask, - keybindings.NO_MODIFIER_MASK, + keybindings.CTHULHU_CTRL_MODIFIER_MASK, self._handlers.get("show_notification_list"))) return bindings