Lines and Drafts

If you’re a Drafts user, you’ve probably noticed this oddity in how it displays—or, more accurately, doesn’t display—the trailing final line number for drafts with a certain type of content. Here, for example, are two ever-so-slightly different versions of a draft:

Drafts lines

Where is the extra character in the version on the right? It’s a trailing linefeed character after the fifth line. If I had timed the screenshots to show the blinking cursor at the end of the file, you’d see that the end of the file on the left is just after the final e, while the end of the file on the right is at the start of the sixth line. But Drafts doesn’t consider that sixth proto-line worthy of numbering.1

Generally speaking, I agree with Greg Pierce’s decision on this. A linefeed character at the end of a file doesn’t really mean there’s another line there. But there are times when I wish the line number was there. For example, when I’m writing a blog post and there are Markdown reference links at the bottom of the draft, I sometimes look at the last number and wonder if there’s a linefeed at the end of it. If there isn’t, that means I’ve screwed something up and the next reference link I add (which is done through this Drafts action) will be tacked onto the end of the last line instead of being on a line of its own.

Draft blog post

Another aspect of how Drafts handles lines came up last night and is the real reason for this post. I was trying to work out an answer to John Catalano’s question about incrementing the number of leading hashes (to denote a Markdown header of a particular level) to the current line. I misread the question and wrote a JavaScript action that would increment the number of hashes in all the lines of the current selection (which would reduce to the cursor’s current line if there’s no selection). When I looked back at the question this morning, I saw my mistake—and that Greg had given John a better answer—but I kept thinking about my solution and how part of it could be reused to handle many situations in which a series of lines need to be processed.

The script I ended up with was an improvement on the one you’ll see in my forum answer, but it does basically the same thing: increment the number of hashes in the selected lines until it gets to three hashes, at which point it resets to zero. Here’s the script:

javascript:
 1:  // Extend selection to full lines without final trailing newline
 2:  var [start, len] = editor.getSelectedLineRange();
 3:  var full = editor.getTextInRange(start, len);
 4:  full = full.replace(/\n$/, "");
 5:  len = full.length;
 6:  editor.setSelectedRange(start, len);
 7:  var lines = full.split("\n");
 8:  
 9:  // Adjust the leading hashes for each line in turn
10:  // Skip empty lines
11:  for (var i=0; i<lines.length; i++) {
12:    if (lines[i].length == 0) {
13:      continue;
14:    } else if (lines[i].substring(0, 4) == "### ") {
15:      lines[i] = lines[i].substring(4);
16:    } else if (lines[i].substring(0, 1) == "#") {
17:      lines[i] = "#" + lines[i];
18:    } else {
19:      lines[i] = "# " + lines[i];
20:    }
21:  }
22:  
23:  // Rejoin the lines and replace the selected text in the editor
24:  var s = lines.join("\n");
25:  editor.setSelectedText(s);

In a nutshell, the first stanza extends the selection to encompass all the lines that are at least partially selected and then splits that into an array, with each element consisting of one line of text (with no trailing linefeed). The second stanza operates on each of those lines in turn. The third stanza then glues the lines back together with linefeed characters and replaces the selected lines in the editor with the glued-together string.

The part I really care about—the part I think is reusable—is the first stanza, Lines 1–7. There are many situations in which appending, prepending, or otherwise transforming a series of lines is useful, and this section of code is a nice setup for that kind of operation.

It starts by calling the getSelectedLineRange function of the global editor object on Line 2. According to the documentation, this

Get[s] the current selected text range extended to the beginning and end of the lines it encompasses.

The value returned is a two-element array consisting of the starting character position and the length of the text range. The key to getSelectedLineRange, and what makes it different from getSelectedRange, is that the starting character position is the beginning of the line containing the start of the selection and the range extends to the end of the line containing the end of the selection.

If the action is called with this selection

Drafts selection

start will be 11 (there are 11 characters [remember the linefeed] before the S at the start of the second line) and len will be 35 (12 from the second line, 11 from the third, and 12 from the fourth). Note that the end of the fourth line is just after the linefeed that separates it from the fifth.

