Catching up on my RSS feeds, I’ve just been through Dr Drang’s posts from nearly a year ago about switching away from TextExpander (1, 2, 3, 4). I’m a bit behind.

And since everything on here is effectively hero-worship to Dr Drang, it’s time to catch up. Well, that, or it’s actually sensible to think about switching away from TextExpander; the last update to the non-subscription version (5.1.4) was on February 21 2016.

There isn’t anything in the subscription version for me. I barely use the features of TextExpander 5, rarely create new snippets, and generally am bad at using it.

A TextExpander reminder notification.

I get the snippet reminder notification all the time, to the point where it’s frustrating. I am not good at remembering snippet abbreviations. That’s not necessarily helped by TextExpander encouraging me to create snippets for things that I type frequently for a short amount of time, only to then not type the phrase and forget the snippet exists entirely. Then we’re back to the snippet reminder again the next time I do type the phrase.

TextExpander’s inline search.

For a while I was using the inline search, which is really good. And then I forgot the shortcut for that and would periodically open the menubar — where of course the shortcut for inline search is not listed — and get frustrated each time.

I am bad at using TextExpander and I feel bad.

A TextExpander suggestion notification.

Because of this, I didn’t really want to switch snippets to Keyboard Maestro or one of the TextExpander-alikes.

Instead I settled on using LaunchBar’s snippets, which you search in exactly the same way that you search the main LaunchBar catalogue. Setting a keyboard shortcut for snippets and enabling “sub-search only” means that they’re not cluttering up your usual results either: they’re available when I type ⌃⌥⌘Space but otherwise out the way.

It’s a similar advantage to when I moved my Safari bookmarklets to LaunchBar: if I forget the shortcut I can just type what I want.

A search for ‘star’ in LaunchBar’s snippets.

But so far this is just like TextExpander’s own inline search. And because snippets are part of LaunchBar there are some restrictions in using them with LaunchBar. Why bother?

Well, the overriding reason is still that TextExpander 5 is done. Sooner or later it’s going to stop working. It’s not a matter of whether I should switch but what to.

LaunchBar is a good fit because I use it everywhere, all the time. The snippets are just text files on disk, and so can be deleted or renamed directly from LaunchBar like any other file. They can be created too by sending items to the Add Snippet action.

Honestly I was a bit dismissive when snippets appeared in LaunchBar (“Why wouldn’t you just use TextExpander?”) but as a long-standing LaunchBar user they slot into my workflow seamlessly.

The placeholders are more limited than in TextExpander, which is fine, and snippets are plain-text only, which is again fine as I had very few script snippets and fewer that I used. Anything I keep around will find a new home in Keyboard Maestro or FastScripts.

As with Keyboard Maestro, the snippet dates are formatted with Unicode patterns rather than strftime — which is a bit uncomfortable for me but never mind.

I do have one major criticism of LaunchBar though. If you followed the link to its snippet documentation you’ll perhaps notice the old-style LaunchBar interface. Why?

Help not yet available

The Help for LaunchBar 6 is already in the works and will be available soon. In the meantime you might take a look at the Help of LaunchBar 5. Most of the information found there is still valid for LaunchBar 6.

LaunchBar 6 was released in June 2014. Maybe the docs will be in LaunchBar 7? In the meantime, time to actually read that copy of Take Control of LaunchBar.

Exporting your bookmarks from TextExpander

TextExpander doesn’t actually have an export option but it’s pretty easy to get your snippets out of the settings file, which for me was a single XML plist file Settings.textexpander.

As a warning, at the end of this I still had to do some manual work to tidy the snippets up, and none of the placeholders (where applicable) are converted. Excluding spelling correction snippets, I only have about 120 (many of which I deleted), but if you have many more this could be a problem.

But with that in mind hopefully it shows how simple it is to get at the actual data and reconfigure it into something useful for you. It’s broken up into parts and isn’t the most refined because it’s the result of mucking about in a Jupyter notebook until I got something that worked well enough.

First, the set-up (with the TextExpander settings file copied to my desktop):

import plistlib
import json

