Advertisement

Alfred Workflows for Advanced Users

by

This Cyber Monday Tuts+ courses will be reduced to just $3 (usually $15). Don't miss out.

Well, you made it to the advanced tutorial. Well done! Now for the real work. Alfred can be used to do some complicated things, including recursive programming! The concepts here are not easy and not for the inexperienced programmer.

Script Filters

Script Filter is the last block type to discuss. Like the File Filter, a Script Filter gives the user a list of options from which to select. The difference being, a Script Filter allows the programmer to create the list of options with a script.

A simple choice selection for creating a project in the project folder will show off the use of a Script Filter. First, add a Script Filter to the Project Manager Workflow from the intermediate tutorial.

Script Filters Configuration Screen
Script Filters: Configuration Screen

Set the Keyword to pm:create, check the with space checkbox, set the argument type to Argument Optional, the Placeholder Title to Create a Project, the Placeholder Subtext to Project Manager, the "Please Wait" Subtext to Loading Options…. Most of these options are the same as in other boxes.

The with space checkbox tells Alfred to expect a space before the optional argument. Therefore, it will strip off the leading space before passing the argument to the script. Otherwise, it will simply pass everything after the Keyword to the script for this block.

The Argument Type selector has three values: Argument Required, Argument Optional, and No Argument. The Argument Required will not accept a carriage return until an argument is given after the keyword. It will pass everything to the script as the user types. The Argument Optional will allow the user to hit enter without an argument given. The No Argument will move to the next matching keyword if the user starts to give an argument. Otherwise, it drops out to a default search.

The "Please Wait" Subtext allows you to give a subtext to the user while the script in the Script Filter is being executed. This lets the user of the script know if all the choices are available or not.

The script box area is just like a Run Script block. You select the programming language you want, what escaping options you need, and the script itself. Set it to /usr/bin/php, all escaping off, and the following script:

	echo <<<EOF
	<?xml version="1.0"?>
	<items>
	<item uid="1PM" arg="basic" valid="yes">
		<title>Basic HTML Site</title>
		<subtitle>Project Manager</subtitle>
		<icon>icon.png</icon>
	</item>
	<item uid="2PM" arg="backbone" valid="yes">
		<title>Backbone HTML Site</title>
		<subtitle>Project Manager</subtitle>
		<icon>icon.png</icon>
	</item>
	</items>
	EOF;

This PHP script simply echos out the XML for the results of the script. A Script Filter expects the script to output a valid XML statement containing an array of items: one for each option available to the user.

Each item has a uid, arg, valid, and autocomplete options. The uid is a unique number or name for that item. If it always is the same unique name for that item, the more a user selects it, the more it will be placed towards the top of the list. The arg is what the next block will receive if that item is selected. The valid argument can only be yes or no. If no, then that item can not be selected, but the autocomplete value will be placed in the Alfred Prompt with the keyword. If yes, then it will accept an enter to send the argument to the next block and the autocomplete value is ignored.

The title, subtitle, and icon children of each item allows the script to set the title, subtitle, and icon used to display the results to the user. Therefore, each item can have a different icon. If you have a subdirectory in your workflow area called icons, you can use the icons/day.png path to refer to an picture called day.png in that subfolder. This allows you to personalize your results for easier selection.

Script Filters Displaying
Script Filters: Displaying

Now, when the Script Filter is activated, the above pictures shows the results. It still will not perform anything except send the selection to Alfred.

Script Filters Adding a Run Script Block
Script Filters: Adding a Run Script Block

