diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/core.py | 44 | ||||
-rw-r--r-- | src/data_forms.py | 21 | ||||
-rw-r--r-- | src/plugin.py | 54 | ||||
-rw-r--r-- | src/plugin_manager.py | 116 |
4 files changed, 234 insertions, 1 deletions
diff --git a/src/core.py b/src/core.py index 766be909..0e09cf04 100644 --- a/src/core.py +++ b/src/core.py @@ -36,6 +36,8 @@ import windows import connection import timed_events +from plugin_manager import PluginManager + from data_forms import DataFormsTab from config import config, options from logger import logger @@ -102,6 +104,7 @@ class Core(object): # a completion function, taking a Input as argument. Can be None) # The completion function should return True if a completion was # made ; False otherwise + self.plugin_manager = PluginManager(self) self.commands = { 'help': (self.command_help, '\_o< KOIN KOIN KOIN', self.completion_help), 'join': (self.command_join, _("Usage: /join [room_name][@server][/nick] [password]\nJoin: Join the specified room. You can specify a nickname after a slash (/). If no nickname is specified, you will use the default_nick in the configuration file. You can omit the room name: you will then join the room you\'re looking at (useful if you were kicked). You can also provide a room_name without specifying a server, the server of the room you're currently in will be used. You can also provide a password to join the room.\nExamples:\n/join room@server.tld\n/join room@server.tld/John\n/join room2\n/join /me_again\n/join\n/join room@server.tld/my_nick password\n/join / password"), self.completion_join), @@ -125,7 +128,8 @@ class Core(object): 'connect': (self.command_reconnect, _('Usage: /connect\nConnect: disconnect from the remote server if you are currently connected and then connect to it again'), None), 'server_cycle': (self.command_server_cycle, _('Usage: /server_cycle [domain] [message]\nServer Cycle: disconnect and reconnects in all the rooms in domain.'), None), 'bind': (self.command_bind, _('Usage: /bind <key> <equ>\nBind: bind a key to an other key or to a “command”. For example "/bind ^H KEY_UP" makes Control + h do the same same than the Up key.'), None), -# nope 'pubsub': (self.command_pubsub, _('Usage: /pubsub <domain>\nPubsub: Open a pubsub browser on the given domain'), None), + 'load': (self.command_load, _('Usage: /load <plugin>\nLoad: Load the specified plugin'), self.plugin_manager.completion_load), + 'unload': (self.command_unload, _('Usage: /unload <plugin>\nUnload: Unload the specified plugin'), self.plugin_manager.completion_unload), } self.key_func = { @@ -170,6 +174,12 @@ class Core(object): self.xmpp.add_event_handler("chatstate_inactive", self.on_chatstate_inactive) self.timed_events = set() + self.autoload_plugins() + + def autoload_plugins(self): + plugins = config.get('plugins_autoload', '') + for plugin in plugins.split(): + self.plugin_manager.load(plugin) def coucou(self): self.command_pubsub('pubsub.louiz.org') @@ -1124,6 +1134,28 @@ class Core(object): def completion_status(self, the_input): return the_input.auto_completion([status for status in possible_show], ' ') + def command_load(self, arg): + """ + /load <plugin> + """ + args = arg.split() + if len(args) != 1: + self.command_help('load') + return + filename = args[0] + self.plugin_manager.load(filename) + + def command_unload(self, arg): + """ + /unload <plugin> + """ + args = arg.split() + if len(args) != 1: + self.command_help('unload') + return + filename = args[0] + self.plugin_manager.unload(filename) + def command_message(self, arg): """ /message <jid> [message] @@ -1604,3 +1636,13 @@ class Core(object): if not self.running or self.background is True: return curses.doupdate() + + def send_message(self, msg): + """ + Function to use in plugins to send a message in the current conversation. + Returns False if the current tab is not a conversation tab + """ + if not isinstance(self.current_tab(), tabs.ChatTab): + return False + self.current_tab().command_say(msg) + return True diff --git a/src/data_forms.py b/src/data_forms.py index 873aef85..99d08caa 100644 --- a/src/data_forms.py +++ b/src/data_forms.py @@ -152,6 +152,26 @@ class DummyInput(FieldInput, windows.Win): def is_dummy(self): return True +class ColoredLabel(windows.Win): + def __init__(self, text): + self.text = text + self.color = 14 + windows.Win.__init__(self) + + def resize(self, height, width, y, x): + self._resize(height, width, y, x) + + def set_color(self, color): + self.color = color + self.refresh() + + def refresh(self): + with g_lock: + self._win.attron(curses.color_pair(self.color)) + self.addstr(0, 0, self.text) + self._win.attroff(curses.color_pair(self.color)) + self._refresh() + class BooleanWin(FieldInput, windows.Win): def __init__(self, field): FieldInput.__init__(self, field) @@ -502,6 +522,7 @@ class FormWin(object): for i, inp in enumerate(self.inputs): if i >= self.height: break + inp['label'].refresh() inp['input'].refresh() inp['label'].refresh() if self.current_input < self.height-1: diff --git a/src/plugin.py b/src/plugin.py new file mode 100644 index 00000000..d332ca01 --- /dev/null +++ b/src/plugin.py @@ -0,0 +1,54 @@ +import os +from configparser import ConfigParser + +class PluginConfig(ConfigParser): + def __init__(self, filename): + ConfigParser.__init__(self) + self.__config_file__ = filename + self.read() + + def read(self): + """Read the config file""" + ConfigParser.read(self, self.__config_file__) + + def write(self): + """Write the config to the disk""" + try: + fp = open(self.__config_file__, 'w') + ConfigParser.write(self, fp) + fp.close() + return True + except IOError: + return False + +class BasePlugin(object): + """ + Class that all plugins derive from. Any methods beginning with command_ + are interpreted as a command and beginning with on_ are interpreted as + event handlers + """ + + def __init__(self, plugin_manager, core, plugins_conf_dir): + self.core = core + self.plugin_manager = plugin_manager + conf = os.path.join(plugins_conf_dir, self.__module__+'.cfg') + self.config = PluginConfig(conf) + self.init() + + def init(self): + pass + + def cleanup(self): + pass + + def unload(self): + self.cleanup() + + def add_command(self, name, handler, help, completion=None): + return self.plugin_manager.add_command(self.__module__, name, handler, help, completion) + + def add_event_handler(self, event_name, handler): + return self.plugin_manager.add_event_handler(self.__module__, event_name, handler) + + def del_event_handler(self, event_name, handler): + return self.plugin_manager.del_event_handler(self.__module__, event_name, handler) diff --git a/src/plugin_manager.py b/src/plugin_manager.py new file mode 100644 index 00000000..df96e9ab --- /dev/null +++ b/src/plugin_manager.py @@ -0,0 +1,116 @@ +import imp +import os +import sys +from config import config +from gettext import gettext as _ + +plugins_dir = config.get('plugins_dir', '') +plugins_dir = plugins_dir or\ + os.path.join(os.environ.get('XDG_DATA_HOME') or\ + os.path.join(os.environ.get('HOME'), '.local', 'share'), + 'poezio', 'plugins') + +config_home = os.environ.get("XDG_CONFIG_HOME") +if not config_home: + config_home = os.path.join(os.environ.get('HOME'), '.config') +plugins_conf_dir = os.path.join(config_home, 'poezio', 'plugins') + +try: + os.makedirs(plugins_dir) +except OSError: + pass + +try: + os.makedirs(plugins_conf_dir) +except OSError: + pass + +sys.path.append(plugins_dir) + +class PluginManager(object): + def __init__(self, core): + self.core = core + self.modules = {} # module name -> module object + self.plugins = {} # module name -> plugin object + self.commands = {} # module name -> dict of commands loaded for the module + self.event_handlers = {} # module name -> list of event_name/handler pairs loaded for the module + + def load(self, name): + if name in self.plugins: + self.unload(name) + + try: + if name in self.modules: + imp.acquire_lock() + module = imp.reload(self.modules[name]) + imp.release_lock() + else: + file, filename, info = imp.find_module(name, [plugins_dir]) + imp.acquire_lock() + module = imp.load_module(name, file, filename, info) + imp.release_lock() + except Exception as e: + import traceback + self.core.information(_("Could not load plugin: ") + traceback.format_exc(), 'Error') + return + finally: + if imp.lock_held(): + imp.release_lock() + + self.modules[name] = module + self.commands[name] = {} + self.event_handlers[name] = [] + self.plugins[name] = module.Plugin(self, self.core, plugins_conf_dir) + + def unload(self, name): + if name in self.plugins: + try: + for command in self.commands[name].keys(): + del self.core.commands[command] + for event_name, handler in self.event_handlers[name]: + self.core.xmpp.del_event_handler(event_name, handler) + + self.plugins[name].unload() + del self.plugins[name] + del self.commands[name] + del self.event_handlers[name] + except Exception as e: + import traceback + self.core.information(_("Could not unload plugin (may not be safe to try again): ") + traceback.format_exc()) + + def add_command(self, module_name, name, handler, help, completion=None): + if name in self.core.commands: + raise Exception(_("Command '%s' already exists") % (name,)) + + commands = self.commands[module_name] + commands[name] = (handler, help, completion) + self.core.commands[name] = (handler, help, completion) + + def add_event_handler(self, module_name, event_name, handler): + eh = self.event_handlers[module_name] + eh.append((event_name, handler)) + self.core.xmpp.add_event_handler(event_name, handler) + + def del_event_handler(self, module_name, event_name, handler): + self.core.xmpp.del_event_handler(event_name, handler) + eh = self.event_handlers[module_name] + eh = list(filter(lambda e : e != (event_name, handler), eh)) + + def completion_load(self, the_input): + """ + completion function that completes the name of the plugins, from + all .py files in plugins_dir + """ + try: + names = os.listdir(plugins_dir) + except OSError as e: + self.core.information(_('Completion failed: %s' % e), 'Error') + return + plugins_files = [name[:-3] for name in names if name.endswith('.py')] + return the_input.auto_completion(plugins_files, '') + + def completion_unload(self, the_input): + """ + completion function that completes the name of the plugins that are loaded + """ + return the_input.auto_completion(list(self.plugins.keys()), '') |