Fixing TextExpander prefixes

Like all right-thinking TextExpander users, I have the expansion preference set to “immediately when typed” because I’ve never found a set of delimiter characters that always works. In my early days with TextExpander, I fiddled with the delimiter set, but inevitably got either expansion when I didn’t want it or no expansion when I did.

TextExpander preferences

After switching to this setting, I found that the only way to get snippet abbreviations that were both memorable and not actual words was to start each abbreviation with a character that’d never be part of a word. I chose the semicolon because it’s on the home row of keys and I never type a semicolon without putting a space or newline between it and the following text. Even when programming in a language that uses semicolons as statement delimiters, I never squeeze the next statement up against the semicolon that ends the previous one.

Lots of TextExpander users design their abbreviations this way, but they don’t always use a semicolon. Brett Terpstra, for example, uses a pair of commas as the prefix for his snippet abbreviations. If I want to import a set of snippets shared by someone like Brett, how do I change the abbreviations to match my prefix without going through the set and editing them one by one?

One answer comes from the recognition that TextExpander snippet libraries are just plist files, the common format for OS X configuration settings. You create snippet libraries by chosing Save a Copy of Group… from TextExpander’s little gear menu.

Saving a TextExpander library

These library files, which have a .textexpander extension, are how snippets are shared on sites like the TextExpander Google+ page.

Luckily for me, the Python Standard Library includes the plistlib module, which has several methods for reading, parsing, and writing plist files. I used it to make a little script for changing the prefixes (and suffixes, if that’s what you’re into) of snippets in a TextExpander library. I call it reaffix, and I typically use it this way:

reaffix -r --old-prefix=',,' --new-prefix=';' Markdown.textexpander

This particular example would rewrite the Markdown.textexpander library to have abbreviations that start with semicolons instead of double commas.

Here’s the source code for reaffix:

python:
 1:  #!/usr/bin/env python
 2:  
 3:  import plistlib
 4:  import sys
 5:  import os.path
 6:  import docopt
 7:  
 8:  usage = '''Usage: reaffix [options] TEFILE
 9:  
10:  Change the abbreviation prefix or suffix in a TextExpander plist file.
11:  
12:  Options:
13:    --old-prefix=OP     old prefix
14:    --new-prefix=NP     new prefix
15:    --old-suffix=OS     old suffix
16:    --new-suffix=NS     new suffix
17:    --replace, -r       write over the original file instead of
18:                        creating a new one
19:    --help, -h          print this message
20:  
21:  Normally, there should be at least one "old" and one "new" option
22:  given. Old affixes are stripped from all the abbreviations that have
23:  them. New affixes are added to all the abbreviations.'''
24:  
25:  # Handle the command line options.
26:  args = docopt.docopt(usage)
27:  oldP = args['--old-prefix']
28:  newP = args['--new-prefix']
29:  oldS = args['--old-suffix']
30:  newS = args['--new-suffix']
31:  replace = args['--replace']
32:  infile = args['TEFILE']
33:   
34:  # Extract folder and filename
35:  basename, extension = os.path.splitext(infile)
36:  
37:  # Make sure it's a TextExpander file.
38:  if extension != '.textexpander':
39:    print("%s is not a TextExpander file." % infile)
40:    sys.exit(-1)
41:  
42:  # Generate the new filename.
43:  if replace:
44:    outfile = infile
45:  else:
46:    outfile = basename + "-2" + extension
47:  
48:  # Parse the snippet file.
49:  try:
50:    te = plistlib.readPlist(infile)
51:  except IOError:
52:    print("Couldn't open %s." % infile)
53:    sys.exit(-1)
54:  
55:  # Go through the snippets, changing affixes for each abbreviation.
56:  # Skip snippets that have an empty abbreviation.
57:  for i, a in enumerate(te['snippetsTE2']):
58:    if a['abbreviation'] == '':
59:      continue
60:    else:
61:      newabbrev = a['abbreviation']
62:      if oldP is not None and a['abbreviation'].startswith(oldP):
63:        newabbrev = newabbrev[len(oldP):]
64:      if oldS is not None and a['abbreviation'].endswith(oldS):
65:        newabbrev = newabbrev[:-len(oldS)]
66:      if newP is not None:
67:        newabbrev = newP + newabbrev
68:      if newS is not None:
69:        newabbrev = newabbrev + newS
70:      te['snippetsTE2'][i]['abbreviation'] = newabbrev
71:  
72:  # Write out the new .textexpander file.
73:  try:
74:    plistlib.writePlist(te, outfile)
75:  except IOError:
76:    print "Couldn't write new textexpander file", outfile
77:    sys.exit(-1)

It uses the docopt module, which you’ll have to install if you want to use reaffix yourself. All the other modules are already on your Mac.

Lines 8–23 define the usage message, which docopt parses into the args dictionary in Line 26. There is, frankly, no need for Lines 27–32, but I prefer using variables with short names later in the program.

After some filename fiddling, plistlib reads the snippet library file in Line 50 and parses it into the dictionary te. Lines 57–70 then walk through the dictionary and change the abbreviations according to the directions given on the command line.

For me, an important use of reaffix is to make my snippets easier to use on my iPhone. I chose the semicolon prefix long before there was such a thing as TextExpander and I’m loath to change it because it’s part of my muscle memory. But it stinks as a prefix on the iPhone because the semicolon isn’t on the main keyboard. On iOS I use the same “base” abbreviations, but with a “zz” prefix.

Because my snippets have this difference between platforms, I can’t use TextExpander’s various syncing mechanisms. But I can do this:

  1. On the Mac, save a library to a .textexpander file.
  2. Change the prefix through a command like

    reaffix --old-prefix=';' --new-prefix='zz' -r Symbols.textexpander
    
  3. Upload the changed file to a publicly accessible server and get its URL.
  4. Import it into iOS TextExpander using the Add via URL option.

iOS TextExpander addition

This isn’t as burdensome as it seems, because my static snippets (those that don’t run scripts and therefore can be used on both platforms) almost never change. Still, it would be nice if Smile gave TextExpander the ability to import .textexpander files attached to emails.

Update 7/17/15 7:01 PM
I read Smile’s blog, so I’m not sure how I missed this, but it has a post from last fall called “iOS-ify your MacSnippet Abbreviations,” and it discusses the punctuation problem on iOS keyboards and provides an AppleScript that will do something parallel to what my script does. It’s basically an automated way to do what Greg Scown suggested as a one-off on the TextExpander Google+ site: for every snippet with a Mac-style abbreviation, it creates a new snippet with an iOS-style abbreviation. The new snippets all have content that looks like this:

%snippet:;cmd%

So the iOS snippets aren’t independent; they reference and run the Mac snippets.

This lets you sync your entire snippet set between platforms, but you have to have two versions of each snippet on each platform. Is this redundancy a bad thing? Probably not, but it’s not to my taste. If the redundancy doesn’t bother you, check out that Smile blog post and start converting.