Skip to content

Latest commit

 

History

History
331 lines (228 loc) · 20.9 KB

README.mdown

File metadata and controls

331 lines (228 loc) · 20.9 KB

Alfred.framework for Objective-C

Alfred.framework is a lightweight Objective-C framework for developing workflow components for Alfred v2. Modeled in part on alp, Alfred.framework is designed to automate a number of repetitive and annoying workflow tasks with the goal of making it faster and easier to generate fewer lines of code. The library is still in its developmental infancy, but current features include:

  • Painless generation of feedback XML.
  • Fuzzy searching, modeled on alp.
  • Methods for accessing cache folders, storage folders, and the local folder.
  • An argument parser.
  • Silent error logging.

You can download the latest version here; for more information about this and prior releases, see the changelog. You can also browse and download the source code at Github.

Installation

Adding Alfred.framework to XCode is more of a chore than is ideal but less of a chore than you might fear. To get started, download the latest version and extract the contents of the zip. Then, in XCode, click the plus sign in the lower-left-hand corner of the window, select 'Add Files to "MyAlfredWorkflow"...,' and navigate to the folder where you saved Alfred.framework. Ensure that "Copy items to the destination group's folder" and "MyAlfredWorkflow" (under "Add to targets") are checked, then select Alfred.framework and click "Add."

Now select the main project item from the pane to the left, then choose the target "MyAlfredWorkflow." Under "Build Phases," click the disclosure triangle next to "Link binary with libraries" and make sure it includes Alfred.framework. If not, click the plus sign, then "Add Other...," then navigate to the file on your hard drive and add it manually.

Now, in your main source file main.m, add a line lines below #import <Foundation/Foundation.h>:

//
//  main.m
//  MyAlfredWorkflow
//
//  Created by Daniel Shannon on 5/25/13.
//  Copyright (c) 2013 Daniel Shannon. All rights reserved.
//

#import <Foundation/Foundation.h>
#import <Alfred/Alfred.h>

