TextExpander Snippets with Variables

Just over two years after my snippet renaissance, I’ve finally put some time into working together a hack to address one of my biggest wants — snippet variables. As someone who heavily uses TextExpander’s Script Snippets feature, the benefit of being able to pass a variable to a snippet has long seemed obvious. Nothing illustrates this better than the snippet I use for getting the url of the current tab from Safari:

var safari = Application("Safari Technology Preview");
safari.windows[0].currentTab().url();

So now I can easily grab the URL in Safari whenever typing “;aUrl”. Pretty handy, right? But there’s a problem. What if the url I need is on another tab? The best example of this is when the application I am using happens to be web-based, like Google Docs, then that web application will always be the current tab. The site in question may be just one tab over, but I am stuck taking my attention away to manually copy its URL. Barbaric. What I have long wanted to do is simply tell TextExpander to get me the URL of the next, previous, or any other tab. In theory I could have brute forced this and have some number of different hardcoded snippets, but that would be inelegant and cumbersome to maintain1. Having not found a solution on the web, I decided to try to figure out my own workaround. For those not interested in the history of this project, you can bail now and view the source here.

The Proof of Concept

Because TextExpander supports AppleScript and its younger sibling, JavaScript for Automation, I knew it’d have access to System Events so my idea was simple.

  1. Precede my snippet with some extra text to represent my variable.
  2. Create a JavaScript snippet that uses System Events to select and copy that text, then read it back from the clipboard into a JavaScript variable.

This seemed like a fairly straight forward plan, but executing it presented surprising challenges.

Challenge 1 — TextExpander Keyboard Interruption

Perhaps the biggest hurdle was learning that TextExpander interrupts keyboard input while expanding. You read that right. Learning this behavior was harder than finding the workaround. This is because my original attempt to select and copy text didn’t simply fail. Instead the snippet would only fail sometimes. Other times it would seemingly respond with the copied text as expected while even other times it would return the copied text in some garbled state. This led me down a path of assuming the issue was timing. I thought that maybe some combination of JavaScript for Automation and TextExpander wasn’t handling the asynchronous System Events. After literally hours of banging my head against this did I happen to notice the following behavior, and only after adding various delays.

  1. I would type the variable text followed by the snippet.
  2. TextExpander would remove the snippet, and try to expand with what was on the clipboard.
  3. Only after expansion would my System Event driven keystrokes fire, if ever.

Because my test largely used the same sample text, the fact that TextExpander was returning the clipboard data from the previous run wasn’t immediately obvious.

After a bit more tinkering, the solution to this was simply to use JavaScript for Automation to disable expansion, which returned keyboard functionality. Then I could simply re-enable expansion upon completion of the snippet.

But that led to a new challenge.

Challenge 2 — Disabling Expansion Disables Expansion

Re-enabling expansion for the application TextExpander did not re-enable expansion for this current snippet. Since I had already tapped into the TextExpander application for scripting, the solution for this particular challenge was fairly obvious. Simply save my results to the clipboard and call another snippet, which could then read the results and expand accordingly. A few tries in and voilà, I had my proof of concept.

The Application

Armed with a working proof of concept, I set out to design my solution for a snippet that returned a specific tab’s url based on a query supplied via the preceding text. The design was as follows:

  1. Precede my snippet with a “query character” followed by a “query word or character.” The query character would determine the method used to retrieve a specified tab’s url. For example, one query method would retrieve a tab’s url relative to the current tab while another would retrieve a url based on it’s tab’s absolute order in Safari’s window. The query word would be the parameter supplied to the method determined by the query character.
  2. During expansion, I would save the original clipboard data2, disable expansion and tell system events to arrow left twice – once while holding the option and shift keys to select the preceding word, and again while only holding the shift key to select another preceding character. Then press the “c” key while holding the command key to copy that chunk of text.
  3. If the new clipboard data matches a query format, the script would delete the preceding text and query Safari to get a url from a tab. Otherwise it would default to get the current tab’s url.
  4. The script would then save both the original clipboard data and the url to the clipboard, re-enable expansion, then expand the second snippet.
  5. The second snippet would parse out and expand the url while restoring the original clipboard.

One Last Touch — Reusability.

I actually use this type of get-the-url snippet in a few different ways. For example, I also use JavaScript snippets to create Markdown links. Given the bulk of the code was in handling the query, I did not want duplicate that logic across multiple snippets. Since I already had the mechanism for chaining together snippets, the solution was simply adding a brief snippet upfront to gather snippet abbreviation and add it to my data via the clipboard. The result is a three snippet execution:

  1. The first snippet saves the original clipboard along with the called abbreviation, then calls the second snippet.
  2. The second snippet saves the clipboard data, searches for and executes the query against Safari, then saves the url alongside all other data to the clipboard, and calls the third snippet.
  3. The third snippet reads data from the clipboard, restores the clipboard’s original data, then returns a formatted url based on the called abbreviation.

Bonus Challenge 3 — TextExpander verses TextExpander

Like the disruption of keyboard input, there was another behavior that TextExpander exhibited which took me a while to figure out. When using JavaScipt content, a TextExpander object along with built-in macros are provided for easy access to common features. Here’s the thing though — this convenience object is different from the TextExpander application object available to scripting and all of these features are disabled if you use JavaScript for Automation in such a way that overlaps with any of their functionality. This includes querying the clipboard or interfacing directly with the TextExpander application. As far as I can tell, the detection of this overlap happens statically in that it even occurred when the overlapping code was commented out. Because this isn’t immediately apparent3, this behavior also drove me nuts. I am documenting it here in hopes that I can save someone else’s sanity (not to mention time.)

Next Steps and Final Notes

Having used this solution for about two months now, here are some improvements I’d like to make:

  • Improved reliability. A mechanism that involves copying text is somewhat brittle, even when automated. My best guess is that this solution works somewhere in the ballpark of 9 out of 10 times.
  • Support for returning multiple urls in a list. Right now if the query matches multiple tabs (e.g. multiple tabs have the same word in their titles), this solution will only return the first match. Ideally, I’d like to be able to select from all matches.
  • Support for multiple words when querying titles and urls. Because this solution relies on copying, spaces and any other characters that serve as a word break is not supported. A better solution would be support any text in the query.

I don’t know when I will get around to addressing any of the above. For better or worse, this solution has been great and solves the problem I set to out to solve, even with the flakiness and limitations. Maybe in another two years, Smile will have added some officially supported mechanism for passing in variables.


  1. Also, where’s the fun in that? ↩︎

  2. I considered leaving out perserving clipboard data for vague security concerns, but I ultimately found losing my clipboard data infuriating. ↩︎

  3. It’s possible that TextExpander provides some sort of warning around this, but such a warning would have happened for me long enough ago that I don’t remember. ↩︎