200 lines
6.7 KiB
Python
200 lines
6.7 KiB
Python
# Terminator by Chris Jones <cmsj@tenshu.net>
|
|
# GPL v2 only
|
|
"""plugin.py - Base plugin system
|
|
Inspired by Armin Ronacher's post at
|
|
http://lucumr.pocoo.org/2006/7/3/python-plugin-system
|
|
Used with permission (the code in that post is to be
|
|
considered BSD licenced, per the authors wishes)
|
|
|
|
>>> registry = PluginRegistry()
|
|
>>> isinstance(registry.instances, dict)
|
|
True
|
|
>>> registry.enable('TestPlugin')
|
|
>>> registry.load_plugins()
|
|
>>> plugins = registry.get_plugins_by_capability('test')
|
|
>>> len(plugins)
|
|
1
|
|
>>> plugins[0] #doctest: +ELLIPSIS
|
|
<testplugin.TestPlugin object at 0x...>
|
|
>>> registry.get_plugins_by_capability('this_should_not_ever_exist')
|
|
[]
|
|
>>> plugins[0].do_test()
|
|
'TestPluginWin'
|
|
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
from . import borg
|
|
from .config import Config
|
|
from .util import dbg, err, get_config_dir
|
|
from .terminator import Terminator
|
|
|
|
class Plugin(object):
|
|
"""Definition of our base plugin class"""
|
|
capabilities = None
|
|
|
|
def __init__(self):
|
|
"""Class initialiser."""
|
|
pass
|
|
|
|
def unload(self):
|
|
"""Prepare to be unloaded"""
|
|
pass
|
|
|
|
class PluginRegistry(borg.Borg):
|
|
"""Definition of a class to store plugin instances"""
|
|
available_plugins = None
|
|
instances = None
|
|
path = None
|
|
done = None
|
|
|
|
def __init__(self):
|
|
"""Class initialiser"""
|
|
borg.Borg.__init__(self, self.__class__.__name__)
|
|
self.prepare_attributes()
|
|
|
|
def prepare_attributes(self):
|
|
"""Prepare our attributes"""
|
|
if not self.instances:
|
|
self.instances = {}
|
|
if not self.path:
|
|
self.path = []
|
|
(head, _tail) = os.path.split(borg.__file__)
|
|
self.path.append(os.path.join(head, 'plugins'))
|
|
self.path.append(os.path.join(get_config_dir(), 'plugins'))
|
|
dbg('Plugin path: %s' % self.path)
|
|
if not self.done:
|
|
self.done = False
|
|
if not self.available_plugins:
|
|
self.available_plugins = {}
|
|
|
|
def load_plugins(self, force=False):
|
|
"""Load all plugins present in the plugins/ directory in our module"""
|
|
if self.done and (not force):
|
|
dbg('Already loaded')
|
|
return
|
|
|
|
dbg('loading plugins, force:(%s)' % force)
|
|
|
|
config = Config()
|
|
|
|
for plugindir in self.path:
|
|
sys.path.insert(0, plugindir)
|
|
try:
|
|
files = os.listdir(plugindir)
|
|
except OSError:
|
|
sys.path.remove(plugindir)
|
|
continue
|
|
for plugin in files:
|
|
if plugin == '__init__.py':
|
|
continue
|
|
pluginpath = os.path.join(plugindir, plugin)
|
|
if os.path.isfile(pluginpath) and plugin[-3:] == '.py':
|
|
dbg('Importing plugin %s' % plugin)
|
|
try:
|
|
module = __import__(plugin[:-3], None, None, [''])
|
|
for item in getattr(module, 'AVAILABLE'):
|
|
func = getattr(module, item)
|
|
if item not in list(self.available_plugins.keys()):
|
|
self.available_plugins[item] = func
|
|
|
|
if item not in config['enabled_plugins']:
|
|
dbg('plugin %s not enabled, skipping' % item)
|
|
continue
|
|
if item not in self.instances:
|
|
self.instances[item] = func()
|
|
elif force:
|
|
#instead of multiple copies of loaded
|
|
#plugin objects, unload where plugins
|
|
#can clean up and then re-init so there
|
|
#is one plugin object
|
|
self.instances[item].unload()
|
|
self.instances.pop(item, None)
|
|
self.instances[item] = func()
|
|
except Exception as ex:
|
|
err('PluginRegistry::load_plugins: Importing plugin %s \
|
|
failed: %s' % (plugin, ex))
|
|
|
|
self.done = True
|
|
|
|
def get_plugins_by_capability(self, capability):
|
|
"""Return a list of plugins with a particular capability"""
|
|
result = []
|
|
dbg('searching %d plugins \
|
|
for %s' % (len(self.instances), capability))
|
|
for plugin in self.instances:
|
|
if capability in self.instances[plugin].capabilities:
|
|
result.append(self.instances[plugin])
|
|
return result
|
|
|
|
def get_all_plugins(self):
|
|
"""Return all plugins"""
|
|
return(self.instances)
|
|
|
|
def get_available_plugins(self):
|
|
"""Return a list of all available plugins whether they are enabled or
|
|
disabled"""
|
|
return(list(self.available_plugins.keys()))
|
|
|
|
def is_enabled(self, plugin):
|
|
"""Return a boolean value indicating whether a plugin is enabled or
|
|
not"""
|
|
return(plugin in self.instances)
|
|
|
|
def enable(self, plugin):
|
|
"""Enable a plugin"""
|
|
if plugin in self.instances:
|
|
err("Cannot enable plugin %s, already enabled" % plugin)
|
|
dbg("Enabling %s" % plugin)
|
|
self.instances[plugin] = self.available_plugins[plugin]()
|
|
|
|
def disable(self, plugin):
|
|
"""Disable a plugin"""
|
|
dbg("Disabling %s" % plugin)
|
|
self.instances[plugin].unload()
|
|
del(self.instances[plugin])
|
|
|
|
# This is where we should define a base class for each type of plugin we
|
|
# support
|
|
|
|
# URLHandler - This adds a regex match to the Terminal widget and provides a
|
|
# callback to turn that into a URL.
|
|
class URLHandler(Plugin):
|
|
"""Base class for URL handlers"""
|
|
capabilities = ['url_handler']
|
|
handler_name = None
|
|
match = None
|
|
nameopen = None
|
|
namecopy = None
|
|
|
|
def __init__(self):
|
|
"""Class initialiser"""
|
|
Plugin.__init__(self)
|
|
terminator = Terminator()
|
|
for terminal in terminator.terminals:
|
|
terminal.match_add(self.handler_name, self.match)
|
|
|
|
def callback(self, url):
|
|
"""Callback to transform the enclosed URL"""
|
|
raise NotImplementedError
|
|
|
|
def unload(self):
|
|
"""Handle being removed"""
|
|
if not self.handler_name:
|
|
err('unload called without self.handler_name being set')
|
|
return
|
|
terminator = Terminator()
|
|
for terminal in terminator.terminals:
|
|
terminal.match_remove(self.handler_name)
|
|
|
|
# MenuItem - This is able to execute code during the construction of the
|
|
# context menu of a Terminal.
|
|
class MenuItem(Plugin):
|
|
"""Base class for menu items"""
|
|
capabilities = ['terminal_menu']
|
|
|
|
def callback(self, menuitems, menu, terminal):
|
|
"""Callback to transform the enclosed URL"""
|
|
raise NotImplementedError
|