Attempt to get pluggy working.

This commit is contained in:
Storm Dragon 2025-04-03 20:10:54 -04:00
parent 6bbe6e47fc
commit 084d4fe85f
21 changed files with 320 additions and 153 deletions

View File

@ -46,6 +46,7 @@ AM_PATH_PYTHON(3.3)
AM_CHECK_PYMOD(gi,,,[AC_MSG_ERROR(Could not find python module: gi)])
AM_CHECK_PYMOD(json,,,[AC_MSG_ERROR(Could not find python module: json)])
AM_CHECK_PYMOD(pluggy,,[pluggy_available="yes"],[pluggy_available="no"])
AM_CHECK_PYMOD(brlapi,,[brlapi_available="yes"],[brlapi_available="no"])
AM_CHECK_PYMOD(speechd,,[speechd_available="yes"],[speechd_available="no"])
AC_ARG_WITH([liblouis],
@ -168,6 +169,7 @@ if test "$have_libpeas" = "no"; then
fi
echo
echo Use pluggy: $pluggy_available
echo Use speech-dispatcher: $speechd_available
echo Use brltty: $brlapi_available
echo Use liblouis: $louis_available

View File

@ -34,6 +34,94 @@ __copyright__ = "Copyright (c) 2004-2009 Sun Microsystems Inc." \
__license__ = "LGPL"
import faulthandler
class APIHelper:
"""Helper class for plugin API interactions, including keybindings."""
def __init__(self, app):
"""Initialize the APIHelper.
Arguments:
- app: the Cthulhu application
"""
self.app = app
self._gestureBindings = {}
def registerGestureByString(self, function, name, gestureString,
inputEventType='default', normalizer='cthulhu',
learnModeEnabled=True, contextName=None):
"""Register a gesture by string.
Arguments:
- function: the function to call when the gesture is performed
- name: a human-readable name for this gesture
- gestureString: string representation of the gesture (e.g., 'kb:cthulhu+z')
- inputEventType: the type of input event
- normalizer: the normalizer to use
- learnModeEnabled: whether this should be available in learn mode
- contextName: the context for this gesture (e.g., plugin name)
Returns the binding ID or None if registration failed
"""
if not gestureString.startswith("kb:"):
return None
# Extract the key portion from the gesture string
key = gestureString.split(":", 1)[1]
# Handle Cthulhu modifier specially
if "cthulhu+" in key.lower():
from . import keybindings
key = key.lower().replace("cthulhu+", "")
# Create a keybinding handler
class GestureHandler:
def __init__(self, function, description):
self.function = function
self.description = description
def __call__(self, script, inputEvent):
return self.function(script, inputEvent)
handler = GestureHandler(function, name)
# Register the binding with the active script
from . import cthulhu_state
if cthulhu_state.activeScript:
bindings = cthulhu_state.activeScript.getKeyBindings()
binding = keybindings.KeyBinding(
key,
keybindings.defaultModifierMask,
keybindings.CTHULHU_MODIFIER_MASK,
handler)
bindings.add(binding)
# Store binding for later reference
if contextName not in self._gestureBindings:
self._gestureBindings[contextName] = []
self._gestureBindings[contextName].append(binding)
return binding
return None
def unregisterShortcut(self, binding, contextName=None):
"""Unregister a previously registered shortcut.
Arguments:
- binding: the binding to unregister
- contextName: the context for this gesture
"""
# Remove from script's keybindings
from . import cthulhu_state
if cthulhu_state.activeScript:
bindings = cthulhu_state.activeScript.getKeyBindings()
bindings.remove(binding)
# Remove from our tracking
if contextName in self._gestureBindings:
if binding in self._gestureBindings[contextName]:
self._gestureBindings[contextName].remove(binding)
import gi
import importlib
import os
@ -927,6 +1015,7 @@ class Cthulhu(GObject.Object):
self.dynamicApiManager = dynamic_api_manager.DynamicApiManager(self)
self.translationManager = translation_manager.TranslationManager(self)
self.debugManager = debug
self.APIHelper = APIHelper(self)
self.createCompatAPI()
self.pluginSystemManager = plugin_system_manager.PluginSystemManager(self)
def getAPIHelper(self):
@ -986,6 +1075,7 @@ class Cthulhu(GObject.Object):
# cthulhu lets say, special compat handling....
self.getDynamicApiManager().registerAPI('EmitRegionChanged', emitRegionChanged)
self.getDynamicApiManager().registerAPI('LoadUserSettings', loadUserSettings)
self.getDynamicApiManager().registerAPI('APIHelper', self.APIHelper)
cthulhuApp = Cthulhu()

View File

@ -193,7 +193,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
# ***** Key Bindings treeview initialization *****
self.keyBindView = self.get_widget("keyBindingsTreeview")
if self.keyBindView.get_columns():
for column in self.keyBindView.get_columns():
self.keyBindView.remove_column(column)
@ -337,7 +337,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
column.set_resizable(True)
column.set_sort_column_id(EDITABLE)
self.keyBindView.append_column(column)
# Populates the treeview with all the keybindings:
#
self._populateKeyBindings()
@ -582,7 +582,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self.get_widget("rateScale").set_value(rate)
else:
self.get_widget("rateScale").set_value(50.0)
pitch = self._getPitchForVoiceType(voiceType)
if pitch is not None:
self.get_widget("pitchScale").set_value(pitch)
@ -1150,7 +1150,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
grid = self.get_widget('flashMessageDurationGrid')
grid.set_sensitive(not checkbox.get_active())
self.prefsDict["flashIsPersistent"] = checkbox.get_active()
def textAttributeSpokenToggled(self, cell, path, model):
"""The user has toggled the state of one of the text attribute
checkboxes to be spoken. Update our model to reflect this, then
@ -1596,7 +1596,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
elif dateFormat == messages.DATE_FORMAT_ABBREVIATED_YMD:
indexdate = DATE_FORMAT_ABBREVIATED_YMD
combobox2.set_active (indexdate)
combobox3 = self.get_widget("timeFormatCombo")
self.populateComboBox(combobox3,
[sdtime(messages.TIME_FORMAT_LOCALE, ltime()),
@ -1757,7 +1757,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
prefs["enableEchoByWord"])
self.get_widget("enableEchoBySentenceCheckButton").set_active( \
prefs["enableEchoBySentence"])
# Text attributes pane.
#
self._createTextAttributesTreeView()
@ -1785,7 +1785,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self.get_widget("generalDesktopButton").set_active(True)
else:
self.get_widget("generalLaptopButton").set_active(True)
combobox = self.get_widget("sayAllStyle")
self.populateComboBox(combobox, [guilabels.SAY_ALL_STYLE_LINE,
guilabels.SAY_ALL_STYLE_SENTENCE])
@ -2748,7 +2748,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
elif dateFormatCombo == DATE_FORMAT_ABBREVIATED_YMD:
newFormat = messages.DATE_FORMAT_ABBREVIATED_YMD
self.prefsDict["presentDateFormat"] = newFormat
def timeFormatChanged(self, widget):
"""Signal handler for the "changed" signal for the timeFormat
GtkComboBox widget. Set the 'timeFormat' preference to the

View File

@ -66,7 +66,7 @@ class DynamicApiManager():
def getAPI(self, key, application = '', fallback = True):
# get dynamic API
api = None
try:
api = self.api[application][key]
return api
@ -83,5 +83,5 @@ class DynamicApiManager():
api = self.api[application]['']
except:
print('API Key: "{}/{}" not found,'.format(application, key))
return api

View File

@ -44,7 +44,7 @@ CTHULHU_SHIFT_MODIFIER_MASK = keybindings.CTHULHU_SHIFT_MODIFIER_MASK
CTHULHU_CTRL_MODIFIER_MASK = keybindings.CTHULHU_CTRL_MODIFIER_MASK
keymap = (
("9", defaultModifierMask, CTHULHU_MODIFIER_MASK,
"routePointerToItemHandler"),

View File

@ -19,6 +19,12 @@ try:
except ImportError:
# Fallback if pluggy is not available
def cthulhu_hookimpl(func=None, **kwargs):
"""Fallback decorator when pluggy is not available.
This is a no-op decorator that returns the original function.
It allows the code to continue working without pluggy, though
plugins will be disabled.
"""
if func is None:
return lambda f: f
return func

View File

@ -270,6 +270,9 @@ class PluginSystemManager:
plugin_instance = plugin_class()
pluginInfo.instance = plugin_instance
# Ensure plugins have a reference to the app
plugin_instance.app = self.getApp()
if hasattr(plugin_instance, 'set_app'):
plugin_instance.set_app(self.getApp())

View File

@ -1,6 +0,0 @@
[Plugin]
Module=ByeCthulhu
Loader=python3
Name=Stop announcement for cthulhu
Description=Test plugin for cthulhu
Authors=Chrys chrys@linux-a11y.org

View File

@ -1,52 +0,0 @@
#!/usr/bin/env python3
#
# Copyright (c) 2024 Stormux
# Copyright (c) 2010-2012 The Orca Team
# Copyright (c) 2012 Igalia, S.L.
# Copyright (c) 2005-2010 Sun Microsystems Inc.
#
# 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.
#
# Fork of Orca Screen Reader (GNOME)
# Original source: https://gitlab.gnome.org/GNOME/orca
from cthulhu import plugin
import gi
gi.require_version('Peas', '1.0')
from gi.repository import GObject
from gi.repository import Peas
import time
class ByeCthulhu(GObject.Object, Peas.Activatable, plugin.Plugin):
#__gtype_name__ = 'ByeCthulhu'
object = GObject.Property(type=GObject.Object)
def __init__(self):
plugin.Plugin.__init__(self)
def do_activate(self):
API = self.object
self.connectSignal("stop-application-completed", self.process)
def do_deactivate(self):
API = self.object
def do_update_state(self):
API = self.object
def process(self, app):
messages = app.getDynamicApiManager().getAPI('Messages')
activeScript = app.getDynamicApiManager().getAPI('CthulhuState').activeScript
activeScript.presentationInterrupt()
activeScript.presentMessage(messages.STOP_CTHULHU, resetStyles=False)

View File

@ -1,7 +1,7 @@
cthulhu_python_PYTHON = \
__init__.py \
ByeCthulhu.plugin \
ByeCthulhu.py
plugin.info \
plugin.py
cthulhu_pythondir=$(pkgpythondir)/plugins/ByeCthulhu

View File

@ -0,0 +1,8 @@
name = Bye Cthulhu
version = 1.0.0
description = Says goodbye when Cthulhu is shutting down
authors = Stormux <storm_dragon@stormux.org>
website = https://stormux.org
copyright = Copyright 2025
builtin = false
hidden = false

View File

@ -0,0 +1,74 @@
#!/usr/bin/env python3
#
# Copyright (c) 2025 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.
"""Bye Cthulhu plugin for Cthulhu."""
import logging
import time
from cthulhu.plugin import Plugin, cthulhu_hookimpl
logger = logging.getLogger(__name__)
class ByeCthulhu(Plugin):
"""Plugin that speaks a goodbye message when Cthulhu is shutting down."""
def __init__(self, *args, **kwargs):
"""Initialize the plugin."""
super().__init__(*args, **kwargs)
logger.info("ByeCthulhu plugin initialized")
self._signal_handler_id = None
@cthulhu_hookimpl
def activate(self, plugin=None):
"""Activate the plugin."""
# Skip if this activation call isn't for us
if plugin is not None and plugin is not self:
return
logger.info("Activating ByeCthulhu plugin")
try:
# Connect to the stop-application-completed signal
signal_manager = self.app.getSignalManager()
self._signal_handler_id = signal_manager.connectSignal(
"stop-application-completed",
self.process
)
except Exception as e:
logger.error(f"Error activating ByeCthulhu plugin: {e}")
@cthulhu_hookimpl
def deactivate(self, plugin=None):
"""Deactivate the plugin."""
# Skip if this deactivation call isn't for us
if plugin is not None and plugin is not self:
return
logger.info("Deactivating ByeCthulhu plugin")
try:
# Disconnect signal if we have an ID
if self._signal_handler_id is not None:
signal_manager = self.app.getSignalManager()
signal_manager.disconnectSignal(
"stop-application-completed",
self._signal_handler_id
)
self._signal_handler_id = None
except Exception as e:
logger.error(f"Error deactivating ByeCthulhu plugin: {e}")
def process(self, app):
"""Process the stop-application-completed signal."""
try:
messages = app.getDynamicApiManager().getAPI('Messages')
state = app.getDynamicApiManager().getAPI('CthulhuState')
if state.activeScript:
state.activeScript.presentationInterrupt()
state.activeScript.presentMessage(messages.STOP_CTHULHU, resetStyles=False)
except Exception as e:
logger.error(f"Error in ByeCthulhu process: {e}")

View File

@ -1,6 +0,0 @@
[Plugin]
Module=DisplayVersion
Loader=python3
Name=Display Version
Description=Announce the current version of Cthulhu
Authors=Storm Dragon <storm_dragon@stormux.org>

View File

@ -1,61 +0,0 @@
#!/usr/bin/env python3
#
# Copyright (c) 2024 Stormux
# Copyright (c) 2010-2012 The Orca Team
# Copyright (c) 2012 Igalia, S.L.
# Copyright (c) 2005-2010 Sun Microsystems Inc.
#
# 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.
#
# Fork of Orca Screen Reader (GNOME)
# Original source: https://gitlab.gnome.org/GNOME/orca
from cthulhu import plugin
import gi
gi.require_version('Peas', '1.0')
from gi.repository import GObject
from gi.repository import Peas
from cthulhu import cthulhuVersion
class DisplayVersion(GObject.Object, Peas.Activatable, plugin.Plugin):
__gtype_name__ = 'displayversion'
object = GObject.Property(type=GObject.Object)
def __init__(self):
plugin.Plugin.__init__(self)
self._api = None
def do_activate(self):
self._api = self.object
self.registerGestureByString(
self.speakText,
_(f'Cthulhu screen reader version {cthulhuVersion.version}-{cthulhuVersion.codeName}'),
'kb:cthulhu+shift+v'
)
def do_deactivate(self):
self._api = None
def speakText(self, script=None, inputEvent=None):
if not self._api:
return False
self._api.app.getDynamicApiManager().getAPI('CthulhuState').activeScript.presentMessage(
f'Cthulhu screen reader version {cthulhuVersion.version}-{cthulhuVersion.codeName}',
resetStyles=False
)
return True

View File

@ -1,7 +1,7 @@
cthulhu_python_PYTHON = \
__init__.py \
DisplayVersion.plugin \
DisplayVersion.py
plugin.info \
plugin.py
cthulhu_pythondir=$(pkgpythondir)/plugins/DisplayVersion

View File

@ -0,0 +1,8 @@
name = Display Version
version = 1.0.0
description = Announces the Cthulhu version with Cthulhu+Shift+V
authors = Stormux <storm_dragon@stormux.org>
website = https://stormux.org
copyright = Copyright 2025
builtin = false
hidden = false

View File

@ -0,0 +1,68 @@
#!/usr/bin/env python3
#
# Copyright (c) 2025 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.
"""Display Version plugin for Cthulhu."""
import logging
from cthulhu.plugin import Plugin, cthulhu_hookimpl
from cthulhu import cthulhuVersion
logger = logging.getLogger(__name__)
class DisplayVersion(Plugin):
"""Plugin that announces the current Cthulhu version."""
def __init__(self, *args, **kwargs):
"""Initialize the plugin."""
super().__init__(*args, **kwargs)
logger.info("DisplayVersion plugin initialized")
self._kb_binding = None
@cthulhu_hookimpl
def activate(self, plugin=None):
"""Activate the plugin."""
# Skip if this activation call isn't for us
if plugin is not None and plugin is not self:
return
try:
logger.info("Activating DisplayVersion plugin")
# Register keyboard shortcut
self._kb_binding = self.registerGestureByString(
self.speakText,
f'Cthulhu screen reader version {cthulhuVersion.version}-{cthulhuVersion.codeName}',
'kb:cthulhu+shift+v'
)
except Exception as e:
logger.error(f"Error activating DisplayVersion plugin: {e}")
@cthulhu_hookimpl
def deactivate(self, plugin=None):
"""Deactivate the plugin."""
# Skip if this deactivation call isn't for us
if plugin is not None and plugin is not self:
return
logger.info("Deactivating DisplayVersion plugin")
def speakText(self, script=None, inputEvent=None):
"""Speak the Cthulhu version when shortcut is pressed."""
try:
if self.app:
state = self.app.getDynamicApiManager().getAPI('CthulhuState')
if state.activeScript:
state.activeScript.presentMessage(
f'Cthulhu screen reader version {cthulhuVersion.version}-{cthulhuVersion.codeName}',
resetStyles=False
)
return True
except Exception as e:
logger.error(f"Error in DisplayVersion speakText: {e}")
return False

View File

@ -0,0 +1,33 @@
# Hello World Plugin for Cthulhu
This is a simple test plugin for the Cthulhu screen reader that demonstrates how to use the pluggy-based plugin system.
## Features
- Registers a keyboard shortcut (Cthulhu+z) that speaks "hello world" when pressed
- Demonstrates how to use the Plugin base class
- Shows how to use cthulhu_hookimpl for hook implementation
## Implementation Details
The plugin uses the following key features:
- `pluggy` for hook specification and implementation
- The `Plugin` base class from cthulhu.plugin
- The `cthulhu_hookimpl` decorator to mark functions as plugin hooks
- The `registerGestureByString` method to register keyboard shortcuts
## Structure
- `plugin.py`: The main plugin implementation
- `plugin.info`: Metadata about the plugin
## Requirements
Requires the pluggy package to be installed:
```
pip install pluggy
```
## Usage
The plugin will be automatically loaded when Cthulhu starts if it's listed in the activePlugins setting.

View File

@ -113,7 +113,7 @@ class ResourceContext():
self.settings[profile][application] = {}
# add entry
self.settings[profile][application][sub_setting_name] = entry
print('add', 'settings', self.getName(), profile, application, entry.getResourceText())
@ -214,7 +214,7 @@ class ResourceContext():
self.unregisterAllAPI()
except Exception as e:
print(e)
def unregisterAllAPI(self):
dynamicApiManager = self.app.getDynamicApiManager()
for application, value in self.getAPIs().copy().items():
@ -226,7 +226,7 @@ class ResourceContext():
print('unregister api ', self.getName(), entry.getEntryType(), entry.getResourceText())
def unregisterAllGestures(self):
APIHelper = self.app.getAPIHelper()
for profile, profileValue in self.getGestures().copy().items():
for application, applicationValue in profileValue.copy().items():
for gesture, entry in applicationValue.copy().items():
@ -272,12 +272,12 @@ class ResourceManager():
def removeResourceContext(self, contextName):
if not contextName:
return
try:
self.resourceContextDict[contextName].unregisterAllResources()
except:
pass
# temp
try:
print('_________', 'summery', self.resourceContextDict[contextName].getName(), '_________')
@ -302,7 +302,7 @@ class ResourceManager():
return self.resourceContextDict[contextName]
except KeyError:
return None
def addAPI(self, application, api, contextName = None):
if not contextName:
return

View File

@ -3558,10 +3558,10 @@ class Utilities:
@staticmethod
def unicodeValueString(character):
""" Returns a four hex digit representation of the given character
Arguments:
- The character to return representation
Returns a string representaition of the given character unicode vlue
"""

View File

@ -69,7 +69,7 @@ class SpeechServer(speechserver.SpeechServer):
# See the parent class for documentation.
_active_servers = {}
DEFAULT_SERVER_ID = 'default'
_SERVER_NAMES = {DEFAULT_SERVER_ID: guilabels.DEFAULT_SYNTHESIZER}
@ -93,7 +93,7 @@ class SpeechServer(speechserver.SpeechServer):
Attempt to create the server if it doesn't exist yet. Returns None
when it is not possible to create the server.
"""
if serverId not in cls._active_servers:
cls(serverId)
@ -781,16 +781,16 @@ class SpeechServer(speechserver.SpeechServer):
def reset(self, text=None, acss=None):
self._client.close()
self._init()
def list_output_modules(self):
"""Return names of available output modules as a tuple of strings.
This method is not a part of Cthulhu speech API, but is used internally
by the Speech Dispatcher backend.
The returned tuple can be empty if the information can not be
obtained (e.g. with an older Speech Dispatcher version).
"""
try:
return self._send_command(self._client.list_output_modules)