54 Commits

Author SHA1 Message Date
Storm Dragon 5d48f4770c latest version with plugin code fixed. 2025-06-06 18:00:35 -04:00
Storm Dragon 81cc4627f7 Merge branch 'testing' plugin with keybindings bug potentially fixed. 2025-06-06 17:58:58 -04:00
Storm Dragon 408fb85730 Updated clipboard plugin to work with the now fixed plugin system. 2025-06-05 14:05:23 -04:00
Storm Dragon 0f25245d3d OMG it actually works! Just some finishing touches. 2025-06-05 13:55:30 -04:00
Storm Dragon 13f110ab34 Updated bindings code. 2025-06-05 13:50:36 -04:00
Storm Dragon 62f46c0eb7 Maybe getting closer. 2025-06-05 13:45:12 -04:00
Storm Dragon e2364a154a Another try to get keybindings working. 2025-06-05 13:40:44 -04:00
Storm Dragon 2090767794 Fixed error in event manager. 2025-06-05 13:34:23 -04:00
Storm Dragon ea50d8b024 Fix error in script manager. 2025-06-05 13:31:11 -04:00
Storm Dragon a1d90a7245 more work on keybindings. 2025-06-05 13:27:25 -04:00
Storm Dragon 2eb6d3c7dd updated keybindings.py 2025-06-05 13:20:27 -04:00
Storm Dragon 0edbefac47 more debugging. 2025-06-05 13:11:47 -04:00
Storm Dragon 90aecf8055 Changes to the plugin manager. 2025-06-05 04:13:24 -04:00
Storm Dragon 48d99e8813 Improved debugging to help track down this bug. 2025-06-05 04:02:28 -04:00
Storm Dragon 314aa18a1b Another another attempt to fix the plugin keybindings. 2025-06-05 03:42:02 -04:00
Storm Dragon a21f1aa13b another attempt to fix the keybinding problem for plugins. 2025-06-05 03:32:01 -04:00
Storm Dragon 5181944de0 Move timestamps to the end of the log message instead of the beginning. Makes debugging much less of a PITA. 2025-04-20 15:27:59 -04:00
Storm Dragon 0a8bb684ec Fixed an error in a call to a cthulhu module. 2025-04-20 03:20:12 -04:00
Storm Dragon d94ba1accb Add optional parameter to _on_settings_changed() 2025-04-20 03:13:43 -04:00
Storm Dragon 399f449484 Fixed method call. 2025-04-20 03:07:37 -04:00
Storm Dragon ecd122786f Fixed an error with logging, for real this time. 2025-04-20 02:58:12 -04:00
Storm Dragon 01273618a7 Fixed an error with logging. 2025-04-20 02:50:32 -04:00
Storm Dragon d8df2ed757 Another try at getting keybindings working. 2025-04-20 02:43:59 -04:00
Storm Dragon c376b2489a Getting closer to working bindings. 2025-04-20 02:30:04 -04:00
Storm Dragon 39dca0574a Improve key detection in registerGestureByString 2025-04-19 15:39:43 -04:00
Storm Dragon 8b1f501fe7 fixed an error. 2025-04-19 14:54:32 -04:00
Storm Dragon 96335baf5d Fixed indentation issues. 2025-04-19 14:48:38 -04:00
Storm Dragon 51984a6540 Hopefully got this keybinding thing once and for all... Fingers crossed. 2025-04-19 14:41:17 -04:00
Storm Dragon 3296e5d571 Fix broken method. 2025-04-19 14:15:30 -04:00
Storm Dragon 1e6f4b8913 fixed import error. 2025-04-19 14:06:48 -04:00
Storm Dragon 331b1c3ad5 More debugging efforts. 2025-04-19 14:01:20 -04:00
Storm Dragon 04b8592ed3 Indentation error was worse than I thought. 2025-04-18 14:53:52 -04:00
Storm Dragon c64591a162 Fixed indentation error. 2025-04-18 14:45:33 -04:00
Storm Dragon 80212d616f Added some logging to try and figure out what's going on. 2025-04-18 14:42:02 -04:00
Storm Dragon 9790a8d494 More attempts to fix keyboard. 2025-04-18 14:28:16 -04:00
Storm Dragon ec90906052 Maybe finally solved the plugin keybinding issue... 2025-04-18 13:00:31 -04:00
Storm Dragon f01374d15e One more try before sleep. 2025-04-14 05:04:59 -04:00
Storm Dragon 0347b7feea Another attempt at fixing plugin keyboard shortcuts. 2025-04-14 04:54:48 -04:00
Storm Dragon 0580dda131 A few documentation updates. 2025-04-11 13:17:26 -04:00
Storm Dragon d36b664319 Merge branch 'testing'
Plugins are in a much better state now, mostly working. The exception is, plugins that create a keyboard shortcut don't actually bind the shortcut. That one is turning out to be a lot harder to fix than I originally thought.
2025-04-05 16:32:17 -04:00
Storm Dragon 02be96aa69 Try to fix clipboard and simple plugins. 2025-04-04 18:04:58 -04:00
Storm Dragon 48575ab6cd Removed the old plugin manager. It didn't work anyway and needs to be rewritten. 2025-04-04 17:28:31 -04:00
Storm Dragon 2c28021ed4 Removed the old plugin manager. It didn't work anyway and needs to be rewritten. 2025-04-04 17:27:30 -04:00
Storm Dragon 8a79725df8 Update clipboard plugin to new pluggy format. 2025-04-04 17:08:18 -04:00
Storm Dragon 1b4c4916e3 Hopefully fixed an error in simple plugin system. 2025-04-04 16:33:53 -04:00
Storm Dragon 35a83327ac Convert simple plugin plugin to new plugin format. Hmm, gotta get in a couple more... plugin plugin plugin! lol 2025-04-04 16:25:28 -04:00
Storm Dragon c712bea421 Attempt to fix keybindings not working from plugins. 2025-04-04 16:03:35 -04:00
Storm Dragon 815d39fc3f Remove a couple plugins that were not being used and won't be ported over. If needed, they can be rewritten later. 2025-04-04 14:54:18 -04:00
Storm Dragon 231d74efa0 Try to fix repeating welcome message. 2025-04-04 14:32:03 -04:00
Storm Dragon 7876a18c12 Working on plugin conversion. 2025-04-04 14:19:09 -04:00
Storm Dragon 0b7cf681c3 Forgot to update the configure.ac file. 2025-04-04 02:48:43 -04:00
Storm Dragon 4b8ebcb599 Removed the Date plugin it was causing traceback. 2025-04-04 02:46:01 -04:00
Storm Dragon d6a373c726 Fixed some errors with plugins. 2025-04-03 20:46:11 -04:00
Storm Dragon dfe20fca30 More work on pluggy. 2025-04-03 20:38:27 -04:00
57 changed files with 1331 additions and 2358 deletions
-1
View File
@@ -1 +0,0 @@
See http://wiki.gnome.org/Projects/Cthulhu
+6 -2
View File
@@ -1,5 +1,10 @@
# Cthulhu
## Note
If you somehow stumbled across this while looking for a desktop screen reader for Linux, you most likely want [Orca](https://orca.gnome.org/) instead. Cthulhu is currently a supplemental screen reader that fills a nitch for some advanced users. E.g. some older QT based programs may work with Cthulhu, and if you use certain window managers like i3, Mozilla applications like Firefox and Thunderbird may work better.
## Introduction
Cthulhu is a free, open source, flexible, and extensible screen reader
@@ -20,7 +25,7 @@ Cthulhu has the following dependencies:
* Python 3 - Python platform
* pygobject-3.0 - Python bindings for the GObject library
* libpeas - GObject based Plugin engine
* pluggy - Plugin and hook calling mechanisms for python
* gtk+-3.0 - GTK+ toolkit
* json-py - a JSON (<https://json.org/>) reader and writer in Python
* python-speechd - Python bindings for Speech Dispatcher (optional)
@@ -30,7 +35,6 @@ Cthulhu has the following dependencies:
* py-setproctitle - Python library to set the process title (optional)
* gstreamer-1.0 - GStreamer - Streaming media framework (optional)
* socat - Used for self-voicing functionality.
* libpeas - For the plugin system.
You are strongly encouraged to also have the latest stable versions
of AT-SPI2 and ATK.
-5
View File
@@ -127,15 +127,10 @@ src/cthulhu/scripts/toolkits/gtk/Makefile
src/cthulhu/plugins/Makefile
src/cthulhu/plugins/ByeCthulhu/Makefile
src/cthulhu/plugins/HelloCthulhu/Makefile
src/cthulhu/plugins/PluginManager/Makefile
src/cthulhu/plugins/Clipboard/Makefile
src/cthulhu/plugins/DisplayVersion/Makefile
src/cthulhu/plugins/hello_world/Makefile
src/cthulhu/plugins/CapsLockHack/Makefile
src/cthulhu/plugins/self_voice/Makefile
src/cthulhu/plugins/Date/Makefile
src/cthulhu/plugins/Time/Makefile
src/cthulhu/plugins/MouseReview/Makefile
src/cthulhu/plugins/SimplePluginSystem/Makefile
src/cthulhu/backends/Makefile
src/cthulhu/cthulhu_bin.py
+97 -41
View File
@@ -37,77 +37,118 @@ 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."""
import logging
logger = logging.getLogger(__name__)
def registerGestureByString(self, function, name, gestureString,
inputEventType='default', normalizer='cthulhu',
learnModeEnabled=True, contextName=None):
"""Register a gesture by string.
logger.info(f"=== APIHelper.registerGestureByString called ===")
logger.info(f"gestureString: {gestureString}")
logger.info(f"name: {name}")
logger.info(f"contextName: {contextName}")
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:"):
logger.warning(f"Gesture string doesn't start with 'kb:': {gestureString}")
return None
# Extract the key portion from the gesture string
key = gestureString.split(":", 1)[1]
logger.info(f"Extracted key: {key}")
# Handle Cthulhu modifier specially
if "cthulhu+" in key.lower():
from . import keybindings
key = key.lower().replace("cthulhu+", "")
key_parts = key.lower().split("+")
logger.info(f"Key parts: {key_parts}")
# Start with the base Cthulhu modifier
modifiers = keybindings.CTHULHU_MODIFIER_MASK
# Extract the final key (without modifiers)
final_key = key_parts[-1]
logger.info(f"Final key: {final_key}")
# Check for additional modifiers and combine them properly
if "shift" in key_parts:
# Use the pre-defined combined mask rather than trying to OR them
modifiers = keybindings.CTHULHU_SHIFT_MODIFIER_MASK
logger.info(f"Using CTHULHU_SHIFT_MODIFIER_MASK: {modifiers}")
else:
logger.info(f"Using CTHULHU_MODIFIER_MASK: {modifiers}")
# 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)
try:
logger.info(f"=== DisplayVersion keybinding handler called! ===")
return function(script, inputEvent)
except Exception as e:
import logging
logging.getLogger(__name__).error(f"Error in keybinding handler: {e}")
return True
handler = GestureHandler(function, name)
# Register the binding with the active script
logger.info(f"Created handler: {handler}")
# Create the binding object regardless of whether there's an active script
# This allows plugins to define bindings that will work when a script becomes active
from . import cthulhu_state
from . import keybindings
binding = keybindings.KeyBinding(
final_key,
keybindings.defaultModifierMask,
modifiers,
handler)
logger.info(f"Created binding: keysym={binding.keysymstring}, modifiers={binding.modifiers}, mask={binding.modifier_mask}")
# Store binding for later reference
if contextName not in self._gestureBindings:
self._gestureBindings[contextName] = []
self._gestureBindings[contextName].append(binding)
logger.info(f"Stored binding in context '{contextName}'")
# Only add to active script if one exists
if cthulhu_state.activeScript:
logger.info(f"Adding binding to active script: {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)
# Register key grab at the system level
grab_ids = self.app.addKeyGrab(binding)
logger.info(f"Key grab IDs: {grab_ids}")
return binding
# For later removal
if grab_ids:
binding._grab_ids = grab_ids
else:
logger.warning("No active script available - binding stored for later registration")
debug.printMessage(debug.LEVEL_INFO, f"Created binding: {binding.keysymstring} with modifiers {binding.modifiers}", True)
logger.info("=== APIHelper.registerGestureByString completed ===")
return binding
else:
logger.warning(f"Key doesn't contain 'cthulhu+': {key}")
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
@@ -117,11 +158,18 @@ class APIHelper:
if cthulhu_state.activeScript:
bindings = cthulhu_state.activeScript.getKeyBindings()
bindings.remove(binding)
# Remove from our tracking
# Remove key grab at system level
if hasattr(binding, '_grab_ids'):
for grab_id in binding._grab_ids:
self.app.removeKeyGrab(grab_id)
# Remove from tracking
if contextName in self._gestureBindings:
if binding in self._gestureBindings[contextName]:
self._gestureBindings[contextName].remove(binding)
import gi
import importlib
import os
@@ -615,6 +663,12 @@ def loadUserSettings(script=None, inputEvent=None, skipReloadMessage=False):
_scriptManager.activate()
_eventManager.activate()
# Refresh keybindings to include plugin bindings (after script manager is active)
cthulhuApp.getPluginSystemManager().refresh_active_script_keybindings()
cthulhuApp.getSignalManager().emitSignal('load-setting-begin')
# cthulhuApp.getPluginSystemManager().register_plugin_keybindings_with_active_script()
cthulhuApp.getSignalManager().emitSignal('load-setting-completed')
@@ -1018,6 +1072,8 @@ class Cthulhu(GObject.Object):
self.APIHelper = APIHelper(self)
self.createCompatAPI()
self.pluginSystemManager = plugin_system_manager.PluginSystemManager(self)
# Scan for available plugins at startup
self.pluginSystemManager.rescanPlugins()
def getAPIHelper(self):
return self.APIHelper
def getPluginSystemManager(self):
+2 -2
View File
@@ -23,5 +23,5 @@
# Fork of Orca Screen Reader (GNOME)
# Original source: https://gitlab.gnome.org/GNOME/orca
version = "2025.04.03"
codeName = "testing"
version = "2025.06.06"
codeName = "master"
+1 -1
View File
@@ -298,7 +298,7 @@ def println(level, text="", timestamp=False, stack=False):
text = text.replace("\ufffc", "[OBJ]")
if timestamp:
text = text.replace("\n", f"\n{' ' * 18}")
text = f"{datetime.now().strftime('%H:%M:%S.%f')} - {text}"
text = f"{text} - {datetime.now().strftime('%H:%M:%S.%f')}"
if stack:
text += f" {_stackAsString()}"
+7
View File
@@ -112,6 +112,13 @@ class EventManager:
cthulhu_state.device.key_watcher = cthulhu_state.device.add_key_watcher(
self._processNewKeyboardEvent)
self.newKeyHandlingActive = True
# Notify plugin system that device is now available for keybinding registration
from . import cthulhu
if hasattr(cthulhu, 'cthulhuApp') and cthulhu.cthulhuApp:
plugin_manager = cthulhu.cthulhuApp.getPluginSystemManager()
if plugin_manager:
pass # plugin_manager.register_plugin_keybindings_with_active_script()
def activateLegacyKeyHandling(self):
if not self.legacyKeyHandlingActive:
+60 -2
View File
@@ -251,6 +251,23 @@ class KeyBinding:
if not self.keycode:
self.keycode = getKeycode(self.keysymstring)
# Debug logging for DisplayVersion plugin specifically
if self.keysymstring == 'v' and self.modifiers == 257:
with open('/tmp/displayversion_matches.log', 'a') as f:
f.write(f"=== DisplayVersion matches() debug ===\n")
f.write(f"Self keycode: {self.keycode}\n")
f.write(f"Self keysymstring: {self.keysymstring}\n")
f.write(f"Self modifiers: {self.modifiers}\n")
f.write(f"Self modifier_mask: {self.modifier_mask}\n")
f.write(f"Input keycode: {keycode}\n")
f.write(f"Input modifiers: {modifiers}\n")
f.write(f"Keycode match: {self.keycode == keycode}\n")
if self.keycode == keycode:
result = modifiers & self.modifier_mask
f.write(f"Modifier calculation: {modifiers} & {self.modifier_mask} = {result}\n")
f.write(f"Modifier match: {result == self.modifiers}\n")
f.write(f"Overall match: {self.keycode == keycode and (modifiers & self.modifier_mask) == self.modifiers}\n")
if self.keycode == keycode:
result = modifiers & self.modifier_mask
return result == self.modifiers
@@ -470,13 +487,41 @@ class KeyBindings:
given keycode and modifiers, or None if no match exists.
"""
import logging
logger = logging.getLogger(__name__)
# Check if this might be the DisplayVersion key combination
event_str = keyboardEvent.event_string if hasattr(keyboardEvent, 'event_string') else 'unknown'
if event_str.lower() == 'v':
logger.info(f"=== KeyBindings.getInputHandler: Looking for handler ===")
logger.info(f"Event string: {event_str}")
logger.info(f"Hardware code: {keyboardEvent.hw_code}")
logger.info(f"Modifiers: {keyboardEvent.modifiers}")
logger.info(f"Total keybindings to check: {len(self.keyBindings)}")
with open('/tmp/keybinding_lookup.log', 'a') as f:
f.write(f"=== Looking for 'v' key handler ===\n")
f.write(f"Event string: {event_str}\n")
f.write(f"Hardware code: {keyboardEvent.hw_code}\n")
f.write(f"Modifiers: {keyboardEvent.modifiers}\n")
f.write(f"Total keybindings: {len(self.keyBindings)}\n")
# Log all keybindings for comparison
for i, kb in enumerate(self.keyBindings):
if 'v' in kb.keysymstring.lower() or 'version' in kb.handler.description.lower():
logger.info(f"Binding {i}: keysym={kb.keysymstring}, modifiers={kb.modifiers}, mask={kb.modifier_mask}, desc={kb.handler.description}")
with open('/tmp/keybinding_lookup.log', 'a') as f:
f.write(f"Found V-related binding {i}: keysym={kb.keysymstring}, modifiers={kb.modifiers}, mask={kb.modifier_mask}, desc={kb.handler.description}\n")
matches = []
candidates = []
clickCount = keyboardEvent.getClickCount()
for keyBinding in self.keyBindings:
if keyBinding.matches(keyboardEvent.hw_code, keyboardEvent.modifiers):
if keyBinding.modifier_mask == keyboardEvent.modifiers and \
keyBinding.click_count == clickCount:
if event_str.lower() == 'v':
logger.info(f"MATCH found! keysym={keyBinding.keysymstring}, desc={keyBinding.handler.description}")
if (keyboardEvent.modifiers & keyBinding.modifier_mask) == keyBinding.modifiers and \
keyBinding.click_count == clickCount:
matches.append(keyBinding)
# If there's no keysymstring, it's unbound and cannot be
# a match.
@@ -484,8 +529,17 @@ class KeyBindings:
if keyBinding.keysymstring:
candidates.append(keyBinding)
if event_str.lower() == 'v':
logger.info(f"Exact matches: {len(matches)}")
logger.info(f"Candidates: {len(candidates)}")
with open('/tmp/keybinding_lookup.log', 'a') as f:
f.write(f"Exact matches: {len(matches)}\n")
f.write(f"Candidates: {len(candidates)}\n")
self._checkMatchingBindings(keyboardEvent, matches)
if matches:
if event_str.lower() == 'v':
logger.info(f"Returning exact match handler: {matches[0].handler.description}")
return matches[0].handler
if keyboardEvent.isKeyPadKeyWithNumlockOn():
@@ -499,8 +553,12 @@ class KeyBindings:
self._checkMatchingBindings(keyboardEvent, candidates)
for candidate in candidates:
if candidate.click_count <= clickCount:
if event_str.lower() == 'v':
logger.info(f"Returning candidate handler: {candidate.handler.description}")
return candidate.handler
if event_str.lower() == 'v':
logger.info("No handler found!")
return None
def load(self, keymap, handlers):
+21 -3
View File
@@ -16,11 +16,12 @@ try:
import pluggy
cthulhu_hookimpl = pluggy.HookimplMarker("cthulhu")
PLUGGY_AVAILABLE = True
logging.getLogger(__name__).info("Successfully imported pluggy")
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.
@@ -29,7 +30,9 @@ except ImportError:
return lambda f: f
return func
PLUGGY_AVAILABLE = False
logging.getLogger(__name__).info("Pluggy not available, plugins will be disabled")
logging.getLogger(__name__).warning("Pluggy not available, plugins will be disabled")
import traceback
logging.getLogger(__name__).debug(traceback.format_exc())
logger = logging.getLogger(__name__)
@@ -44,6 +47,8 @@ class Plugin:
self.name = ''
self.version = ''
self.description = ''
self._bindings = None
self._gestureBindings = {}
def set_app(self, app):
"""Set the application reference."""
@@ -72,12 +77,16 @@ class Plugin:
return
logger.info(f"Deactivating plugin: {self.name}")
def get_bindings(self):
"""Get keybindings for this plugin. Override in subclasses."""
return self._bindings
def registerGestureByString(self, function, name, gestureString, learnModeEnabled=True):
"""Register a gesture by string."""
if self.app:
api_helper = self.app.getAPIHelper()
if api_helper:
return api_helper.registerGestureByString(
binding = api_helper.registerGestureByString(
function,
name,
gestureString,
@@ -86,4 +95,13 @@ class Plugin:
learnModeEnabled,
contextName=self.module_name
)
# Also store the binding locally so get_bindings() can use it
if binding:
if not self._bindings:
from . import keybindings
self._bindings = keybindings.KeyBindings()
self._bindings.add(binding)
return binding
return None
+323 -6
View File
@@ -22,7 +22,12 @@ except ImportError:
PLUGGY_AVAILABLE = False
logging.getLogger(__name__).info("Pluggy not available, plugins will be disabled")
# Set to True for more detailed plugin loading debug info
PLUGIN_DEBUG = True
logger = logging.getLogger(__name__)
if PLUGIN_DEBUG:
logger.setLevel(logging.DEBUG)
class PluginType(IntEnum):
"""Types of plugins we support."""
@@ -74,9 +79,11 @@ class PluginSystemManager:
def __init__(self, app):
self.app = app
logger.info("Initializing PluginSystemManager")
# Initialize plugin manager
if PLUGGY_AVAILABLE:
logger.info("Pluggy is available, setting up plugin manager")
self.plugin_manager = pluggy.PluginManager("cthulhu")
# Define hook specifications
@@ -93,8 +100,10 @@ class PluginSystemManager:
"""Called when the plugin is deactivated."""
pass
logger.info("Adding hook specifications to plugin manager")
self.plugin_manager.add_hookspecs(CthulhuHookSpecs)
else:
logger.warning("Pluggy is not available, plugins will be disabled")
self.plugin_manager = None
# Plugin storage
@@ -104,6 +113,189 @@ class PluginSystemManager:
# Create plugin directories
self._setup_plugin_dirs()
# Log available plugins directory paths
logger.info(f"System plugins directory: {PluginType.SYSTEM.get_root_dir()}")
logger.info(f"User plugins directory: {PluginType.USER.get_root_dir()}")
def register_plugin_global_keybindings(self, plugin):
"""Register a plugin's keybindings with all scripts."""
if not hasattr(plugin, 'get_bindings'):
return
try:
bindings = plugin.get_bindings()
if not bindings or not bindings.keyBindings:
return
logger.info(f"Registering global keybindings for plugin: {plugin.name}")
# First register with the active script
from . import cthulhu_state
if cthulhu_state.activeScript:
active_script = cthulhu_state.activeScript
for binding in bindings.keyBindings:
active_script.getKeyBindings().add(binding)
grab_ids = self.app.addKeyGrab(binding)
if grab_ids:
binding._grab_ids = grab_ids
# Store these bindings for future script changes
plugin_name = plugin.name or plugin.module_name
if not hasattr(self, '_plugin_global_bindings'):
self._plugin_global_bindings = {}
self._plugin_global_bindings[plugin_name] = bindings
# Connect to script changes to ensure bindings work with all scripts
if not hasattr(self, '_connected_to_script_changes'):
signal_manager = self.app.getSignalManager()
if signal_manager:
signal_manager.connectSignal('load-setting-completed', self._on_settings_changed, None)
self._connected_to_script_changes = True
except Exception as e:
logger.error(f"Error registering global keybindings for plugin {plugin.name}: {e}")
import traceback
logger.error(traceback.format_exc())
def refresh_active_script_keybindings(self):
"""Force active script to refresh its keybindings to include plugin bindings."""
from . import cthulhu_state
if cthulhu_state.activeScript:
active_script = cthulhu_state.activeScript
with open('/tmp/plugin_registration.log', 'a') as f:
f.write(f"=== refresh_active_script_keybindings() CALLED ===\n")
f.write(f"Active script: {active_script.name}\n")
# Force the script to recreate its keybindings to include plugin bindings
old_keybindings = active_script.keyBindings
active_script.keyBindings = active_script.getKeyBindings()
with open('/tmp/plugin_registration.log', 'a') as f:
f.write(f"Keybindings refreshed: old={len(old_keybindings.keyBindings) if old_keybindings else 0}, new={len(active_script.keyBindings.keyBindings)}\n")
def register_plugin_keybindings_with_active_script(self):
"""Register all plugin keybindings with the active script."""
logger.info("=== register_plugin_keybindings_with_active_script() CALLED ===")
with open('/tmp/plugin_registration.log', 'a') as f:
f.write("=== register_plugin_keybindings_with_active_script() CALLED ===\n")
if not PLUGGY_AVAILABLE:
logger.warning("PLUGGY not available")
with open('/tmp/plugin_registration.log', 'a') as f:
f.write("ERROR: PLUGGY not available\n")
return
from . import cthulhu_state
if not cthulhu_state.activeScript:
logger.warning("No active script available to register plugin keybindings")
with open('/tmp/plugin_registration.log', 'a') as f:
f.write("ERROR: No active script available\n")
return
active_script = cthulhu_state.activeScript
logger.info(f"Registering plugin keybindings with active script: {active_script}")
with open('/tmp/plugin_registration.log', 'a') as f:
f.write(f"Active script: {active_script}\n")
# First, register keybindings from APIHelper's stored bindings
# This is where plugin keybindings actually get stored
from . import cthulhu
api_helper = cthulhu.cthulhuApp.getAPIHelper()
if api_helper and hasattr(api_helper, '_gestureBindings'):
logger.info("=== FOUND APIHelper with _gestureBindings ===")
with open('/tmp/plugin_registration.log', 'a') as f:
f.write("=== Registering stored gesture bindings from APIHelper ===\n")
f.write(f"Total contexts: {len(api_helper._gestureBindings)}\n")
for context_name, bindings_list in api_helper._gestureBindings.items():
logger.info(f"Processing context '{context_name}' with {len(bindings_list)} bindings")
with open('/tmp/plugin_registration.log', 'a') as f:
f.write(f"Context '{context_name}': {len(bindings_list)} bindings\n")
for binding in bindings_list:
logger.info(f"Adding stored binding: {binding.keysymstring} with modifiers {binding.modifiers}")
with open('/tmp/plugin_registration.log', 'a') as f:
f.write(f" Binding: {binding.keysymstring} modifiers={binding.modifiers} desc={binding.handler.description}\n")
# Check if binding already exists to avoid duplicates
if not active_script.getKeyBindings().hasKeyBinding(binding, "keysNoMask"):
active_script.getKeyBindings().add(binding)
with open('/tmp/plugin_registration.log', 'a') as f:
f.write(f" ADDED to active script!\n")
# Force recalculation of keycode if it wasn't set when device was None
if not binding.keycode and binding.keysymstring:
from . import keybindings
binding.keycode = keybindings.getKeycode(binding.keysymstring)
# Register key grab at system level - this was missing!
grab_ids = cthulhu.addKeyGrab(binding)
if grab_ids:
binding._grab_ids = grab_ids
else:
logger.warning(f"Failed to create key grab for {binding.keysymstring} - device may not be available")
else:
logger.info(f"Binding already exists: {binding.keysymstring}")
with open('/tmp/plugin_registration.log', 'a') as f:
f.write(f" Already exists, skipped\n")
else:
logger.warning("=== NO APIHelper or no _gestureBindings found ===")
with open('/tmp/plugin_registration.log', 'a') as f:
f.write("ERROR: No APIHelper or no _gestureBindings found!\n")
if api_helper:
f.write(f"APIHelper exists but _gestureBindings: {hasattr(api_helper, '_gestureBindings')}\n")
else:
f.write("No APIHelper found!\n")
# Also check the old method for any plugins that use get_bindings()
for pluginInfo in self._plugins.values():
if not pluginInfo.loaded or not pluginInfo.instance:
continue
plugin = pluginInfo.instance
if not hasattr(plugin, 'get_bindings') or not plugin.get_bindings():
continue
logger.info(f"Registering keybindings for plugin: {plugin.name}")
bindings = plugin.get_bindings()
for binding in bindings.keyBindings:
logger.info(f"Adding binding: {binding.keysymstring} with modifiers {binding.modifiers}")
# Check if binding already exists to avoid duplicates
if not active_script.getKeyBindings().hasKeyBinding(binding, "keysNoMask"):
active_script.getKeyBindings().add(binding)
# Force recalculation of keycode if it wasn't set when device was None
if not binding.keycode and binding.keysymstring:
from . import keybindings
binding.keycode = keybindings.getKeycode(binding.keysymstring)
# Register key grab at system level - this was missing!
grab_ids = cthulhu.addKeyGrab(binding)
if grab_ids:
binding._grab_ids = grab_ids
else:
logger.warning(f"Failed to create key grab for {binding.keysymstring} - device may not be available")
def _on_settings_changed(self, app=None):
"""Re-register all plugin keybindings when settings change."""
if not hasattr(self, '_plugin_global_bindings'):
return
from . import cthulhu_state
if not cthulhu_state.activeScript:
return
active_script = cthulhu_state.activeScript
for plugin_name, bindings in self._plugin_global_bindings.items():
logger.info(f"Re-registering keybindings for plugin: {plugin_name}")
for binding in bindings.keyBindings:
# Check if binding already exists
if active_script.getKeyBindings().hasKeyBinding(binding, "keysNoMask"):
continue
active_script.getKeyBindings().add(binding)
from . import cthulhu
grab_ids = cthulhu.addKeyGrab(binding)
if grab_ids:
binding._grab_ids = grab_ids
def _setup_plugin_dirs(self):
"""Ensure plugin directories exist."""
os.makedirs(PluginType.SYSTEM.get_root_dir(), exist_ok=True)
@@ -136,19 +328,31 @@ class PluginSystemManager:
def _scan_plugins_in_directory(self, directory):
"""Scan for plugins in a directory."""
if not os.path.exists(directory) or not os.path.isdir(directory):
logger.warning(f"Plugin directory not found or not a directory: {directory}")
return
logger.info(f"Scanning for plugins in directory: {directory}")
for item in os.listdir(directory):
plugin_dir = os.path.join(directory, item)
if not os.path.isdir(plugin_dir):
continue
# Check for the traditional structure first (plugin.py & plugin.info)
plugin_file = os.path.join(plugin_dir, "plugin.py")
metadata_file = os.path.join(plugin_dir, "plugin.info")
# Fall back to [PluginName].py if plugin.py doesn't exist
if not os.path.isfile(plugin_file):
alternative_plugin_file = os.path.join(plugin_dir, f"{item}.py")
if os.path.isfile(alternative_plugin_file):
plugin_file = alternative_plugin_file
logger.info(f"Using alternative plugin file: {alternative_plugin_file}")
# Check if we have any valid plugin file
if os.path.isfile(plugin_file):
# Extract plugin info
module_name = os.path.basename(plugin_dir)
logger.info(f"Found plugin: {module_name} in {plugin_dir}")
metadata = self._load_plugin_metadata(metadata_file)
plugin_info = PluginInfo(
@@ -162,7 +366,10 @@ class PluginSystemManager:
plugin_info.builtin = metadata.get('builtin', 'false').lower() == 'true'
plugin_info.hidden = metadata.get('hidden', 'false').lower() == 'true'
logger.info(f"Adding plugin to registry: {module_name}")
self._plugins[module_name] = plugin_info
else:
logger.warning(f"No plugin file found in directory: {plugin_dir}")
def _load_plugin_metadata(self, metadata_file):
"""Load plugin metadata from a file."""
@@ -190,8 +397,34 @@ class PluginSystemManager:
def setActivePlugins(self, activePlugins):
"""Set active plugins and sync their state."""
logger.info(f"=== PluginSystemManager.setActivePlugins called ===")
logger.info(f"Setting active plugins: {activePlugins}")
# Make sure we have scanned for plugins first
if not self._plugins:
logger.info("No plugins found, rescanning...")
self.rescanPlugins()
self._active_plugins = activePlugins
# Log active vs available plugins
available_plugins = [p.get_module_name() for p in self.plugins]
logger.info(f"Available plugins: {available_plugins}")
logger.info(f"Active plugins: {self._active_plugins}")
# Check specifically for DisplayVersion
if 'DisplayVersion' in self._active_plugins:
logger.info("DisplayVersion is in active plugins list!")
else:
logger.warning("DisplayVersion is NOT in active plugins list!")
# Find missing plugins
missing_plugins = [p for p in self._active_plugins if p not in available_plugins]
if missing_plugins:
logger.warning(f"Active plugins not found: {missing_plugins}")
self.syncAllPluginsActive()
logger.info("=== PluginSystemManager.setActivePlugins completed ===")
def setPluginActive(self, pluginInfo, active):
"""Set the active state of a plugin."""
@@ -211,25 +444,66 @@ class PluginSystemManager:
def isPluginActive(self, pluginInfo):
"""Check if a plugin is active."""
module_name = pluginInfo.get_module_name()
# Builtin plugins are always active
if pluginInfo.builtin:
logger.debug(f"Plugin {module_name} is builtin, active by default")
return True
# If a plugin is already loaded, it's active
if pluginInfo.loaded:
logger.debug(f"Plugin {module_name} is already loaded, considered active")
return True
return pluginInfo.get_module_name() in self.getActivePlugins()
# Check case-insensitive match in active plugins list
active_plugins = self.getActivePlugins()
# Try exact match first
if module_name in active_plugins:
logger.debug(f"Plugin {module_name} found in active plugins list")
return True
# Try case-insensitive match
module_name_lower = module_name.lower()
is_active = any(plugin.lower() == module_name_lower for plugin in active_plugins)
if is_active:
logger.debug(f"Plugin {module_name} found in active plugins list (case-insensitive match)")
else:
logger.debug(f"Plugin {module_name} not found in active plugins list")
return is_active
def syncAllPluginsActive(self):
"""Sync the active state of all plugins."""
logger.info("Syncing active state of all plugins")
# Log plugin status before syncing
if PLUGIN_DEBUG:
for pluginInfo in self.plugins:
is_active = self.isPluginActive(pluginInfo)
is_loaded = pluginInfo.loaded
logger.debug(f"Plugin {pluginInfo.get_module_name()}: active={is_active}, loaded={is_loaded}")
# First unload inactive plugins
for pluginInfo in self.plugins:
if not self.isPluginActive(pluginInfo) and pluginInfo.loaded:
logger.info(f"Unloading inactive plugin: {pluginInfo.get_module_name()}")
self.unloadPlugin(pluginInfo)
# Then load active plugins
for pluginInfo in self.plugins:
if self.isPluginActive(pluginInfo) and not pluginInfo.loaded:
self.loadPlugin(pluginInfo)
logger.info(f"Loading active plugin: {pluginInfo.get_module_name()}")
result = self.loadPlugin(pluginInfo)
logger.info(f"Plugin {pluginInfo.get_module_name()} load result: {result}")
# Log final plugin status
active_plugins = [p.get_module_name() for p in self.plugins if p.loaded]
logger.info(f"Active plugins after sync: {active_plugins}")
inactive_plugins = [p.get_module_name() for p in self.plugins if not p.loaded]
logger.info(f"Inactive plugins after sync: {inactive_plugins}")
def loadPlugin(self, pluginInfo):
"""Load a plugin."""
@@ -239,15 +513,38 @@ class PluginSystemManager:
return False
module_name = pluginInfo.get_module_name()
logger.info(f"=== PluginSystemManager.loadPlugin starting for: {module_name} ===")
try:
# Already loaded?
if pluginInfo.loaded:
logger.info(f"Plugin {module_name} already loaded, skipping")
return True
# Import plugin module
plugin_file = os.path.join(pluginInfo.get_module_dir(), "plugin.py")
# Try to find the plugin file
module_name = pluginInfo.get_module_name()
plugin_dir = pluginInfo.get_module_dir()
# Check for plugin.py first (standard format)
plugin_file = os.path.join(plugin_dir, "plugin.py")
# Fall back to [PluginName].py if plugin.py doesn't exist
if not os.path.exists(plugin_file):
alternative_plugin_file = os.path.join(plugin_dir, f"{module_name}.py")
if os.path.exists(alternative_plugin_file):
plugin_file = alternative_plugin_file
logger.info(f"Using alternative plugin file: {alternative_plugin_file}")
if not os.path.exists(plugin_file):
logger.error(f"Plugin file not found: {plugin_file}")
return False
logger.info(f"Loading plugin from: {plugin_file}")
spec = importlib.util.spec_from_file_location(module_name, plugin_file)
if spec is None:
logger.error(f"Failed to create spec for plugin: {module_name}")
return False
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
pluginInfo.module = module
@@ -260,6 +557,7 @@ class PluginSystemManager:
attr.__module__ == module.__name__ and
hasattr(attr, 'activate')):
plugin_class = attr
logger.info(f"Found plugin class: {attr.__name__} in {module_name}")
break
if not plugin_class:
@@ -267,32 +565,51 @@ class PluginSystemManager:
return False
# Create and initialize plugin instance
logger.info(f"Creating instance of plugin class: {plugin_class.__name__}")
plugin_instance = plugin_class()
pluginInfo.instance = plugin_instance
# Ensure plugins have a reference to the app
plugin_instance.app = self.getApp()
logger.info(f"Set app reference for plugin: {module_name}")
if hasattr(plugin_instance, 'set_app'):
plugin_instance.set_app(self.getApp())
logger.info(f"Called set_app() for plugin: {module_name}")
if hasattr(plugin_instance, 'set_plugin_info'):
plugin_instance.set_plugin_info(pluginInfo)
logger.info(f"Called set_plugin_info() for plugin: {module_name}")
# Register with pluggy and activate
if self.plugin_manager is None:
logger.error(f"Plugin manager is None when loading {module_name}")
return False
logger.info(f"Registering plugin with pluggy: {module_name}")
self.plugin_manager.register(plugin_instance)
try:
logger.info(f"Activating plugin: {module_name}")
self.plugin_manager.hook.activate(plugin=plugin_instance)
except Exception as e:
logger.error(f"Error activating plugin {module_name}: {e}")
import traceback
logger.error(traceback.format_exc())
return False
pluginInfo.loaded = True
logger.info(f"Loaded plugin: {module_name}")
logger.info(f"Successfully loaded plugin: {module_name}")
# Register any global keybindings from the plugin
self.register_plugin_global_keybindings(pluginInfo.instance)
return True
except Exception as e:
logger.error(f"Failed to load plugin {module_name}: {e}")
import traceback
logger.error(traceback.format_exc())
return False
def unloadPlugin(self, pluginInfo):
+5 -4
View File
@@ -37,7 +37,8 @@ class ByeCthulhu(Plugin):
signal_manager = self.app.getSignalManager()
self._signal_handler_id = signal_manager.connectSignal(
"stop-application-completed",
self.process
self.process,
"default" # Add profile parameter
)
except Exception as e:
logger.error(f"Error activating ByeCthulhu plugin: {e}")
@@ -54,9 +55,9 @@ class ByeCthulhu(Plugin):
# 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
# Use disconnectSignalByFunction instead since disconnectSignal doesn't exist
signal_manager.disconnectSignalByFunction(
self.process
)
self._signal_handler_id = None
except Exception as e:
@@ -1,6 +0,0 @@
[Plugin]
Module=CapsLockHack
Loader=python3
Name=Caps Lock Hack
Description=Fix Capslock sometimes switch on / off when its used as modifier
Authors=Chrys chrys@linux-a11y.org
@@ -1,144 +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 threading import Thread, Lock
import subprocess, time, re, os
class CapsLockHack(GObject.Object, Peas.Activatable, plugin.Plugin):
__gtype_name__ = 'CapsLockHack'
object = GObject.Property(type=GObject.Object)
def __init__(self):
plugin.Plugin.__init__(self)
self.lock = Lock()
self.active = False
self.workerThread = Thread(target=self.worker)
def do_activate(self):
API = self.object
"""Enable or disable use of the caps lock key as an Cthulhu modifier key."""
self.interpretCapsLineProg = re.compile(
r'^\s*interpret\s+Caps[_+]Lock[_+]AnyOfOrNone\s*\(all\)\s*{\s*$', re.I)
self.normalCapsLineProg = re.compile(
r'^\s*action\s*=\s*LockMods\s*\(\s*modifiers\s*=\s*Lock\s*\)\s*;\s*$', re.I)
self.interpretShiftLineProg = re.compile(
r'^\s*interpret\s+Shift[_+]Lock[_+]AnyOf\s*\(\s*Shift\s*\+\s*Lock\s*\)\s*{\s*$', re.I)
self.normalShiftLineProg = re.compile(
r'^\s*action\s*=\s*LockMods\s*\(\s*modifiers\s*=\s*Shift\s*\)\s*;\s*$', re.I)
self.disabledModLineProg = re.compile(
r'^\s*action\s*=\s*NoAction\s*\(\s*\)\s*;\s*$', re.I)
self.normalCapsLine = ' action= LockMods(modifiers=Lock);'
self.normalShiftLine = ' action= LockMods(modifiers=Shift);'
self.disabledModLine = ' action= NoAction();'
self.activateWorker()
def do_deactivate(self):
API = self.object
self.deactivateWorker()
def do_update_state(self):
API = self.object
def deactivateWorker(self):
with self.lock:
self.active = False
self.workerThread.join()
def activateWorker(self):
with self.lock:
self.active = True
self.workerThread.start()
def isActive(self):
with self.lock:
return self.active
def worker(self):
"""Makes an Cthulhu-specific Xmodmap so that the keys behave as we
need them to do. This is especially the case for the Cthulhu modifier.
"""
API = self.object
capsLockCleared = False
settings = API.app.getDynamicApiManager().getAPI('Settings')
time.sleep(3)
while self.isActive():
if "Caps_Lock" in settings.cthulhuModifierKeys \
or "Shift_Lock" in settings.cthulhuModifierKeys:
self.setCapsLockAsCthulhuModifier(True)
capsLockCleared = True
elif capsLockCleared:
self.setCapsLockAsCthulhuModifier(False)
capsLockCleared = False
time.sleep(1)
def setCapsLockAsCthulhuModifier(self, enable):
originalXmodmap = None
lines = None
try:
originalXmodmap = subprocess.check_output(['xkbcomp', os.environ['DISPLAY'], '-'])
lines = originalXmodmap.decode('UTF-8').split('\n')
except:
return
foundCapsInterpretSection = False
foundShiftInterpretSection = False
modified = False
for i, line in enumerate(lines):
if not foundCapsInterpretSection and not foundShiftInterpretSection:
if self.interpretCapsLineProg.match(line):
foundCapsInterpretSection = True
elif self.interpretShiftLineProg.match(line):
foundShiftInterpretSection = True
elif foundCapsInterpretSection:
if enable:
if self.normalCapsLineProg.match(line):
lines[i] = self.disabledModLine
modified = True
else:
if self.disabledModLineProg.match(line):
lines[i] = self.normalCapsLine
modified = True
if line.find('}'):
foundCapsInterpretSection = False
else: # foundShiftInterpretSection
if enable:
if self.normalShiftLineProg.match(line):
lines[i] = self.disabledModLine
modified = True
else:
if self.disabledModLineProg.match(line):
lines[i] = self.normalShiftLine
modified = True
if line.find('}'):
foundShiftInterpretSection = False
if modified:
newXmodMap = bytes('\n'.join(lines), 'UTF-8')
self.setXmodmap(newXmodMap)
def setXmodmap(self, xkbmap):
"""Set the keyboard map using xkbcomp."""
try:
p = subprocess.Popen(['xkbcomp', '-w0', '-', os.environ['DISPLAY']],
stdin=subprocess.PIPE, stdout=None, stderr=None)
p.communicate(xkbmap)
except:
pass
@@ -1,7 +0,0 @@
cthulhu_python_PYTHON = \
__init__.py \
CapsLockHack.plugin \
CapsLockHack.py
cthulhu_pythondir=$(pkgpythondir)/plugins/CapsLockHack
@@ -1,25 +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
@@ -1,6 +0,0 @@
[Plugin]
Module=Clipboard
Loader=python3
Name=Clipboard
Description=Present the content of the current clipboard
Authors=Chrys chrys@linux-a11y.org
-101
View File
@@ -1,101 +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, os
gi.require_version('Peas', '1.0')
from gi.repository import GObject
from gi.repository import Peas
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk
class Clipboard(GObject.Object, Peas.Activatable, plugin.Plugin):
#__gtype_name__ = 'Clipboard'
object = GObject.Property(type=GObject.Object)
def __init__(self):
plugin.Plugin.__init__(self)
def do_activate(self):
API = self.object
self.registerGestureByString(self.speakClipboard, _('clipboard'), 'kb:cthulhu+c')
def do_deactivate(self):
API = self.object
def do_update_state(self):
API = self.object
def speakClipboard(self, script=None, inputEvent=None):
API = self.object
Message = self.getClipboard()
API.app.getDynamicApiManager().getAPI('CthulhuState').activeScript.presentMessage(Message, resetStyles=False)
return True
def getClipboard(self):
Message = ""
FoundClipboardContent = False
# Get Clipboard
ClipboardObj = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
ClipboardText = ClipboardObj.wait_for_text()
ClipboardImage = ClipboardObj.wait_for_image()
ClipboardURI = ClipboardObj.wait_for_uris()
if (ClipboardText != None):
FoundClipboardContent = True
if (ClipboardObj.wait_is_uris_available()):
noOfObjects = 0
noOfFolder = 0
noOfFiles = 0
noOfDisks = 0
noOfLinks = 0
for Uri in ClipboardURI:
if Uri == '':
continue
noOfObjects += 1
uriWithoutProtocoll = Uri[Uri.find('://') + 3:]
Message += " " + Uri[Uri.rfind('/') + 1:] + " "
if (os.path.isdir(uriWithoutProtocoll)):
noOfFolder += 1
Message = Message + _("Folder") #Folder
if (os.path.isfile(uriWithoutProtocoll)):
noOfFiles += 1
Message = Message + _("File") #File
if (os.path.ismount(uriWithoutProtocoll)):
noOfDisks += 1
Message = Message + _("Disk") #Mountpoint
if (os.path.islink(uriWithoutProtocoll)):
noOfLinks += 1
Message = Message + _("Link") #Link
if (noOfObjects > 1):
Message = str(noOfObjects) + _(" Objects in clipboard ") + Message # X Objects in Clipboard Object Object
else:
Message = str(noOfObjects) + _(" Object in clipboard ") + Message # 1 Object in Clipboard Object
else:
Message = _("Text in clipboard ") + ClipboardText # Text in Clipboard
if (ClipboardImage != None):
FoundClipboardContent = True
Message = _("The clipboard contains a image") # Image is in Clipboard
if (not FoundClipboardContent):
Message = _("The clipboard is empty")
return Message
+2 -2
View File
@@ -1,7 +1,7 @@
cthulhu_python_PYTHON = \
__init__.py \
Clipboard.plugin \
Clipboard.py
plugin.info \
plugin.py
cthulhu_pythondir=$(pkgpythondir)/plugins/Clipboard
@@ -23,3 +23,8 @@
# Fork of Orca Screen Reader (GNOME)
# Original source: https://gitlab.gnome.org/GNOME/orca
"""Clipboard plugin package."""
from .plugin import Clipboard
__all__ = ['Clipboard']
@@ -0,0 +1,7 @@
[Plugin]
Name = Clipboard
Module = Clipboard
Description = Present the content of the current clipboard
Authors = Storm Dragon <storm_dragon@stormux.org>
Version = 1.0
Category = Utilities
+158
View File
@@ -0,0 +1,158 @@
#!/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
"""Clipboard plugin for Cthulhu."""
import os
import logging
import gettext
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk
from cthulhu.plugin import Plugin, cthulhu_hookimpl
# Set up translation function
_ = gettext.gettext
logger = logging.getLogger(__name__)
class Clipboard(Plugin):
"""Plugin to read the clipboard contents."""
def __init__(self, *args, **kwargs):
"""Initialize the plugin."""
super().__init__(*args, **kwargs)
logger.info("Clipboard 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 Clipboard plugin")
try:
# Register keyboard shortcut
self.registerGestureByString(self.speakClipboard, _('clipboard'), 'kb:cthulhu+shift+c')
logger.debug("Registered shortcut for clipboard")
except Exception as e:
logger.error(f"Error activating Clipboard 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 Clipboard plugin")
# Note: Currently no unregister method needed as keybindings are managed by APIHelper
def speakClipboard(self, script=None, inputEvent=None):
"""Present the contents of the clipboard."""
try:
message = self.getClipboard()
state = self.app.getDynamicApiManager().getAPI('CthulhuState')
if state and state.activeScript:
state.activeScript.presentMessage(message, resetStyles=False)
logger.debug("Presented clipboard contents")
else:
logger.warning("Could not present clipboard: no active script")
return True
except Exception as e:
logger.error(f"Error in speakClipboard: {e}")
return False
def getClipboard(self):
"""Get the contents of the clipboard."""
try:
message = ""
found_clipboard_content = False
# Get Clipboard
clipboard_obj = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
clipboard_text = clipboard_obj.wait_for_text()
clipboard_image = clipboard_obj.wait_for_image()
clipboard_uri = clipboard_obj.wait_for_uris()
if clipboard_text is not None:
found_clipboard_content = True
if clipboard_obj.wait_is_uris_available():
no_of_objects = 0
no_of_folder = 0
no_of_files = 0
no_of_disks = 0
no_of_links = 0
for uri in clipboard_uri:
if uri == '':
continue
no_of_objects += 1
uri_without_protocol = uri[uri.find('://') + 3:]
message += " " + uri[uri.rfind('/') + 1:] + " "
if os.path.isdir(uri_without_protocol):
no_of_folder += 1
message = message + _("Folder")
if os.path.isfile(uri_without_protocol):
no_of_files += 1
message = message + _("File")
if os.path.ismount(uri_without_protocol):
no_of_disks += 1
message = message + _("Disk")
if os.path.islink(uri_without_protocol):
no_of_links += 1
message = message + _("Link")
if no_of_objects > 1:
message = str(no_of_objects) + _(" Objects in clipboard ") + message
else:
message = str(no_of_objects) + _(" Object in clipboard ") + message
else:
message = _("Text in clipboard ") + clipboard_text
if clipboard_image is not None:
found_clipboard_content = True
message = _("The clipboard contains a image")
if not found_clipboard_content:
message = _("The clipboard is empty")
return message
except Exception as e:
logger.error(f"Error getting clipboard content: {e}")
return _("Error accessing clipboard")
-6
View File
@@ -1,6 +0,0 @@
[Plugin]
Module=Date
Loader=python3
Name=Date
Description=Present the current date
Authors=Chrys chrys@linux-a11y.org
-58
View File
@@ -1,58 +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, time
gi.require_version('Peas', '1.0')
from gi.repository import GObject
from gi.repository import Peas
class Date(GObject.Object, Peas.Activatable, plugin.Plugin):
#__gtype_name__ = 'Date'
object = GObject.Property(type=GObject.Object)
def __init__(self):
plugin.Plugin.__init__(self)
def do_activate(self):
API = self.object
self.connectSignal("setup-inputeventhandlers-completed", self.setupCompatBinding)
def setupCompatBinding(self, app):
cmdnames = app.getDynamicApiManager().getAPI('Cmdnames')
inputEventHandlers = app.getDynamicApiManager().getAPI('inputEventHandlers')
inputEventHandlers['presentDateHandler'] = app.getAPIHelper().createInputEventHandler(self.presentDate, cmdnames.PRESENT_CURRENT_DATE)
def do_deactivate(self):
API = self.object
inputEventHandlers = API.app.getDynamicApiManager().getAPI('inputEventHandlers')
del inputEventHandlers['presentDateHandler']
def presentDate(self, script=None, inputEvent=None):
""" Presents the current time. """
API = self.object
settings_manager = API.app.getDynamicApiManager().getAPI('SettingsManager')
_settingsManager = settings_manager.getManager()
dateFormat = _settingsManager.getSetting('presentDateFormat')
message = time.strftime(dateFormat, time.localtime())
API.app.getDynamicApiManager().getAPI('CthulhuState').activeScript.presentMessage(message, resetStyles=False)
return True
-7
View File
@@ -1,7 +0,0 @@
cthulhu_python_PYTHON = \
__init__.py \
Date.plugin \
Date.py
cthulhu_pythondir=$(pkgpythondir)/plugins/Date
-25
View File
@@ -1,25 +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
+50 -3
View File
@@ -32,16 +32,62 @@ class DisplayVersion(Plugin):
return
try:
logger.info("Activating DisplayVersion plugin")
logger.info("=== DisplayVersion plugin activation starting ===")
logger.info(f"Plugin name: {self.name}")
logger.info(f"App reference: {self.app}")
# Also write to a debug file
with open('/tmp/displayversion_debug.log', 'a') as f:
f.write("=== DisplayVersion plugin activation starting ===\n")
f.write(f"Plugin name: {self.name}\n")
f.write(f"App reference: {self.app}\n")
# Check if we have access to the API helper
if not self.app:
logger.error("DisplayVersion: No app reference available!")
return
api_helper = self.app.getAPIHelper()
if not api_helper:
logger.error("DisplayVersion: No API helper available!")
return
logger.info(f"API helper: {api_helper}")
# Register keyboard shortcut
gesture_string = 'kb:cthulhu+shift+v'
logger.info(f"DisplayVersion: Attempting to register gesture: {gesture_string}")
self._kb_binding = self.registerGestureByString(
self.speakText,
f'Cthulhu screen reader version {cthulhuVersion.version}-{cthulhuVersion.codeName}',
'kb:cthulhu+shift+v'
gesture_string
)
logger.info(f"DisplayVersion: Registered keybinding result: {self._kb_binding}")
if self._kb_binding:
logger.info(f"Binding keysymstring: {self._kb_binding.keysymstring}")
logger.info(f"Binding modifiers: {self._kb_binding.modifiers}")
logger.info(f"Binding modifier_mask: {self._kb_binding.modifier_mask}")
logger.info(f"Binding handler: {self._kb_binding.handler}")
with open('/tmp/displayversion_debug.log', 'a') as f:
f.write(f"SUCCESS: Keybinding created!\n")
f.write(f" keysymstring: {self._kb_binding.keysymstring}\n")
f.write(f" modifiers: {self._kb_binding.modifiers}\n")
f.write(f" modifier_mask: {self._kb_binding.modifier_mask}\n")
else:
logger.error("DisplayVersion: Failed to create keybinding!")
with open('/tmp/displayversion_debug.log', 'a') as f:
f.write("ERROR: Failed to create keybinding!\n")
logger.info("=== DisplayVersion plugin activation completed ===")
with open('/tmp/displayversion_debug.log', 'a') as f:
f.write("=== DisplayVersion plugin activation completed ===\n\n")
except Exception as e:
logger.error(f"Error activating DisplayVersion plugin: {e}")
import traceback
logger.error(traceback.format_exc())
@cthulhu_hookimpl
def deactivate(self, plugin=None):
@@ -55,6 +101,7 @@ class DisplayVersion(Plugin):
def speakText(self, script=None, inputEvent=None):
"""Speak the Cthulhu version when shortcut is pressed."""
try:
logger.info("DisplayVersion plugin: speakText called")
if self.app:
state = self.app.getDynamicApiManager().getAPI('CthulhuState')
if state.activeScript:
@@ -65,4 +112,4 @@ class DisplayVersion(Plugin):
return True
except Exception as e:
logger.error(f"Error in DisplayVersion speakText: {e}")
return False
return False
@@ -1,6 +0,0 @@
[Plugin]
Module=HelloCthulhu
Loader=python3
Name=Cthulhu say hello
Description=startup announcement for Cthulhu
Authors=Chrys chrys@linux-a11y.org
@@ -1,48 +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
class HelloCthulhu(GObject.Object, Peas.Activatable, plugin.Plugin):
#__gtype_name__ = 'HelloCthulhu'
object = GObject.Property(type=GObject.Object)
def __init__(self):
plugin.Plugin.__init__(self)
def do_activate(self):
API = self.object
self.connectSignal("start-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')
app.getDynamicApiManager().getAPI('CthulhuState').activeScript.presentMessage(messages.START_CTHULHU, resetStyles=False)
+2 -2
View File
@@ -1,7 +1,7 @@
cthulhu_python_PYTHON = \
__init__.py \
HelloCthulhu.plugin \
HelloCthulhu.py
plugin.info \
plugin.py
cthulhu_pythondir=$(pkgpythondir)/plugins/HelloCthulhu
@@ -0,0 +1,7 @@
[Plugin]
Name = Cthulhu say hello
Module = HelloCthulhu
Description = Startup announcement for Cthulhu
Authors = Storm Dragon <storm_dragon@stormux.org>
Version = 1.0
Category = Interaction
+113
View File
@@ -0,0 +1,113 @@
#!/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.
#
"""Hello Cthulhu plugin for Cthulhu."""
import logging
import weakref
from cthulhu.plugin import Plugin, cthulhu_hookimpl
logger = logging.getLogger(__name__)
# Class-level variable to track if the greeting has been presented
# This ensures the greeting is only shown once even if multiple instances exist
_greeting_shown = False
class HelloCthulhu(Plugin):
"""Plugin that speaks a welcome message when Cthulhu starts up."""
def __init__(self, *args, **kwargs):
"""Initialize the plugin."""
super().__init__(*args, **kwargs)
logger.info("HelloCthulhu plugin initialized")
self._signal_handler_id = None
self._is_connected = False
@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 HelloCthulhu plugin")
try:
# Only connect the signal if we haven't already
if not self._is_connected:
signal_manager = self.app.getSignalManager()
self._signal_handler_id = signal_manager.connectSignal(
"start-application-completed",
self.process,
"default" # Add profile parameter
)
self._is_connected = True
logger.debug("Connected to start-application-completed signal")
except Exception as e:
logger.error(f"Error activating HelloCthulhu 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 HelloCthulhu plugin")
try:
# Only disconnect if we're connected
if self._is_connected and self._signal_handler_id is not None:
signal_manager = self.app.getSignalManager()
signal_manager.disconnectSignalByFunction(
self.process
)
self._signal_handler_id = None
self._is_connected = False
logger.debug("Disconnected from start-application-completed signal")
except Exception as e:
logger.error(f"Error deactivating HelloCthulhu plugin: {e}")
def process(self, app):
"""Process the start-application-completed signal."""
global _greeting_shown
# Only present the message if it hasn't been shown yet
if _greeting_shown:
logger.debug("Greeting already shown, skipping")
return
try:
messages = app.getDynamicApiManager().getAPI('Messages')
state = app.getDynamicApiManager().getAPI('CthulhuState')
if state.activeScript:
state.activeScript.presentMessage(messages.START_CTHULHU, resetStyles=False)
_greeting_shown = True
logger.info("Greeting message presented")
# Disconnect the signal after presenting the message
if self._is_connected and self._signal_handler_id is not None:
signal_manager = app.getSignalManager()
signal_manager.disconnectSignalByFunction(
self.process
)
self._signal_handler_id = None
self._is_connected = False
logger.debug("Disconnected signal after presenting greeting")
except Exception as e:
logger.error(f"Error in HelloCthulhu process: {e}")
+1 -1
View File
@@ -1,4 +1,4 @@
SUBDIRS = Clipboard DisplayVersion hello_world self_voice Time MouseReview Date ByeCthulhu HelloCthulhu PluginManager CapsLockHack SimplePluginSystem
SUBDIRS = Clipboard DisplayVersion hello_world self_voice ByeCthulhu HelloCthulhu SimplePluginSystem
cthulhu_pythondir=$(pkgpythondir)/plugins
@@ -1,7 +0,0 @@
cthulhu_python_PYTHON = \
__init__.py \
MouseReview.plugin \
MouseReview.py
cthulhu_pythondir=$(pkgpythondir)/plugins/MouseReview
@@ -1,6 +0,0 @@
[Plugin]
Module=MouseReview
Loader=python3
Name=Mouse Review
Description=Review whats below the mouse coursor
Authors=Chrys chrys@linux-a11y.org
@@ -1,759 +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
"""Mouse review mode."""
__id__ = "$Id$"
__version__ = "$Revision$"
__date__ = "$Date$"
__copyright__ = "Copyright (c) 2008 Eitan Isaacson" \
"Copyright (c) 2016 Igalia, S.L."
__license__ = "LGPL"
from cthulhu import plugin
import gi, math, time
gi.require_version('Peas', '1.0')
from gi.repository import GObject
from gi.repository import Peas
gi.require_version("Atspi", "2.0")
from gi.repository import Atspi
from gi.repository import Gdk
try:
gi.require_version("Wnck", "3.0")
from gi.repository import Wnck
_mouseReviewCapable = True
except Exception:
_mouseReviewCapable = False
# compatibility layer, see MouseReview.do_activate
debug = None
event_manager = None
cthulhu = None
cthulhu_state = None
script_manager = None
settings_manager = None
speech = None
messages = None
cmdnames = None
emitRegionChanged = None
_scriptManager = None
_settingsManager = None
AXObject = None
AXUtilities = None
keybindings = None
input_event = None
class MouseReview(GObject.Object, Peas.Activatable, plugin.Plugin):
#__gtype_name__ = 'MouseReview'
object = GObject.Property(type=GObject.Object)
def __init__(self):
plugin.Plugin.__init__(self)
def do_activate(self):
API = self.object
global _mouseReviewCapable
if not _mouseReviewCapable:
return
global debug
global event_manager
global cthulhu_state
global script_manager
global settings_manager
global speech
global _scriptManager
global _settingsManager
global emitRegionChanged
global messages
global cmdnames
global AXObject
global AXUtilities
global keybindings
global input_event
debug= API.app.getDynamicApiManager().getAPI('Debug')
event_manager = API.app.getDynamicApiManager().getAPI('EventManager')
messages = API.app.getDynamicApiManager().getAPI('Messages')
cmdnames = API.app.getDynamicApiManager().getAPI('Cmdnames')
cthulhu_state = API.app.getDynamicApiManager().getAPI('CthulhuState')
script_manager = API.app.getDynamicApiManager().getAPI('ScriptManager')
settings_manager = API.app.getDynamicApiManager().getAPI('SettingsManager')
speech = API.app.getDynamicApiManager().getAPI('Speech')
emitRegionChanged = API.app.getDynamicApiManager().getAPI('EmitRegionChanged')
_scriptManager = script_manager.getManager()
_settingsManager = settings_manager.getManager()
AXObject = API.app.getDynamicApiManager().getAPI('AXObject')
AXUtilities = API.app.getDynamicApiManager().getAPI('AXUtilities')
keybindings = API.app.getDynamicApiManager().getAPI('Keybindings')
input_event = API.app.getDynamicApiManager().getAPI('InputEvent')
mouse_review = MouseReviewer()
self.registerAPI('MouseReview', mouse_review)
self.Initialize(API.app)
self.connectSignal("setup-inputeventhandlers-completed", self.setupCompatBinding)
self.connectSignal("load-setting-completed", self.Initialize)
def do_deactivate(self):
API = self.object
global _mouseReviewCapable
if not _mouseReviewCapable:
return
mouse_review = API.app.getDynamicApiManager().getAPI('MouseReview')
mouse_review.deactivate()
def do_update_state(self):
API = self.object
def setupCompatBinding(self, app):
API = self.object
mouse_review = API.app.getDynamicApiManager().getAPI('MouseReview')
cmdnames = API.app.getDynamicApiManager().getAPI('Cmdnames')
inputEventHandlers = API.app.getDynamicApiManager().getAPI('inputEventHandlers')
inputEventHandlers['toggleMouseReviewHandler'] = API.app.getAPIHelper().createInputEventHandler(mouse_review.toggle, cmdnames.MOUSE_REVIEW_TOGGLE)
def Initialize(self, app):
mouse_review = app.getDynamicApiManager().getAPI('MouseReview')
settings_manager = app.getDynamicApiManager().getAPI('SettingsManager')
_settingsManager = settings_manager.getManager()
if _settingsManager.getSetting('enableMouseReview'):
mouse_review.activate()
else:
mouse_review.deactivate()
class _StringContext:
"""The textual information associated with an _ItemContext."""
def __init__(self, obj, script=None, string="", start=0, end=0):
"""Initialize the _StringContext.
Arguments:
- string: The human-consumable string
- obj: The accessible object associated with this string
- start: The start offset with respect to entire text, if one exists
- end: The end offset with respect to the entire text, if one exists
- script: The script associated with the accessible object
"""
self._obj = obj
self._script = script
self._string = string
self._start = start
self._end = end
self._boundingBox = 0, 0, 0, 0
if script:
self._boundingBox = script.utilities.getTextBoundingBox(obj, start, end)
def __eq__(self, other):
return other is not None \
and self._obj == other._obj \
and self._string == other._string \
and self._start == other._start \
and self._end == other._end
def isSubstringOf(self, other):
"""Returns True if this is a substring of other."""
if other is None:
return False
if not (self._obj and other._obj):
return False
thisBox = self.getBoundingBox()
if thisBox == (0, 0, 0, 0):
return False
otherBox = other.getBoundingBox()
if otherBox == (0, 0, 0, 0):
return False
# We get various and sundry results for the bounding box if the implementor
# included newline characters as part of the word or line at offset. Try to
# detect this and adjust the bounding boxes before getting the intersection.
if thisBox[3] != otherBox[3] and self._obj == other._obj:
thisNewLineCount = self._string.count("\n")
if thisNewLineCount and thisBox[3] / thisNewLineCount == otherBox[3]:
thisBox = *thisBox[0:3], otherBox[3]
if self._script.utilities.intersection(thisBox, otherBox) != thisBox:
return False
if not (self._string and self._string.strip() in other._string):
return False
msg = f"MOUSE REVIEW: '{self._string}' is substring of '{other._string}'"
debug.println(debug.LEVEL_INFO, msg, True)
return True
def getBoundingBox(self):
"""Returns the bounding box associated with this context's range."""
return self._boundingBox
def getString(self):
"""Returns the string associated with this context."""
return self._string
def present(self):
"""Presents this context to the user."""
if not self._script:
msg = "MOUSE REVIEW: Not presenting due to lack of script"
debug.println(debug.LEVEL_INFO, msg, True)
return False
if not self._string:
msg = "MOUSE REVIEW: Not presenting due to lack of string"
debug.println(debug.LEVEL_INFO, msg, True)
return False
voice = self._script.speechGenerator.voice(obj=self._obj, string=self._string)
string = self._script.utilities.adjustForRepeats(self._string)
# TODO
#cthulhu.emitRegionChanged(self._obj, self._start, self._end, cthulhu.MOUSE_REVIEW)
emitRegionChanged(self._obj, self._start, self._end, "mouse-review")
self._script.speakMessage(string, voice=voice, interrupt=False)
self._script.displayBrailleMessage(self._string, -1)
return True
class _ItemContext:
"""Holds all the information of the item at a specified point."""
def __init__(self, x=0, y=0, obj=None, boundary=None, frame=None, script=None):
"""Initialize the _ItemContext.
Arguments:
- x: The X coordinate
- y: The Y coordinate
- obj: The accessible object of interest at that coordinate
- boundary: The accessible-text boundary type
- frame: The containing accessible object (often a top-level window)
- script: The script associated with the accessible object
"""
self._x = x
self._y = y
self._obj = obj
self._boundary = boundary
self._frame = frame
self._script = script
self._string = self._getStringContext()
self._time = time.time()
self._boundingBox = 0, 0, 0, 0
if script:
self._boundingBox = script.utilities.getBoundingBox(obj)
def __eq__(self, other):
return other is not None \
and self._frame == other._frame \
and self._obj == other._obj \
and self._string == other._string
def _treatAsDuplicate(self, prior):
if self._obj != prior._obj or self._frame != prior._frame:
msg = "MOUSE REVIEW: Not a duplicate: different objects"
debug.println(debug.LEVEL_INFO, msg, True)
return False
if self.getString() and prior.getString() and not self._isSubstringOf(prior):
msg = "MOUSE REVIEW: Not a duplicate: not a substring of"
debug.println(debug.LEVEL_INFO, msg, True)
return False
if self._x == prior._x and self._y == prior._y:
msg = "MOUSE REVIEW: Treating as duplicate: mouse didn't move"
debug.println(debug.LEVEL_INFO, msg, True)
return True
interval = self._time - prior._time
if interval > 0.5:
msg = f"MOUSE REVIEW: Not a duplicate: was {interval:.2f}s ago"
debug.println(debug.LEVEL_INFO, msg, True)
return False
msg = "MOUSE REVIEW: Treating as duplicate"
debug.println(debug.LEVEL_INFO, msg, True)
return True
def _treatAsSingleObject(self):
if not AXObject.supports_text(self._obj):
return True
if not self._obj.queryText().characterCount:
return True
return False
def _getStringContext(self):
"""Returns the _StringContext associated with the specified point."""
if not (self._script and self._obj):
return _StringContext(self._obj)
if self._treatAsSingleObject():
return _StringContext(self._obj, self._script)
string, start, end = self._script.utilities.textAtPoint(
self._obj, self._x, self._y, boundary=self._boundary)
if string:
string = self._script.utilities.expandEOCs(self._obj, start, end)
return _StringContext(self._obj, self._script, string, start, end)
def _getContainer(self):
roles = [Atspi.Role.DIALOG,
Atspi.Role.FRAME,
Atspi.Role.LAYERED_PANE,
Atspi.Role.MENU,
Atspi.Role.PAGE_TAB,
Atspi.Role.TOOL_BAR,
Atspi.Role.WINDOW]
return AXObject.find_ancestor(self._obj, lambda x: AXObject.get_role(x) in roles)
def _isSubstringOf(self, other):
"""Returns True if this is a substring of other."""
return self._string.isSubstringOf(other._string)
def getObject(self):
"""Returns the accessible object associated with this context."""
return self._obj
def getBoundingBox(self):
"""Returns the bounding box associated with this context."""
x, y, width, height = self._string.getBoundingBox()
if not (width or height):
return self._boundingBox
return x, y, width, height
def getString(self):
"""Returns the string associated with this context."""
return self._string.getString()
def getTime(self):
"""Returns the time associated with this context."""
return self._time
def _isInlineChild(self, prior):
if not self._obj or not prior._obj:
return False
if AXObject.get_parent(prior._obj) != self._obj:
return False
if self._treatAsSingleObject():
return False
return AXUtilities.is_link(prior._obj)
def present(self, prior):
"""Presents this context to the user."""
if self == prior or self._treatAsDuplicate(prior):
msg = "MOUSE REVIEW: Not presenting due to no change"
debug.println(debug.LEVEL_INFO, msg, True)
return False
interrupt = self._obj and self._obj != prior._obj \
or math.sqrt((self._x - prior._x)**2 + (self._y - prior._y)**2) > 25
if interrupt:
self._script.presentationInterrupt()
if self._frame and self._frame != prior._frame:
self._script.presentObject(self._frame,
alreadyFocused=True,
inMouseReview=True,
interrupt=True)
if self._script.utilities.containsOnlyEOCs(self._obj):
msg = "MOUSE REVIEW: Not presenting object which contains only EOCs"
debug.println(debug.LEVEL_INFO, msg, True)
return False
if self._obj and self._obj != prior._obj and not self._isInlineChild(prior):
priorObj = prior._obj or self._getContainer()
# TODO
#cthulhu.emitRegionChanged(self._obj, mode=cthulhu.MOUSE_REVIEW)
emitRegionChanged(self._obj, mode="mouse-review")
self._script.presentObject(self._obj, priorObj=priorObj, inMouseReview=True)
if self._string.getString() == AXObject.get_name(self._obj):
return True
if not self._script.utilities.isEditableTextArea(self._obj):
return True
if AXUtilities.is_table_cell(self._obj) \
and self._string.getString() == self._script.utilities.displayedText(self._obj):
return True
if self._string != prior._string and self._string.present():
return True
return True
class MouseReviewer:
"""Main class for the mouse-review feature."""
def __init__(self):
self._active = _settingsManager.getSetting("enableMouseReview")
self._currentMouseOver = _ItemContext()
self._pointer = None
self._workspace = None
self._windows = []
self._all_windows = []
self._handlerIds = {}
self._eventListener = Atspi.EventListener.new(self._listener)
self.inMouseEvent = False
self._handlers = self._setup_handlers()
self._bindings = self._setup_bindings()
if not _mouseReviewCapable:
msg = "MOUSE REVIEW ERROR: Wnck is not available"
debug.println(debug.LEVEL_INFO, msg, True)
return
display = Gdk.Display.get_default()
try:
seat = Gdk.Display.get_default_seat(display)
self._pointer = seat.get_pointer()
except AttributeError:
msg = "MOUSE REVIEW ERROR: Gtk+ 3.20 is not available"
debug.println(debug.LEVEL_INFO, msg, True)
return
except Exception:
msg = "MOUSE REVIEW ERROR: Exception getting pointer for default seat."
debug.println(debug.LEVEL_INFO, msg, True)
return
if not self._pointer:
msg = "MOUSE REVIEW ERROR: No pointer for default seat."
debug.println(debug.LEVEL_INFO, msg, True)
return
if not self._active:
return
self.activate()
def get_bindings(self):
"""Returns the mouse-review keybindings."""
return self._bindings
def get_handlers(self):
"""Returns the mouse-review handlers."""
return self._handlers
def _setup_handlers(self):
"""Sets up and returns the mouse-review input event handlers."""
handlers = {}
handlers["toggleMouseReviewHandler"] = \
input_event.InputEventHandler(
self.toggle,
cmdnames.MOUSE_REVIEW_TOGGLE)
return handlers
def _setup_bindings(self):
"""Sets up and returns the mouse-review key bindings."""
bindings = keybindings.KeyBindings()
bindings.add(
keybindings.KeyBinding(
"",
keybindings.defaultModifierMask,
keybindings.NO_MODIFIER_MASK,
self._handlers.get("toggleMouseReviewHandler")))
return bindings
def activate(self):
"""Activates mouse review."""
if not _mouseReviewCapable:
msg = "MOUSE REVIEW ERROR: Wnck is not available"
debug.println(debug.LEVEL_INFO, msg, True)
return
# Set up the initial object as the one with the focus to avoid
# presenting irrelevant info the first time.
obj = cthulhu_state.locusOfFocus
script = None
frame = None
if obj:
script = _scriptManager.getScript(AXObject.get_application(obj), obj)
if script:
frame = script.utilities.topLevelObject(obj)
self._currentMouseOver = _ItemContext(obj=obj, frame=frame, script=script)
self._eventListener.register("mouse:abs")
screen = Wnck.Screen.get_default()
if screen:
# On first startup windows and workspace are likely to be None,
# but the signals we connect to will get emitted when proper values
# become available; but in case we got disabled and re-enabled we
# have to get the initial values manually.
stacked = screen.get_windows_stacked()
if stacked:
stacked.reverse()
self._all_windows = stacked
self._workspace = screen.get_active_workspace()
if self._workspace:
self._update_workspace_windows()
i = screen.connect("window-stacking-changed", self._on_stacking_changed)
self._handlerIds[i] = screen
i = screen.connect("active-workspace-changed", self._on_workspace_changed)
self._handlerIds[i] = screen
self._active = True
def deactivate(self):
"""Deactivates mouse review."""
self._eventListener.deregister("mouse:abs")
for key, value in self._handlerIds.items():
value.disconnect(key)
self._handlerIds = {}
self._workspace = None
self._windows = []
self._all_windows = []
self._active = False
def getCurrentItem(self):
"""Returns the accessible object being reviewed."""
if not _mouseReviewCapable:
return None
if not self._active:
return None
obj = self._currentMouseOver.getObject()
if time.time() - self._currentMouseOver.getTime() > 0.1:
msg = f"MOUSE REVIEW: Treating {obj} as stale"
debug.println(debug.LEVEL_INFO, msg, True)
return None
return obj
def toggle(self, script=None, event=None):
"""Toggle mouse reviewing on or off."""
if not _mouseReviewCapable:
return
self._active = not self._active
_settingsManager.setSetting("enableMouseReview", self._active)
if not self._active:
self.deactivate()
msg = messages.MOUSE_REVIEW_DISABLED
else:
self.activate()
msg = messages.MOUSE_REVIEW_ENABLED
if cthulhu_state.activeScript:
cthulhu_state.activeScript.presentMessage(msg)
def _update_workspace_windows(self):
self._windows = [w for w in self._all_windows
if w.is_on_workspace(self._workspace)]
def _on_stacking_changed(self, screen):
"""Callback for Wnck's window-stacking-changed signal."""
stacked = screen.get_windows_stacked()
stacked.reverse()
self._all_windows = stacked
if self._workspace:
self._update_workspace_windows()
def _on_workspace_changed(self, screen, prev_ws=None):
"""Callback for Wnck's active-workspace-changed signal."""
self._workspace = screen.get_active_workspace()
self._update_workspace_windows()
def _contains_point(self, obj, x, y, coordType=None):
if coordType is None:
coordType = Atspi.CoordType.SCREEN
try:
return obj.queryComponent().contains(x, y, coordType)
except Exception:
return False
def _has_bounds(self, obj, bounds, coordType=None):
"""Returns True if the bounding box of obj is bounds."""
if coordType is None:
coordType = Atspi.CoordType.SCREEN
try:
extents = obj.queryComponent().getExtents(coordType)
except Exception:
return False
return list(extents) == list(bounds)
def _accessible_window_at_point(self, pX, pY):
"""Returns the accessible window at the specified coordinates."""
window = None
for w in self._windows:
if w.is_minimized():
continue
x, y, width, height = w.get_geometry()
if x <= pX <= x + width and y <= pY <= y + height:
window = w
break
if not window:
return None
windowApp = window.get_application()
if not windowApp:
return None
app = AXUtilities.get_application_with_pid(windowApp.get_pid())
if not app:
return None
candidates = [o for o in AXObject.iter_children(
app, lambda x: self._contains_point(x, pX, pY))]
if len(candidates) == 1:
return candidates[0]
name = window.get_name()
matches = [o for o in candidates if AXObject.get_name(o) == name]
if len(matches) == 1:
return matches[0]
bbox = window.get_client_window_geometry()
matches = [o for o in candidates if self._has_bounds(o, bbox)]
if len(matches) == 1:
return matches[0]
return None
def _on_mouse_moved(self, event):
"""Callback for mouse:abs events."""
screen, pX, pY = self._pointer.get_position()
window = self._accessible_window_at_point(pX, pY)
msg = "MOUSE REVIEW: Window at (%i, %i) is %s" % (pX, pY, window)
debug.println(debug.LEVEL_INFO, msg, True)
if not window:
return
script = _scriptManager.getScript(AXObject.get_application(window))
if not script:
return
if script.utilities.isDead(cthulhu_state.locusOfFocus):
menu = None
elif AXUtilities.is_menu(cthulhu_state.locusOfFocus):
menu = cthulhu_state.locusOfFocus
else:
menu = AXObject.find_ancestor(cthulhu_state.locusOfFocus, AXUtilities.is_menu)
screen, nowX, nowY = self._pointer.get_position()
if (pX, pY) != (nowX, nowY):
msg = "MOUSE REVIEW: Pointer moved again: (%i, %i)" % (nowX, nowY)
debug.println(debug.LEVEL_INFO, msg, True)
return
obj = script.utilities.descendantAtPoint(menu, pX, pY) \
or script.utilities.descendantAtPoint(window, pX, pY)
msg = "MOUSE REVIEW: Object at (%i, %i) is %s" % (pX, pY, obj)
debug.println(debug.LEVEL_INFO, msg, True)
script = _scriptManager.getScript(AXObject.get_application(window), obj)
if menu and obj and not AXObject.find_ancestor(obj, AXUtilities.is_menu):
if script.utilities.intersectingRegion(obj, menu) != (0, 0, 0, 0):
msg = f"MOUSE REVIEW: {obj} believed to be under {menu}"
debug.println(debug.LEVEL_INFO, msg, True)
return
objDocument = script.utilities.getTopLevelDocumentForObject(obj)
if objDocument and script.utilities.inDocumentContent():
document = script.utilities.activeDocument()
if document != objDocument:
msg = f"MOUSE REVIEW: {obj} is not in active document {document}"
debug.println(debug.LEVEL_INFO, msg, True)
return
screen, nowX, nowY = self._pointer.get_position()
if (pX, pY) != (nowX, nowY):
msg = "MOUSE REVIEW: Pointer moved again: (%i, %i)" % (nowX, nowY)
debug.println(debug.LEVEL_INFO, msg, True)
return
boundary = None
x, y, width, height = self._currentMouseOver.getBoundingBox()
if y <= pY <= y + height and self._currentMouseOver.getString():
boundary = Atspi.TextBoundaryType.WORD_START
elif obj == self._currentMouseOver.getObject():
boundary = Atspi.TextBoundaryType.LINE_START
elif AXUtilities.is_selectable(obj):
boundary = Atspi.TextBoundaryType.LINE_START
elif script.utilities.isMultiParagraphObject(obj):
boundary = Atspi.TextBoundaryType.LINE_START
new = _ItemContext(pX, pY, obj, boundary, window, script)
if new.present(self._currentMouseOver):
self._currentMouseOver = new
def _listener(self, event):
"""Generic listener, mainly to output debugging info."""
startTime = time.time()
msg = f"\nvvvvv PROCESS OBJECT EVENT {event.type} vvvvv"
debug.println(debug.LEVEL_INFO, msg, False)
if event.type.startswith("mouse:abs"):
self.inMouseEvent = True
self._on_mouse_moved(event)
self.inMouseEvent = False
msg = f"TOTAL PROCESSING TIME: {time.time() - startTime:.4f}\n"
msg += f"^^^^^ PROCESS OBJECT EVENT {event.type} ^^^^^\n"
debug.println(debug.LEVEL_INFO, msg, False)
@@ -1,25 +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
@@ -1,8 +0,0 @@
cthulhu_python_PYTHON = \
__init__.py \
PluginManager.plugin \
PluginManager.py \
PluginManagerUi.py
cthulhu_pythondir=$(pkgpythondir)/plugins/PluginManager
@@ -1,14 +0,0 @@
[Plugin]
Module=PluginManager
Loader=python3
Name=Plugin Manager
Description=Activate and Deactivate plugins
Authors=Chrys chrys@linux-a11y.org
Website=
Version=1.0
Copyright=
Builtin=true
Hidden=true
Depends=
Icon=
Help=
@@ -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
import PluginManagerUi
class PluginManager(GObject.Object, Peas.Activatable, plugin.Plugin):
#__gtype_name__ = 'PluginManager'
object = GObject.Property(type=GObject.Object)
def __init__(self):
plugin.Plugin.__init__(self)
self.pluginManagerUi = None
def do_activate(self):
API = self.object
self.registerGestureByString(self.startPluginManagerUi, _('plugin manager'), 'kb:cthulhu+e')
def do_deactivate(self):
API = self.object
def startPluginManagerUi(self, script=None, inputEvent=None):
self.showUI()
return True
def showUI(self):
API = self.object
if self.pluginManagerUi == None:
self.pluginManagerUi = PluginManagerUi.PluginManagerUi(API.app)
self.pluginManagerUi.setTranslationContext(self.getTranslationContext())
self.pluginManagerUi.createUI()
self.pluginManagerUi.run()
self.pluginManagerUi = None
else:
self.pluginManagerUi.present()
@@ -1,307 +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
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk
class PluginManagerUi(Gtk.ApplicationWindow):
def __init__(self, app, *args, **kwargs):
super().__init__(*args, **kwargs, title=_("Cthulhu Plugin Manager"))
self.app = app
self.translationContext = None
self.connect("destroy", self._onCancelButtonClicked)
self.connect('key-press-event', self._onKeyPressWindow)
def createUI(self):
self.set_default_size(650, 650)
self.set_position(Gtk.WindowPosition.CENTER_ALWAYS)
# pluginInfo (object) = 0
# name (str) = 1
# active (bool) = 2
# buildIn (bool) = 3
# dataDir (str) = 4
# moduleDir (str) = 5
# dependencies (object) = 6
# moduleName (str) = 7
# description (str) = 8
# authors (object) = 9
# website (str) = 10
# copyright (str) = 11
# version (str) = 12
# helpUri (str) = 13
# iconName (str) = 14
self.listStore = Gtk.ListStore(object,str, bool, bool, str, str,object,str,str,object,str,str,str,str,str)
self.treeView = Gtk.TreeView(model=self.listStore)
self.treeView.connect("row-activated", self._rowActivated)
self.treeView.connect('key-press-event', self._onKeyPressTreeView)
self.rendererText = Gtk.CellRendererText()
self.columnText = Gtk.TreeViewColumn(_("Name"), self.rendererText, text=1)
self.treeView.append_column(self.columnText)
self.rendererToggle = Gtk.CellRendererToggle()
self.rendererToggle.connect("toggled", self._onCellToggled)
self.columnToggle = Gtk.TreeViewColumn(_("Active"), self.rendererToggle, active=2)
self.treeView.append_column(self.columnToggle)
self.buttomBox = Gtk.Box(spacing=6)
self.mainVBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
self.mainVBox.pack_start(self.treeView, True, True, 0)
self.mainVBox.pack_start(self.buttomBox, False, True, 0)
self.add(self.mainVBox)
self.oKButton = Gtk.Button.new_with_mnemonic(_("_Details"))
self.oKButton.connect("clicked", self._onDetailsButtonClicked)
self.buttomBox.pack_start(self.oKButton, True, True, 0)
self.oKButton = Gtk.Button.new_with_mnemonic(_("_OK"))
self.oKButton.connect("clicked", self._onOkButtonClicked)
self.buttomBox.pack_start(self.oKButton, True, True, 0)
self.applyButton = Gtk.Button.new_with_mnemonic(_("_Apply"))
self.applyButton.connect("clicked", self._onApplyButtonClicked)
self.buttomBox.pack_start(self.applyButton, True, True, 0)
self.applyButton = Gtk.Button.new_with_mnemonic(_("_Install"))
self.applyButton.connect("clicked", self._onInstallButtonClicked)
self.buttomBox.pack_start(self.applyButton, True, True, 0)
self.applyButton = Gtk.Button.new_with_mnemonic(_("_Uninstall"))
self.applyButton.connect("clicked", self._onUninstallButtonClicked)
self.buttomBox.pack_start(self.applyButton, True, True, 0)
self.cancelButton = Gtk.Button.new_with_mnemonic(_("_Cancel"))
self.cancelButton.connect("clicked", self._onCancelButtonClicked)
self.buttomBox.pack_start(self.cancelButton, True, True, 0)
def setTranslationContext(self, translationContext):
self.translationContext = translationContext
global _
_ = translationContext.gettext
def closeWindow(self):
Gtk.main_quit()
def uninstallPlugin(self):
selection = self.treeView.get_selection()
model, list_iter = selection.get_selected()
try:
if model.get_value(list_iter,0):
pluginInfo = model.get_value(list_iter,0)
pluginName = self.app.getPluginSystemManager().getPluginName(pluginInfo)
dialog = Gtk.MessageDialog(None,
Gtk.DialogFlags.MODAL,
type=Gtk.MessageType.INFO,
buttons=Gtk.ButtonsType.YES_NO)
dialog.set_markup("<b>%s</b>" % _('Remove Plugin {}?').format(pluginName))
dialog.format_secondary_markup(_('Do you really want to remove Plugin {}?').format(pluginName))
response = dialog.run()
dialog.destroy()
if response != Gtk.ResponseType.YES:
return
self.app.getPluginSystemManager().uninstallPlugin(model.get_value(list_iter,0))
self.refreshPluginList()
except:
pass
def installPlugin(self):
ok, filePath = self.chooseFile()
if not ok:
return
self.app.getPluginSystemManager().installPlugin(filePath)
self.refreshPluginList()
def _onKeyPressWindow(self, _, event):
_, key_val = event.get_keyval()
if key_val == Gdk.KEY_Escape:
self.closeWindow()
def _onKeyPressTreeView(self, _, event):
_, key_val = event.get_keyval()
if key_val == Gdk.KEY_Return:
self.applySettings()
self.closeWindow()
if key_val == Gdk.KEY_Escape:
self.closeWindow()
# CTRL + Q
#modifiers = event.get_state()
#if modifiers == Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.MOD2_MASK:
# if key_val == Gdk.KEY_q:
# self._on_scan()
def applySettings(self):
for row in self.listStore:
pluginInfo = row[0]
isActive = row[2]
self.app.getPluginSystemManager().setPluginActive(pluginInfo, isActive)
gsettingsManager = self.app.getGsettingsManager()
gsettingsManager.set_settings_value_list('active-plugins', self.app.getPluginSystemManager().getActivePlugins())
self.app.getPluginSystemManager().syncAllPluginsActive()
self.refreshPluginList()
def _rowActivated(self, tree_view, path, column):
print('rowActivated')
def showDetails(self):
selection = self.treeView.get_selection()
model, list_iter = selection.get_selected()
try:
if model.get_value(list_iter,0):
pluginInfo = model.get_value(list_iter,0)
name = self.app.getPluginSystemManager().getPluginName(pluginInfo)
description = self.app.getPluginSystemManager().getPluginDescription(pluginInfo)
authors = self.app.getPluginSystemManager().getPluginAuthors(pluginInfo)
website =self.app.getPluginSystemManager().getPluginWebsite(pluginInfo)
copyright = self.app.getPluginSystemManager().getPluginCopyright(pluginInfo)
license = '' #self.app.getPluginSystemManager().getPluginName(pluginInfo)
version = self.app.getPluginSystemManager().getPluginVersion(pluginInfo)
dialog = Gtk.AboutDialog(self)
dialog.set_authors(authors)
dialog.set_website(website)
dialog.set_copyright(copyright)
dialog.set_license(license)
dialog.set_version(version)
dialog.set_program_name(name)
dialog.set_comments(description)
dialog.run()
dialog.destroy()
except:
pass
def _onDetailsButtonClicked(self, widget):
self.showDetails()
def _onOkButtonClicked(self, widget):
self.applySettings()
self.closeWindow()
def _onApplyButtonClicked(self, widget):
self.applySettings()
def _onInstallButtonClicked(self, widget):
self.installPlugin()
def _onUninstallButtonClicked(self, widget):
self.uninstallPlugin()
def _onCancelButtonClicked(self, widget):
self.closeWindow()
def refreshPluginList(self):
self.clearPluginList()
pluginList = self.app.getPluginSystemManager().plugins
for pluginInfo in pluginList:
self.addPlugin(pluginInfo)
def clearPluginList(self):
self.listStore.clear()
def addPlugin(self, pluginInfo):
ignoredPlugins = self.app.getPluginSystemManager().getIgnoredPlugins()
moduleDir = self.app.getPluginSystemManager().getPluginModuleDir(pluginInfo)
if moduleDir in ignoredPlugins:
return
hidden = self.app.getPluginSystemManager().isPluginHidden(pluginInfo)
if hidden:
return
moduleName = self.app.getPluginSystemManager().getPluginModuleName(pluginInfo)
name = self.app.getPluginSystemManager().getPluginName(pluginInfo)
version = self.app.getPluginSystemManager().getPluginVersion(pluginInfo)
website = self.app.getPluginSystemManager().getPluginWebsite(pluginInfo)
authors = self.app.getPluginSystemManager().getPluginAuthors(pluginInfo)
buildIn = self.app.getPluginSystemManager().isPluginBuildIn(pluginInfo)
description = self.app.getPluginSystemManager().getPluginDescription(pluginInfo)
iconName = self.app.getPluginSystemManager().getPluginIconName(pluginInfo)
copyright = self.app.getPluginSystemManager().getPluginCopyright(pluginInfo)
dependencies = self.app.getPluginSystemManager().getPluginDependencies(pluginInfo)
#settings = self.app.getPluginSystemManager().getPluginSettings(pluginInfo)
#hasDependencies = self.app.getPluginSystemManager().hasPluginDependency(pluginInfo)
loaded = self.app.getPluginSystemManager().isPluginLoaded(pluginInfo)
available = self.app.getPluginSystemManager().isPluginAvailable(pluginInfo)
active = self.app.getPluginSystemManager().isPluginActive(pluginInfo)
#externalData = self.app.getPluginSystemManager().getPluginExternalData(pluginInfo)
helpUri = self.app.getPluginSystemManager().getPlugingetHelpUri(pluginInfo)
dataDir = self.app.getPluginSystemManager().getPluginDataDir(pluginInfo)
# pluginInfo (object) = 0
# name (str) = 1
# active (bool) = 2
# buildIn (bool) = 3
# dataDir (str) = 4
# moduleDir (str) = 5
# dependencies (object) = 6
# moduleName (str) = 7
# description (str) = 8
# authors (object) = 9
# website (str) = 10
# copyright (str) = 11
# version (str) = 12
# helpUri (str) = 13
# iconName (str) = 14
self.listStore.append([pluginInfo, name, active, buildIn, dataDir, moduleDir, dependencies, moduleName, description, authors, website, copyright, version, helpUri, iconName])
def chooseFile(self):
dialog = Gtk.FileChooserDialog(
title=_("Please choose a file"), parent=self, action=Gtk.FileChooserAction.OPEN
)
dialog.add_buttons(
Gtk.STOCK_CANCEL,
Gtk.ResponseType.CANCEL,
Gtk.STOCK_OPEN,
Gtk.ResponseType.OK,
)
filter_plugin = Gtk.FileFilter()
filter_plugin.set_name(_("Plugin Archive"))
filter_plugin.add_mime_type("application/gzip")
dialog.add_filter(filter_plugin)
response = dialog.run()
filePath = ''
ok = False
if response == Gtk.ResponseType.OK:
ok = True
filePath = dialog.get_filename()
dialog.destroy()
return ok, filePath
def _onCellToggled(self, widget, path):
self.listStore[path][2] = not self.listStore[path][2]
def present(self):
cthulhu_state = self.app.getDynamicApiManager().getAPI('CthulhuState')
ts = 0
try:
ts = cthulhu_state.lastInputEvent.timestamp
except:
pass
if ts == 0:
ts = Gtk.get_current_event_time()
self.present_with_time(ts)
def run(self):
self.refreshPluginList()
self.present()
self.show_all()
Gtk.main()
self.destroy()
@@ -1,107 +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
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class ListBoxRowWithData(Gtk.ListBoxRow):
def __init__(self, data):
super(Gtk.ListBoxRow, self).__init__()
self.data = data
self.add(Gtk.Label(label=data))
class PluginManagerUi(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self)
self.pluginList = []
self.set_default_size(200, -1)
self.connect("destroy", Gtk.main_quit)
self.listBox = Gtk.ListBox()
self.buttomBox = Gtk.Box(spacing=6)
self.mainVBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=20)
self.mainVBox.pack_start(self.listBox, True, True, 0)
self.mainVBox.pack_start(self.buttomBox, True, True, 0)
self.add(self.mainVBox)
self.oKButton = Gtk.Button(label="OK")
self.oKButton.connect("clicked", self.on_oKButton_clicked)
self.buttomBox.pack_start(self.oKButton, True, True, 0)
self.applyButton = Gtk.Button(label="Apply")
self.applyButton.connect("clicked", self.on_applyButton_clicked)
self.buttomBox.pack_start(self.applyButton, True, True, 0)
self.cancelButton = Gtk.Button(label="Cancel")
self.cancelButton.connect("clicked", self.on_cancelButton_clicked)
self.buttomBox.pack_start(self.cancelButton, True, True, 0)
self.listBox.connect("row-activated", self.on_row_activated)
def on_row_activated(self, listBox, listboxrow):
print("Row %i activated" % (listboxrow.get_index()))
def on_oKButton_clicked(self, widget):
print("OK")
def on_applyButton_clicked(self, widget):
print("Apply")
def on_cancelButton_clicked(self, widget):
print("Cancel")
def addPlugin(self, Name, Active, Description = ''):
self.pluginList.append([Name, Active, Description])
def run(self):
for plugin in self.pluginList:
print(plugin)
box = Gtk.Box(spacing=10)
pluginNameLabel = Gtk.Label(plugin[0])
#pluginActiveCheckButton = Gtk.CheckButton(label="_Active", use_underline=True)
#pluginActiveCheckButton.set_active(plugin[1])
pluginActiveSwitch = Gtk.Switch()
pluginActiveSwitch.set_active(plugin[1])
pluginDescriptionLabel = Gtk.Label(plugin[2])
box.pack_start(pluginNameLabel, True, True, 0)
box.pack_start(pluginActiveSwitch, True, True, 0)
box.pack_start(pluginDescriptionLabel, True, True, 0)
self.listBox.add(box)
self.show_all()
Gtk.main()
if __name__ == "__main__":
ui = PluginManagerUi()
ui.addPlugin('plugin1', True, 'bla')
ui.addPlugin('plugin2', True, 'bla')
ui.addPlugin('plugin3', True, 'bla')
ui.run()
@@ -1,117 +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
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
class ListBoxRowWithData(Gtk.ListBoxRow):
def __init__(self, data):
super(Gtk.ListBoxRow, self).__init__()
self.data = data
self.add(Gtk.Label(label=data))
class ListBoxWindow(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title="ListBox Demo")
self.set_border_width(10)
box_outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
self.add(box_outer)
listbox = Gtk.ListBox()
listbox.set_selection_mode(Gtk.SelectionMode.NONE)
box_outer.pack_start(listbox, True, True, 0)
row = Gtk.ListBoxRow()
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
row.add(hbox)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
hbox.pack_start(vbox, True, True, 0)
label1 = Gtk.Label(label="Automatic Date & Time", xalign=0)
label2 = Gtk.Label(label="Requires internet access", xalign=0)
vbox.pack_start(label1, True, True, 0)
vbox.pack_start(label2, True, True, 0)
switch = Gtk.Switch()
switch.props.valign = Gtk.Align.CENTER
hbox.pack_start(switch, False, True, 0)
listbox.add(row)
row = Gtk.ListBoxRow()
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
row.add(hbox)
label = Gtk.Label(label="Enable Automatic Update", xalign=0)
check = Gtk.CheckButton()
hbox.pack_start(label, True, True, 0)
hbox.pack_start(check, False, True, 0)
listbox.add(row)
row = Gtk.ListBoxRow()
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
row.add(hbox)
label = Gtk.Label(label="Date Format", xalign=0)
combo = Gtk.ComboBoxText()
combo.insert(0, "0", "24-hour")
combo.insert(1, "1", "AM/PM")
hbox.pack_start(label, True, True, 0)
hbox.pack_start(combo, False, True, 0)
listbox.add(row)
listbox_2 = Gtk.ListBox()
items = "This is a sorted ListBox Fail".split()
for item in items:
listbox_2.add(ListBoxRowWithData(item))
def sort_func(row_1, row_2, data, notify_destroy):
return row_1.data.lower() > row_2.data.lower()
def filter_func(row, data, notify_destroy):
return False if row.data == "Fail" else True
listbox_2.set_sort_func(sort_func, None, False)
listbox_2.set_filter_func(filter_func, None, False)
def on_row_activated(listbox_widget, row):
print(row.data)
listbox_2.connect("row-activated", on_row_activated)
box_outer.pack_start(listbox_2, True, True, 0)
listbox_2.show_all()
win = ListBoxWindow()
win.connect("destroy", Gtk.main_quit)
win.show_all()
Gtk.main()
@@ -1,25 +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
@@ -1,7 +1,7 @@
cthulhu_python_PYTHON = \
__init__.py \
SimplePluginSystem.plugin \
SimplePluginSystem.py
plugin.info \
plugin.py
cthulhu_pythondir=$(pkgpythondir)/plugins/SimplePluginSystem
@@ -1,11 +0,0 @@
[Plugin]
Module=SimplePluginSystem
Loader=python3
Name=Simple Plugin System
Description=Simple plugin system implementation for Cthulhu
Authors=Chrys <chrys@linux-a11y.org>;Storm Dragon <storm_dragon@stormux.org>
Copyright=Copyright Â2024 Chrys, Storm Dragon
Website=https://git.stormux.org/storm/cthulhu
Version=1.0
Builtin=true
@@ -1,291 +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
from gi.repository import GObject, Peas
import glob
import os
import importlib.util
import random
import string
import _thread
from subprocess import Popen, PIPE
settings = None
speech = None
braille = None
input_event = None
def outputMessage( Message):
if (settings.enableSpeech):
speech.speak(Message)
if (settings.enableBraille):
braille.displayMessage(Message)
class SimplePluginSystem(GObject.Object, Peas.Activatable, plugin.Plugin):
__gtype_name__ = 'SimplePluginSystem'
object = GObject.Property(type=GObject.Object)
def __init__(self):
plugin.Plugin.__init__(self)
self.plugin_list = []
self.loaded = False
self.plugin_repo = os.path.expanduser('~') + "/.local/share/cthulhu/simple-plugins-enabled/"
def do_activate(self):
API = self.object
global settings
global speech
global braille
global input_event
settings = API.app.getDynamicApiManager().getAPI('Settings')
speech = API.app.getDynamicApiManager().getAPI('Speech')
braille = API.app.getDynamicApiManager().getAPI('Braille')
input_event = API.app.getDynamicApiManager().getAPI('InputEvent')
"""Required method for plugins"""
if not self.loaded:
self.load_plugins()
def do_deactivate(self):
"""Required method for plugins"""
# Remove all registered keybindings
for plugin in self.plugin_list:
self.unregisterShortcut(plugin['function'], plugin['shortcut'])
self.loaded = False
self.plugin_list = []
def SetupShortcutAndHandle(self, currPluginSetting):
shortcut = ''
# just the modifier
if not currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and not currPluginSetting['altkey']:
shortcut = 'kb:cthulhu+' + currPluginSetting['key']
# cthulhu + alt
if not currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and currPluginSetting['altkey']:
shortcut = 'kb:cthulhu+alt+' + currPluginSetting['key']
# cthulhu + CTRL
if not currPluginSetting['shiftkey'] and currPluginSetting['ctrlkey'] and not currPluginSetting['altkey']:
shortcut = 'kb:cthulhu+control+' + currPluginSetting['key']
# cthulhu + alt + CTRL
if not currPluginSetting['shiftkey'] and currPluginSetting['ctrlkey'] and currPluginSetting['altkey']:
shortcut = 'kb:cthulhu+alt+control+ ' + currPluginSetting['key']
# cthulhu + shift
if currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and not currPluginSetting['altkey']:
shortcut = 'kb:cthulhu+shift+' + currPluginSetting['key']
# alt + shift
if currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and currPluginSetting['altkey']:
shortcut = 'kb:alt+shift+' + currPluginSetting['key']
if shortcut != '':
print(shortcut)
currPluginSetting['shortcut'] = shortcut
self.registerGestureByString(currPluginSetting['function'], _(currPluginSetting['pluginname']), shortcut)
return currPluginSetting
def id_generator(self, size=7, chars=string.ascii_letters):
return ''.join(random.choice(chars) for _ in range(size))
def initSettings(self):
currPluginSetting={
'pluginname':'',
'functionname':'',
'key':'',
'shiftkey':False,
'ctrlkey':False,
'altkey':False,
'startnotify':False,
'stopnotify':False,
'blockcall':False,
'error':False,
'exec': False,
'parameters':'',
'function':None,
'inputeventhandler':None,
'valid':False,
'supressoutput':False,
'shortcut': ''
}
return currPluginSetting
def getPluginSettings(self, filepath, currPluginSetting):
try:
currPluginSetting['file'] = filepath
fileName, fileExtension = os.path.splitext(filepath)
if (fileExtension and (fileExtension != '')): #if there is an extension
currPluginSetting['loadable'] = (fileExtension.lower() == '.py') # only python is loadable
filename = os.path.basename(filepath) #filename
filename = os.path.splitext(filename)[0] #remove extension if we have one
#remove pluginname seperated by __-__
filenamehelper = filename.split('__-__')
filename = filenamehelper[len(filenamehelper) - 1 ]
currPluginSetting['permission'] = os.access(filepath, os.X_OK )
currPluginSetting['pluginname'] = 'NoNameAvailable'
if len(filenamehelper) == 2:
currPluginSetting['pluginname'] = filenamehelper[0]
#now get shortcuts seperated by __+__
filenamehelper = filename.split('__+__')
if len([y for y in filenamehelper if 'parameters_' in y.lower()]) == 1 and\
len([y for y in filenamehelper if 'parameters_' in y.lower()][0]) > 11:
currPluginSetting['parameters'] = [y for y in filenamehelper if 'parameters_' in y.lower()][0][11:]
if len([y for y in filenamehelper if 'key_' in y.lower()]) == 1 and\
len([y for y in filenamehelper if 'key_' in y.lower()][0]) > 4 :
currPluginSetting['key'] = [y for y in filenamehelper if 'key_' in y.lower()][0][4]
if currPluginSetting['key'] == '':
settcurrPluginSetting = 'shift' in map(str.lower, filenamehelper)
currPluginSetting['ctrlkey'] = 'control' in map(str.lower, filenamehelper)
currPluginSetting['altkey'] = 'alt' in map(str.lower, filenamehelper)
currPluginSetting['startnotify'] = 'startnotify' in map(str.lower, filenamehelper)
currPluginSetting['stopnotify'] = 'stopnotify' in map(str.lower, filenamehelper)
currPluginSetting['blockcall'] = 'blockcall' in map(str.lower, filenamehelper)
currPluginSetting['error'] = 'error' in map(str.lower, filenamehelper)
currPluginSetting['supressoutput'] = 'supressoutput' in map(str.lower, filenamehelper)
currPluginSetting['exec'] = 'exec' in map(str.lower, filenamehelper)
currPluginSetting['loadmodule'] = 'loadmodule' in map(str.lower, filenamehelper)
currPluginSetting = self.readSettingsFromPlugin(currPluginSetting)
if not currPluginSetting['loadmodule']:
if not currPluginSetting['permission']: #subprocessing only works with exec permission
return self.initSettings()
if currPluginSetting['loadmodule'] and not currPluginSetting['loadable']: #sorry.. its not loadable only .py is loadable
return self.initSettings()
if (len(currPluginSetting['key']) > 1): #no shortcut
if not currPluginSetting['exec']: # and no exec -> the plugin make no sense because it isnt hooked anywhere
return self.initSettings() #so not load it (sets valid = False)
else:
currPluginSetting['key'] = '' #there is a strange key, but exec? ignore the key..
currPluginSetting['valid'] = True # we could load everything
return currPluginSetting
except:
return self.initSettings()
def readSettingsFromPlugin(self, currPluginSetting):
if not os.access(currPluginSetting['file'], os.R_OK ):
return currPluginSetting
fileName, fileExtension = os.path.splitext(currPluginSetting['file'])
if (fileExtension and (fileExtension != '')): #if there is an extension
if (fileExtension.lower() != '.py') and \
(fileExtension.lower() != '.sh'):
return currPluginSetting
else:
return currPluginSetting
with open(currPluginSetting['file'], "r") as pluginFile:
for line in pluginFile:
currPluginSetting['shiftkey'] = ('sopsproperty:shift' in line.lower().replace(" ", "")) or currPluginSetting['shiftkey']
currPluginSetting['ctrlkey'] = ('sopsproperty:control' in line.lower().replace(" ", "")) or currPluginSetting['ctrlkey']
currPluginSetting['altkey'] = ('sopsproperty:alt' in line.lower().replace(" ", "")) or currPluginSetting['altkey']
currPluginSetting['startnotify'] = ('sopsproperty:startnotify' in line.lower().replace(" ", "")) or currPluginSetting['startnotify']
currPluginSetting['stopnotify'] = ('sopsproperty:stopnotify' in line.lower().replace(" ", "")) or currPluginSetting['stopnotify']
currPluginSetting['blockcall'] = ('sopsproperty:blockcall' in line.lower().replace(" ", "")) or currPluginSetting['blockcall']
currPluginSetting['error'] = ('sopsproperty:error' in line.lower().replace(" ", "")) or currPluginSetting['error']
currPluginSetting['supressoutput'] = ('sopsproperty:supressoutput' in line.lower().replace(" ", "")) or currPluginSetting['supressoutput']
currPluginSetting['exec'] = ('sopsproperty:exec' in line.lower().replace(" ", "")) or currPluginSetting['exec']
currPluginSetting['loadmodule'] = ('sopsproperty:loadmodule' in line.lower().replace(" ", "")) or currPluginSetting['loadmodule']
return currPluginSetting
def buildPluginSubprocess(self, currPluginSetting):
currplugin = "\'\"" + currPluginSetting['file'] + "\" " + currPluginSetting['parameters'] + "\'"
pluginname = currPluginSetting['pluginname']
if currPluginSetting['blockcall']:
pluginname = "blocking " + pluginname
fun_body = "global " + currPluginSetting['functionname']+"\n"
fun_body += "def " + currPluginSetting['functionname'] + "(script=None, inputEvent=None):\n"
if currPluginSetting['startnotify']:
fun_body +=" outputMessage('start " + pluginname + "')\n"
fun_body +=" p = Popen(" + currplugin + ", stdout=PIPE, stderr=PIPE, shell=True)\n"
fun_body +=" stdout, stderr = p.communicate()\n"
fun_body +=" message = ''\n"
fun_body +=" if not " + str(currPluginSetting['supressoutput']) + " and stdout:\n"
fun_body +=" message += str(stdout, \"utf-8\")\n"
fun_body +=" if " + str(currPluginSetting['error']) + " and stderr:\n"
fun_body +=" message += ' error: ' + str(stderr, \"utf-8\")\n"
fun_body +=" outputMessage( message)\n"
if currPluginSetting['stopnotify']:
fun_body +=" outputMessage('finish " + pluginname + "')\n"
fun_body +=" return True\n\n"
fun_body += "global " + currPluginSetting['functionname']+"T\n"
fun_body +="def " + currPluginSetting['functionname'] + "T(script=None, inputEvent=None):\n"
fun_body +=" _thread.start_new_thread("+ currPluginSetting['functionname'] + ",(script, inputEvent))\n\n"
return fun_body
def buildPluginExec(self, currPluginSetting):
pluginname = currPluginSetting['pluginname']
if currPluginSetting['blockcall']:
pluginname = "blocking " + pluginname
fun_body = "global " + currPluginSetting['functionname']+"\n"
fun_body += "def " + currPluginSetting['functionname'] + "(script=None, inputEvent=None):\n"
if currPluginSetting['startnotify']:
fun_body +=" outputMessage('start " + pluginname + "')\n"
fun_body += " try:\n"
fun_body += " spec = importlib.util.spec_from_file_location(\"" + currPluginSetting['functionname'] + "\",\""+ currPluginSetting['file']+"\")\n"
fun_body += " "+currPluginSetting['functionname'] + "Module = importlib.util.module_from_spec(spec)\n"
fun_body += " spec.loader.exec_module(" + currPluginSetting['functionname'] + "Module)\n"
fun_body += " except:\n"
fun_body += " pass\n"
if currPluginSetting['error']:
fun_body += " outputMessage(\"Error while executing " + pluginname + "\")\n"
if currPluginSetting['stopnotify']:
fun_body +=" outputMessage('finish " + pluginname + "')\n"
fun_body += " return True\n\n"
fun_body += "global " + currPluginSetting['functionname']+"T\n"
fun_body +="def " + currPluginSetting['functionname'] + "T(script=None, inputEvent=None):\n"
fun_body +=" _thread.start_new_thread("+ currPluginSetting['functionname'] + ",(script, inputEvent))\n\n"
return fun_body
def getFunctionName(self, currPluginSetting):
currPluginSetting['functionname'] = ''
while currPluginSetting['functionname'] == '' or currPluginSetting['functionname'] + 'T' in globals() or currPluginSetting['functionname'] in globals():
currPluginSetting['functionname'] = self.id_generator()
return currPluginSetting
def load_plugins(self):
if not self.loaded:
self.plugin_list = glob.glob(self.plugin_repo+'*')
for currplugin in self.plugin_list:
currPluginSetting = self.initSettings()
currPluginSetting = self.getPluginSettings(currplugin, currPluginSetting)
if not currPluginSetting['valid']:
continue
currPluginSetting = self.getFunctionName(currPluginSetting)
if currPluginSetting['loadmodule']:
exec(self.buildPluginExec(currPluginSetting)) # load as python module
else:
exec(self.buildPluginSubprocess(currPluginSetting)) # run as subprocess
if currPluginSetting['blockcall']:
currPluginSetting['function'] = globals()[currPluginSetting['functionname']] # non threaded
else:
currPluginSetting['function'] = globals()[currPluginSetting['functionname']+"T"] # T = Threaded
if currPluginSetting['exec']: # exec on load if we want
currPluginSetting['function']()
if not currPluginSetting['key'] == '':
currPluginSetting = self.SetupShortcutAndHandle(currPluginSetting)
print(currPluginSetting)
self.plugin_list.append(currPluginSetting) # store in a list
self.loaded = True
@@ -0,0 +1,9 @@
[Plugin]
Name = Simple Plugin System
Module = SimplePluginSystem
Description = Simple plugin system implementation for Cthulhu
Authors = Storm Dragon <storm_dragon@stormux.org>
Copyright = Copyright (c) 2025 Stormux
Website = https://git.stormux.org/storm/cthulhu
Version = 1.0
Category = System
@@ -0,0 +1,407 @@
#!/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.
#
"""Simple Plugin System for Cthulhu."""
import glob
import os
import importlib.util
import random
import string
import _thread
import logging
from subprocess import Popen, PIPE
import gettext
from cthulhu.plugin import Plugin, cthulhu_hookimpl
# Set up translation function
_ = gettext.gettext
logger = logging.getLogger(__name__)
# Global variables for API access
settings = None
speech = None
braille = None
input_event = None
def outputMessage(Message):
"""Output a message via speech and/or braille depending on settings."""
if (settings.enableSpeech):
speech.speak(Message)
if (settings.enableBraille):
braille.displayMessage(Message)
class SimplePluginSystem(Plugin):
"""Simple plugin system implementation for Cthulhu.
This plugin allows loading and managing simple script-based plugins
from a designated directory.
"""
def __init__(self, *args, **kwargs):
"""Initialize the plugin system."""
super().__init__(*args, **kwargs)
logger.info("SimplePluginSystem plugin initialized")
self.plugin_list = []
self.loaded = False
self.plugin_repo = os.path.expanduser('~') + "/.local/share/cthulhu/simple-plugins-enabled/"
self._signal_handler_id = None
@cthulhu_hookimpl
def activate(self, plugin=None):
"""Activate the plugin system."""
# Skip if this activation call isn't for us
if plugin is not None and plugin is not self:
return
logger.info("Activating SimplePluginSystem plugin")
try:
global settings
global speech
global braille
global input_event
settings = self.app.getDynamicApiManager().getAPI('Settings')
speech = self.app.getDynamicApiManager().getAPI('Speech')
braille = self.app.getDynamicApiManager().getAPI('Braille')
input_event = self.app.getDynamicApiManager().getAPI('InputEvent')
if not self.loaded:
self.load_plugins()
except Exception as e:
logger.error(f"Error activating SimplePluginSystem plugin: {e}")
@cthulhu_hookimpl
def deactivate(self, plugin=None):
"""Deactivate the plugin system."""
# Skip if this deactivation call isn't for us
if plugin is not None and plugin is not self:
return
logger.info("Deactivating SimplePluginSystem plugin")
try:
# Remove all registered keybindings
for plugin in self.plugin_list:
self.unregisterShortcut(plugin['function'], plugin['shortcut'])
self.loaded = False
self.plugin_list = []
except Exception as e:
logger.error(f"Error deactivating SimplePluginSystem plugin: {e}")
def SetupShortcutAndHandle(self, currPluginSetting):
"""Set up keyboard shortcuts for a plugin."""
shortcut = ''
# just the modifier
if not currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and not currPluginSetting['altkey']:
shortcut = 'kb:cthulhu+' + currPluginSetting['key']
# cthulhu + alt
if not currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and currPluginSetting['altkey']:
shortcut = 'kb:cthulhu+alt+' + currPluginSetting['key']
# cthulhu + CTRL
if not currPluginSetting['shiftkey'] and currPluginSetting['ctrlkey'] and not currPluginSetting['altkey']:
shortcut = 'kb:cthulhu+control+' + currPluginSetting['key']
# cthulhu + alt + CTRL
if not currPluginSetting['shiftkey'] and currPluginSetting['ctrlkey'] and currPluginSetting['altkey']:
shortcut = 'kb:cthulhu+alt+control+ ' + currPluginSetting['key']
# cthulhu + shift
if currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and not currPluginSetting['altkey']:
shortcut = 'kb:cthulhu+shift+' + currPluginSetting['key']
# alt + shift
if currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and currPluginSetting['altkey']:
shortcut = 'kb:alt+shift+' + currPluginSetting['key']
if shortcut != '':
logger.debug(f"Registering shortcut: {shortcut}")
currPluginSetting['shortcut'] = shortcut
try:
# Try to use the translation function, fall back to plain text if it fails
plugin_name = _(currPluginSetting['pluginname'])
except Exception:
# If translation fails, use the original name
plugin_name = currPluginSetting['pluginname']
logger.warning(f"Translation failed for plugin: {currPluginSetting['pluginname']}")
self.registerGestureByString(currPluginSetting['function'], plugin_name, shortcut)
return currPluginSetting
def id_generator(self, size=7, chars=string.ascii_letters):
"""Generate a random ID string."""
return ''.join(random.choice(chars) for _ in range(size))
def initSettings(self):
"""Initialize default settings for a plugin."""
currPluginSetting = {
'pluginname': '',
'functionname': '',
'key': '',
'shiftkey': False,
'ctrlkey': False,
'altkey': False,
'startnotify': False,
'stopnotify': False,
'blockcall': False,
'error': False,
'exec': False,
'parameters': '',
'function': None,
'inputeventhandler': None,
'valid': False,
'supressoutput': False,
'shortcut': ''
}
return currPluginSetting
def getPluginSettings(self, filepath, currPluginSetting):
"""Parse plugin settings from filename and content."""
try:
currPluginSetting['file'] = filepath
fileName, fileExtension = os.path.splitext(filepath)
if (fileExtension and (fileExtension != '')): # if there is an extension
currPluginSetting['loadable'] = (fileExtension.lower() == '.py') # only python is loadable
filename = os.path.basename(filepath) # filename
filename = os.path.splitext(filename)[0] # remove extension if we have one
# remove pluginname seperated by __-__
filenamehelper = filename.split('__-__')
filename = filenamehelper[len(filenamehelper) - 1]
currPluginSetting['permission'] = os.access(filepath, os.X_OK)
currPluginSetting['pluginname'] = 'NoNameAvailable'
if len(filenamehelper) == 2:
currPluginSetting['pluginname'] = filenamehelper[0]
# now get shortcuts seperated by __+__
filenamehelper = filename.split('__+__')
if len([y for y in filenamehelper if 'parameters_' in y.lower()]) == 1 and\
len([y for y in filenamehelper if 'parameters_' in y.lower()][0]) > 11:
currPluginSetting['parameters'] = [y for y in filenamehelper if 'parameters_' in y.lower()][0][11:]
if len([y for y in filenamehelper if 'key_' in y.lower()]) == 1 and\
len([y for y in filenamehelper if 'key_' in y.lower()][0]) > 4:
currPluginSetting['key'] = [y for y in filenamehelper if 'key_' in y.lower()][0][4]
if currPluginSetting['key'] == '':
settcurrPluginSetting = 'shift' in map(str.lower, filenamehelper)
currPluginSetting['ctrlkey'] = 'control' in map(str.lower, filenamehelper)
currPluginSetting['altkey'] = 'alt' in map(str.lower, filenamehelper)
currPluginSetting['startnotify'] = 'startnotify' in map(str.lower, filenamehelper)
currPluginSetting['stopnotify'] = 'stopnotify' in map(str.lower, filenamehelper)
currPluginSetting['blockcall'] = 'blockcall' in map(str.lower, filenamehelper)
currPluginSetting['error'] = 'error' in map(str.lower, filenamehelper)
currPluginSetting['supressoutput'] = 'supressoutput' in map(str.lower, filenamehelper)
currPluginSetting['exec'] = 'exec' in map(str.lower, filenamehelper)
currPluginSetting['loadmodule'] = 'loadmodule' in map(str.lower, filenamehelper)
currPluginSetting = self.readSettingsFromPlugin(currPluginSetting)
if not currPluginSetting['loadmodule']:
if not currPluginSetting['permission']: # subprocessing only works with exec permission
return self.initSettings()
if currPluginSetting['loadmodule'] and not currPluginSetting['loadable']: # sorry.. its not loadable only .py is loadable
return self.initSettings()
if (len(currPluginSetting['key']) > 1): # no shortcut
if not currPluginSetting['exec']: # and no exec -> the plugin make no sense because it isnt hooked anywhere
return self.initSettings() # so not load it (sets valid = False)
else:
currPluginSetting['key'] = '' # there is a strange key, but exec? ignore the key..
currPluginSetting['valid'] = True # we could load everything
return currPluginSetting
except Exception as e:
logger.error(f"Error getting plugin settings: {e}")
return self.initSettings()
def readSettingsFromPlugin(self, currPluginSetting):
"""Read settings from plugin file content."""
if not os.access(currPluginSetting['file'], os.R_OK):
return currPluginSetting
fileName, fileExtension = os.path.splitext(currPluginSetting['file'])
if (fileExtension and (fileExtension != '')): # if there is an extension
if (fileExtension.lower() != '.py') and \
(fileExtension.lower() != '.sh'):
return currPluginSetting
else:
return currPluginSetting
try:
with open(currPluginSetting['file'], "r") as pluginFile:
for line in pluginFile:
currPluginSetting['shiftkey'] = ('sopsproperty:shift' in line.lower().replace(" ", "")) or currPluginSetting['shiftkey']
currPluginSetting['ctrlkey'] = ('sopsproperty:control' in line.lower().replace(" ", "")) or currPluginSetting['ctrlkey']
currPluginSetting['altkey'] = ('sopsproperty:alt' in line.lower().replace(" ", "")) or currPluginSetting['altkey']
currPluginSetting['startnotify'] = ('sopsproperty:startnotify' in line.lower().replace(" ", "")) or currPluginSetting['startnotify']
currPluginSetting['stopnotify'] = ('sopsproperty:stopnotify' in line.lower().replace(" ", "")) or currPluginSetting['stopnotify']
currPluginSetting['blockcall'] = ('sopsproperty:blockcall' in line.lower().replace(" ", "")) or currPluginSetting['blockcall']
currPluginSetting['error'] = ('sopsproperty:error' in line.lower().replace(" ", "")) or currPluginSetting['error']
currPluginSetting['supressoutput'] = ('sopsproperty:supressoutput' in line.lower().replace(" ", "")) or currPluginSetting['supressoutput']
currPluginSetting['exec'] = ('sopsproperty:exec' in line.lower().replace(" ", "")) or currPluginSetting['exec']
currPluginSetting['loadmodule'] = ('sopsproperty:loadmodule' in line.lower().replace(" ", "")) or currPluginSetting['loadmodule']
except Exception as e:
logger.error(f"Error reading plugin file: {e}")
return currPluginSetting
def buildPluginSubprocess(self, currPluginSetting):
"""Build a function to execute a plugin as a subprocess."""
currplugin = "\'\"" + currPluginSetting['file'] + "\" " + currPluginSetting['parameters'] + "\'"
pluginname = currPluginSetting['pluginname']
if currPluginSetting['blockcall']:
pluginname = "blocking " + pluginname
fun_body = "global " + currPluginSetting['functionname'] + "\n"
fun_body += "def " + currPluginSetting['functionname'] + "(script=None, inputEvent=None):\n"
if currPluginSetting['startnotify']:
fun_body += " outputMessage('start " + pluginname + "')\n"
fun_body += " p = Popen(" + currplugin + ", stdout=PIPE, stderr=PIPE, shell=True)\n"
fun_body += " stdout, stderr = p.communicate()\n"
fun_body += " message = ''\n"
fun_body += " if not " + str(currPluginSetting['supressoutput']) + " and stdout:\n"
fun_body += " message += str(stdout, \"utf-8\")\n"
fun_body += " if " + str(currPluginSetting['error']) + " and stderr:\n"
fun_body += " message += ' error: ' + str(stderr, \"utf-8\")\n"
fun_body += " outputMessage(message)\n"
if currPluginSetting['stopnotify']:
fun_body += " outputMessage('finish " + pluginname + "')\n"
fun_body += " return True\n\n"
fun_body += "global " + currPluginSetting['functionname'] + "T\n"
fun_body += "def " + currPluginSetting['functionname'] + "T(script=None, inputEvent=None):\n"
fun_body += " _thread.start_new_thread(" + currPluginSetting['functionname'] + ",(script, inputEvent))\n\n"
return fun_body
def buildPluginExec(self, currPluginSetting):
"""Build a function to execute a plugin as a Python module."""
pluginname = currPluginSetting['pluginname']
if currPluginSetting['blockcall']:
pluginname = "blocking " + pluginname
fun_body = "global " + currPluginSetting['functionname'] + "\n"
fun_body += "def " + currPluginSetting['functionname'] + "(script=None, inputEvent=None):\n"
if currPluginSetting['startnotify']:
fun_body += " outputMessage('start " + pluginname + "')\n"
fun_body += " try:\n"
fun_body += " spec = importlib.util.spec_from_file_location(\"" + currPluginSetting['functionname'] + "\",\"" + currPluginSetting['file'] + "\")\n"
fun_body += " " + currPluginSetting['functionname'] + "Module = importlib.util.module_from_spec(spec)\n"
fun_body += " spec.loader.exec_module(" + currPluginSetting['functionname'] + "Module)\n"
fun_body += " except Exception as e:\n"
fun_body += " logger.error(f\"Error executing plugin {pluginname}: {e}\")\n"
if currPluginSetting['error']:
fun_body += " outputMessage(\"Error while executing " + pluginname + "\")\n"
if currPluginSetting['stopnotify']:
fun_body += " outputMessage('finish " + pluginname + "')\n"
fun_body += " return True\n\n"
fun_body += "global " + currPluginSetting['functionname'] + "T\n"
fun_body += "def " + currPluginSetting['functionname'] + "T(script=None, inputEvent=None):\n"
fun_body += " _thread.start_new_thread(" + currPluginSetting['functionname'] + ",(script, inputEvent))\n\n"
return fun_body
def getFunctionName(self, currPluginSetting):
"""Generate a unique function name for a plugin."""
currPluginSetting['functionname'] = ''
while currPluginSetting['functionname'] == '' or currPluginSetting['functionname'] + 'T' in globals() or currPluginSetting['functionname'] in globals():
currPluginSetting['functionname'] = self.id_generator()
return currPluginSetting
def registerGestureByString(self, function, description, shortcut):
"""Register a keyboard shortcut for a function.
This is a compatibility wrapper for the new plugin system.
"""
try:
if self.app:
api_helper = self.app.getAPIHelper()
if api_helper:
api_helper.registerGestureByString(
function,
description,
shortcut,
'default',
'cthulhu',
True,
contextName=self.module_name
)
logger.debug(f"Registered shortcut {shortcut} for {description}")
return True
else:
logger.error("Could not get APIHelper")
else:
logger.error("No app reference available")
except Exception as e:
logger.error(f"Error registering shortcut {shortcut}: {e}")
def unregisterShortcut(self, function, shortcut):
"""Unregister a keyboard shortcut for a function.
This is a compatibility wrapper for the new plugin system.
"""
try:
if self.app:
api_helper = self.app.getAPIHelper()
if api_helper and hasattr(api_helper, 'unregisterShortcut'):
api_helper.unregisterShortcut(shortcut)
logger.debug(f"Unregistered shortcut {shortcut}")
return True
else:
logger.error("Could not get APIHelper or unregisterShortcut method")
else:
logger.error("No app reference available")
except Exception as e:
logger.error(f"Error unregistering shortcut {shortcut}: {e}")
def load_plugins(self):
"""Load and setup all plugins in the plugin repository."""
if not self.loaded:
try:
logger.info(f"Loading plugins from {self.plugin_repo}")
plugin_files = glob.glob(self.plugin_repo + '*')
self.plugin_list = [] # Reset the plugin list to avoid confusion
for currplugin in plugin_files:
try:
# Ensure currplugin is a valid path string
if not isinstance(currplugin, (str, bytes, os.PathLike)):
logger.error(f"Invalid plugin path: {type(currplugin)}")
continue
currPluginSetting = self.initSettings()
currPluginSetting = self.getPluginSettings(currplugin, currPluginSetting)
if not currPluginSetting['valid']:
logger.debug(f"Skipping invalid plugin: {currplugin}")
continue
currPluginSetting = self.getFunctionName(currPluginSetting)
if currPluginSetting['loadmodule']:
exec(self.buildPluginExec(currPluginSetting)) # load as python module
else:
exec(self.buildPluginSubprocess(currPluginSetting)) # run as subprocess
if currPluginSetting['blockcall']:
currPluginSetting['function'] = globals()[currPluginSetting['functionname']] # non threaded
else:
currPluginSetting['function'] = globals()[currPluginSetting['functionname'] + "T"] # T = Threaded
if currPluginSetting['exec']: # exec on load if we want
currPluginSetting['function']()
if not currPluginSetting['key'] == '':
currPluginSetting = self.SetupShortcutAndHandle(currPluginSetting)
logger.debug(f"Loaded plugin: {currPluginSetting['pluginname']}")
self.plugin_list.append(currPluginSetting) # store in a list
except Exception as e:
logger.error(f"Error loading plugin {currplugin}: {e}")
self.loaded = True
logger.info(f"Loaded {len(self.plugin_list)} plugins")
except Exception as e:
logger.error(f"Error in load_plugins: {e}")
-7
View File
@@ -1,7 +0,0 @@
cthulhu_python_PYTHON = \
__init__.py \
Time.plugin \
Time.py
cthulhu_pythondir=$(pkgpythondir)/plugins/Time
-6
View File
@@ -1,6 +0,0 @@
[Plugin]
Module=Time
Loader=python3
Name=Time
Description=Present current time
Authors=Chrys chrys@linux-a11y.org
-60
View File
@@ -1,60 +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
import gi, time
gi.require_version('Peas', '1.0')
from gi.repository import GObject
from gi.repository import Peas
from cthulhu import plugin
class Time(GObject.Object, Peas.Activatable, plugin.Plugin):
#__gtype_name__ = 'Time'
object = GObject.Property(type=GObject.Object)
def __init__(self):
plugin.Plugin.__init__(self)
def do_activate(self):
API = self.object
self.connectSignal("setup-inputeventhandlers-completed", self.setupCompatBinding)
def setupCompatBinding(self, app):
cmdnames = app.getDynamicApiManager().getAPI('Cmdnames')
inputEventHandlers = app.getDynamicApiManager().getAPI('inputEventHandlers')
inputEventHandlers['presentTimeHandler'] = app.getAPIHelper().createInputEventHandler(self.presentTime, cmdnames.PRESENT_CURRENT_TIME)
def do_deactivate(self):
API = self.object
inputEventHandlers = API.app.getDynamicApiManager().getAPI('inputEventHandlers')
del inputEventHandlers['presentTimeHandler']
def do_update_state(self):
API = self.object
def presentTime(self, script=None, inputEvent=None):
""" Presents the current time. """
API = self.object
settings_manager = API.app.getDynamicApiManager().getAPI('SettingsManager')
_settingsManager = settings_manager.getManager()
timeFormat = _settingsManager.getSetting('presentTimeFormat')
message = time.strftime(timeFormat, time.localtime())
API.app.getDynamicApiManager().getAPI('CthulhuState').activeScript.presentMessage(message, resetStyles=False)
return True
-25
View File
@@ -1,25 +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
@@ -20,6 +20,7 @@ class HelloWorld(Plugin):
def __init__(self, *args, **kwargs):
"""Initialize the plugin."""
super().__init__(*args, **kwargs)
print("Plugin hello world initialized.")
logger.info("HelloWorld plugin initialized")
@cthulhu_hookimpl
-5
View File
@@ -1,9 +1,6 @@
#!/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
@@ -20,8 +17,6 @@
# 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
"""Self Voice plugin for Cthulhu screen reader."""
+7
View File
@@ -318,6 +318,13 @@ class ScriptManager:
return
newScript.activate()
# Register plugin keybindings with the new active script
from . import cthulhu
plugin_manager = cthulhu.cthulhuApp.getPluginSystemManager()
if plugin_manager:
pass # plugin_manager.register_plugin_keybindings_with_active_script()
tokens = ["SCRIPT MANAGER: Setting active script to", newScript, "reason:", reason]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
+37
View File
@@ -363,6 +363,8 @@ class Script(script.Script):
return keyBindings
def getExtensionBindings(self):
with open('/tmp/extension_bindings_debug.log', 'a') as f:
f.write(f"=== getExtensionBindings() called ===\n")
keyBindings = keybindings.KeyBindings()
bindings = self.notificationPresenter.get_bindings()
@@ -407,6 +409,41 @@ class Script(script.Script):
for keyBinding in bindings.keyBindings:
keyBindings.add(keyBinding)
# Add plugin keybindings from APIHelper storage
try:
import cthulhu.cthulhu as cthulhu
if hasattr(cthulhu, 'cthulhuApp') and cthulhu.cthulhuApp:
api_helper = cthulhu.cthulhuApp.getAPIHelper()
with open('/tmp/extension_bindings_debug.log', 'a') as f:
f.write(f"=== Checking for plugin bindings ===\n")
f.write(f"api_helper exists: {api_helper is not None}\n")
if api_helper:
f.write(f"api_helper has _gestureBindings: {hasattr(api_helper, '_gestureBindings')}\n")
if hasattr(api_helper, '_gestureBindings'):
f.write(f"_gestureBindings content: {api_helper._gestureBindings}\n")
f.write(f"Available contexts: {list(api_helper._gestureBindings.keys())}\n")
if api_helper and hasattr(api_helper, '_gestureBindings'):
with open('/tmp/extension_bindings_debug.log', 'a') as f:
f.write(f"=== Adding plugin bindings in getExtensionBindings() ===\n")
for context_name, context_bindings in api_helper._gestureBindings.items():
for binding in context_bindings:
keyBindings.add(binding)
with open('/tmp/extension_bindings_debug.log', 'a') as f:
f.write(f"Added plugin binding: {binding.keysymstring} modifiers={binding.modifiers} desc={binding.handler.description}\n")
else:
with open('/tmp/extension_bindings_debug.log', 'a') as f:
f.write(f"=== No plugin bindings available ===\n")
else:
with open('/tmp/extension_bindings_debug.log', 'a') as f:
f.write(f"=== cthulhuApp not available ===\n")
except Exception as e:
import cthulhu.debug as debug
debug.printMessage(debug.LEVEL_WARNING, f"Failed to add plugin bindings: {e}", True)
with open('/tmp/extension_bindings_debug.log', 'a') as f:
f.write(f"Exception in plugin binding addition: {e}\n")
return keyBindings
def getKeyBindings(self):
+1 -1
View File
@@ -413,4 +413,4 @@ presentChatRoomLast = False
presentLiveRegionFromInactiveTab = False
# Plugins
activePlugins = ['Clipboard', 'DisplayVersion', 'MouseReview', 'Date', 'ByeCthulhu', 'Time', 'HelloCthulhu', 'hello_world', 'self_voice', 'PluginManager', 'SimplePluginSystem']
activePlugins = ['Clipboard', 'DisplayVersion', 'ByeCthulhu', 'HelloCthulhu', 'hello_world', 'self_voice', 'SimplePluginSystem']