Snippet surgery

Have you been to TE-Snippets yet? It’s a community site for sharing TextExpander tips and snippets set up by Alex Poslavsky. It’s been around for only a month or so but looks like a winner. A recent addition is TE-Tool, written by Brett Terpstra.

TE-Tool excerpt

TE-Tool is described here by Brett. The basic idea is this: It’s nice to share TextExpander snippet libraries, but everyone has different ideas about what makes a good abbreviation. I, for example, like my abbreviations to start with a semicolon; Brett likes his to start with a pair of commas. TE-Tool is a way we can share libraries, still keep our preferred abbreviation prefixes, and not have to spend a lot of time tediously editing dozens of snippets one at a time.

As you can guess from the screenshot above, TE-Tool allows you to download a library of snippets with the abbreviations already set to your prefix of choice. As I write this, the only snippet libraries in its repository are Brett’s, but he’s working on a system to allow uploads from anyone. Based on how quickly Brett can get things coded up, I’m guessing uploads will be ready in a few days at most.1

The thing is, TE-Tool needs your snippet library in a special form. When you save a group of snippets from within TextExpander

TextExpander save group command

it’s saved as a plist file with a .textexpander extension. Here’s an excerpt from the top of a .textexpander file I just saved from my Symbols snippet library:

 1:  <?xml version="1.0" encoding="UTF-8"?>
 2:  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "">
 3:  <plist version="1.0">
 4:  <dict>
 5:     <key>groupInfo</key>
 6:     <dict>
 7:         <key>expandAfterMode</key>
 8:         <integer>0</integer>
 9:         <key>groupName</key>
10:         <string>Symbols</string>
11:     </dict>
12:     <key>snippetsTE2</key>
13:     <array>
14:         <dict>
15:             <key>abbreviation</key>
16:             <string>;1/2</string>
17:             <key>abbreviationMode</key>
18:             <integer>0</integer>
19:             <key>creationDate</key>
20:             <date>2009-05-20T14:59:57Z</date>
21:             <key>flags</key>
22:             <integer>0</integer>
23:             <key>label</key>
24:             <string>½</string>
25:             <key>lastUsed</key>
26:             <date>2011-03-25T07:59:35Z</date>
27:             <key>modificationDate</key>
28:             <date>2009-05-20T15:00:12Z</date>
29:             <key>plainText</key>
30:             <string>½</string>
31:             <key>snippetType</key>
32:             <integer>0</integer>
33:             <key>useCount</key>
34:             <integer>15</integer>
35:             <key>uuidString</key>
36:             <string>00D526BD-EC79-4A54-8B1B-2AE17F27BC0B</string>
37:         </dict>

You can see in Line 16 that the abbreviation for my ½ snippet is ;1/2. What TE-Tool needs is that line changed to a more generic, standardized form:

16:             <string>[[PREFIX]]1/2</string>

Brett thinks, with good reason, that people can open their .textexpander files in a text editor and change their prefixes to the standard form with a search-and-replace. That’s probably fine for a guy who uses a weird prefix like he does, but could be a disaster for someone like me. If I did a global search-and-replace on a semicolon, I know I’d screw up at least one of my snippets that has a semicolon in the expansion.

So I wrote a simple Python script called “tedist,” which reads in the .textexpander file and replaces the prefix with [[PREFIX]]. It uses the clever plistlib library to restrict itself to doing the replacement only in abbreviations and only at the beginning of abbreviations. You use it this way,

tedist Symbols.textexpander ';'

where the first argument is the name of the .textexpander file and the second is the prefix used in it (protected from shell interpretation by single quotes). It creates a new file with the same base name and a .tedist extension, e.g., Symbols.tedist. Brett uses the .tedist extension to denote files that have exactly the form of .textexpander files but with the standardized prefix.

The tedist script is in this GitHub repository. You can’t do much with it now, but you’ll be able to use it to prepare your TextExpander libraries when TE-Tool allows uploads.

After writing tedist, I thought about a more general way to quickly change abbreviations and wrote the other script in the repository, reabbrev. What reabbrev does is run through all the snippets in a .textexpander file, showing you the label of the snippet and giving you the opportunity to change its abbreviation. This isn’t restricted to prefixes; it allows you to change any abbreviation to anything you want. It’s an interactive program that runs on the command line. Here’s an excerpt from a sample session with the user’s responses in blue:

reabbrev Symbols.textexpander 
Label: ½
Abbreviation [;1/2]: 

Label: ¼
Abbreviation [;1/4]: 1//4

Label: ⅛
Abbreviation [;1/8]: 1//8

Label: ¾
Abbreviation [;3/4]: 3//4

Label: ⅜
Abbreviation [;3/8]: 3//8

Label: ⅝
Abbreviation [;5/8]: 5//8

Label: ⅞
Abbreviation [;7/8]: 7//8

Note that the current abbreviation is given in brackets. If you simply tap the Return key—as I did with the ½ snippet—the current abbreviation will be unchanged. When you’re done, the new snippet library is saved with same name as the original, but with a -2 appended. In the example above, the new library is Symbols-2.textexpander.

Here’s the code for reabbrev:

 1:  #!/usr/bin/python
 3:  import plistlib
 4:  import sys
 5:  import os.path
 7:  # Get the TextExpander snippet filename from the command line.
 8:  fn = os.path.abspath(sys.argv[1])
10:  # Extract the parts of the filename.
11:  folder = os.path.dirname(fn)
12:  fnbase = os.path.basename(fn)
14:  # Make sure it's a TextExpander file.
15:  if fnbase[-13:] != '.textexpander':
16:    sys.exit(fnbase + " is not a TextExpander file.")
18:  # Generate the new filename.
19:  newfn = folder + '/' + fnbase[:-13] + '-2.textexpander'
21:  # Parse the snippet file.
22:  te = plistlib.readPlist(fn)
24:  # Go through the snippets, allowing the user to change each abbreviation.
25:  for i in range(len(te['snippetsTE2'])):
26:    print 'Label: ' + te['snippetsTE2'][i]['label']
27:    newabbrev = raw_input('Abbreviation [' + te['snippetsTE2'][i]['abbreviation'] + ']: ')
28:    if newabbrev != '':
29:      te['snippetsTE2'][i]['abbreviation'] = newabbrev
30:    print
32:  # Write out the new .textexpander file.
33:  plistlib.writePlist(te, newfn)

The code is pretty straightforward, I think. The heavy lifting is done by the calls to plistlib.readPlist in Line 22 and plistlib.writePlist in Line 33. What makes these functions so useful is that they convert between the plist format and a standard Python dictionary. Arrays within the plist are turned into Python lists. This allows you to use all of Python’s many dictionary and list processing methods to fiddle with the plist before writing it back out.

The reabbrev script doesn’t do anything you can’t already do in TextExpander itself, but it’s much faster at editing abbreviations because it eliminates all the clicking and scrolling you have to do in the TE window. If you like someone else’s snippet library but don’t like their abbreviations, run the .textexpander file through reabbrev and you’ll get things customized to your taste.

As for the tedist script: as I said, it’s useless now but will be a big help when TE-Tool gets finished—unless Brett includes its functionality in the uploading process.

Update 4/3/11
If you download the scripts from the repository, you’ll see that reabbrev differs from what’s shown above. Reader urschrei forked the repo and made some good changes to the I/O portions of the code, which I’ve now incorporated in the master.

  1. Honestly, I’m thinking I should just stop programming altogether and just think of things to suggest to Brett. In the wee hours of March 25, I suggested, in a comment on his site, that a command-line tool for converting prefixes would be a good thing for the TE community. The next day he’d gone one better by writing the TE-Tool web app.