Creating a custom url scheme via AppleScript and Python

I wrote recently about my somewhat obsessive notetaking strategies that include a personal wiki and summary pages for each of the scientific articles I read. Well, if you thought that was crazy, please avert your eyes because things are about to get geeky.

My frustration began when I wanted the note summary pages in my wiki to contain hyperlinks to specific pages of a PDF. The primary use case for something like this is when I remember something I want to cite and can’t remember where I learned it. When that happens, I want to be able to open my personal notetaking wiki and search for it. Then, when I find what I am looking for, I can click a hyperlink and the original source document will open on the right page.

I quickly realized that it is perfectly possible to open a specific PDF in my default PDF reader using a URL like “file://localhost/PATHtoFILE/PDFofInterest.pdf”.

If I wanted to open the PDF in a Web browser window, I could just add an additional page number string (e.g., “#page=X”) to the end of that URL to open the file to a specific page. The problem is that I don’t like reading PDFs in Web browsers and my favorite PDF reader on the Mac, Skim, does not support this style of link. Thankfully, though, it does support AppleScript.

Writing the AppleScript

After a bit of research, I realized that it is possible to create an application wrapper that can receive a custom URL scheme, process it, and run AppleScript commands to tell Skim what to do.

If you want to follow along, open AppleScript Editor.app and write your script. This is the one I am using:

on open location skimmerURL
--When the skimmer url is clicked, we have to extract the arguments from the url and pass them to this script.
    set oldDelims to AppleScript's text item delimiters
    --This saves Applescript's old text item delimiters to the variable oldDelims.
    set newDelims to {"skimmer://", "#page="}
    --This sets the variable newDelims to our new custom url handler prefix and the prefix for the page number argument.
    set AppleScript's text item delimiters to newDelims
    --This sets Applescript's text item delimiters to the newDelims.
    set pdfPath to item 2 of the text items of skimmerURL
    --This extracts the file name from the passed url using the new text item delimiters. The file name is the second text item.
    set pdfPath to do shell script "python " & "/Applications/Skimmer.app/Contents/Resources/Scripts/buildFilePath.py" & " \"" & pdfPath & "\""
    set pdfPage to item -1 of the text items of skimmerURL as integer
    --This extracts the desired page number because it is the last argument of the passed url.
    set AppleScript's text item delimiters to oldDelims
    --This resets Applescript's text item delimiters.

    tell application "Skim"
        open pdfPath
        go document 1 to page pdfPage of document 1
    end tell

end open location

When you are done, save your script as an App bundle instead of a naked script. Then, you can right click on the resulting file and choose the option to “Show Package Contents.” You will find the script file you just wrote at ~/YourNewApp.app/Contents/Resources/Scripts/main.scpt.

The folder where I keep my PDFs is in my Dropbox, but it has a very specific heirarchical structure. I use Papers2 to automatically file my PDFs according to the year of publication and the first letter of the first author’s last name. Papers2 also names the files automatically based on this same scheme. This means that a paper I wrote in 2007 would be referred to like this: ~/Papers/2007/J/Jones2007eh.pdf. The two extra letters are added automatically by Papers2 to ensure a unique file name. Since all of my PDFs follow this rigid convention, I can predict the file’s location based only on the name.

As you can see in the above script’s comments, it runs whenever the system detects a custom URL that follows this pattern: skimmer://filename.pdf#page=X. Since I designed the URLs to include only the file name and a page number argument, if we want to extract the file name, our text item delimiters should be skimmer:// and #page=X. If you do this, be sure to store and reset your AppleScript text item delimiters when you are done.

Using Python to process the file name

Once the script pulls the file name out of the URL string, it sends it to a separate Python script at ~/YourNewApp.app/Contents/Resources/Scripts/buildFilePath.py. This script uses regular expressions to process the PDF’s file name according to the logic I mentioned above and return the PDF’s path. This path may look a little odd to you because AppleScript likes file paths with colons instead of slashes.

#!/usr/bin/env python

import sys
import string
fileName = sys.argv[1]
import re
pdfPath = re.sub("([A-Z](?=\\D))(\\D*)(\\d+?)([a-z]{2}(?=\\.pdf))", "Users:UserName:Dropbox:Papers:\\3:\\1:\\1\\2\\3\\4", fileName)
print pdfPath

After receiving the path from the Python script, the AppleScript then takes it and sends it to Skim along with the desired page number.

Registering the custom URL scheme with OS X

Once the two scripts are in place, the only thing left to do is register the new URL scheme. To do this, open the Info.plist file in your new app’s “Contents” directory using your favorite text editor. Add the following right before the final closing and tags:

<key>CFBundleIdentifier</key>
<string>com.drosophiliac.AppleScript.Skimmer</string>
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLName</key>
        <string>Skimmer</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>skimmer</string>
        </array>
        </dict>
</array>

The most important thing here is that the second string entry matches your App’s name and the last string entry matches your custom url scheme. Mine are Skimmer and skimmer because I called the app Skimmer.app and the custom urls start with skimmer://.

I used info from lots of different sites to accomplish this, but I took inspiration from Stian Håklev’s site and from the link trigger page over at Mac OS X Automation. I am sure it is possible to accomplish all of this using just AppleScript or just Python using py-appscript, but this works for me. Still, if you have any ideas on how to improve things, feel free to say so.

Posted: 2012-09-15

Category: notebook

Tags: blog tips

Comments