diff --git a/configure.ac b/configure.ac index b3356e3..6f76e83 100644 --- a/configure.ac +++ b/configure.ac @@ -132,7 +132,6 @@ src/cthulhu/plugins/Clipboard/Makefile src/cthulhu/plugins/DisplayVersion/Makefile src/cthulhu/plugins/hello_world/Makefile src/cthulhu/plugins/self_voice/Makefile -src/cthulhu/plugins/Time/Makefile src/cthulhu/plugins/SimplePluginSystem/Makefile src/cthulhu/backends/Makefile src/cthulhu/cthulhu_bin.py diff --git a/src/cthulhu/plugins/Makefile.am b/src/cthulhu/plugins/Makefile.am index b90e9d5..9066cb3 100644 --- a/src/cthulhu/plugins/Makefile.am +++ b/src/cthulhu/plugins/Makefile.am @@ -1,4 +1,4 @@ -SUBDIRS = Clipboard DisplayVersion hello_world self_voice Time ByeCthulhu HelloCthulhu PluginManager SimplePluginSystem +SUBDIRS = Clipboard DisplayVersion hello_world self_voice ByeCthulhu HelloCthulhu PluginManager SimplePluginSystem cthulhu_pythondir=$(pkgpythondir)/plugins diff --git a/src/cthulhu/plugins/SimplePluginSystem/Makefile.am b/src/cthulhu/plugins/SimplePluginSystem/Makefile.am index 6bd1d0a..5f64b8d 100644 --- a/src/cthulhu/plugins/SimplePluginSystem/Makefile.am +++ b/src/cthulhu/plugins/SimplePluginSystem/Makefile.am @@ -1,7 +1,7 @@ cthulhu_python_PYTHON = \ __init__.py \ - SimplePluginSystem.plugin \ - SimplePluginSystem.py + plugin.info \ + plugin.py cthulhu_pythondir=$(pkgpythondir)/plugins/SimplePluginSystem diff --git a/src/cthulhu/plugins/SimplePluginSystem/SimplePluginSystem.plugin b/src/cthulhu/plugins/SimplePluginSystem/SimplePluginSystem.plugin deleted file mode 100644 index 60dde6d..0000000 --- a/src/cthulhu/plugins/SimplePluginSystem/SimplePluginSystem.plugin +++ /dev/null @@ -1,11 +0,0 @@ -[Plugin] -Module=SimplePluginSystem -Loader=python3 -Name=Simple Plugin System -Description=Simple plugin system implementation for Cthulhu -Authors=Chrys ;Storm Dragon -Copyright=Copyright Â2024 Chrys, Storm Dragon -Website=https://git.stormux.org/storm/cthulhu -Version=1.0 -Builtin=true - diff --git a/src/cthulhu/plugins/SimplePluginSystem/SimplePluginSystem.py b/src/cthulhu/plugins/SimplePluginSystem/SimplePluginSystem.py deleted file mode 100644 index 5fa6b64..0000000 --- a/src/cthulhu/plugins/SimplePluginSystem/SimplePluginSystem.py +++ /dev/null @@ -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 diff --git a/src/cthulhu/plugins/SimplePluginSystem/plugin.info b/src/cthulhu/plugins/SimplePluginSystem/plugin.info new file mode 100644 index 0000000..cd4aad3 --- /dev/null +++ b/src/cthulhu/plugins/SimplePluginSystem/plugin.info @@ -0,0 +1,9 @@ +[Plugin] +Name = Simple Plugin System +Module = SimplePluginSystem +Description = Simple plugin system implementation for Cthulhu +Authors = Storm Dragon +Copyright = Copyright (c) 2025 Stormux +Website = https://git.stormux.org/storm/cthulhu +Version = 1.0 +Category = System diff --git a/src/cthulhu/plugins/SimplePluginSystem/plugin.py b/src/cthulhu/plugins/SimplePluginSystem/plugin.py new file mode 100644 index 0000000..f53c663 --- /dev/null +++ b/src/cthulhu/plugins/SimplePluginSystem/plugin.py @@ -0,0 +1,343 @@ +#!/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 + +from cthulhu.plugin import Plugin, cthulhu_hookimpl + +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 + self.registerGestureByString(currPluginSetting['function'], _(currPluginSetting['pluginname']), 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 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}") + self.plugin_list = glob.glob(self.plugin_repo + '*') + + for currplugin in self.plugin_list: + try: + 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}") diff --git a/src/cthulhu/plugins/Time/Makefile.am b/src/cthulhu/plugins/Time/Makefile.am deleted file mode 100644 index e729ddc..0000000 --- a/src/cthulhu/plugins/Time/Makefile.am +++ /dev/null @@ -1,7 +0,0 @@ -cthulhu_python_PYTHON = \ - __init__.py \ - Time.plugin \ - Time.py - -cthulhu_pythondir=$(pkgpythondir)/plugins/Time - diff --git a/src/cthulhu/plugins/Time/Time.plugin b/src/cthulhu/plugins/Time/Time.plugin deleted file mode 100644 index 2f290b5..0000000 --- a/src/cthulhu/plugins/Time/Time.plugin +++ /dev/null @@ -1,6 +0,0 @@ -[Plugin] -Module=Time -Loader=python3 -Name=Time -Description=Present current time -Authors=Chrys chrys@linux-a11y.org diff --git a/src/cthulhu/plugins/Time/Time.py b/src/cthulhu/plugins/Time/Time.py deleted file mode 100644 index 02114c2..0000000 --- a/src/cthulhu/plugins/Time/Time.py +++ /dev/null @@ -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 diff --git a/src/cthulhu/plugins/Time/__init__.py b/src/cthulhu/plugins/Time/__init__.py deleted file mode 100644 index 782103c..0000000 --- a/src/cthulhu/plugins/Time/__init__.py +++ /dev/null @@ -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 -