9

Writing Terminator plugins

Posted on Sunday, 18 April 2010

Terminator Plugin HOWTO


One of the features of the new 0.9x series of Terminator releases that hasn't had a huge amount of announcement/discussion yet is the plugin system. I've posted previously about the decisions that went into the design of the plugin framework, but I figured now would be a good time to look at how to actually take advantage of it.

While the plugin system is really generic, so far there are only two points in the Terminator code that actually look for plugins - the Terminal context menu and the default URL opening code. If you find you'd like to write a plugin that interacts with a different part of Terminator, please let me know, I'd love to see some clever uses of plugins and I definitely want to expand the number of points that plugins can hook into.

The basics of a plugin


A plugin is a class in a .py file in terminatorlib/plugins or ~/.config/terminator/plugins, but not all classes are automatically treated as plugins. Terminator will examine each of the .py files it finds for a list called 'available' and it will load each of the classes mentioned therein.

Additionally, it would be a good idea to import terminatorlib.plugin as that contains the base classes that other plugins should be derived from.

A quick example:
import terminatorlib.plugin as plugin
available = ['myfirstplugin']
class myfirstplugin(plugin.SomeBasePluginClass):
etc.

So now let's move on to the simplest type of plugin currently available in Terminator, a URL handler.

URL Handlers


This type of plugin adds new regular expressions to match text in the terminal that should be handled as URLs. We ship an example of this with Terminator, it's a handler that adds support for the commonly used format for Launchpad. Ignoring the comments and the basics above, this is ultimately all it is:
class LaunchpadBugURLHandler(plugin.URLHandler):
  capabilities = ['url_handler']
  handler_name = 'launchpad_bug'
  match = '\\b(lp|LP):?\s?#?[0-9]+(,\s*#?[0-9]+)*\\b'

  def callback(self, url):
    for item in re.findall(r'[0-9]+', url):
      return('https://bugs.launchpad.net/bugs/%s' % item)

That's it! Let's break it down a little to see the important things here:

  • inherit from plugin.URLHandler if you want to handle URLs.

  • include 'url_handler' in your capabilities list

  • URL handlers must specify a unique handler_name (no enforcement of uniqueness is performed by Terminator, so use some common sense with the namespace)

  • Terminator will call a method in your class called callback() and pass it the text that was matched. You must return a valid URL which will probably be based on this text.


and that's all there is to it really. Next time you start terminator you should find the pattern you added gets handled as a URL!

Context menu items


This type of plugin is a little more involved, but not a huge amount and as with URLHandler we ship an example in terminatorlib/plugins/custom_commands.py which is a plugin that allows users to add custom commands to be sent to the terminal when selected. This also brings a second aspect of making more complex plugins - storing configuration. Terminator's shiny new configuration system (based on the excellent ConfigObj) exposes some API for plugins to use for loading and storing their configuration. The nuts and bolts here are:
import terminatorlib.plugin as plugin
from terminatorlib.config import Config
available = ['CustomCommandsMenu']
class CustomCommandsMenu(plugin.MenuItem):
capabilities = ['terminal_menu']
config = None
def __init__(self):
self.config = Config()
myconfig = self.config.plugin_get_config(self.__class__.__name__)
# Now extract valid data from sections{}
def callback(self, menuitems, menu, terminal):
menuitems.append(gtk.MenuItem('some jazz'))

This is a pretty simplified example, but it's sufficient to insert a menu item that says "some jazz". I'm not going to go into the detail of hooking up a handler to the 'activate' event of the MenuItem or other PyGTK mechanics, but this gives you the basic detail. The method that Terminator will call from your class is again "callback()" and you get passed a list you should add your menu structure to, along with references to the main menu object and the related Terminal. As the plugin system expands and matures I'd like to be more formal about the API that plugins should expect to be able to rely on, rather than having them poke around inside classes like Config and Terminal. Suggestions are welcome :)