int main(int argc, const char * argv[])
{

If you've done everything correctly so far, XCode should try to autocomplete <Alfred/Alfred.h> as you type, and your screen should look something like this:

Note that the organization of the files in the left pane doesn't make much of a difference, so long as you don't delete the framework.

Now, in order to use Alfred.framework, you simply must ensure that the framework bundle is copied to the same directory as your executable. Then import <Alfred/Alfred.h> and you're ready to go!

Usage

Currently, Alfred.framework provides the following core features:

  • Painless generation of feedback XML.
  • Fuzzy searching, modeled on alp.
  • Methods for accessing cache folders, storage folders, and the local folder.
  • An argument parser.
  • Silent error logging.

Each one is accessible through <Alfred/Alfred.h>. The two main classes currently in use are AWWorkflow and AWFeedbackItem. AWWorkflow is an interface to most of the methods above, whereas AWFeedbackItem is pretty much just what it says on the can.

Feedback XML

To generate feedback XML in Alfred.framework, you must first create and configure one or more AWFeedbackItems. There are a number of ways to do this. The first and most cumbersome involves using getter and setter methods to set up feedback items. For example, this would be perfectly valid:

#import <Alfred/Alfred.h>

// [...]

AWFeedbackItem *i = [[AWFeedbackItem alloc] init];
i.title = @"Information";
i.subtitle = @"S'more information.";
i.valid = @YES;
i.arg = @"get info";

...and so on. The following properties are presently used to pass data back to Alfred:

@property NSString      *title;
@property NSString      *subtitle;
@property NSString      *uid;
@property NSNumber      *valid;
@property NSString      *autocomplete;
@property NSString      *icon;
@property NSNumber      *fileicon;
@property NSNumber      *filetype;
@property NSString      *arg;
@property NSString      *type;

However, because setting each one would be tedious, AWFeedbackItem provides a class method, + itemWithObjectsAndKeys:(id)..., and an instance method, - initWithObjects:(NSArray *) forKeys:(NSArray *), to make it simpler. As their first arguments, both take a BOOL that determines whether the XML item is considered actionable by Alfred. The class method then allows you to specify a nil-terminated list of alternative objects and keys (property names) to set up the item. With the exception of the three NSNumber objects, which should be @YES or @NO, all keys and values must be strings. For example:

#import <Alfred/Alfred.h>

// [...]

AWFeedbackItem *i = [AWFeedbackItem itemWithObjectsAndKeys:@"Entitlement", @"title", @"Subversion", @"subtitle", @"iconography.png", @"icon", @"argumentation", @"arg", @YES, @"valid", nil];
NSLog(@"xml=%@", [i xml]);

...would log the following to the console:

AlfredWorkflowTest[31201:303] xml=<item valid="yes" arg="argumentation"><title>Entitlement</title><subtitle>Subversion</subtitle><icon>iconography.png</icon></item>

You could do something similar with the customized constructor method. This method, rather than taking a list of NSString objects, takes two NSArray objects---one of keys, one of values---which must contain NSStrings. Here's an example:

#import <Alfred/Alfred.h>

// [...]

AWFeedbackItem *j = [[AWFeedbackItem alloc] initWithObjects:@[@"Titillation", @"Submission & Subdual", @"automation", @NO] forKeys:@[@"title", @"subtitle", @"autocomplete", @"valid"];
NSLog(@"xml=%@", [j xml]);
AlfredWorkflowTest[31201:303] xml=<item valid="no" autocomplete="automation"><title>Titillation</title><subtitle>Submission &amp; Subdual</subtitle><icon>icon.png</icon></item>

Alfred.framework automatically escapes XML strings passed to AWFeedbackItem. Additionally, the way that arguments are treated will vary depending on their content. From Alfred v2.0.4 on, arguments can be specified as either attributes of <item></item> or separate XML keys. The framework supports this feature by designating any arg string containing newlines a tag, and any arg string without newlines an attribute. For example:

AWFeedbackItem *i = [AWFeedbackItem itemWithObjectsAndKeys:@NO, @"valid", @"Old and Busted", @"title", @"I'm a simple man", @"arg", nil];
AWFeedbackItem *j = [AWFeedbackItem itemWithObjectsAndKeys:@YES, @"valid", @"New Hotness", @"title", @"I\nthink\ncomplexly", @"arg", nil];

AWWorkflow *wf = [[AWWorkflow alloc] init];
[wf flush:YES feedbackItems:i, j, nil];
<?xml version="1.0"?><items><item valid="no" arg="I&apos;m a simple man"><title>Old and Busted</title><subtitle></subtitle><icon>icon.png</icon></item><item valid="yes"><title>New Hotness</title><subtitle></subtitle><icon>icon.png</icon><arg>I
think
complexly</arg></item></items>

Note that the feedback item's - xml method does not generate the complete XML string that Alfred needs to provide feedback. That functionality is provided through a method of AWWorkflow, and - xml is only used here for the purposes of demonstration.

To feed data back to Alfred, you must call AWWorkflow's instance method - flush:(BOOL) feedbackItems:(AWFeedbackItem *).... Like the feedback item's class method, this too takes a nil-terminated list of objects; in this case, however, each one should be an AWFeedbackItem instance. The first argument is a BOOL; if set to YES, it will flush the standard output buffer to Alfred and terminate your program. Otherwise, it will only spool the XML into the standard output. Call it like this:

#import <Alfred/Alfred.h>

// [...]

AWWorkflow *wf = [[AWWorkflow alloc] init];

AWFeedbackItem *i = [AWFeedbackItem itemWithObjectsAndKeys:@"Entitlement", @"title", @"Subversion", @"subtitle", @"iconography.png", @"icon", @"argumentation", @"arg", @YES, @"valid", nil];
AWFeedbackItem *j = [[AWFeedbackItem alloc] initWithObjects:@[@"Titillation", @"Submission & Subdual", @"automation", @YES] forKeys:@[@"title", @"subtitle", @"autocomplete", @"valid"];

[wf flush:YES feedbackItems:i, j, nil];

This ends the program immediately, printing the following XML to the standard output:

<?xml version="1.0"?><items><item valid="yes" arg="argumentation"><title>Entitlement</title><subtitle>Subversion</subtitle><icon>iconography.png</icon></item><item valid="no" autocomplete="automation"><title>Titillation</title><subtitle>Submission &amp; Subdual</subtitle><icon>icon.png</icon></item></items>

If you're generating feedback items on-the-fly, of course, you'll want to be able to send them all at once. Rather than passing them one by one to - flush:feedbackItems: and only flushing at the last object, you can also call - flush:(BOOL) feedbackArray:(NSArray *) with an NSArary containing AWFeedbackItems. For example, you might want to rewrite the sample above with a condition:

#import <Alfred/Alfred.h>

// [...]

AWWorkflow *wf = [AWWorkflow workflow];     // Returns an initialized AWWorkflow
NSMutableArray *fbi = [NSMutableArray new];

if (myCondition == YES)
{
    AWFeedbackItem *i = [AWFeedbackItem itemWithObjectsAndKeys:@"Entitlement", @"title", @"Subversion", @"subtitle", @"iconography.png", @"icon", @"argumentation", @"arg", @YES, @"valid", nil];
    [fbi addObject:i];
}
AWFeedbackItem *j = [[AWFeedbackItem alloc] initWithObjects:@[@"Titillation", @"Submission & Subdual", @"automation", @YES] forKeys:@[@"title", @"subtitle", @"autocomplete", @"valid"];
[fbi addObject:j];

[wf flush:YES feedbackArray:fbi];

Fuzzy Searching

Alfred.framework includes a port of alp's fuzzy searching feature, which is considerably sped up in compiled code without losing any of its power. To use it, simply fill an NSArray with a number of objects. These objects can be of any type, so long as an NSString to be matched against can be extracted from them. The framework will return another NSArray with any matching elements, sorted by the quality of the match.

The method that handles this is also declared in AWWorkflow, as - fuzzySearchFor:(NSString *) in:(NSArray *) withKeyBlock:(NSString *(^)(id)). It takes a string with the search query, an array of objects, and a block function that will be applied to each element of the array to get a search string. The block should take a single object, though that object can contain many other objects, and return a string to be searched.

Let's take a look at some sample code, using data drawn from alp's sample case:

#import <Alfred/Alfred.h>

// [...]

AWWorkflow *wf = [AWWorkflow workflow];

NSArray *k = @[@"key", @"author", @"title"];
NSArray *v0 = @[@"ZB7K535R", @"Reskin 2003", @"Including Mechanisms in Our Models of Ascriptive Inequality: 2002 Presidential Address"];
NSArray *v1 = @[@"DBTD3HQS", @"Igor & Ronald 2008", @"Die Zunahme der Lohnungleichheit in der Bundesrepublik. Aktuelle Befunde für den Zeitraum von 1998 bis 2005"];
NSArray *v2 = @[@"MQ3BHTBJ", @"Marx 1978", @"Alienation and Social Class"];
NSArray *v3 = @[@"7G4BRU45", @"Marx 1978", @"The German Ideology"];
NSArray *v4 = @[@"9ANAZXQB", @"Llorente 2006", @"Analytical Marxism and the Division of Labor"];
NSArray *ofDicts = @[[NSDictionary dictionaryWithObjects:v0 forKeys:k],
                     [NSDictionary dictionaryWithObjects:v1 forKeys:k],
                     [NSDictionary dictionaryWithObjects:v2 forKeys:k],
                     [NSDictionary dictionaryWithObjects:v3 forKeys:k],
                     [NSDictionary dictionaryWithObjects:v4 forKeys:k]];

NSArray *res = [wf fuzzySearchFor:@"marx" in:ofDicts withKeyBlock: ^(id obj) {
    NSDictionary *d = (NSDictionary *)obj;
    NSString *s = [NSString stringWithFormat:@"%@ - %@",
                    [d objectForKey:@"author"],
                    [d objectForKey:@"title"]];
    return s;
}];
NSLog(@"res=%@", res);

As you can see, it's okay that the strings we need to search are buried deep in the actual object so long as we know how to return a string from our key block; otherwise, the search function won't know what it should be applying its filters to. What's actually being searched in each case is the string "{author} - {title}"; what's returned, on the other hand, is the complete object that yielded each match:

AlfredWorkflowTest[32039:303] res=(
    {
        author = "Marx 1978";
        key = MQ3BHTBJ;
        title = "Alienation and Social Class";
    },
    {
        author = "Marx 1978";
        key = 7G4BRU45;
        title = "The German Ideology";
    },
    {
        author = "Llorente 2006";
        key = 9ANAZXQB;
        title = "Analytical Marxism and the Division of Labor";
    }
)

It should be considered purely coincidental that the order of the objects returned is identical to the order in which they were presented. The algorithm calculates quality of match based on the number of characters in sequence ("marx" is preferred to "moarx"---unbroken sequences are better) and the position of the sequence in the search string ("marx" is preferred to "smarx"---earlier is better).

Argument Parsing

The AWWorkflow object provides a method for converting arguments to the workflow in the form of --key=value into an NSDictionary with usable objects. To invoke it, you must first create an array of dictionaries that define the expected arguments. Each dictionary requires three key--value pairs---@"name", a string with the long name of the argument key, @"flag", a single-letter string with the short name of the argument key, and @"has_arg", an NSNumber that determines whether the argument key is an on/off flag (set to @NO), requires a value (set to @YES), or takes an optional value (set to anything else). Once you've determined your expected arguments, you can invoke AWWorkflow's - parseArguments:(const char **) withKeys:(NSArray *) count:(int) method. This returns a dictionary object whose keys are the arguments' names and whose values were passed in to the program.

#import <Foundation/Foundation.h>
#import <Alfred/Alfred.h>

int main(int argc, const char *argv[])
{
    @autoreleasepool {
        AWWorkflow *wf = [AWWorkflow workflow];
        NSArray *k = @[@{@"name": @"qux", @"has_arg": @YES, @"flag": @"q"},
                       @{@"name": @"baz", @"has_arg": @NO,  @"flag": @"b"},
                       @{@"name": @"cog", @"has_arg": @2,   @"flag": @"c"}];
        NSDictionary *args = [wf parseArguments:argv withKeys:k count:argc];
        NSLog(@"qux=%@, baz=%@, cog=%@", [args valueForKey:@"qux"], [args valueForKey:@"baz"], [args valueForKey:@"cog"]);
        // NSLog(@"query=%@", [args valueForKey:@"{query}"]);
    }
    return 0;
}

You can compile this program, drop the framework into its folder (remember, it must be in the same directory as your executable), and run it from a Terminal window to check that it works:

auntieclimactic:Debug danielsh$ ./ArgumentativeWorkflow --qux=foo --baz --cog
2013-05-26 08:20:22.847 DTWorkflow[48183:707] qux=foo, baz=1, cog=1
auntieclimactic:Debug danielsh$ ./ArgumentativeWorkflow --qux=foo --baz --cog=bar
2013-05-26 08:25:57.439 DTWorkflow[51394:707] qux=foo, baz=1, cog=bar
auntieclimactic:Debug danielsh$ ./DTWorkflow --qux=foo --cog=bar
2013-05-26 08:27:02.340 DTWorkflow[51421:707] qux=foo, baz=(null), cog=bar

When implementing this in your workflow, remember that arguments to long-style command parameters require an equals sign between the key and the value.

What's more, anything that follows the arguments list will be recognized as potentially constituting an Alfred query, and placed in the returned dictionary under the reserved key @"{query}". Remove the comment from the second NSLog() statement above, recompile, and try this:

auntieclimactic:Debug danielsh$ ./ArgumentativeWorkflow --qux=fool --baz --cog=me once shame on you
2013-05-26 08:51:04.838 DTWorkflow[51785:707] qux=fool, baz=1, cog=me
2013-05-26 08:51:04.839 DTWorkflow[51785:707] query=once shame on you

Bundle Basics

Currently, only two methods are defined to interact with the Alfred bundle, both in AWWorkflow. The first of these is - log:(NSString *)..., which accepts a format string followed by a variable number of format arguments---just like NSLog() or [NSString stringWithFormat:]. However, rather than writing to the console, - log: saves text to a file called framework.log in the workflow's local folder. - bundleID, meanwhile, returns a string with your bundle ID, as extracted from the workflow's info.plist file.

Filesystem Interaction

The framework also defines a number of methods for quickly accessing files in the local folder, the canonical cache folder, and the canonical storage folder. These, too, are provided through the workhorse AWWorkflow class:

  • - local
    Without an argument, [wf local] returns the path to the folder containing your executable.
  • - local:(NSString *)
    With an optional NSString argument, [wf local:@"baz.png"] returns the argument appended as a path component to the folder containing your executable.
  • - cache and - cache:(NSString *)
    Returns the path to your workflow's cache folder, optionally with an additional path component appended. If the cache folder does not already exist, the framework will create it; however, the file or folder referenced by - cache: will not be created automatically.
  • - storage and - storage:(NSString *)
    Returns the path to your workflow's storage folder, optionally with an additional path component appended. If the storage folder does not already exist, the framework will create it; however, the file or folder referenced by - storage: will not be created.

Roadmap

In the not-too-distant future, more features from alp---including settings, notifications, and Keychain access---will be ported to Alfred.framework. However, I'm always open to hearing interesting feature requests. Objective-C and Cocoa provide almost limitless possibilities for what can be done with this sort of thing, and I suspect that I'm not imaginative enough to see them all. Please don't hesitate to get in touch via the Alfred forums, Twitter, or e-mail.

License

Alfred.framework is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 license. This means that you may share and redistribute the package, but only for non-commercial purposes, only so long as you credit the original author---moi---and only if your derivative work is similarly licensed.

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.