Given that all the other linefeeds in this range are included, it’s consistent to include the one at the end of the last line in the range. As we’ll see, though, there’s a situation for which the last character in the range won’t be a linefeed, and we’ll have to code defensively to cover that situation.

Line 3 uses getTextInRange to grab the range we just described and put it in the variable full.

Looking ahead to Line 7, we see that we’re going to use split("\n") to convert full into the array lines. Because we don’t want lines to include an empty element at the end, we should strip off any final linefeed from full before running split. That’s what the replace in Line 4 deals with. It insures that the last character of full is removed if it’s a linefeed.

Why can’t we just remove the last character of full without checking for what it is? When won’t full have a linefeed at the end? That’s where the case we looked at back at the beginning of this post comes in. If the last line of the draft doesn’t end with a linefeed, and that line is part of the selection, then full won’t end with a linefeed.

Line 5 adjusts the value of len to account for the (usually) new length of full, and Line 6 selects all the text we’re going to be transforming.2 Finally, Line 7 splits full into lines.

After the operations on lines is done, we use join("\n") to put lines back together into a single string (Line 24) and replace the selection in the editor with that string (Line 25).

If you’re thinking this is one of those posts I write mostly for myself, you’re right. With luck, writing this will help me remember to use this split-operate-rejoin process then next time I need to process a series of lines in Drafts. If it helps you too, that’s a bonus.


  1. In the Editor settings, Drafts calls the little numbers in the left margin paragraph numbers instead of line numbers, which makes sense because of the way word wrapping works. But if you’re writing code or other structured text, it’s better to think of them as line numbers. 

  2. This selection isn’t strictly necessary, but it makes the later substitution of the transformed text into the editor easy, and I like to highlight the text being worked on. 


Commenting on Shortcuts, Part 2

In yesterday’s post, I showed how I use my splitflow script to help me explain the workings of whatever shortcuts I write about here. Today, we’ll go through splitflow itself to see how it works.

Let’s start with the code itself:

