July 17, 2015 at 5:06 PM by Dr. Drang
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.
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 chosingfrom TextExpander’s little gear menu.
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
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:
- On the Mac, save a library to a
Change the prefix through a command like
reaffix --old-prefix=';' --new-prefix='zz' -r Symbols.textexpander
- Upload the changed file to a publicly accessible server and get its URL.
- Import it into iOS TextExpander using the option.
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:
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.