Coding: Writing a Quicksilver Plugin

Wednesday, 20 September 2006


Recently I wrote myself a to do list app.  I wanted a tiny to do list that would sit in a corner and each to do entry would launch an associated item (doc, app, url, contact).  i.e. I could have an ‘Assessments’ list that listed the names of the students I had to write assessments for, each one with a link that would open their assessment file.  The Application is here if you want it.

 

I also use Quicksilver a *lot*.  If you haven’t seen Quicksilver, download it and have a play.  It’s a great little app that allows you to search, launch and manipulate docs, applications and data on your Mac, with minimal keystrokes.  Once you get used to using it, you pretty much only use you mouse to launch Quicksilver.  Also, it looks very cool ;-)

 

Anyway, since I use Quicksilver a lot, once I started using my ToDo app I wanted to be able to control it from Quicksilver.  Quicksilver has a plugin architecture that allows it to be extended to add extra functionality and to interact with other applications.  I figured I would knock up a ToDo plugin, but it turns out that the documentation for the plug in architecture is pretty sparse, and very spread out.  I thought I would pull together here all the pieces I found useful and the code for my plugin.  Hopefully making it easier for the next person to go searching for documentation... ;-)

 

Getting the Tools

 

The Quicksilver website has some developers documentation.  There is an XCode project template there, and that is the first thing to download and install.  Once downloaded it simply needs moved to ~/Library/Application Support/Apple/Developer Tools/Project Templates/.  XCode will then include it in the new project wizard list.  The project template links against the Quicksilver frameworks, and the installation instructions describe how to point XCode to the frameworks within the Quicksilver application bundle.  From the site:

 

“Go to the Preferences > Source Trees section, add a new entry with the setting name as QSFrameworks. Set the path for this item to /Applications/Quicksilver.app/Contents/Frameworks/ or the equivalent.”

 

However, I grew very frustrated when I reached this point, as the Quicksilver application does not include the full frameworks with headers.  More frustratingly, when I realized this the Quicksilver forum site went down and I couldn’t post to ask about it.  After much Googling I found this tutorial that points out that to get the full frameworks, you need the *developers version* of Quicksilver, which doesn’t seem to be mentioned or linked to anywhere on the Quicksilver site. :-/  (It’s here, btw).  Instead of putting the developers version in my apps folder, I just copied the frameworks out the application bundle (by right clicking on the developers version and selecting ‘show package contents’) into my ~/Developer folder and pointed the XCode source tree there.

 

Plug In Components.

 

Again, there is documentation on the Quicksilver site describing the roles and content of the files in the plugin template, but I didn’t find it particularly clear.  Also amongst the documentation is an example plugin called Sherlock Module.  It’s worth downloading the example and having a look through the example, but it’s a reasonably big plugin, which I found a bit difficult to extract the concepts from.  Amongst the Quicksilver forums I found another example plugin for a simple calculator action.  This is a much smaller, simpler plugin and I found it a lot easier to identify required/expected functionality by reading through it.  It’s definitely worth a look.

 

The important parts of the plugin project are the info.plist file and the PluginName.h and PluginName.m obj-C files.  The info.plist file informs Quicksilver about what the plugin can to, which methods to call on which objects in the plug in and of any external dependancies.  At the root of my info.plist are the  following entries:

 

      <key>CFBundleDevelopmentRegion</key>

    <string>English</string>

    <key>CFBundleExecutable</key>    // the name of the compiled plugin

    <string>ToDo Module</string>

    <key>CFBundleIdentifier</key>

    <string>net.jimmcgowan.Quicksilver.ToDo_Module</string>

    <key>CFBundleInfoDictionaryVersion</key>

    <string>6.0</string>

    <key>CFBundleName</key>

    <string>ToDo_Module</string>    // the name that appears in QS’s preferences

    <key>CFBundlePackageType</key>

    <string>BNDL</string>

    <key>CFBundleVersion</key>

    <string>44</string>            // the octal build number, for version checking

    <key>NSPrincipalClass</key>

    <string>ToDo_Module</string>    // the class of the main object in the plugin

 

These entries inform Quicksilver as to how to identify the plugin.  It is important, of course, that the names used in the project code match these ;-)

 

There is also a dictionary called QSPlugIn in the plist that gives further meta info about the plugin.  Mine reads as follows:

 

    <dict>

        <key>author</key>

        <string>Jim McGowan</string>

        <key>description</key>

        <string>Create ToDo items from Quicksilver (requires ToDo.app)</string>

        <key>icon</key>

        <string>net.jimmcgowan.ToDo</string>

        <key>qsversion</key>

        <string>29CC</string>

        <key>relatedBundles </key>

        <array>

            <string>net.jimmcgowan.ToDo</string>

        </array>

    </dict>

 

The Icon entry is just the bundle identifier for my main ToDo app.  Quicksilver can fetch the application’s icon from the id and use it as the plugin’s icon.  The relatedBundles array is a list of bundle identifiers of other applications of plugins that the plugin requires.  Mine is only dependent on the ToDo app.

 

The actual functionality is described in the QSActions dictionary in the info.plist.  Each entry in this dict is a dictionary describing one action and how quicksilver can call it.  one example is my ToDoModuleTextAction, which takes text entered in Quicksilver and creates a new to do item from it.  It’s dict is  as follows:

 

        <key>ToDoModuleTextAction</key>

        <dict>

            <key>actionClass</key>

            <string>ToDoModuleActionProvider</string>

            <key>actionSelector</key>

            <string>performToDoTextActionOnObject:</string>

            <key>directTypes</key>

            <array>

                <string>NSStringPboardType</string>

            </array>

            <key>icon</key>

            <string>net.jimmcgowan.ToDo</string>

            <key>name</key>

            <string>Make ToDo Item</string>

            <key>validatesObjects</key>

            <false/>

        </dict>

 