python:
  1:  #!/usr/bin/env -S ${HOME}/opt/anaconda3/envs/py35/bin/python
  2:  
  3:  import cv2
  4:  import numpy as np
  5:  from datetime import date
  6:  from docopt import docopt
  7:  from urllib.parse import quote
  8:  import sys
  9:  
 10:  usage = """Usage:
 11:    splitflow [-dt sss] FILE...
 12:  
 13:  Split out the individual steps from a series of screenshots of
 14:  Shortcuts and output a <table> for inserting into a blog post.
 15:  
 16:  Options:
 17:    -d     Do NOT add the date to the beginning of the filename
 18:    -t sss Add an overall title to the output filenames
 19:    -h     Show this help message and exit
 20:  """
 21:  
 22:  # Handle arguments
 23:  args = docopt(usage)
 24:  sNames = args['FILE']    # the screenshot filenames
 25:  namePrefix = args['-t'] + ' ' if args['-t'] else ''
 26:  if args['-d']:
 27:    datePrefix = ''
 28:  else:
 29:    today = date.today()
 30:    datePrefix = today.strftime('%Y%m%d-')
 31:  
 32:  # There are three template images we want to search for
 33:  accept = cv2.imread("accepts.png")
 34:  topAction = cv2.imread("topAction.png")
 35:  botAction = cv2.imread("botAction.png")
 36:  
 37:  # Template dimensions we'll use later
 38:  acceptHeight = accept.shape[0]
 39:  bActHeight = botAction.shape[0]
 40:  
 41:  # The threshold for matches was determined through trial and error
 42:  threshold = 0.99998
 43:  
 44:  # Keep track of the step numbers as we go through the files
 45:  n = 1
 46:  
 47:  # Set the URL path prefix
 48:  URLPrefix = 'https://leancrew.com/all-this/images2020/'
 49:  
 50:  # Start assembling the table HTML
 51:  table = []
 52:  table.append('<table width="100%">')
 53:  table.append('<tr><th>Step</th><th>Action</th><th>Comment</th></tr>')
 54:  
 55:  rowTemplate = '''<tr>
 56:    <td>{0}</td>
 57:    <td width="50%"><img width="100%" src="{1}{2}" alt="{3}" title="{3}" /></td>
 58:    <td>Step_{0}_comment</td>
 59:  </tr>'''
 60:  
 61:  # Loop through all the screenshot images
 62:  for sName in sNames:
 63:    sys.stderr.write(sName + '\n')
 64:    whole = cv2.imread(sName)
 65:    width = whole.shape[1]
 66:  
 67:    # Find the Accepts box, if there is one
 68:    aMatch = cv2.matchTemplate(whole, accept, cv2.TM_CCORR_NORMED)
 69:    minScore, maxScore, minLoc, (left, top) = cv2.minMaxLoc(aMatch)
 70:    if maxScore >= threshold:
 71:      bottom = top + acceptHeight
 72:      box = whole[top:bottom, :, :]
 73:      desc = '{}Step 00'.format(namePrefix)
 74:      fname = '{}{}Step 00.png'.format(datePrefix, namePrefix)
 75:      cv2.imwrite(fname, box)
 76:      table.append(rowTemplate.format(0, URLPrefix, quote(fname), desc))
 77:      sys.stderr.write('  ' + fname + '\n')
 78:  
 79:    # Set up all the top and bottom corner action matches
 80:    tMatch = cv2.matchTemplate(whole, topAction, cv2.TM_CCORR_NORMED)
 81:    bMatch = cv2.matchTemplate(whole, botAction, cv2.TM_CCORR_NORMED)
 82:    actionTops = np.where(tMatch >= threshold)[0]
 83:    actionBottoms = np.where(bMatch >= threshold)[0]
 84:  
 85:    # Adjust the coordinate lists so they don't include partial steps
 86:    # If the highest bottom is above the highest top, get rid of it
 87:    if actionBottoms[0] < actionTops[0]:
 88:      actionBottoms = actionBottoms[1:]
 89:  
 90:    # If the lowest top is below the lowest bottom, get rid of it
 91:    if actionTops[-1] > actionBottoms[-1]:
 92:      actionTops = actionTops[:-1]
 93:  
 94:    # Save the steps
 95:    for i, (top, bot) in enumerate(zip(actionTops, actionBottoms)):
 96:      step = n + i
 97:      bottom = bot + bActHeight
 98:      box = whole[top:bottom, :, :]
 99:      desc = '{}Step {:02d}'.format(namePrefix, step)
100:      fname = '{}{}Step {:02d}.png'.format(datePrefix, namePrefix, step)
101:      cv2.imwrite(fname, box)
102:      table.append(rowTemplate.format(step, URLPrefix, quote(fname), desc))
103:      sys.stderr.write('  ' + fname + '\n')
104:    
105:    # Increment the starting number for the next set of steps
106:    n = step + 1
107:  
108:  sys.stderr.write('\n')
109:  
110:  # End the table HTML and print it
111:  table.append('</table>')
112:  print('\n'.join(table))

It’s reasonably well commented, I think, but there can always be further explanation.

The first thing you probably notice is the weird shebang line at the top. Normally, that would be simply

#!/usr/bin/env python

to have the script run by my default Python installation. That’s currently a Python 3.7 system that I installed through Anaconda, which is a really good system for getting you away from the outdated version of Python that comes with macOS and for managing different Python “environments.” Unfortunately, the key module needed for splitflow is the OpenCV module, and Anaconda currently doesn’t have an OpenCV module that works with Python 3.7. It does, however, have an OpenCV module that’s compatible with Python 3.5, so I installed a Python 3.5 environment in order to run splitflow. Because it’s Python 3.5 is not my default Python, I have to tell the shebang line explicitly where it is. The -S option tells env to interpret environment variables, and that combined with

${HOME}/opt/anaconda3/envs/py35/bin/python

point the script to the Python 3.5 interpreter.1

Of the modules imported in Lines 3–8, only cv2, which is the OpenCV module, and docopt, which I learned about from Rob Wells, aren’t in the standard library. I installed both of them via Anaconda.

The usage string that starts on Line 10 is what’s shown when you invoke splitflow with the -h option. It describes how the command works and, through the magic of docopt tells the script how to handle its command line options.

The command-line parsing is done by docopt in Lines 23–30. Three variables get defined here for later use:

