Creating a Plugin

Nate Parrott edited this page Jun 10, 2015 · 23 revisions

This is a basic walkthrough for creating a plugin. We'll create a simple say plugin, which wraps the OS X command-line say command to speak whatever text the user gives us.

If you're writing a plugin that's functionally similar to an existing plugin (like searching a website), you may instead want to try copying and modifying an existing plugin (like websearch). Find these in PluginDirectories/1.

Download the tool

Flashlight Tool is a tool for testing and debugging Flashlight plugins. It's not perfect, but it's the best way to debug your Python or Javascript.

What makes a plugin

A plugin needs a couple things, which we'll explain below:

  • a .bundle directory, containing:
  • an info.json
  • an examples.txt
  • a plugin.py
  • an Icon.png (optional)

Creating the bundle

All plugins live in ~/Library/FlashlightPlugins. To begin, create a folder and name it say.bundle. OS X will immediately give it a fancy lego-brick icon. Right-click and Show Package Contents.

info.json

The first thing we need is an info.json file. Here's what to put in it:

{
    "name": "say", // this must be the same as your folder name
    "displayName": "Say Something",
    "description": "Speaks your input text",
    "examples": ["say hello world", "say good morning"], // these appear by your plugin's description
    "categories": ["Utilities"],
    "creator_name": "Your name",
    "creator_url": "A link of your choosing"
}

(Remember to remove the inline comments, or else the JSON won't validate and the plugin will show up blank.)

Once that's there, you should be able to open up the Flashlight Installed list and see your plugin. It doesn't do anything yet.

examples.txt

We need to give Flashlight some examples of commands that should invoke your plugin. Create an examples.txt file.

If this were a simple plugin like shutdown, which shuts down your computer, the examples would the things like shutdown, shut down, and turn off my computer. Our plugin, though, takes text input (the text to speak), so we need to add a field — a slot that Flashlight will fill with text, and pass on to us. That looks like this:

say ~message(Good Morning)
speak ~message(Hello, world)
please speak ~message(what's up) out loud

As you can imagine, Flashlight will recognize phrases like speak good morning out loud and pass good morning on to our plugin.

If you have more complex queries you want to process, you should learn more about the parser and examples.txt.

plugin.py

When someone types a command like speak good morning (but before they press enter), our plugin.py file will be loaded, and the results function is going to be invoked. It'll pass in a dictionary containing all our fields — in this case, just ~message, which will be set to good morning. It'll also pass the entire original query if we need it.

All we need to do is return a dictionary containing information about what should appear in Spotlight.

def results(fields, original_query):
    message = fields['~message']
    return {
        "title": "Say '{0}'".format(message),
        "run_args": [message]  # ignore for now
    }

Now, if we type "speak hello spotlight" into Spotlight, we'll see the title our plugin returned.

Detour: debugging

Sometimes, your Python script might crash. That's okay. If you're using FlashlightTool, you'll see error messages in the "errors" panel. You can use the logging module to output trace messages. If you want to debug your Javascript or HTML, right-click your web content and Inspect Element.

Running the plugin

Of course, the plugin doesn't actually do anything yet — ideally, we want it to speak something out loud when we hit Enter. That's easy. Just add a function run to plugin.py.

Now, we need some way of passing the message that we're supposed to speak to run. That's why we returned an array containing the message in the run_args field of our results dictionary. run is invoked with the arguments from the run_args list (in fact, you need a run_args list for run to even be called, although it can be empty.)

def run(message):
    import os
    os.system('say "{0}"'.format(message)) # TODO: proper escaping via pipes.quote

There. Now our plugin should work. Type "say hello" into Spotlight, hit enter, watch it go.

Note: FlashlightTool automatically reloads plugin code pretty often, but Flashlight itself is very conservative about reloading. If things aren't working, try restarting Flashlight by toggling Enable Spotlight Plugins in the Flashlight window.

Showing HTML inline in Spotlight

Many plugins, like Weather and Google, return HTML and JavaScript to show content inline in the Spotlight window. You can do this by returning an HTML string from your results function:

def results(fields, original_query):
    message = fields['~message']
    html = "<h1>{0}</h1>".format(message)
    return {
        "title": "Say '{0}'".format(message),
        "run_args": [message],  # ignore for now,
        "html": html
    }

If you'd like to load a web URL, you should return a delayed Javascript redirect, which looks like this:

<script>
    setTimeout(function() {
      window.location = 'http://google.com'
    }, 500); // delay so we don't get rate-limited by doing a request after every keystroke
</script>

There are two more fields you can return in your results json that may be relevant if you're using webviews:

  • webview_links_open_in_browser: optional when the user clicks links in the webview, they'll close Spotlight and open in a browser
  • webview_user_agent: optional override the user agent in the webview. Useful if you want to load a mobile-optimized site that fits the size of the Spotlight window.

Performance

Your results function should return fast. There's no time to perform HTTP requests. If you need to fetch data from the web (like the Weather plugin), you should return HTML and Javascipt that make the request. This way, users can see your result while it's loading. The JS you return from results is not subject to the same-origin policy.

Your run function should also return fast. Results functions get killed after a couple seconds. If you're doing something long-running, spin up another process and fire a notification when you're done.

Adding icons

Add a 512x512 (or smaller) icon to your bundle, and call it Icon.png. Circular icons are preferred.

You can provide a separate icon for users in Yosemite's Dark Mode — call it Icon-dark.png.

Screenshots

Online plugin pages like this one have screenshots of plugins. Take a screenshot of Spotlight showing your plugin by pressing Command + Shift + 4, pressing space and clicking the Spotlight window, then saving that screenshot inside your plugin as Screenshot.png.

Internationalizing your plugin

See internationalization.

Other tasks

If you're doing things like searching contacts or running Applescript, you should check out the useful modules for plugins.

Submitting the plugin

Once you've written a plugin, we'd love for you to submit it so others can download it from within Flashlight. Clone the Flashlight repository, stick your .bundle in PluginDirectories/1, and submit a pull request.