with open('/Users/robjwells/Desktop/Settings.textexpander',
          mode='rb') as plist_file:
    plist_data = plistlib.load(plist_file)
snippets = plist_data['snippetsTE2']

If we then pretty-print the first snippet you can get an idea of the format:

{'abbreviation': ';rjw',
 'abbreviationMode': 0,
 'creationDate': datetime.datetime(2014, 1, 13, 9, 33, 41),
 'label': '',
 'lastUsed': datetime.datetime(2017, 2, 28, 18, 56, 15),
 'modificationDate': datetime.datetime(2014, 1, 13, 9, 33, 47),
 'plainText': 'robjwells',
 'snippetType': 0,
 'useCount': 11,
 'uuidString': '87B39A64-B704-46CC-A82D-C3BB07A9C9B4'}

It’s fairly trivial to then pick out the fields you want:

export_snippets = [
    {
        'abbr': snip['abbreviation'],
        'label': snip['label'],
        'text': snip['plainText'],
        'type': snip['snippetType']
    }
    for snip in snippets
]

The type field represents whether the snippet is plain text (0), AppleScript (2) or (3) a “shell” script (or Python or whatever). I imagine that type 1 is rich text, but I don’t have any rich text snippets. We’re keeping it around for use later.

And that first snippet is now:

{'abbr': ';rjw', 'label': '', 'text': 'robjwells', 'type': 0}

At this point, before doing any further processing, I wanted to get rid of the spelling correction snippets. The ordering of my snippets meant that I could just find the “header” snippet and exclude everything that follows.

for idx, snip in enumerate(export_snippets):
    if snip['text'] == '\u00bb Auto Correct - Spelling':
        # Start of spelling correction snippets
        break
export_snippets = export_snippets[:idx]

For those that remain we now create a filename where the snippet text will be saved:

import string

def make_safe_filename(text):
    invalid_chars = set('/:\t\n\\;')
    text = text.replace('/', '-')
    return ''.join(char for char in text
                   if char not in invalid_chars).strip()

for snip in export_snippets:
    abbr = make_safe_filename(snip['abbr'])
    label = make_safe_filename(snip['label'])
    if not label and len(snip['text']) < 20:
        label = make_safe_filename(snip['text'])
    if label and abbr:
        name = f'{abbr} - {label}'
    elif abbr:
        name = abbr
    snip['file'] = name.strip()

The way I strip the abbreviation and label down is fairly arbitrary, but I exclude characters that may cause problems on the file system and also the semicolon — my no-longer-needed snippet prefix.

The code above is not what I actually used last night to name the snippets, but it could have saved me some work in Name Mangler. The important thing to note is that, in TextExpander, if a snippet doesn’t have a label the snippet content is shown instead. That’s what the length check is doing: if there’s no label and the snippet text is short, use the snippet text itself to name the file.

Now we finally deal with the snippet type. The “shell” snippets aren’t necessarily shell scripts so the “shell-rename” extension is to prompt me to change it to something more appropriate (.py, .sh).

snippet_types = {
    0: 'txt',
    2: 'applescript',
    3: 'shell-rename',
    }
for snip in export_snippets:
    snip['file'] = '.'.join(
        [snip['file'], snippet_types[snip['type']]])

Printing that first snippet again:

{'abbr': ';rjw', 'label': '', 'text': 'robjwells',
 'type': 0, 'file': 'rjw.txt'}

And lastly writing the snippets out:

from pathlib import Path

snippets_folder = Path('/Users/robjwells/Desktop/te-snippets/')
snippets_folder.mkdir(exist_ok=True)
for snip in export_snippets:
    file_path = snippets_folder.joinpath(snip['file'])
    file_path.write_text(snip['text'])

I then went through each snippet to clear out ones that wouldn’t work in LaunchBar (shell and AppleScript snippets), ones I don’t use, and ones that needed placeholders updating. I also renamed many snippets to tidy things up.

I have left the abbreviation for several snippets in the filename, so that any snippet-typing habits aren’t completely wasted, and trained LaunchBar for a couple of snippets where I don’t want an old abbreviation hanging off the front (for example, “iso” gets me “Current Date (ISO Format)”).