The actionClass is the name of the class that provides the action, the actionSelector is the name of the method to be called on that object to carry out the action.  Again it is, of course, very important that these match the project code.  the directTypes array lists the *Quicksilver* types that can be passed to the object to be acted on.  Finding the type strings was another frustrating task, due again to the lack of documentation.  My plugin also acts on contacts that have a email address and contacts that have a phone number.  I ended up rummaging through the info.plists in bundles of other plugins and wading through the headers in the frameworks, finded externally declared type string macros and writing code to print them to the console, and trying the results in my info.plist.  The types I found are in the complete listing below.

 

The documentation of the Quicksilver site list further info.plist entries, but there were not needed for my project, so I haven’t explored them.  The complete listing for my info.plist is as follows:

 

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">

<plist version="1.0">

<dict>

    <key>CFBundleDevelopmentRegion</key>

    <string>English</string>

    <key>CFBundleExecutable</key>

    <string>ToDo Module</string>

    <key>CFBundleIdentifier</key>

    <string>net.jimmcgowan.Quicksilver.ToDo_Module</string>

    <key>CFBundleInfoDictionaryVersion</key>

    <string>6.0</string>

    <key>CFBundleName</key>

    <string>ToDo_Module</string>

    <key>CFBundlePackageType</key>

    <string>BNDL</string>

    <key>CFBundleVersion</key>

    <string>44</string>

    <key>NSPrincipalClass</key>

    <string>ToDo_Module</string>

    <key>QSActions</key>

    <dict>

        <key>ToDoModuleCallAction</key>

        <dict>

            <key>actionClass</key>

            <string>ToDoModuleActionProvider</string>

            <key>actionSelector</key>

            <string>performToDoCallActionOnObject:</string>

            <key>directTypes</key>

            <array>

                <string>ABPeopleUIDsPboardType</string>

            </array>

            <key>icon</key>

            <string>net.jimmcgowan.ToDo</string>

            <key>name</key>

            <string>Create Call Reminder in ToDo</string>

            <key>validatesObjects</key>

            <false/>

        </dict>

        <key>ToDoModuleEmailAction</key>

        <dict>

            <key>actionClass</key>

            <string>ToDoModuleActionProvider</string>

            <key>actionSelector</key>

            <string>performToDoEmailActionOnObject:</string>

            <key>directTypes</key>

            <array>

                <string>qs.contact.email</string>

            </array>

            <key>icon</key>

            <string>net.jimmcgowan.ToDo</string>

            <key>name</key>

            <string>Create Email Reminder in ToDo</string>

            <key>validatesObjects</key>

            <false/>

        </dict>

        <key>ToDoModuleTextAction</key>

        <dict>

            <key>actionClass</key>

            <string>ToDoModuleActionProvider</string>

            <key>actionSelector</key>

            <string>performToDoTextActionOnObject:</string>

            <key>directTypes</key>

            <array>

                <string>NSStringPboardType</string>

            </array>

            <key>icon</key>

            <string>net.jimmcgowan.ToDo</string>

            <key>name</key>

            <string>Make ToDo Item</string>

            <key>validatesObjects</key>

            <false/>

        </dict>

    </dict>

    <key>QSPlugIn</key>

    <dict>

        <key>author</key>

        <string>Jim McGowan</string>

        <key>description</key>

        <string>Create ToDo items from Quicksilver (requires ToDo.app)</string>

        <key>icon</key>

        <string>net.jimmcgowan.ToDo</string>

        <key>qsversion</key>

        <string>29CC</string>

        <key>relatedBundles </key>

        <array>

            <string>net.jimmcgowan.ToDo</string>

        </array>

    </dict>

</dict>

</plist>

 

The header for my action provider class is simply as follows:

 

#import <QSCore/QSObject.h>

#import <Cocoa/Cocoa.h>

#import <AddressBook/AddressBook.h>

 

@interface ToDoModuleActionProvider : QSActionProvider

{

}

@end

 

The implemetation of my action provider has three methods, one for each action described in my info.plist.  The to do from text action looks like this:

 

-(QSObject *)performToDoTextActionOnObject:(QSObject *)dObject

{

    NSPasteboard *pBoard=[NSPasteboard    

            pasteboardWithName:@"net.jimmcgowan.todoImportData"];

    NSString *text=[dObject objectForType:QSTextType];

    [pBoard declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:self];

    [pBoard setString:text forType:NSStringPboardType];

    

    NSString *source=@"tell application \"ToDo\"\nactivate\ncreate new text to do

        with string in private pasteboard\nend tell";

    NSAppleScript *script=[[NSAppleScript alloc] initWithSource:source];

    NSDictionary *errorInfo=nil;

    [script executeAndReturnError:&errorInfo];

    [script release];

    

    return nil;

}

 

The methods simply copies the text to a pasteboard and runs an applescript that includes a convenience command that causes the ToDo app to create and add the new item.  The action is passed a QSObject.  QSObjects are objects that represent the item that the user has selected in Quicksilver, be it some text, a file, URL, contact, etc.  As a result a QSObject can contain multiple representations.  The desired data must be extracted from the QSObject using the objectForType: method.  In this implementation I’ve used that QSTextType which seems (functionally at least) to be the same as NSStringPBoardType.

 

It seems that all that is needed to implement the plugin is to provide these action methods in a subclass of QSActionProvider.  However, the QS classes are not well documented and, as the Quicksilver site says, “This plug-in interface is subject to change a LOT, so don’t don’t be surprised if something stops working....”

 

You can download the complete source for my plugin here.