LaunchBar and LaTeX

When I send a proposal for new work to a client, I do it in the form of a letter addressed to the client and emailed to them as a PDF attachment.1 I write the letter in LaTeX, using TextExpander for the boilerplate and then filling in the job-specific parts. One of the job-specific parts is the name and address of the client, which, in a LaTeX letter, is the second argument to the \begin block:

\begin{letter}{John Appleseed\\
Apple Inc.\\
1 Infinite Loop\\
Cupertino, CA 95014}

In LaTeX, linebreaks in multiline blocks of text have to be preceded by a pair of backslashes. Also, because the ampersand is a special character in LaTeX, it has to be escaped with a backslash. Thus, the firm of Dewey, Cheatham & Howe has to be written in the LaTeX source as Dewey, Cheatham \& Howe.

Over the years, I’ve written several little scripts to format addresses for LaTeX. They’ve all done the job, but they’ve all forced me to either

Neither is the way I want to work.

When writing on the Mac, I use LaunchBar to look up contacts. I tap ⌘-Space, start typing the contact’s name, and up it pops.

Appleseed in LaunchBar

What I want is to be able to run a command at this point that gets the address, puts it in LaTeX form, and pastes it into the letter where the cursor is sitting. This past week, I finally got around to writing a script that does just that.

It uses LaunchBar’s “sending” feature to pass the contact to a script that assembles, formats, and pastes the address—all without leaving LaunchBar. The idea is that after getting the contact as above, I can tap the Tab key to bring up a list of things I can do with it:

Sending contact to LaTeXAddress AppleScript

Choosing the LaTeXAddress script sends the contact to that script, which then inserts the formatted name and address. No switching out of and back into my text editor, and no messing around with the clipboard.

While I was at it, I decided the script should also handle an address sent to it. That would happen if I select the contact’s address instead of the contact itself:

Selecting address of contact

Will I ever use it this way? It adds an extra step, so probably not. But it was fairly easy to add this extra capability, and it’s usually better to finish off a project when it’s fresh in your mind.

Here’s the LateXAddress script:

  1:  use AppleScript version "2.4" -- Yosemite (10.10) or later
  2:  use scripting additions
  4:  (* This script is to be used within LaunchBar. It is sent either a
  5:  contact or the address of a contact. When sent a contact, the argument
  6:  of handle_string is the full name of the contact as a single line of
  7:  text. When sent an address,the argument of handle_string is the street
  8:  address as a multi-line block of text with an attn: line that contains
  9:  the name.*)
 11:  -- The US may show up as any of these names in the country field.
 12:  property usNames : {"USA", "US", "United States", "United States of America"}
 14:  -- Handle string passed in from LaunchBar
 15:  on handle_string(s)
 16:    set sList to paragraphs of s
 18:    -- Get the plain name and address as a list of text lines.
 19:    -- The method called depends on whether a single-line name
 20:    -- or multi-line address (with attn:) is passed in.
 21:    if (count of sList) is 1 then
 22:      set plainList to addressFromName(s)
 23:    else
 24:      set plainList to addressFromAddress(sList)
 25:    end if
 27:    -- Convert from plain name and address to LaTeX
 28:    set blockText to latexify(plainList)
 30:    -- Insert it at the cursor
 31:    tell application "LaunchBar" to paste in frontmost application blockText
 32:  end handle_string
 35:  -- Strip the US line part of the address if it's there
 36:  on stripUS(aList)
 37:    if last item of aList is in usNames then
 38:      return items 1 thru -2 of aList
 39:    end if
 40:  end stripUS
 43:  -- Convert from list of lines of plain address to text block
 44:  -- formatted according to LaTeX rules
 45:  on latexify(aList)
 46:    set oldDelims to AppleScript's text item delimiters
 47:    set AppleScript's text item delimiters to linefeed
 49:    -- Escape the ampersands and end each line except the last with two backslashes
 50:    set cmd to "echo " & quoted form of (aList as text) & ¬
 51:      "| sed 's/&/\\\\&/g' " & ¬
 52:      "| sed '$!s/$/\\\\\\\\/' "
 53:    set newBlock to do shell script cmd
 54:    return newBlock
 56:    set AppleScript's text item delimiters to oldDelims
 57:  end latexify
 60:  -- Handle single-line name
 61:  on addressFromName(fullname)
 62:    tell application "Contacts"
 63:      --Find the contact with that name
 64:      set match to first item of (people whose name is fullname)
 66:      -- Get the address and strip the country if US
 67:      set addr to formatted address of first item of (addresses of match)
 68:      set addrList to paragraphs of addr
 69:      set addrList to my stripUS(addrList)
 71:      -- Add the company to the top of the list of lines
 72:      set org to organization of match
 73:      if org is not missing value then
 74:        set beginning of addrList to org
 75:      end if
 77:      -- Add the name to top of the list of lines
 78:      set beginning of addrList to fullname
 80:    end tell
 82:    return addrList
 83:  end addressFromName
 86:  -- Handle multi-line address
 87:  on addressFromAddress(aList)
 88:    -- Strip the country if US
 89:    set aList to stripUS(aList)
 91:    -- Loop through and find the addressee via the attn: line
 92:    -- Keep track of the attn: line number and save in aLine
 93:    set lCount to 1
 94:    repeat with theLine in aList
 95:      if (word 1 of theLine) is "attn" then
 96:        set aLength to length of theLine
 97:        set addressee to text 7 thru aLength of theLine
 98:        set aLine to lCount
 99:      end if