Lines 33-35 read in the template files that splitflow searches for in the screenshots. We saw them yesterday.

Images for splitflow

The image on the left is accepts.png, the one in the center is topAction.png, and the one on the right is botAction.png. At present, I use a particular directory for running splitflow. This directory is where the template image files are kept and where I save the screenshot files. I cd to this directory, run splitflow there, and that’s where the individual step image files are saved. This is not a great system, as it keeps these permanent template files in the same place as the temporary images I’m processing. At some point, I’ll move the template images to a more protected location, away from where I save the screenshots.

It’s worth pausing here to define exactly what imread does. For color images like we have, it reads the given image file and returns a three-dimensional NumPy array. The first index of the array is for the row; its length is the width of the image. The second index is for the column; its length is the height of the image. The third is for the color values; its length is three (one value each for red, green, and blue). The advantage of using this data structure for an image is that all of the array calculations that NumPy and SciPy provide can now be applied to the image values. That’s how the template images are matched to the screenshots. Also, this allows us to use simple array slicing to copy out parts of an image.

The next few sections define variables that’ll be used later in the script. Lines 38–39 set geometric properties of two of the template images, Line 45 initializes the step number, and Line 48 sets the URL path used in the src attribute of the <img> tags in the table.

The threshold value in Line 42 is a little more complicated. As we’ll see further down, OpenCV has a matchTemplate function that sort of overlays the template onto the target image and runs a calculation to see how well they match. The calculation is run for each possible overlay position and the resulting value is a measure of how good the match is when the template is at that position. The return value of matchTemplate is a two-dimensional array of these goodness-of-match values for each possible overlay position.

OpenCV supports a handful of these goodness-of-match calculations. The one I’ve chosen, a normed cross-correlation, results in a number between 0 and 1. Through trial and error with these template images and a series of screenshots, I learned that 0.99998 was a good threshold value for matching the Accepts box and the top and bottom corners of action steps. Using a smaller threshold resulted in false matches with the template a pixel or two away from its best fit. Using a larger threshold often resulted in no matches.2

Lines 51–53 initialize table, a list variable that will eventually contain all of the lines of the HTML table that will be output. In the old days, Python’s string concatenation routines were inefficient, and Python programmers were encouraged to build up long strings by accumulating all the parts into a list and then joining them. I don’t think that’s still as important as it used to be, but it’s a habit I—and many other Python programmers—still adhere to.

Lines 55–59 define a template string for each row of the table. We’ll use this in a couple of places later in the program.

With Line 62, we’re done with all the setup and will start processing in earnest by looping through the screenshot files in sNames. In a nutshell, the code in this loop does the following:

You’ll notice that there are some sys.stderr.write lines within the loop. I put these in to show the progress of the script as it runs. For the screenshots processed in yesterday’s post, these lines were printed as splitflow ran:

A1.png
  20200126-Unpaid Step 01.png
  20200126-Unpaid Step 02.png
A2.png
  20200126-Unpaid Step 03.png
  20200126-Unpaid Step 04.png
  20200126-Unpaid Step 05.png
A3.png
  20200126-Unpaid Step 06.png
  20200126-Unpaid Step 07.png
  20200126-Unpaid Step 08.png
  20200126-Unpaid Step 09.png
A4.png
  20200126-Unpaid Step 10.png

This works not only as a sort of progress bar, but also as a sanity check. I generally have a decent idea of how many steps are in each of the screenshots, and this shows me whether splitflow is finding them. If we look again at the four screenshots being processed

Shortcut Screenshots

we see that there are indeed two action steps in the first, three in the second, four in the third, and one in the fourth. I have these progress lines sent to standard error instead of standard output because I don’t want them to interfere with the table HTML. If I want to save the table HTML to a file, I can use this redirection

splitflow -t Unpaid A* > table.html

and the progress lines will be displayed on the Terminal while only the table goes into the file.

In searching for the Accepts box, Line 68 runs the matchTemplate function and puts the array of goodness-of-match values into aMatch. Line 69 then uses minMaxLoc to find the best match. But the best match isn’t necessarily good enough—there will always be a best match, even if there isn’t an Accepts box in the screenshot. So Line 70 acts as a gatekeeper. Only matches that exceed the threshold pass.