Regarding the configuration storage API - the value returned by Config.plugin_get_config() is just a dict, it's whatever is currently configured for your plugin's name in the Terminator config file. There's no validation of this data, so you should pay attention to it containing valid data. You can then set whatever you want in this dict and pass it to Config().plugin_set_config() with the name of your class and then call Config().save() to flush this out to disk (I recommend that you be quite liberal about calling save()).

Wrap up


Right now that's all there is to it. Please get in touch if you have any suggestions or questions - I'd love to ship more plugins with Terminator itself, and I can think of some great ideas. Probably the most useful thing would be something to help customise Terminator for heavy ssh users (see the earlier fork of Terminator called 'ssherminator')

Discussion

  1. Hi,

    Great Job with Terminator, I'm working with it everyday. I'd love to see some functionalities that might be implemented as plugins :
    -Local Command Aliases :
    since I ssh to hundreds of different servers I can't deploy my aliases and personal settings on each of these, having terminator dynamicaly alias some commands for me would be nice, for exemple, I'd like to be able to type : "lh" on my terminator window and have it translated to "ls -lh --color=auto", and be able to define locally many aliases like this ...
    also, local syntax highlighting would be very nice as well as "title bar color blink" as error beep replacement ...

    regards

    ReplyDelete
  2. Hi, Just wrote my first plugin to search google for the selected text. So thought I would add a shameless plug here if that is okay.

    http://choffee.posterous.com/terminator-plugin-to-search-on-google-for-sel
    https://github.com/choffee/terminator-plugins/blob/master/searchplugin.py

    Would welcome any feedback.

    ReplyDelete
  3. John: It's absolutely acceptable to do that! The plugin seems reasonable for what it is - any interest in getting it into the main tree?

    ReplyDelete
  4. hi

    is there a way to create a plugin that applies a regexp on all of the output? I would like to colorize some of the logging or gcc output based on the lines (e.g. orange for warnings and red for errors).

    or is there already a plugin for terminator that does something similar?
    that would be nice!

    thanks anyway, I love terminator.

    ReplyDelete
  5. About Dialog (Show Version Number) was missing, so it was a good opportunity to use the plugin system for that

    import gtk
    import terminatorlib.plugin as plugin
    from terminatorlib.version import APP_VERSION

    # Written by xxx
    # Copyright 2011 xxx

    AVAILABLE = [ 'ShowVersion' ]

    class ShowVersion(plugin.Plugin):
        capabilities = ['terminal_menu']

        def _show_version(self, searchMenu):
            dlg = gtk.AboutDialog()
            dlg.set_program_name("Terminator")
            dlg.set_version(APP_VERSION)
            ret = dlg.run()
            dlg.destroy()

        def callback(self, menuitems, menu, terminal):
            self.terminal = terminal
            item = gtk.ImageMenuItem(gtk.STOCK_ABOUT)
            item.set_label("Show Version")
            item.connect('activate', self._show_version)
            menuitems.append(item)

    ReplyDelete
  6. The URLHandler only works for the "Open Link" context, but when you "Copy address" you get the match. Is there a way to make the plugin return the url when you "Copy Address"?

    ReplyDelete
  7. Hi. I would like to write a plugin that would pop up a tooltip showing an image when I hover a url. I deal with a lot of image urls in log files etc at work and I'd prefer not to have to right click and open them in the browser to see them. Is there way to get my callback invoked when I hover over some text with the mouse?

    Great terminal btw.

    ReplyDelete
  8. Here's some hacked up handler that allows me to click on /path/to/file:123 or /path/to/file(123) paths in compilation output and open then in SublimeText at the right line:

    https://gist.github.com/3912465

    I'm posting it since it might inspire some people, though it's pretty hackish:
    1. There is no way to know from which working directory a process outputted a relative path, the editor macro does additional magic by walking the paths from already opened files.
    2. There is no way to pass the path to the macro from the command line with SublimeText, so it uses the clipboard.

    In any case I find it pretty useful :)

    ReplyDelete