In order to do something based on the selection, you will need to add a Run Script block next and fill it out as a bash script without escaping. The script area can be filled with the following script:

	#########################
	# Contants.
	#########################
	VPREFS="${HOME}/Library/Caches/com.runningwithcrayons.Alfred-2/Workflow Data/"
	NVPREFS="${HOME}/Library/Application Support/Alfred 2/Workflow Data/"

	###############################################################################
	# Read the bundleid from the workflow's info.plist
	###############################################################################
	getBundleId() {
		/usr/libexec/PlistBuddy  -c "Print :bundleid" "info.plist"
	}

	###############################################################################
	# Get the workflow data dir
	###############################################################################
	getDataDir() {
		local BUNDLEID=$(getBundleId)
		echo "${NVPREFS}${BUNDLEID}"
	}

	if [ ! -d "$(getDataDir)" ]; then
		mkdir -p "$(getDataDir)";
		touch "$(getDataDir)/projectDir.txt";
	fi

	projD=`cat "$(getDataDir)/projectDir.txt"`;

	if [ ! -d "$projD" ]; then
		mkdir -p "$projD";
	fi

	cd "$projD";

	type="{query}";

	case "$type" in
	basic) echo "Basic";
		curl -o tmp.zip -L "https://codeload.github.com/h5bp/html5-boilerplate/zip/v4.3.0";
		unzip tmp.zip >>/dev/null;
		mv html5-boilerplate-4.3.0/* . ;
		rm -Rf "html5-boilerplate-4.3.0/";
		rm -Rf "tmp.zip";
		echo "HTML5 Boilerplate in place.";
	;;

	backbone) echo "BackBone";
		curl -o tmp.zip -L "https://codeload.github.com/h5bp/html5-boilerplate/zip/v4.3.0";
		unzip tmp.zip >>/dev/null;
		mv html5-boilerplate-4.3.0/* . ;
		rm -Rf "html5-boilerplate-4.3.0/";
		rm -Rf "tmp.zip";
		curl -o js/vendor/backbone-min.js -L "http://backbonejs.org/backbone-min.js";
		echo "HTML5 Boilerplate with Backbone.js in place.";
	;;
	esac

This script simply creates the type of project in the directory that was set as the project’s directory. The first part of the script is just like the scripts used in the intermediate tutorial to access the two places data can be stored. The data directory and the cache directory. Here, only the data directory is being used to get the location of the project directory.

The rest of the script will move to the project directory and fill it in based on the type of project selected. A basic HTML project will download the HTML5 Boilerplate Template and install it into the project directory. A Backbone project will install the HTML5 Boilerplate Template and Backbone.js JavaScript library. This can easily be improved by using Yeoman to install the project components, but that would go beyond the scope of this tutorial.

The problem with the way this Script Filter was written is that it is hard to expand and maintain. It would be much easier to call a function that will create the XML as needed. Also, by using this approach to the program, the user can not narrow down the list of possible actions by typing. They will just have to move down the list or press a hot key for that item.

Since Alfred is all about making the finding and selecting of options easier, there needs to be a way to make this happen. Thanks to all the devoted programmers helping with Alfred, there are a few libraries written that will help make this process much easier.

Libraries of Helper Functions

One of the blessings of the Alfred eco-system is that many people are designing libraries for their favorite script language to make Alfred programming easier. The complete list of Alfred Libraries can be found on the Alfred Forum. There are libraries for Bash/Zsh (pieces of which I used in these scripts), Python, PHP, Ruby, AppleScript, and go (it is listed as an AppleScript library, but it is for the go programming language by Google)!

To make use of the library for PHP, you will need to download the workflows.php file from this site by David Ferguson. Place this in the workflow folder. Now, this script has to be loaded in to the PHP code for the Script Filter. Change that code to:

	//
	// Load the PHP workflow library.
	//
	include_once("workflows.php");

	//
	// Create the workflow object to use.
	//
	$wf = new Workflows();

	//
	// Get the raw query and store it for use.
	//
	$clean = trim("{query}");

	//
	// Set the options.
	//
	$wf->result("1PM", "basic", "Basic HTML Site", "Project Manager", 'icon.png', "yes");

	$wf->result("2PM", "backbone", "Backbone HTML Site", "Project Manager", 'icon.png', "yes");

	//
	// Echo out the xml of the choices.
	//
	echo $wf->toxml();

This is much easier to read and understand. Comments can easily be added to help explain the code for future reference. This will give the exact same results as the bare XML list, but now it is in a form that will make expansion easier.

This script now loads in the library and initializes a class variable, $wf, of the Workflow() class from the library. The result() member function is used to create the XML code with the options. The options by order are: count, arg, title, sub-title, icon file, and valid. The toxml() member function simply returns the final XML code that is echoed out.

Now, to make the Script Filter look better. Take the two image files in the download file for this tutorial and add them to your workflow directory. Then, change the Set the options section in the Script Filter code to:

	//
	// Set the options.
	//
	$wf->result("1PM", "basic", "Basic HTML Site", "Project Manager", 'html5BP.jpeg', "yes");

	$wf->result("2PM", "backbone", "Backbone HTML Site", "Project Manager", 'backbone.png', "yes");

The icon used for each choice now represents the choice. A much better user interface. Alfred can take either jpeg or png graphic files for the icons. It also rescales the icons as needed. The results are shown below:

Create Project with Icons
Create Project with Icons

As can be seen, the second option is displayed first. That is because I used it the most while testing. Alfred remembers which choice is used the most and tries to order the list accordingly. This is useful for commands that are used more frequently.

But, the list can not be narrowed down by typing part of the name. That is a real must if there is to be many choices. Unfortunately, this PHP library does not have a function for that. Therefore, a new function needs to be defined. Change the options area to this:

	//
	// Set the options.
	//
	$added = buildResult($wf,"$clean","1PM", "basic", "Basic HTML Site", "Project   Manager", 'html5BP.jpeg', "yes");

	$added += buildResult($wf,"$clean","2PM", "backbone", "Backbone HTML Site", " Project Manager", 'backbone.png', "yes");

	if($added == 0) {
		$wf->result("999","","No Project Type matches $clean!", "Project Manager", 'icon.png', 'no');
	}

The buildResult() function takes everything that the result() function did, but also has two new parameters: the Workflow() object variable and the text to match to the command.

Now, add the definition of the new function just after the library include:

	function buildResult($wf, $input, $count, $arg, $title, $subtitle, $icon, $valid) {
		$result = 0;
		if(preg_match("/.*$input/i","$arg") === 1) {
	  		$wf->result($count, $arg, $title, $subtitle, $icon, $valid);
	  		$result = 1;
		}
		return($result);
	}

This function makes use of the preg_match() PHP function to do a regular expression match of the string to a wild card added to the input string. If what the user types does not match, it will not add that result. If a result was added, it will return a one. Otherwise, it will return a 0.

Did you notice the extra code added? That was to see if anything was added to the results. If nothing passed the test to include as a result, a default result is added saying that it could not find anything. With the valid parameter set to no, the user can not hit return on it. That keeps your workflow from dumping the user in to the default search with everything on the Alfred Prompt or from sending junk to the next script to figure out.

Project Type not Found Error
Project Type not Found Error

With that little bit of error checking, the workflow now gracefully tells the user there is a problem without causing a dump to the default search. You should always think about error handling in the workflows. It is important for a professional look and feel.

Recursive Programming in Alfred

Since the Script Filter block keeps calling the script for every letter typed by the user, it makes a great place to do recursive programming. Most often recursive programming is mentioned when a routine calls itself. That is direct recursion. What is done in Alfred’s Script Filter would be called indirect recursion.

State Machine Workflows

The first type of recursion to investigate is the state machine. A state machine is program that is controlled by a variable that determines the state or execution level of the program.

Recursion List Projects Script Filter
Recursion: List Projects Script Filter

The Project Manager Workflow needs a way to track projects and perform certain actions on those projects. To start it off, create a Script Filter block as above. Then add this script to the Script area:

	//
	// Load the PHP workflow library.
	//
	include_once("workflows.php");

	//
	// Function:    buildResult
	//
	// Description: Conditionally build the result XML if the
	//              input contains the argument.
	//
	function buildResult($wf, $input, $count, $arg, $title, $subtitle, $icon, $valid, $ret) {
		$result = 0;
		$parts = explode("|",$arg);
		if(count($parts) > 0) {
	 		$arg = $parts[0];
		}
		if(preg_match("/.*$input/i","$arg") === 1) {
	  		$wf->result($count, $arg, $title, $subtitle, $icon, $valid, $ret);
	  		$result = 1;
		}
		return($result);
	}

	//
	// Create the workflow object to use.
	//
	$wf = new Workflows();

	//
	// Get the raw query and store it for use.
	//
	$clean = trim("{query}");

	//
	// Figure the state by the quey items.
	//
	$parts = explode("|",$clean);
	$pcount = count($parts);
	$returncount = 0;

	switch($pcount) {
	case 1:
	  	//
	  	// This is the base state. Here, a list of projects and
	  	// the ability to create a new project is listed.
	  	//

	  	//
	  	// Get and list the projects.
	  	//
	  	$count = 0;
	  	$filename = $wf->data() . "/projects.txt";
	  	if(file_exists($filename)) {
		 	$projs = explode("\n",file_get_contents($filename));
		 	foreach($projs as $proj) {
				$projparts = explode(":",$proj);
				if(strcmp($projparts[0],"") != 0) {
			   $returncount += buildResult($wf, $parts[0], $count, "{$projparts[0]}|", "Project: {$projparts[0]}", "Project Manager", 'icon.png', "no","{$projparts[0]}|");
			   	$count += 1;
				}
		 	}
	  	}

	  	//
	  	// Give an option for a new project.
	  	//
		$wf->result("999","new|","Add a new Project", "Project Manager", 'icon.png', 	'no',"new|");
	  	$returncount += 1;
	  	break;

	case 2:
	  	//
	  	// Two possible areas: Actioning a particular project or
	  	// creating a new project.
	  	//
	  	if(strcmp($parts[0],"new") === 0) {
		 	//
		 	// Create a new project. Get the name.
		 	//
		 	$wf->result("999","new|{$parts[1]}","Project Name: {$parts[1]}", "Project 	Manager", 'icon.png', 'yes',"new|{$parts[1]}");
	  		$returncount += 1;
	  	} else {
		 	//
		 	// Now to perform actions on a project.
		 	//
		 	$count = 1;
		 	$returncount += buildResult($wf, "${parts[1]}", $count++ ."pml", "remove|{$parts[0]}", "Remove Project: {$parts[0]}", "Project Manager", 'icon.png', "yes","");
		 	$returncount += buildResult($wf, "${parts[1]}", $count++ ."pml", "pow|{$parts[0]}", "Add to Pow: {$parts[0]}", "Project Manager", 'icon.png', "yes","");
		 	$returncount += buildResult($wf, "${parts[1]}", $count++ ."pml", "alfred|{$parts[0]}", "View in Alfred: {$parts[0]}", "Project Manager", 'icon.png', "yes","");
		 	$returncount += buildResult($wf, "${parts[1]}", $count++ ."pml", "current|{$parts[0]}", "Make Current Project: {$parts[0]}", "Project Manager", 'icon.png', "yes","");
			$returncount += buildResult($wf, "${parts[1]}", $count++ ."pml", "serve|{$parts[0]}", "Launch Server in Project: {$parts[0]}", "Project Manager", 'icon.png', "yes","");
	  	}
	  	break;
	}

	If($returncount < 1) {
		$wf->result("999","","Invalid State! Please start over.", "Project Manager", 'icon.png', 'no',"");

	}

	//
	// Echo out the xml of the choices.
	//
	echo $wf->toxml();

The beginning of the script is exactly like the last script. Logic was added to keep the first part of a multi-part $arg (In other words, a string given in the $arg parameter that has the | symbol.) to the buildResult() function.

Right after cleaning the input from Alfred, the input is split according to the | charater and the number of parts is figured out. This information gives the state for the state machine. The switch..case statement acts on the different states of the state machine. In this case, the state machine has just two states.

The first state lists the project names from a list of projects, and gives the user the option of adding a new project. The list of projects is created from the file projects.txt in the workflows data directory. Notice, much care is taken to see if the file does exist and that there are projects in the file. I wanted to make sure the workflow never bombs out to the default search.

The second state checks for the new command in the input line. It will then expect whatever the user types in to be a new project name. If it is not a new command, then it is assumed to be the name of a project. It will then create a list of actions that can be taken on the project.

Recursion List Projects in Action
Recursion: List Projects in Action

With the states defined like this, the above picture is the project list with the add a new project.

Recursion List Projects Adding a new Project
Recursion: List Projects: Adding a new Project

This picture shows the new project giving a new name.

Recursion List Projects Actions for Projects
Recursion: List Projects: Actions for Projects

Once the projects are defined, a project can be selected and a list of actions that can be taken on the project is shown. When selected, the script will send the action command with the name of the project.

Workflows That Make Use of Other Workflows

Now that the information is in the pipeline, it needs an action taken upon it. Add a Run Script block right the Script Filter block to play around with a new form of recursion: calling other workflows!

Recursion Actioning the Project
Recursion: Actioning the Project

This Run Script block is a PHP script with the parameters as shown above. Next, you need to put this into the Script area:

	//
	// Load the PHP workflow library.
	//
	include_once("workflows.php");

	//
	// Function:        getProjDirectory
	//
	// Description:     This function is used to get the directory related
	//                  to the given project.
	//
	// Inputs:
	//                  $projname       The name of the project.
	//
	function getProjDirectory($wf, $projname) {
		//
		// Get the Project directory from the projects.txt list of projects.
		//
		$pdir = "";
		$projs = explode("\n",file_get_contents($wf->data() . "/projects.txt"));
		foreach($projs as $proj) {
	 	 	$projparts=explode(":",$proj);
			if(strcmp($projparts[0],$projname) === 0) {
				//
				// This is the projects name, save the directory.
				//
				$pdir = $projparts[1];
	  		}
		}
		return($pdir);
	}

	//
	// Create the workflow object to use.
	//
	$wf = new Workflows();

	//
	// Get the raw query and store it for use.
	//
	$clean = trim("{query}");

	//
	// Figure the state by the query items.
	//
	$parts = explode("|",$clean);
	$pcount = count($parts);

	switch($parts[0]) {
	case "new":
	  	//
	  	// Create a new project listing.
	  	//
	  	file_put_contents($wf->data() ."/projectName.txt",$parts[1]);

	  	//
	  	// Get the directory by calling Alfred.
	  	//
		system("osascript -e 'tell app \"Alfred 2.app\" to search \"pm:setprojectdirectory 	\"'");
	  	break;

	case "pow":
	  	//
	  	// Get the Project directory from the projects.txt list of projects.
	  	//
	  	$pdir = getProjDirectory($wf,$parts[1]);

	  	//
	  	// Create the soft link in the ~/.pow directory.
	  	//
	  	$home = getenv("HOME");
	  	if(symlink($pdir,"$home/.pow/{$parts[1]}")) {
		 	//
		 	// Tell the user.
		 	//
		 	echo "Added {$parts[1]} to POW.";
	  	} else {
		 	//
		 	// Something did not work.
		 	//
		 	echo "Could not make the symbolic link to $pdir!";
	  	}
	  	break;

	case "remove":
	  	//
	  	// Get the list of all projects and check each one.
	  	//
	  	$projs = explode("\n",file_get_contents($wf->data() . "/projects.txt"));
	  	$lines[] = "";
	  	foreach($projs as $proj) {
		 	$projparts=explode(":",$proj);
		 	if(strcmp($projparts[0],$parts[1]) != 0) {
				//
				// This is not the project being removed. Add it back.
				//
				$lines[] = $proj;
		 	}
	  	}
	  	//
	  	// Save all the projects except for the one removed.
	  	//
	  	file_put_contents($wf->data() . "/projects.txt",implode("\n",$lines));

	  	//
	  	// Tell the user.
	  	//
	  	echo "Removed the {$parts[1]} project.";
	  	break;

	case "serve":
	  	//
	  	// Call the POW workflow to see it being served.
	  	//
	  system("osascript -e 'tell app \"Alfred 2.app\" to search \"pow browse {$parts[1]}\"'");
	  	break;

	case "alfred":
	  	//
	  	// Get the Project directory from the projects.txt list of projects.
	  	//
	  	$pdir = getProjDirectory($wf,$parts[1]);

	  	//
	  	// Get the directory by calling Alfred.
	  	//
	  	system("osascript -e 'tell app \"Alfred 2.app\" to search \"$pdir\"'");
	  	break;

	case "current":
	  	//
	  	// Get the Project directory from the projects.txt list of projects.
	  	//
	  	$pdir = getProjDirectory($wf,$parts[1]);

	  	//
	  	// Set the directory by calling Alfred.
	  	//
	  	file_put_contents($wf->data() . "/projectDir.txt",$pdir);
	  	echo "{$parts[1]} is now the current project.";
	  	break;
	}

Just after the loading of the Alfred PHP Workflow library, there is a definition for the function getProjDirectory(). This function takes the name of the project and returns the directory for that project as stored in the projects.txt file in the workflows data directory. Since this piece of code is used often, it is worth putting into a function.

The rest of the code follows exactly the Script Filter, except for the contents of the switch..case statement. This is still one type of state machine, the input line gives the state to process. Here, the different states are: new, pow, remove, serve, alfred, and current. Each state corresponds to an action to be taken on a project as shown in the Script Filter.

New State

The new State is for creating new projects in the list of projects. It does not create anything but puts a link in the projects.txt file in the data directory. This is done by saving the name of the project into the projectName.txt file in the data directory and calling the pm:setprojectdirectory in Alfred to search for the directory to associate to the project name. This is a type of state feedback in that the script recursively calls Alfred in a new state (searching on a new token).

Notice how Alfred is called: the PHP code calls the osascript program to tell Alfred 2.app to search for pm:setprojectdirectory. This technique is the easiest way to request something from Alfred and can be implemented in any scripting language.

The end of the script in Run Script for the pm:setprojectdirectory Run Script block needs to be changed to:

	if [ -f "$(getDataDir)/projectName.txt" ]; then
		currentName=`cat "$(getDataDir)/projectName.txt"`;
		echo "$currentName:{query}" >> "$(getDataDir)/projects.txt";
		rm "$(getDataDir)/projectName.txt";
	fi

	echo "{query}" > "$(getDataDir)/projectDir.txt";

	echo "The project directory is: {query}";

What this now does is to check for the existence of the projectName.txt file when this script is called. If it exists, then the given directory and project name are added to the list of projects in the projects.txt file and the projectName.txt file is removed. Once again, this is a type of state machine where the existence of the projectName.txt file defines a new state of action for that script.

Pow State

The pow State makes use of the POW Alfred Workflow to interact with the POW program. Both of these will need to be installed on the system running this workflow. This state simply takes the project directory and project name and creates a symbolic link in the ~/.pow directory. With the symbolic link there, the POW workflow will be able to browse it and restart the server for it.

Remove State

The remove State takes the project name and removes it from the list of projects. It is up to the user to remove the directory.

Serve State

The serve State recursively calls Alfred to use the POW Alfred Workflow to view the server. The POW program checks to see if the server is running fine before passing the location to be viewed in the browser.

Alfred State

The alfred State recursively calls Alfred with the directory of the project. If Alfred is called with an absolute directory path, it will view that directory in the Alfred Browser.

Current State

The current State makes the project directory the current project for the move files function created in the intermediate tutorial.

With all the states defined this way, it is easy to add new actions to extend the functionality of this workflow. Simply add the state to the Script Filter and the action for the state in the Run Script block.

Conclusion

Wow, that was a lot to cover. Now the Project Manager workflow is useful and can be easily extended. Your homework is to take this base and add more functions! If you add to the workflow, please post your work below in the comments. Sharing workflows is a lot of fun and helps others to learn from what you have done.

Advertisement