If a match passes the gatekeeper, Line 70 uses the height of the Accepts template (saved back on Line 38) and the top value that came out of the minMaxLoc call on Line 69 to calculate the bottom of the Accepts box. Line 72 then uses array slicing to copy the Accepts box out of the screenshot. We then do some simple string processing to get the names right and save the Accepts box as Step 00 in Line 75.

The handling of action steps is similar, except we can’t look for just the best match because there may be more than one. After running matchTemplate against both the top and bottom template images (Lines 80–81), we use the NumPy where function to filter out the goodness-of-match values, retaining only those above the threshold. After Lines 82–83, actionTops contains the vertical coordinate of all the top corners of actions in the screenshot and actionBottoms contains the vertical coordinates of all the bottom corners.3

We then use some simple logic in Lines 85–92 to get rid of partial steps. A bottom corner that’s above the highest top corner must be from a partial step. Similarly for a top corner that’s below the lowest bottom corner. When this is done, actionTops and actionBottoms are lined up—the corresponding elements are the top and bottom coordinates of all the full steps in the screenshot.

Lines 95–103 then loop through these top/bottom pairs and copy the action boxes using the same array slicing logic used with the Accepts box. And the table list gets another row, too.

Finally, with all the step images saved and all the rows added to table, we close out table in Line 111 and print out the HTML in Line 112.

Phew! The overall logic is pretty straightforward, but there are lots of little things that need to be handled correctly.

As I’ve said, this is a new script and is certainly not in its final form. Apart from figuring out where I want to keep the template images, I should add a section that uploads the step images to my server. And there’s some refactoring that could be done; Lines 71–76 are too similar to Lines 97–102.

But even in this early stage, splitflow has been a big help. I like the tabular presentation of shortcuts, but I would never do it if it meant slicing up the screenshots and assembling the table by hand. I’m already thinking about extending it to handle Keyboard Maestro macros.


  1. Yes, I know Python is not strictly an interpreter, but it’s easier to treat it as such when discussing shebang lines. 

  2. When I started writing splitflow, I thought I could use a threshold of 1. Because both the templates and the screenshots are PNGs, I thought the matches should be exact, pixel for pixel. But I suspect that either the antialiasing around the corner edges or some floating point roundoff during the cross-correlation calculations is preventing a perfect match. 

  3. Strictly speaking, the vertical coordinates in actionBottoms are a distance bActHeight above the bottom corners. We correct for this in Line 97. 


Commenting on Shortcuts, Part 1

A couple of weeks ago, I said I was experimenting with a new way to present and comment on Shortcuts. I used it in that post and in my most recent one. Although I can’t say the workflow is in final form (is any automation really finished?), it’s complete enough to present.

In the past, I’ve presented Shortcuts by taking a series of screenshots and stitching them together with PicSew. Then I’ve explained how they work in a paragraph or two below the image. The problem with this approach is that the images are often quite long, which means the reader has to scroll back and forth a lot between the image and the description. Also, there’s no convenient way to refer to individual actions within a shortcut because Shortcuts doesn’t include step numbers in its editor.

I could be more diligent about adding comments to my shortcuts, but comments take up a lot of vertical space in a shortcut and I think they make it harder to understand because you see less of the logic at one time.1 I tend to use fewer comments when programming in Shortcuts than I do when programming in, say, Python for just this reason.

So what I’ve been doing recently is cutting up my shortcuts into individual steps and presenting them in a table with three columns: a left column for the step number, a middle column for the actions themselves, and a right column for a comment. Here’s an example for a shortcut that parses a list of Reminders to show me the total amount of my unpaid invoices:

StepActionComment
1 Unpaid Step 01 Get all the outstanding invoices.
2 Unpaid Step 02 Count them. We’ll use this number later in the output.
3 Unpaid Step 03 Loop through the unpaid invoices.
4 Unpaid Step 04 The Title of each reminder includes the amount in parentheses.
5 Unpaid Step 05 Extract the amount. This includes the parentheses and dollar sign.
6 Unpaid Step 06 Delete the parentheses and dollar sign.
7 Unpaid Step 07 When the loop ends, we have a list of all the amounts.
8 Unpaid Step 08 Add up the amounts.
9 Unpaid Step 09 Make sure we show two decimal places even if the sum doesn’t need them.
10 Unpaid Step 10 Display the number of unpaid invoices from Step 2 and the total amount.

