The 2004 Perl Advent Calendar
[about] | [archives] | [contact] | [home]

On the 17th day of Advent my True Language brought to me..
Mac::Glue

The AppleScript programming language can be used to send send apple events to applications running under Mac OS X. These events can be used to programmatically control the applications, enabling you to do the GUI equivalent of chaining applications together like you would do for non-GUI applications with the shell.

AppleScript was only ever designed to be one language that can be used to send apple events - one with a particularly baroque syntax. Mac::Glue allows you to send the same events from Perl, with a similar (but less...well, confusing) syntax.

We're going to create a little Perl application that we can link from a Finder window that when executed will open a Terminal in the same location.

Before you can do anything with Mac::Glue you need to create a binding for the application that you want to control. This enables Perl to translate the commands you pass to the correct binary signals to send. This can be done with the gluemac command that's installed when you install Mac::Glue

  bash$ sudo gluemac /Applications/Utilities/Terminal.app 
  What is the glue name? [Terminal]: 
 
This creates a file containing the binding:
  /Library/Perl/5.8.1/Mac/Glue/glues/Terminal

It also creates a bunch of documentation in the same directory:

  /Library/Perl/5.8.1/Mac/Glue/glues/Terminal.pod

This is the same documentation that's available from the Open Dictionary command in the Script Editor application, except (obviously) how to send the events from Perl rather from AppleScript. You can read either, whichever makes the most sense to you. Translating between to the two is relatively straight forward.

Once the binding is created we can create a new object:

  # create a new object to access the terminal application
  my $terminal = Mac::Glue->new("Terminal");

We can then call commands on it:

  # tell it to open a new window and run a shell command
  my $terminal->do_script("ls");
  # tell it to come to the front above all the other
  # applications
  $terminal->activate;

Objects and Properties

In addition to issuing commands to the applications we can request the objects it's managing and properties these have. Let's look some code to work out where the finder is pointing at:

  # create a finder object
  my $finder = Mac::Glue->new("Finder");
  # create a command to get the url
  my $url = $finder->obj(window => 1)
                   ->prop('target')
                   ->prop('url');
  # get the result
  my $path = $url->get;

This is roughly the same as the following AppleScript:

  tell application "Finder"
    activate
    set path to URL of target of front window
  end tell

Let's look at the Perl code line by line. First we need to create a finder object that we can call methods on.

   my $finder = Mac::Glue->new("Finder");

We now want to construct a command that we'll send across the wire later to request what we want to know. We grab the first window from the finder (which will be the top one, so the one that's showing) and then get the object that represents the directory that's showing in the window, and then get the path of that directory.

  my $url_command = $finder->obj(window => 1)
                           ->prop('target')
                           ->prop('url');

We then send the command and get the result:

  my $path = $url_command->get;

Putting it Together

Let's build it all together into one script:

  #!/usr/bin/perl
  # turn on perl's safety features
  use strict;
  use warnings;
  # load all the modules
  use String::ShellQuote;
  use Mac::Glue;
  use URI::Escape;
  use Data::Dumper;
  # then open the terminal in the current dir
  terminal_in_dir(url_to_path(get_finder_url()));

We've got three functions wrapped together that is the basics of the script. The first function is the code from above that gets the current location of the top finder window:

  sub get_finder_url
  {
    my $finder = Mac::Glue->new("Finder");
    my $url_command = $finder->obj(window => 1)
                             ->prop('target')
                             ->prop('url');
    return $url_command->get;
  }

The next function turns this url into a Unix style path:

  sub url_to_path
  {
    my $string = shift;
    die "Not a filename"
      unless $string =~ m{^file://localhost};
    # remove leading scheme and host
    $string =~ s{^file://localhost}{};
    # decode from a uri
    $string = uri_unescape($string);
    return $string
  }

Finally we need to use the glue to talk to the Terminal application and tell it to open a terminal in the right directory:

  sub terminal_in_dir
  {
    # work out what the path is
    my $path = shift;
    $path = shell_quote $path;
    # open it in the terminal
    my $terminal = Mac::Glue->new("Terminal");
    $terminal->do_script("cd $path; clear");
    $terminal->activate;
  }

Making Scripts Into Applications

Converting a script into an application that we can simply double click in the Finder is actually quite complicated and requires quite a lot of files to be created.

Luckily, many Mac OS X applications - like Platypus - that can create .app Applications from a script. They pop up dialogs that ask you a few questions - the name of the script, what icon you want - and then create all the files needed from templates. Almost no thought needed.

  • Introducing Mac::Glue article on perl.com
  • Updating iChat Status with Mac::Glue
  • Platypus - turning a script into an .app