100:      set lCount to lCount + 1
101:    end repeat
103:    -- Build a list with everything except the attn: line
104:    set addrList to items 1 thru (aLine - 1) of aList & items (aLine + 1) thru -1 of aList
105:    set beginning of addrList to addressee
107:    return addrList
108:  end addressFromAddress

The handle_string handler, Lines 15–32, is required and is the entry point for the script. Depending on how LaTeXAddress is called, it will get passed either the name of the selected contact—e.g., John Appleseed—or the selected address, which will be a block of text in this form:

Apple, Inc.
attn: John Appleseed
1 Infinite Loop
Cupertino CA 95014

The handle_string function splits its argument into a list of lines (in AppleScript parlance, paragraphs are separated by linefeeds). If there’s only one line, we know that the argument is the full name of the contact, and the addressFromName handler is called. If there’s more than one line, we know the argument is an address block, and the addressFromAddress handler is called. Both of these functions return a list of address lines in the variable plainList, which is then reformatted on Line 28 by the latexify function and then pasted at the cursor position of the current document by the tell application "LaunchBar"… command on Line 31.

The addressFromName function finds the contact from their name (Line 64) and gets the first address listed for that contact (Line 67). It then splits this block of text into lines, stripping off the last line if it’s “USA” or any of the other names in the usNames property defined in Line 12.

The idea here is that the country name is unnecessary for an address in the US. It’s only my Canadian clients that need the country included in the address. The stripping is done by the function stripUS, defined on Lines 36–40.

Lines 72–75 add the company name (if there is one) to the top of the list of lines. Line 78 adds the contact’s name to the top of the list. This list of lines is what addressFromName returns.

The addressFromAddress handler is just a text manipulation function. Line 89 calls stripUS to get rid of the superfluous country name, if present. The rest of the function looks for an attn: line, removes it, and adds the addressee from that line to the top of the list.

The latexify function takes a list of address lines and returns a block of text properly formatted for a LaTeX document. The bulk of the work is done by the pipeline of three sed commands on Lines 51–52. The first one,

sed 's/&/\\\\&/g'

escapes all the ampersands. There are four backslashes in the replacement because this command is processed first by AppleScript, which eats half of the backslashes, and then by the shell, which also eats half of the backslashes. So AppleScript turns four backslashes into two, and then the shell turns two into one. And that’s what we want, one backslash in front of every ampersand. Because there’s no prefix before the s (substitute) command, the command is applied to each line. The g (global) flag at the end of the command tells sed to apply the substitution for every ampersand it finds on a given line.

The second sed command,

sed '$!s/$/\\\\\\\\/'

adds the backslashes required at the ends of the lines. Once again, both AppleScript and the shell consume half of the backslashes, so we need to start with eight to end up with two. The lines to which this substitution is applied are defined by the $! prefix that comes before the s. This is a two-part definition. The $ says to select only the last line, and the ! says to negate (or invert) that selection. The inverse of the last line is every line except the last line, which, if you look at the example up at the top of the post, is exactly what we want.

This system for inserting LaTeX-formatted addresses is more efficient than anything I’ve previously made. It’s possible I can squeeze out another keystroke by building a LaunchBar Action based on this script, but I’m not sure that’s worth the effort. I’ll live with what I have for a while.

  1. This may seem old-fashioned to you, but it’s what my clients prefer.