If the shortcut is meant to be run from the Share Sheet, it starts with a Step 0 that shows what input the shortcut accepts. This example doesn’t have that, but many of my shortcuts do.

The HTML for the table is pretty straightforward:

xml:
<table width="100%">
<tr><th>Step</th><th>Action</th><th>Comment</th></tr>
<tr>
  <td>1</td>
  <td width="50%"><img width="100%" src="https://leancrew.com/all-this/images2020/20200125-Unpaid%20Step%2001.png" alt="Unpaid Step 01" title="Unpaid Step 01" /></td>
  <td>Get all the outstanding invoices.</td>
</tr>
<tr>
  <td>2</td>
  <td width="50%"><img width="100%" src="https://leancrew.com/all-this/images2020/20200125-Unpaid%20Step%2002.png" alt="Unpaid Step 02" title="Unpaid Step 02" /></td>
  <td>Count them. We'll use this number later in the output.</td>
</tr>

[several more rows…]

<tr>
  <td>9</td>
  <td width="50%"><img width="100%" src="https://leancrew.com/all-this/images2020/20200125-Unpaid%20Step%2009.png" alt="Unpaid Step 09" title="Unpaid Step 09" /></td>
  <td>Make sure we show two decimal places even if the sum doesn't need them.</td>
</tr>
<tr>
  <td>10</td>
  <td width="50%"><img width="100%" src="https://leancrew.com/all-this/images2020/20200125-Unpaid%20Step%2010.png" alt="Unpaid Step 10" title="Unpaid Step 10" /></td>
  <td>Display the number of unpaid invoices from Step 2 and the total amount.</td>
</tr>
</table>

Here in the early stages of development, I’m explicitly including width attributes, but I suspect I’ll be switching to classes that can be controlled by CSS. The site already has some generic CSS that give tables a certain look; I’ll simply have to extend that.

To generate this table, and the images for the individual steps it contains, I run a script called splitflow. My workflow consists of

Shortcut Screenshots

As is often the case, it takes me longer to describe what I do than to actually do it.

By the way, in case you’re wondering, I do still write my posts in Markdown and the flavor of Markdown I use does support tables. But I can’t control the width of the columns using Markdown-style tables, so it’s better to go straight to HTML. One of Gruber’s smartest moves was to recognize that he couldn’t cover everything and allow HTML to be embedded in Markdown.

To keep this post to a reasonable length, I’m going to save the details of splitflow for another day. But I will give a brief outline of how it works.

The hard part, of course, is identifying the individual steps within the screenshots. Luckily for me, some very smart people did the difficult work by creating the OpenCV library and the Python interface to it. All I had to do was read the documentation, create some template images to search for, and then experiment with the matching parameters until I got something that worked.

Here are the three template images that splitflow searches for in the screenshots.

Images for splitflow

These were all cut from screenshots of a shortcut. The Accepts image on the left is what we see in every shortcut that runs from the Share Sheet. It’s always the same height, so we can search for the entire left portion of it. The other two are the left top and left bottom corners of actions. Because actions can be of various heights, we need to search for both the top and the bottom to grab the entire action.

Many of you could write your own version of splitflow right now; all you need is the link to OpenCV. But I’ll go through what I did in the next post.


  1. This is a problem with any visual programming environment once the code gets beyond a few steps. 


LaTeX addresses on the Mac and iOS

A couple of years ago, as I was first learning how to use Shortcuts,1 I wrote a shortcut that grabbed the name and address of the selected person in Contacts, formatted it for use in LaTeX, and put it on the clipboard. The result was something like

John Cheatham\\
Dewey, Cheatham \& Howe\\
1515 Loquitor Lane\\
Amicus OH 44100

