TweetFollow Us on Twitter

Writing Contextual Menu Plugins for OS X, part 2

Volume Number: 19 (2003)
Issue Number: 1
Column Tag: Mac OS X

Writing Contextual Menu Plugins for OS X, part 2

Running Unix commands and handling text selections

by Brent Simmons

In part one of this article (August 2002) we built a simple OS X contextual menu plugin that works in the Finder and provides a Copy Path command. It demonstrated the basics of implementing the COM-based interfaces for contextual menu plugins.

In this article we won't revisit the COM interface or other basics of contextual menu plugins--instead we'll go further, show how to run a Unix command from a contextual menu, how to send an update event to the Finder when a file changes, how to handle text selections, how to create a submenu with multiple commands, how to open a URL in a browser, and more.

The project file and source, built using the OS X 10.2 Developer Tools, can be downloaded from

Plugin Overview

This plugin (imaginatively named SamplePlugin) will provide three commands:

    1. Touch--when one or more files (or folders) are selected, a Touch command will appear in the contextual menu. It will run the Unix touch command, which sets the modification date of the selected files to the current date and time.

    2. Copy--when text is selected, this command will appear in a Text Samples submenu. It copies the selected text to the clipboard. (Note that while some applications already supply a Copy command in their contextual menu, many do not, and it's useful to have this command.)

    3. Search with Google--when text is selected, this command will appear in a Text Samples submenu. It runs a search for the selected text in Google in one's default Web browser.

Touch command

Figure 1. Touch command in Finder's contextual menu

The examineContext function in the plugin is called to give the plugin a chance to add commands to the contextual menu that's about to be displayed. This plugin does one of three things:

    1. It adds a Touch command if one or more files or folders are selected.

    2. It adds a Text Samples submenu, with Copy and Search with Google commands, if text is selected.

    3. It adds no commands if neither files nor text is selected.

Listing 1: checking the context

static OSStatus examineContext (void *pluginInstance,
   const AEDesc *context, AEDescList *commandList)
   if (isFileOrListOfFiles (context))
      addFileCommandsToMenu (commandList);
   else if (isTextSelection (context))
      addTextCommandsToMenu (commandList);
   return (noErr);

examineContext first checks to see if one or more files or folders is selected. If so, it adds the file commands (the Touch command) to the contextual menu.

If the selection is not files or folders, then it checks to see if text is selected. If so, then it adds the Text Samples submenu and the Copy and Search with Google commands.

If neither of the above are true, it just returns noErr, having added no commands to the contextual menu.

The check to see if files are selected first checks to see if a single file or folder is selected. If not, then it checks to see if multiple files or folders are selected.

Listing 2: checking if files are selected

static Boolean isFileOrListOfFiles (const AEDesc *desc)
   if (isFileOrFolder (desc))
      return (true);
   return (isListOfFiles (desc));

isFileOrFolder checks a single AEDesc to see if it refers to a file or folder.

Listing 3: checking an AEDesc to see if it's a file or folder

static Boolean isFileOrFolder (const AEDesc *desc)
      return (descIsOfType (desc, typeFSRef));

If the descriptor type is of typeFSRef, or can be coerced to typeFSRef, it's a file or folder, and so it returns true. descIsOfType performs this check. It's general--it can check for types other than typeFSRef. (In fact, this plugin also calls descIsOfType to check for a text selection.)

Listing 4: checking the type of an AEDesc

static Boolean descIsOfType (const AEDesc *desc,
   OSType desiredType)
   AEDesc tempdesc = {typeNull, NULL};
   if ((*desc).descriptorType == desiredType)
      return (true);
   if (AECoerceDesc (desc, desiredType, &tempdesc)
      == noErr) {
      AEDisposeDesc (&tempdesc);
      return (true);
      } /*if*/
   return (false);

Why check for typeFSRef instead of typeAlias or typeFSS? Because Apple's message is that FSRefs are the way to go in OS X. It would probably work as well to substitute one of these other types--but when your platform vendor tells you to do it a certain way, it's probably a good idea to listen.

Lists of files

If it's not just one file or folder selected, isFileOrListOfFiles next checks to see if it's a list of files or folders selected.

isListOfFiles loops through each item in the AEDescList and calls isFileOrFolder for each item. If any calls to isFileOrFolder return false, then the selection therefore contains items other than files or folders, and so isListOfFiles returns false.

Listing 5: looping through a list

static Boolean isListOfFiles (const AEDesc* desc)
   long numitems, i;
   OSErr err;
   err = AECountItems (desc, &numitems);
   if (err != noErr)
      return (false);
   for (i = 1; i <= numitems; i++ ) {
      AEKeyword keyword;
      AEDesc tempdesc = {typeNull, NULL};
      Boolean flFile = true;
      err = AEGetNthDesc (desc, i, typeWildCard,
         &keyword, &tempdesc);
      if (err != noErr)
         return (false);
      flFile = isFileOrFolder (&tempdesc);
      AEDisposeDesc (&tempdesc);
      if (!flFile)
         return (false);
   return (true);

AECountItems gets the number of items in the list. Then a for loop visits each item, calling isFileOrFolder with each. If isFileOrFolder returns false, then isListOfFiles returns false.

Otherwise it returns true, and then isFileOrListOfFiles returns true, and we're back to examineContext, which then calls addFileCommandsToMenu to add the Touch command to the contextual menu.

Adding the Touch menu command

To add the Touch command, this function calls pushCommand with the title of the command, its command ID, and the command list that will become the contextual menu that appears. The final parameter sent to pushCommand is NULL--if this item had a submenu it would be specified in the last parameter.

Listing 6: adding the localizable Touch command

static Boolean addFileCommandsToMenu
   (AEDescList *commandList)
   CFStringRef touchCommand =
      CFCopyLocalizedString (CFSTR("Touch"),
      "Touch Menu Text");
   return (pushCommand (touchCommand, touchCommandID,
      commandList, NULL));

Note that it gets the name of the Touch command by calling CFCopyLocalizedString. This looks up the name in MenuNames.strings (a resource that's included with the project you downloaded). This way you can easily localize the plugin by editing a strings file--which is definitely preferred to hard-coding the names of menu items in the source code.

We'll otherwise skip pushCommand for now--we'll talk about it later when we show how to build submenus. For now just know that it adds the Touch command to the contextual menu.

touchCommandID is defined in SamplePlugin.h. The command ID, since it's just used internally, does not have to be localized, of course.

After this function returns, examineContext returns noErr, and the system handles displaying and tracking the contextual menu.

Handling a command

handleSelection is called by the system to actually run a command chosen by the user.

Listing 7: handling the user's command

static OSStatus handleSelection (void *pluginInstance,
   AEDesc *context, SInt32 commandID)
   switch (commandID) {
      case touchCommandID:
         runTouchCommand (context);
      case copyCommandID:
         runCopyCommand (context);
      case searchCommandID:
         runSearchCommand (context);
   return (noErr);

Based on the command ID associated with the given command, it calls the corresponding function. If Touch is chosen, it calls runTouchCommand. There are similar cases for the Copy and Search with Google commands (more about those later).

Running the Touch command

The goal of this function is to pass to the system a string of text as if typed on the command line. We're calling the Unix touch command with one or more files or folders as arguments. The string that gets passed to the system will look something like this:

/usr/bin/touch "/path/to/some/file" "/path/to/some/other/file"

Each separate file path is enclosed in quotes because there may be spaces in the name.

So the first thing runTouchCommand does is get the list of selected files as a string suitable for passing to the system as command line arguments. Then it actually calls the system to run the Touch command. Finally it sends an update event to the Finder so it updates its display.

Listing 8: running the command

static void runTouchCommand (const AEDesc *desc)
   CFStringRef commandLineParams;
   if (!getFileListAsText (desc, &commandLineParams))
   callSystem (CFSTR("/usr/bin/touch"), commandLineParams);
   CFRelease (commandLineParams);
   sendUpdateEventToFinder (desc);

getFileListAsText is somewhat similar to the isListOfFiles function--it loops through the selected files in the same way. It also double-checks that each item actually is a file or folder.

Listing 9: getting the list of selected files as text

static Boolean getFileListAsText (const AEDesc *desc,
   CFStringRef *fileList)
   long numitems, i;
   OSErr err;
   Boolean flSuccess = false;
   CFMutableStringRef s = CFStringCreateMutable
      (kCFAllocatorDefault, 0);
   if (s == NULL)
      return (false);
   err = AECountItems (desc, &numitems);
   require_noerr (err, getFileListAsText_fail);
   for (i = 1; i <= numitems; i++ ) {
      AEKeyword keyword;
      AEDesc tempdesc = {typeNull, NULL};
      Boolean flFile = false;
      err = AEGetNthDesc (desc, i, typeWildCard, &keyword,
      require_noerr (err, getFileListAsText_fail);
      flFile = (tempdesc.descriptorType == typeFSRef);
      if (!flFile) {
         err = AECoerceDesc (&tempdesc, typeFSRef, &tempdesc);
         require_noerr (err, getFileListAsText_fail);
         flFile = (tempdesc.descriptorType == typeFSRef);
      if (!pushFileAsText (&tempdesc, s))
         flFile = false;
      AEDisposeDesc (&tempdesc);
      if (!flFile)
         return (false);
   flSuccess = true;
   if (flSuccess)
      *fileList = CFStringCreateCopy
         (kCFAllocatorDefault, s);
      *fileList = NULL;
   CFRelease (s);
   return (flSuccess);

One of the parameters, fileList, is a pointer to a CFStringRef that will contain the list of files as quote-enclosed arguments. For each item, pushFileAsText is called, which adds each item to a CFMutableString created at the top of this function.

At the end of the function the CFMutableString is copied to the fileList parameter, which is a non-mutable CFString. (That is, if everything went well. In case of a problem the function returns false and fileList is NULL.)

Adding a single file to the list

pushFileAsText takes one AEDesc that references a file or folder, gets its Unix path, escapes double quotes in the path, escapes $ characters in the path, puts double quotes around the path, adds a space, then adds the text to the passed-in CFMutableString.

Listing 10: adding a single file

static Boolean pushFileAsText (const AEDesc *desc,
   CFMutableStringRef s)
   CFStringRef pathString, escapedPathString;
   if (!getPathStringFromFSRef (desc, &pathString))
      return (false);
   if (!escapeShellText (pathString, &escapedPathString)) {
      CFRelease (pathString);
      return (false);
   CFStringAppend (s, CFSTR("\""));
   CFStringAppend (s, escapedPathString);
   CFStringAppend (s, CFSTR("\" "));
   CFRelease (pathString);
   CFRelease (escapedPathString);
   return (true);

It calls getPathStringFromFSRef to get the path to the file as a CFString.

Then it escapes quotes and $ characters inside the path string--if there are quotes anywhere in the path they must be escaped, otherwise there would be quote mis-matches in the command line text. $ characters must be escaped to avoid variable interpolation. (For instance, if you had a file named $USER for some strange reason, the system would replace $USER with your actual user name. We don't want that to happen here.)

It then adds a quote to the mutable string, then adds the escaped path, then adds another quote and a space. You end up with a string like "/path/to/some/file" or "/path to some/file" or "/path to/\$some\" file/."

Getting the path string

getPathStringFromFSRef gets a Unix path string from an FSRef and puts it into a CFString.

Listing 11: getting a path string from an FSRef

static Boolean getPathStringFromFSRef (const AEDesc *desc,
   CFStringRef *pathString)
   FSRef fileRef;
   Size dataSize = AEGetDescDataSize (desc);
     OSErr err;
   CFURLRef fileURL;
   err = AEGetDescData (desc, &fileRef, dataSize);
   if (err != noErr)
      return (false);
   fileURL = CFURLCreateFromFSRef (kCFAllocatorDefault,
   if (fileURL == NULL)
      return (false);
   *pathString = CFURLCopyFileSystemPath (fileURL,
   return (*pathString != NULL);

First it gets the FSRef object from the AEDesc by calling AEGetDescData. CFURLCreateFromFSRef gets a CFURLRef from the FSRef--and from that it gets the Unix-style path by calling CFURLCopyFileSystemPath.

getCFString is a simple wrapper around CFStringCreateWithCString, which creates a CFString based on a C string.

Escaping text for the command line

Back to pushFileAsText: it next calls escapeShellText. It does a couple find-and-replace operations in a CFString. It escapes double quotes and $ characters to avoid quote mis-matches (when a file contains a double quote in its name) and variable interpolation (on the off chance you have a file named something like $USER).

Listing 12: escapeShellText

static Boolean escapeShellText (CFStringRef source,
   CFStringRef *dest)
   CFStringRef tempString;
   Boolean flSuccess = false;
   if (!replaceAll (CFSTR("\""), CFSTR("\\\""),
      source, &tempString))
      return (false);
   flSuccess = replaceAll (CFSTR("$"), CFSTR("\\$"),
      tempString, dest);
   if (flSuccess)
      CFRelease (tempString);
   return (flSuccess);

replaceAll actually performs the replacements.

Listing 13: replaceAll

static Boolean replaceAll (CFStringRef searchFor, CFStringRef replaceWith, CFStringRef source, 
CFStringRef *dest)
   CFMutableStringRef mutableString;
   CFRange range;
   mutableString = CFStringCreateMutableCopy
      (kCFAllocatorDefault, 0, source);
   if (mutableString == NULL)
      return (false);
   range = CFRangeMake (0,
      CFStringGetLength (mutableString));
   CFStringFindAndReplace (mutableString, searchFor,
      replaceWith, range, 0);
   *dest = CFStringCreateCopy (kCFAllocatorDefault,
   CFRelease (mutableString);
   return (*dest != NULL);

The first parameter to replaceAll is the string to find, the second parameter is the replacement string, the third parameter is the source string in which to search, and the last parameter is a pointer to a CFStringRef that will contain the result.

Aside: CFStrings

Question: why use CFStringRefs and CFMutableStringRefs? Why not traditional C strings or Pascal strings?

Because, as with FSRefs, Apple's message is that CFStrings are the way to go. But beyond that they have several benefits:

    1. They're easy to manipulate. Functions like CFStringAppend make it very easy, for instance, to add text to a string. Check out CFString.h--there is a gold mine of functions which make string manipulation easy.

    2. CFStrings are "toll-free bridged" with Cocoa NSStrings. This means that, among other things, when writing Cocoa code, you can call CFString functions with NSStrings as parameters. This is an example of a kind of convergence, where you can write code that works in Carbon apps as well as Cocoa apps.

    3. CFStrings take some of the headache out of supporting various character encodings. Support for Unicode and other text encodings is an important part of a modern operating system, and CFStrings can store Unicode text as well as other encodings.

Back to runTouchCommand

Now it's time to actually call the system to run the Touch command.

If getFileListAsText succeeds, runTouchCommand calls callSystem, which takes two parameters: the path to the command to execute and the command line parameters (as a single string).

The path to the Touch command is /usr/bin/touch. The command line parameters in this case is the list of files created in getFileListAsText.

Note the line (in runTouchCommand): callSystem (CFSTR("/usr/bin/touch"), commandLineParams);

The CFSTR("/usr/bin/touch") part is a shortcut for creating a CFStringRef from quoted text. (Cocoa developers will note that it's the equivalent of typing @"/usr/bin/touch" to create an NSString.)

Calling the system

It's simple to call the system to execute a command-line string. There's a function actually named system that takes a C string and executes the command as if typed in the Terminal.

(The man page for system is pretty short and worth checking out.)

Listing 14: calling the system

static void callSystem (CFStringRef command,
   CFStringRef params)
   char *buffer;
   CFMutableStringRef fullCommand;
   fullCommand = CFStringCreateMutableCopy
      (kCFAllocatorDefault, 0, command);
   if (fullCommand == NULL)
   CFStringAppend (fullCommand, CFSTR(" "));
   CFStringAppend (fullCommand, params);
   if (!getCString (&buffer, fullCommand,
      goto callSystem_exit;
   printf ("System call: ");
   printf (buffer);
   printf ("\n");
   system (buffer);
   CFRelease (fullCommand);
   if (buffer != NULL)
      free (buffer);

Our callSystem function takes two CFStringRef parameters--the path to the command and the command-line parameters. In order to call the system command it needs to create a C string of this form: /path/to/command param1 param2 param3.

In this plugin it will look something like this: /usr/bin/touch "/path/to/file1" "/path/to/anotherFile"

The fullCommand string is a mutable CFString. It starts by copying the command (the /usr/bin/touch part) to itself.

Then it appends a space, since there needs to be a space between the command and the parameters. Then it appends the parameters.

At this point we have the string in the right form for the system, except that it's a CFString rather than a C string. So it calls getCString to get a C string.

Listing 15: getCString

static Boolean getCString (char **cStringToGet,
   CFStringRef cfString, CFStringEncoding encoding)
   UInt32 lenText = sizeof (UniChar) *
      CFStringGetLength (cfString) + 1;
   *cStringToGet = malloc (lenText);
   if (*cStringToGet == NULL)
      return (false);
   if (!CFStringGetCString (cfString, *cStringToGet,
      lenText, encoding)) {
      free (*cStringToGet);
      *cStringToGet = NULL;
      return (false);
   return (true);

This function is a wrapper for CFStringGetCString, which gets a C string from a CFString. First this function allocates a buffer that's the length of the CFString plus one for zero-termination. Then it calls CFStringGetCString. If that call fails, the buffer is freed and the function returns false. If all's well it returns true.

Back to callSystem

Before calling the system function, notice the trio of printf commands. An important point: one way to debug contextual menus is to use printf commands. The output appears in the Console. (Which lives at /Applications/Utilities/Console.) If you launch it, then run the Touch command from a contextual menu, you'll see a copy of what gets passed to the system command in the Console.

Though not used here, another important function, similar to printf, is the CFShow command. This prints a description of a CoreFoundation object--such as a CFString--to the Console. (See CFShow and CFShowStr in CFString.h and in the documentation.)

Okay, now it finally calls the system with the C string from getCString. This runs the Unix touch command with the list of files as parameters.

This function cleans up, then control goes back to runTouchCommand, then back to handleSelection which returns noErr.

That's it for running a Unix command. You know the basics--you can do just about anything Unix-y from a contextual menu plugin now.

But it didn't work...

Oh yes it did. But it might not have appeared to work.

Here's why: sometimes the Finder doesn't update its display right away when you make a change to a file from a contextual menu. And so it may look like the command didn't work, even though it actually did.

So runTouchCommand sends the Finder a kAESync event via the sendUpdateEventToFinder function:

Listing 16: sending a kAESync event to the Finder

static void sendUpdateEventToFinder (const AEDesc *desc)
   AEAddressDesc finderAddressDesc = {typeNull, NULL};
   AppleEvent event = {typeNull, NULL};
   AppleEvent reply = {typeNull, NULL};
   OSType finderSignature = 'MACS';
   OSErr err;
   err = AECreateDesc (typeApplSignature, &finderSignature,
      sizeof (finderSignature), &finderAddressDesc);
   if (err != noErr)
   err = AECreateAppleEvent (kAEFinderSuite, kAESync,
      &finderAddressDesc, kAutoGenerateReturnID,
      kAnyTransactionID, &event);
   require_noerr (err, sendUpdateEventToFinder_fail);
   err = AEPutParamDesc (&event, keyDirectObject, desc);
   require_noerr (err, sendUpdateEventToFinder_fail2);
   AESend (&event, &reply, kAENoReply + kAECanInteract,
      kAENormalPriority, kAEDefaultTimeout, NULL, NULL);
   AEDisposeDesc (&event); 
   AEDisposeDesc (&finderAddressDesc);

It creates an Apple event that just asks the Finder to update its display. (See FinderRegistry.h for this and other commands you can send the Finder.)

This function passes to the Finder, as the direct object parameter, the same AEDesc context received in handleSelection. One assumes that the Finder knows what to do with this AEDesc since it came from the Finder in the first place.

kAENoReply is specified, since a reply wouldn't be useful, and we don't really care if the Finder failed to handle the event. We hope it handled the event, but if it didn't it's no big deal, since the Touch command succeeded anyway.

Text commands and submenus

Let's back all the way up to examineContext and show how to add a submenu to the contextual menu and how to handle our two text commands (Copy and Search with Google).

Recall that if it's not one or more files or folders that are selected, then examineContext checks to see if it's text that's selected. isTextSelection performs this check. If the check returns true, then examineContext calls addTextCommandsToMenu to add the submenu and its commands.

Figure 2. Text commands in BBEdit's contextual menu


This function checks the AEDesc that examineContext got from the system to see if it's of type text or can be coerced to type text.

Listing 17: checking if a selection is a text selection

static Boolean isTextSelection (const AEDesc *desc)
      return (descIsOfType (desc, typeChar));

typeChar, defined in AEDataModel.h, is 'TEXT.' This function calls descIsOfType, which was also called by isFileOrFolder to determine if an AEDesc refers to a file or folder.

If isTextSelection returns true, then examineContext calls addTextCommandsToMenu with the AEDescList from the system that will become the contextual menu.

Adding text commands

The plugin will build a menu item named Text Samples with a submenu. The submenu will contain two commands: Copy and Search with Google.

Here things are done in what may seem like a backward order. First the submenu (Copy and Search with Google commands) is created, then the menu item is created that will contain the submenu.

Listing 18: adding text commands to the menu

static Boolean addTextCommandsToMenu (AEDescList*
   AEDescList submenuCommands = {typeNull, NULL};
   OSErr err;
   Boolean flSuccess = false;
   CFStringRef textSubmenuName =
      CFCopyLocalizedString (CFSTR("Text Samples"),
         "Text Samples Menu Text");
   err = AECreateList (NULL, 0, false, &submenuCommands);
   if (err != noErr)
      return (false);
   if (!buildTextSubmenu (&submenuCommands)) {
      AEDisposeDesc (&submenuCommands);
      return (false);
   flSuccess = pushCommand (textSubmenuName, nil,
      commandList, &submenuCommands);
   AEDisposeDesc (&submenuCommands);
   return (flSuccess);

The submenu is, like the commandList passed to examineContext from the system, an AEDescList that will specify one or more commands. submenuCommands will hold the submenu: buildTextSubmenu creates this submenu.

Listing 19: building a submenu

static Boolean buildTextSubmenu (AEDescList *commands)
   CFStringRef searchCommand = CFCopyLocalizedString
      (CFSTR("Search with Google"), "Search Menu Text");
   CFStringRef copyCommand = CFCopyLocalizedString
      (CFSTR("Copy"), "Copy Menu Text");
   if (!pushCommand (copyCommand, copyCommandID,
      commands, nil))
      return (false);
   return (pushCommand (searchCommand, searchCommandID,
      commands, nil));

It calls pushCommand twice, with the command names, command IDs, and the AEDescList that will contain the commands.

If it succeeds, then addTextCommandsToMenu calls pushCommand to add the Text Samples menu item that will contain the submenu.

Here's the important point: both the main contextual menu and any submenus are AEDescLists. That means that you add commands the same way, whether it's the main menu or any submenus. pushCommand is thus a reusable piece of code, useful whether you're building one or more submenus or not.

Listing 20: adding a command to a menu

static Boolean pushCommand (CFStringRef commandName,
   SInt32 commandID, AEDescList* commands,
   AEDescList *submenuToAttach)
   OSStatus err = noErr;
   AERecord commandRecord = {typeNull, NULL};
   Boolean flSuccess = false;
   char *commandNameCString;
   err = AECreateList (NULL, 0, true, &commandRecord);
   require_noerr (err, pushCommand_fail);
   if (!getCString (&commandNameCString, commandName,
      goto pushCommand_fail;
   err = AEPutKeyPtr (&commandRecord, keyAEName,
      typeUTF8Text, commandNameCString,
      strlen (commandNameCString) + 1);
   free (commandNameCString);
   require_noerr (err, pushCommand_fail);
   if (commandID != NULL) {
      err = AEPutKeyPtr (&commandRecord,
         typeLongInteger, &commandID, sizeof (commandID));
      require_noerr (err, pushCommand_fail);
   if (submenuToAttach != NULL) {
      err = AEPutKeyDesc (&commandRecord,
         keyContextualMenuSubmenu, submenuToAttach);
      require_noerr (err, pushCommand_fail);
   err = AEPutDesc (commands, 0, &commandRecord);
   if (err == noErr)
      flSuccess = true;
   AEDisposeDesc (&commandRecord);
   return (flSuccess);

When using pushCommand to add a menu item that contains submenu, just call it with a NULL commandID and a non-NULL AEDescList that specifies the submenu it contains (submenuCommands, in this case).

When using pushCommand to add a menu item that does not contain a submenu, specify its command ID, and send a NULL AEDescList, since it will have no submenu.

When addTextCommandsToMenu is finished, control returns to examineContext, which returns noErr. Then the system displays and tracks the contextual menu with the Text Samples menu item and its submenu.

Handling the text commands

Back to handleSelection--if the command ID is the ID of the copy command, runCopyCommand is called. If it's the command ID of the search command, runSearchCommand is called. Let's do runCopyCommand first.

The Copy command copies the selected text to the clipboard (unsurprisingly).

Listing 21: running the Copy command

static void runCopyCommand (const AEDesc *desc)
   CFStringRef selectedText;
   if (!getTextFromDesc (desc, &selectedText,
   writeStringToClipboard (selectedText);
   CFRelease (selectedText);

First it gets the selected text from the AEDesc via getTextFromDesc, then it writes that text to the clipboard via writeStringToClipboard, then it cleans up and returns to handleSelection.


Any time you're handling text selections you need to get the selected text. getTextFromDesc gets it as a CFString.

Listing 22: getting text from an AEDesc

static Boolean getTextFromDesc (const AEDesc *desc,
   CFStringRef *text, CFStringEncoding encoding)
   long len = AEGetDescDataSize (desc);
   char *s;
   Boolean flSuccess = false;
   s = malloc (len + 1);
   if (s == NULL)
      return (false);
   AEGetDescData (desc, s, len);
   s [len] = '\0';
   flSuccess = getCFString (text,
      (const char *) s, encoding);
   free (s);
   return (flSuccess);

AEGetDescDataSize tells us how long the selected text is. It then allocates a buffer to get that text, then calls AEGetDescData to put it into the buffer. It zero-terminates the buffer so that it's a C string.

The function then calls getCFString to get a CFString based on the C string it got from the AEDesc.

After freeing the buffer (which is no longer needed) it returns true if the CFString was created and false otherwise.

getCFString is a simple wrapper around CFStringCreateWithCString.

Listing 23: getting a CFString from a C string

static Boolean getCFString (CFStringRef *stringToGet, const char *cString, CFStringEncoding encoding)
   *stringToGet = CFStringCreateWithCString
      (kCFAllocatorDefault, cString, encoding);
   return (*stringToGet != NULL);

runCopyCommand then writes the text to the clipboard via writeStringToClipboard.

Listing 24: writing text to the clipboard

static void writeStringToClipboard (CFStringRef s)
   ScrapRef scrap;
   char *cString;
   if (!getCString (&cString, s, kCFStringEncodingMacRoman))
   ClearCurrentScrap ();
   GetCurrentScrap (&scrap);
   PutScrapFlavor (scrap, kScrapFlavorTypeText,
      kScrapFlavorMaskNone, strlen (cString),
      (const void *) cString);
   free (cString);

Note that this seems a little crazy--in getTextFromDesc we created a CFString from a C string, then in writeStringToClipboard we turn around and get a C string from a CFString. Why?

Well, in the interests of maintainability, I like having just one function that gets the selected text. Instead there could be one function that returns it as a CFString and another that returns it as a C string. I prefer getting a CFString because CFStrings are so easy to manipulate, and chances are you're going to want to do some operation on the selected text.

In this case, however, we're not manipulating the selected text in any way, just writing it to the clipboard.

Again, the writeStringToClipboard function could take a C string rather than a CFString, but my bet is that most of the time I'll have a CFString, since I use CFStrings so much.

In other words, what I've done is standardize on using CFStrings internally, so it's only at the edges--when getting text from an AEDesc, writing text to the clipboard, or calling the system function--where it's sometimes necessary to convert to or from a CFStringRef. (And, as you'll note later in the Search with Google command, there are cases where no conversion is necessary as more and more APIs take CFStrings.)

So writeStringToClipboard works in some ways like the callSystem command--it takes the CFStringRef and gets a C string. A difference here is what it does with it--it clears the current scrap (the clipboard), gets a reference to the current scrap, then writes the C string as text to the clipboard. Finally it frees the C string it got.

That's it for the Copy command. On to Search with Google.

Running the Search with Google command

This command gets the selected text then creates a search URL string based on a base URL. The search URL is then opened in one's default Web browser.

Listing 25: running the Search command

static void runSearchCommand (const AEDesc *desc)
   CFStringRef selectedText, urlString;
   if (!getTextFromDesc (desc, &selectedText,
   if (!createEncodedSearchURLString
      (CFSTR(googleSearchURL), selectedText, &urlString)) {
      CFRelease (selectedText);
   openInBrowser (urlString);   
   CFRelease (selectedText);
   CFRelease (urlString);

As in runCopyCommand, it calls getTextFromDesc to get the selected text as a CFString.

Then it creates the URL string to pass to the browser by calling createEncodedSearchURLString. Then it calls openInBrowser with that URL string to open it in the browser. Then it cleans up and returns.

Encoding a URL string

This function demonstrates some of the utility of CFStrings. It takes a string like as the base URL and a string like "some selected text" as the search arguments. It creates a string that a Web browser would understand, as in

In our case the base URL is (defined in SamplePlugin.h), so the final URL string will look something like

Listing 26: Encoding a URL string

static Boolean createEncodedSearchURLString (CFStringRef
   baseURL, CFStringRef searchArgs, CFStringRef *dest)
   CFStringRef urlString, urlStringPlusEncoded;
   urlString = CFStringCreateWithFormat
      (kCFAllocatorDefault, NULL, ("%@%@"),
      baseURL, searchArgs);
   if (!replaceAll (CFSTR(" "), CFSTR("+"), urlString,
      &urlStringPlusEncoded)) {
      CFRelease (urlString);
      return (false);
   *dest = CFURLCreateStringByAddingPercentEscapes
      (kCFAllocatorDefault, urlStringPlusEncoded, NULL, NULL,
   CFRelease (urlStringPlusEncoded);
   CFRelease (urlString);
   return (*dest != NULL);

First it concatenates the base URL and the search args via CFStringCreateWithFormat. (Cocoa developers, note how similar this is to NSString's stringWithFormat method.)

Then it calls replaceAll to replace spaces with + characters (which is what Web browsers and servers want us to do).

Then it's necessary to URL-encode the URL string--characters such as e and so on have to be converted to percent-encoding. CFURLCreateStringByAddingPercentEscapes does this.

Finally it cleans up, and *dest is the encoded string.

Opening a URL in the default Web browser

runSearchCommand then calls openInBrowser to actually open the URL in one's default Web browser.

Listing 27: opening a URL

static void openInBrowser (CFStringRef urlString)
   CFURLRef urlRef = CFURLCreateWithString
      (kCFAllocatorDefault, urlString, NULL);
   if (urlRef != NULL) {
      LSOpenCFURLRef (urlRef, NULL);
      CFRelease (urlRef);

The sole parameter is a CFStringRef, the encoded URL string created in createEncodedSearchURLString. The system call for opening a URL in a browser wants a CFURLRef, which is created via CFURLCreateWithString. (Cocoa developers: this is like NSURL's URLWithString method.)

Then the LaunchServices function LSOpenCFURLRef is called. This useful function also opens local URLs, doing different things depending on what kind of URL it has. In this case it's an HTTP URL, and so it opens the URL in the default browser. Note that the code doesn't even know or care what the default browser is: the system handles it for us. (Cocoa developers note how this is like NSWorkspace's openURL method.)

You can get more control over how the default browser opens the URL by calling LSOpenFromURLSpec--for instance, you can tell the browser not to come to the front. But in this sample we use LSOpenCFURLRef because the default behavior is what we want anyway.

After calling openInBrowser, runSearchCommand cleans up and returns control to handleSelection, which returns noErr.

Room for improvement

There some things this plugin could do better--but since this an article rather than a book, we'll leave that up to you. A couple obvious things:

When getting text to write to the clipboard or run a search on, it naively assumes MacRoman text encoding, which is not necessarily the case. It's unlikely the results will be satisfactory on, for instance, Japanese systems.

Another issue is error reporting--this plugin just fails silently when something goes wrong. At a minimum it could write an error message to the Console. But since most people don't leave their Console running, a better choice would be to display a dialog box letting the user know the error.

Bonus Tips


I mentioned earlier that printf, CFShow, and CFShowStr are useful for debugging--you can print to the Console to help you figure out what's going on.

But what if you want to really debug--that is, set breakpoints and step through your code? A plug-in isn't an application, so you can't just run and debug it as-is. Here's what you do:

From the Project menu choose New Custom Executable. Click the Set... button in the dialog that appears. Choose an application that supports system contextual menus. Me, I always use Script Editor, just because it launches very quickly on my machine. Click the Finish button.

Figure 3. Attaching an executable to the plugin

Then, before debugging your plugin, set a breakpoint somewhere--in examineContext, for instance. To begin debugging, choose Debug Executable from the Debug menu. Script Editor (or whatever app you chose) will launch.

Type some text in the app you chose, select it, then ctrl-click (or right-click) on the selected text. Project Builder's debugger should come to the front with execution stopped at your breakpoint. From there you can debug as normal.

(Unfortunately I have not figured out how to get this to work with the Finder, but it works very well for other apps.)

So your cycle becomes like this: build your plugin, delete the old version from your ~/Library/Contextual Menu Items/ folder, copy the new build to your Contextual Menu Items folder, choose Debug Executable, then debug. Lather, rinse, repeat.

Quitting apps

Remember that, when you rebuild a plugin and put it in the Contextual Menu Items folder, you need to quit any app you're testing with in order to force it to reload the plugin. What's the fastest way to quit an app that's not in front? Ctrl-click (or right-click) on its icon in the Dock and choose Quit from the pop-up menu.

This doesn't work with the Finder. However, you can enable a Quit menu item for the Finder so that you can type Cmd-Q in the Finder. In Terminal, type: defaults write QuitMenuItem Enabled

You will have to log out then log back in for the changes to take effect. If later on you want to disable the Quit menu item in the Finder, in Terminal type: defaults write QuitMenuItem Disabled

Quit the Finder and re-start it (by clicking its icon in the Dock) and the Quit menu item will be gone.


To summarize a few important points from this article:

    - To run a Unix command, call the system function with a C string, with text that looks like what you would type on the command line. Remember to add and escape quotes and $ characters as needed.

    - CFStrings are good to use for lots of reasons, but the best one is that it makes string manipulation easy.

    - Whether adding a command to the main contextual menu or to a submenu, you're doing the same thing, adding an item to an AEDescList. There's no difference.

    - There are other high-level routines like LSOpenCFURLRef that do things like open a URL in the default browser. If you find yourself doing things like writing code to support different browsers, there's a good chance you're working way too hard. Pay special attention to the CoreFoundation and LaunchServices frameworks. Even Cocoa developers can benefit by looking at these frameworks, since there are useful functions there that have no Cocoa equivalent but that work with NSStrings and NSURLs and so on.

    - printf, CFShow, and CFShowStr are useful for debugging--but, even better, you can truly debug a plugin by attaching an executable and setting a breakpoint.

    - Remember to quit apps you're testing with after rebuilding your plugin. You can even enable a Quit item for the Finder via the defaults Terminal command.

Thanks to...

For review and feedback for this article, thanks to Jim Correia, Quentin D. Carnicelli, and George Warner. Any remaining errors or oddities are mine.


Brent Simmons is a Seattle-based independent Mac OS X developer and writer. He runs a Mac developer news weblog at; he can be contacted at


Community Search:
MacTech Search:

Software Updates via MacUpdate

1Password 6.5.5 - Powerful password mana...
1Password is a password manager that uniquely brings you both security and convenience. It is the only program that provides anti-phishing protection and goes beyond password management by adding Web... Read more
Apple Remote Desktop Client 3.9 - Client...
Apple Remote Desktop Client is the best way to manage the Mac computers on your network. Distribute software, provide real-time online help to end users, create detailed software and hardware reports... Read more
Art Text 3.2.2 - $49.99
Art Text is graphic design software specifically tuned for lettering, typography, text mockups and various artistic text effects. Supplied with a great variety of ready to use styles and materials,... Read more
WhatRoute 2.0.15 - Geographically trace...
WhatRoute is designed to find the names of all the routers an IP packet passes through on its way from your Mac to a destination host. It also measures the round-trip time from your Mac to the router... Read more
Sparkle 2.1.1 - $79.99
Sparkle will change your mind if you thought building websites wasn't for you. Sparkle is the intuitive site builder that lets you create sites for your online portfolio, team or band pages, or... Read more
Dash 4.0.1 - Instant search and offline...
Dash is an API documentation browser and code snippet manager. Dash helps you store snippets of code, as well as instantly search and browse documentation for almost any API you might use (for a full... Read more
TextSoap 8.3.2 - Automate tedious text d...
TextSoap can automatically remove unwanted characters, fix up messed up carriage returns, and do pretty much anything else that we can think of to text. Save time and effort. Be more productive. Stop... Read more
Apple Remote Desktop 3.9 - Remotely cont...
Apple Remote Desktop is the best way to manage the Mac computers on your network. Distribute software, provide real-time online help to end users, create detailed software and hardware reports, and... Read more
Paragraphs 1.1.4 - Writing tool just for...
Paragraphs is an app just for writers. It was built for one thing and one thing only: writing. It gives you everything you need to create brilliant prose and does away with the rest. Features... Read more
Amazon Chime 4.0.5528 - Amazon-based com...
Amazon Chime is a communications service that transforms online meetings with a secure, easy-to-use application that you can trust. Amazon Chime works seamlessly across your devices so that you can... Read more

Blasty Bubs is a colorful Pinball and Br...
QuickByte Games has another arcade treat in the works -- this time it's a mishmash of brick breaking and Pinball mechanics. It's called Blasty Bubs, and it's a top down brickbreaker that has you slinging balls around a board. [Read more] | Read more »
Corsola and Heracross are the new region...
Generation 2 finally launched in Pokémon GO, unleashing a brand new batch of Pokémon into the wild. Even before the update went live people were speculating on how to catch elusive Pokémon like the legendary "dogs", Unknown, and whether or not... | Read more »
The Warlock of Firetop Mountain (Games)
The Warlock of Firetop Mountain 1.0 Device: iOS Universal Category: Games Price: $4.99, Version: 1.0 (iTunes) Description: An epic adventure through a mysterious mountain filled with monsters, magic and mayhem! “ looks downright... | Read more »
Fantasy MMORPG MU Origin’s receives a hu...
Developer Webzen are looking to take their highly popular fantasy battler MU Origin to the next level this month, with its most ambitious overhaul yet. The latest update introduces the long sought after Server Arena, new treasure dungeons, and much... | Read more »
RPG Djinn Caster (Games)
RPG Djinn Caster 1.0.0 Device: iOS Universal Category: Games Price: $4.99, Version: 1.0.0 (iTunes) Description: SPECIAL PRICE 38% OFF(USD 7.99 -> USD 4.99)!!!A Fantasy Action RPG of far foreign lands! Summon the Djinns and rise to... | Read more »
Alto's Odyssey gets its first trail...
There's finally video evidence of Alto's Odyssey, the follow up to the 2015 App Store hit, Alto's Adventure. It looks just as soothing and atmospheric as Alto's last outing, but this time players will be journeying to the desert. Whereas Alto's... | Read more »
Last week on Pocket Gamer
What’s going on in the wider world of portable gaming? Each week we ask that question of our sister website Pocket Gamer. The PG team covers iOS gaming, just like 148Apps, but it also strays into the world of Android games and handheld consoles... | Read more »
Pokémon GO Generation 2 evolution guide
At long last, Niantic Labs finally unleashed the Generation 2 Pokémon into the wild. Pokémon GO trainers are scrambling to grab up this new set of 80 Pokémon. There are some special new tricks required to catch all of these new beasties, though.... | Read more »
The best new games we played this week
It feels as though the New Year got off to a creaking start as far as mobile games go, but that's changed over the past few weeks. The last few days alone have seen the debut of a number of wonderful games, so we thought we'd take the time to... | Read more »
Recruit more scallywags and discover new...
Get ready to show off your sea legs all over again in Oceans & Empires’ new grand update, which aims to make the act of rising to the role of seven seas ruler even more fresh and appealing, thanks to a richness of new content on both iOS and... | Read more »

Price Scanner via

Apple’s New iPad Ads Don’t Address Pro Users’...
Apple launched a new tranche of iPad Pro TV ads last week addressing actual queries and challenges from the Twitterverse, albeit using actors for the visuals. That’s great. As an iPad fan and heavy... Read more
Free Verbum Catholic Bible Study App For iOS
The Verbum mobile app runs on Logos’ powerful Bible software and is an advanced resource for mobile Catholic study. The Verbum app surrounds the Bible with the Tradition. Verbum comes with 15 free... Read more
27-inch Apple iMacs on sale for up to $200 of...
B&H Photo has 27″ Apple iMacs on sale for up to $200 off MSRP, each including free shipping plus NY sales tax only: - 27″ 3.3GHz iMac 5K: $2099.99 $200 off MSRP - 27″ 3.2GHz/1TB Fusion iMac 5K: $... Read more
15-inch 2.2GHz Retina MacBook Pro on sale for...
Amazon has 2015 15″ 2.2GHz Retina MacBook Pros (MJLQ2LL/A) available for $1849.99 including free shipping. Apple charges $1999 for this model, so Amazon’s price is represents a $150 savings. Read more
Apple refurbished iPad Air 2s available start...
Apple has Certified Refurbished iPad Air 2 WiFis available for starting at $319 including free shipping. A standard Apple one-year warranty is included: - 16GB iPad Air 2 WiFi: $319 $60 off original... Read more
Apple refurbished iPad Pros available for up...
Apple has Certified Refurbished 9″ and 12″ Apple iPad Pros available for up to $160 off the cost of new iPads. An Apple one-year warranty is included with each model, and shipping is free: - 32GB 9″... Read more
Apple restocks refurbished 2015 and 2016 13-i...
Apple has Certified Refurbished 2015 and 2016 13″ MacBook Airs available starting at $759. An Apple one-year warranty is included with each MacBook, and shipping is free: - 2016 13″ 1.6GHz/8GB/128GB... Read more
13-inch 2.5GHz MacBook Pro (Apple refurbished...
Apple has Certified Refurbished 13″ 2.5GHz MacBook Pros (MD101LL/A) available for $829, or $270 off original MSRP. Apple’s one-year warranty is standard, and shipping is free: - 13″ 2.5GHz MacBook... Read more
QuickerTek Announces 5TB Apple AC AirPort Tim...
QuickerTek Inc. has announced their new 5TB hard drive upgrade for Apple’s AC AirPort Time Capsule. By customer request, this upgrade also features six external antennas and offers the highest... Read more
Apple Certified Refurbished iMacs available f...
Apple has Certified Refurbished 2015 21″ & 27″ iMacs available for up to $350 off MSRP. Apple’s one-year warranty is standard, and shipping is free. The following models are available: - 21″ 3.... Read more

Jobs Board

*Apple* Retail - Multiple Positions - Apple,...
Job Description: Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
Manager *Apple* Systems Administration - Pu...
Req ID 3315BR Position Title Manager, Apple Systems Administration Job Description The Manager of Apple Systems Administration oversees the administration and Read more
*Apple* Retail - Multiple Positions - Apple,...
Job Description: Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
Manager *Apple* Systems Administration - Pu...
Req ID 3315BR Position Title Manager, Apple Systems Administration Job Description The Manager of Apple Systems Administration oversees the administration and Read more
*Apple* Technician - nfrastructure (United S...
Let’s Work Together Apple Technician This position is based in Portland, ME Life at nfrastructure At nfrastructure, we understand that our success results from our Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.