where the key is to end each line (except the last) with double backslashes and to backslash escape an ampersands. The shortcut was run from the Share Sheet, and while I’ve made some minor changes to it, it still serves me well when I’m writing on the iPad.

Recently, though, I realized that I didn’t have anything similar when writing on the Mac. Oh, I have a command-line tool for looking up a contact and outputting a LaTeX-formatted address—it’s an extension of this ancient script—but what I wanted was something that worked from the Contacts app, a system parallel to what I have on iOS.2 I got that through a combination of AppleScript and Keyboard Maestro

Let’s start by looking at the current version of my LaTeX Address shortcut.

StepActionComment
0 LaTeX Address Step 00 This will appear in the Share Sheet for Contacts
1 LaTeX Address Step 01 Get the Street Address from the contact. We’ll pull more info from it later.
2 LaTeX Address Step 02 Assemble the name and address. By default, the Shortcut Input for contacts returns the full name. The Company also comes directly from the contact. The parts of the address come from the Street Address extracted in Step 1.
3 LaTeX Address Step 03 LaTeX treats ampersands as special characters, so we have to escape them.
4 LaTeX Address Step 04 This looks like an empty text field, but it contains a newline character. The magic variable associated with this action has been renamed Newline.
5 LaTeX Address Step 05 Put two backslashes in front of all the newlines. You might think would be simpler to just add the backslashes directly in Step 2, but that wouldn’t work when the Street part of an address is two lines long.
6 LaTeX Address Step 06 Put the LaTeXified name and address on the clipboard.

If you compare it to the original version, you’ll see that it’s cleaner now, mainly because Shortcuts’ inclusion of parameters in iOS 13 has reduced the need for Set Variable and Use Variable actions. Also, I’ve simplified the text replacement in Step 5 to avoid the messy regular expression I used to use. If you have a need for something like this, you can download it.

So to get the LaTeXified address onto the clipboard when working on iOS, I

  1. Open Contacts.
  2. Find the person.
  3. Open the Share Sheet.
  4. Choose the LaTeX Address shortcut.

For the analogous behavior on the Mac, I use this Keyboard Maestro macro:

LaTeX address macro

It’s triggered by the ⌃⌥⌘L key combination, but only when Contacts is the frontmost application. It runs an AppleScript to put the LaTeXified name and address of the currently selected contact on the clipboard and then hides Contacts itself.3 Here’s the AppleScript:

applescript:
 1:  set latexList to {}
 2:  tell application "Contacts"
 3:    tell the selection as list
 4:      set thePerson to item 1
 5:      tell thePerson
 6:        set end of latexList to name
 7:        set end of latexList to organization
 8:        set addr to formatted address of address 1
 9:        set latexList to latexList & (paragraphs of addr)
10:        if last item of latexList is equal to "USA" then
11:          set latexList to items 1 through -2 of latexList
12:        end if
13:      end tell
14:    end tell
15:  end tell
16:  
17:  set AppleScript's text item delimiters to "\\\\
18:  "
19:  set the clipboard to latexList as string

Basically, it creates a list, latexList, of lines for the name and address and then combines the list items with two backslashes and a newline as separators. Lines 17–18 define the separator (it has to double each backslash because AppleScript treats them as special characters), and the latexList as string part of Line 19 combines them. The only other tricky part of the script is Lines 10–11, which strip the country from the end of the address if it’s “USA.” I’m able to get away with this rather simple way of deleting the country from domestic addresses because I standardized on “USA” many years ago and have stuck with it as I add new Contacts entries.

To get a LaTeXified address onto the clipboard when working on a Mac, I

  1. Activate LaunchBar.
  2. Start typing the person’s name.
  3. Press the Return key when LaunchBar shows the match. This activates Contacts with that person selected.
  4. Type ⌃⌥⌘L to run the LaTeX Address macro.

I would dearly love to be able to run a command directly from LaunchBar after Step 2, never activating Contacts, but I don’t know how. I guess typing Return and then ⌃⌥⌘L isn’t especially onerous.


  1. It was still Workflow back then. 

  2. As we’ll see, what I really wanted was something I could run directly after a contact was found by LaunchBar

  3. I typically work with Contacts open but hidden, so this puts my Mac back to its normal state after getting the address.