Modular URL shortening TextExpander snippets

Yesterday, John Flavin pointed out that my TextExpander snippets for getting Google-shortened URLs could be built better:

@drdrang Why repeat the ;furl code here? Why not just use %snippet:;furl% and make the whole thing a TE shell script?
John Flavin (@JFlavin) Aug 3 2014 11:11 AM

He’s right. Although I was thinking my snippets had to be self contained, they can call other snippets and incorporate the results—even if the snippet that’s doing the calling is a shell script or AppleScript snippet. TextExpander does the expansion first and then runs the script. So I refactored my snippets this way:

First, there’s my old snippet for getting the (unshortened) URL of the active browser tab, ;furl. It’s an AppleScript snippet with the following content:

applescript:
 1:  tell application "System Events"
 2:    set numSafari to count (every process whose name is "Safari")
 3:    set numChrome to count (every process whose name is "Google Chrome")
 4:  end tell
 5:  
 6:  if numSafari > 0 then
 7:    tell application "Safari" to get URL of front document
 8:  else
 9:    if numChrome > 0 then
10:      tell application "Google Chrome" to get URL of active tab of front window
11:    end if
12:  end if

(Note that I’m now using Vítor Galvão’s one-liner for Chrome.)

Next, I have a Python script, gshorten, that’s structured like the Pythonista script I use for shortening URLs on iOS, except that it takes the original URL from the command line and returns the shortened URL to standard output. It’s saved in my ~/Dropbox/bin folder. Here it is:

python:
 1:  #!/usr/bin/python
 2:  
 3:  import requests
 4:  import json
 5:  import sys
 6:  
 7:  # Build the request.
 8:  shortener = "https://www.googleapis.com/urlshortener/v1/url"
 9:  longURL = sys.argv[1]
10:  headers = {'content-type': 'application/json'}
11:  payload = {'longUrl': longURL}
12:  
13:  # Get the shortened URL and print it.
14:  r = requests.post(shortener, headers=headers, data=json.dumps(payload))
15:  sys.stdout.write(r.json()['id'])

The snippet I use to shorten the URL of the front browser tab uses both of these. It’s a shell script snippet with abbreviation ;surl and this content:

bash:
#!/bin/bash
~/Dropbox/bin/gshorten '%snippet:;furl%'

It runs the ;furl snippet and uses the result as the argument to gshorten. What’s nice about this solution is that it’s using AppleScript for what it’s good for (communicating with applications) and Python for what it’s good for (everything else).

Finally, the snippet I use to shorten a URL on the clipboard has the abbreviation ;scurl and this content:

bash:
#!/bin/bash
~/Dropbox/bin/gshorten '%clipboard'

It’s basically the same as ;surl except that it uses the clipboard as the argument to gshorten.

Splitting things up this way makes my system a bit more complicated, but there’s less repetition and, more important, each component does one thing. If I need to change the code for getting the front tab’s URL (to add another browser, for example, or if a browser changes its AppleScript library), I only have to change the code in ;furl. Similarly, if Google changes its API, I only have to fix the code in gshorten.

Someone my age should know enough to use modular design in the first place. Thanks to John for straightening me out.


Automatic shortened URLs via Google

Back in the old days (that is, maybe three years ago), everyone on Twitter used some specialized URL shortening service, like bit.ly or tr.im, to keep their tweets under 140 characters.1 That became counterproductive when Twitter started its own shortening service and built an infrastructure around it. Nowadays, it’s much better for your readers if you just paste in the full, true URL of the page you want to link to and let Twitter handle the shortening and the display.

Except…

Except when you want to include a link in a direct message. Twitter refuses most links in DMs because

  1. it doesn’t want to pass along spam or phishing attacks in DMs; but
  2. it doesn’t want to take responsibility for vetting the links before shortening them.

If you try to send a link via DM in the web interface, it responds with a terse “Your message can’t be sent.” In Tweetbot, you get a longer but no more satisfying explanation.

Tweetbot DM error

By “later,” I guess they mean “when Twitter changes its policy.”

Some links are allowed in direct messages. I don’t know all the possibilities, but I know you can include those from Google’s goo.gl service. I don’t send a lot of direct messages, and those I send generally don’t include links, but when I need to send links, goo.gl is what I use.

Yesterday I decided to automate the process of getting goo.gl-shortened links by using Google’s URL Shortener API. I signed up for an API key, but after some experimentation, I found that the key wasn’t necessary. Maybe you need it if you want to track links, but I have no interest in that, especially for one-off links in direct messages.

I followed Google’s examples to learn how to shorten a URL from the command line using curl. Here’s a long example, split up over three lines:

curl  https://www.googleapis.com/urlshortener/v1/url \
 -H 'Content-Type: application/json' \
 -d '{"longUrl": "http://www.leancrew.com/all-this/2014/07/alfred/"}'

The -d argument is what’s POSTed, and as you can see, it’s in JSON format. The result is in JSON format, too:

{
 "kind": "urlshortener#url",
 "id": "http://goo.gl/GZ5t0e",
 "longUrl": "http://www.leancrew.com/all-this/2014/07/alfred/"
}

What I wanted to make was a TextExpander snippet that would take the URL of the frontmost browser tab and return a shortened version of it. Here it is.

TextExpander URL shortener

It’s an AppleScript snippet with an abbreviation of ;surl.2 The AppleScript content is this script:

applescript:
 1:  tell application "System Events"
 2:    set numSafari to count (every process whose name is "Safari")
 3:    set numChrome to count (every process whose name is "Google Chrome")
 4:  end tell
 5:  set longURL to "none"
 6:  
 7:  if numSafari > 0 then
 8:    tell application "Safari" to set longURL to URL of front document
 9:  else
10:    if numChrome > 0 then
11:      tell application "Google Chrome" to set longURL to URL of active tab of front window
12:    end if
13:  end if
14:  
15:  set payload to "{\"longUrl\": \"" & longURL & "\"}"
16:  
17:  set cmd to "curl -s https://www.googleapis.com/urlshortener/v1/url -H 'Content-Type: application/json' -d '" & payload & "' | awk '/^ \"id\":/ {gsub(/\"|,/, \"\", $2); print $2}'"
18:  
19:  return do shell script cmd

Lines 1–13 are essentially the same as my ;furl snippet. They get the URL of the active browser tab, from Chrome if it’s running or from Safari if it’s running. Line 15 then constructs the JSON payload, and Line 17 builds a call to curl just like the example above. It also pipes the JSON output to an awk one-liner to extract just the shortened URL. There’s a lot of backslash escaping of double quotation marks because AppleScript. Line 19 runs the command and returns the result.

(I decided to use awk here because the JSON returned by the API is pretty simple. If I were dealing with more complex output, I’d mimic what Greg Scown of Smile did in this blog post. He used a headless application called JSON Helper [free on the Mac App Store] to parse the longer JSON data returned from a currency conversion API call.)

I also made a similar snippet for shortening a URL on the clipboard. Its abbreviation is ;scurl and its AppleScript content is this script:

applescript:
1:  set longURL to the clipboard
2:  
3:  set payload to "{\"longUrl\": \"" & longURL & "\"}"
4:  
5:  set cmd to "curl -s https://www.googleapis.com/urlshortener/v1/url -H 'Content-Type: application/json' -d '" & payload & "' | awk '/^ \"id\":/ {gsub(/\"|,/, \"\", $2); print $2}'"
6:  
7:  return do shell script cmd

That takes care of OS X, how about iOS? There’s no AppleScript on iOS, and TextExpander doesn’t run scripts, so I went with a Pythonista script that replaces a URL on the clipboard with a shortened version. Here’s the script:

python:
 1:  import requests
 2:  import json
 3:  import clipboard
 4:  
 5:  # Build the request.
 6:  shortener = "https://www.googleapis.com/urlshortener/v1/url"
 7:  longURL = clipboard.get()
 8:  headers = {'content-type': 'application/json'}
 9:  payload = {'longUrl': longURL}
10:  
11:  # Get the shortened URL and put it on the clipboard.
12:  r = requests.post(shortener, headers=headers, data=json.dumps(payload))
13:  clipboard.set(r.json()['id'])

On iOS, I copy the URL I want to shorten, switch to Pythonista, run the script, then switch to Tweetbot to paste the shortened URL into a DM. There’s probably a way to cut out a step or two by using an x-callback URL, but I haven’t messed around with that yet.

None of these scripts are forgiving. Inputs that aren’t URLs will produce nasty output that isn’t handled gracefully. Be aware of this if you want to adapt these scripts for your own use.

Update 8/5/14
I rewrote these scripts to make them easier to maintain.


  1. Ironically, the granddaddy of URL shorteners, tinyurl.com, was too long for use on Twitter. 

  2. Following the principle of conservation of abbreviations, this is the same abbreviation I used to use for my old URL shortening snippet. No sense wasting strings. 


Alfred

I’m giving Alfred a try. I’ve been using LaunchBar for 6½ years (ever since Alcor warned us that Quicksilver wouldn’t be stabilizing any time soon) and have ignored the many Alfred recommendations I’ve seen. LaunchBar was fast and reliable, and my fingers were trained to its rhythms. I even gave up on Jumpcut a couple of months ago to use LaunchBar’s clipboard history.

So why the change? Well, I’m not certain that I am changing. I’m going to be running Alfred exclusively for a couple of weeks, but that doesn’t mean I won’t be going back to LaunchBar. But what inspired me to start this trial is the prospect of running Yosemite on the machine I’m typing this on: a 2010 MacBook Air with a Core 2 Duo processor and 4 GB of RAM.

By all accounts, Yosemite will run on this computer, but my dismal experience running Lion on a 2006 iMac has made me leery of Apple’s assurances.1 A few days ago, I started wondering about the memory footprint of the apps I commonly run, so I opened up Activity Monitor and had a look. It showed LaunchBar using over 900 MB.

This was on a system that hadn’t been rebooted in ages, so the LaunchBar process was quite old. I quit and restarted LaunchBar to see if the 900 MB figure was an anomaly. LaunchBar’s footprint started out at about 100 MB but quickly climbed to over 400 MB. This was when I decided a look at Alfred would be prudent. I downloaded the basic (free) version and started it up. Much different.

Unless Activity Monitor is lying to me, LaunchBar regularly uses over 400 MB of memory and Alfred (sans Powerpack) uses around 25 MB.
Dr. Drang (@drdrang) Jul 29 2014 4:43 PM

Replies to this tweet told me that I could expect similar memory use by Alfred even with the Powerpack. That’s when I decided to buy the Powerpack and give it serious trial.

(To be fair to LaunchBar, I must mention that I was able to reduce its memory use by tweaking some of its indexing settings. But it was still using over 100 MB, which seems like too much for an app that’s always running in the background.)

Alfred is, for the most part, enough like LaunchBar that my fingers do the right thing, but there are some exceptions. My thumb keeps hitting the spacebar to drill down through folders but all that does in Alfred is add a space to the search string. I have to retrain myself to use the arrow keys. Conversely, instead of thumbing the spacebar to start a file search, I tend to start typing the name of the file right away, which usually causes Alfred to start a Google search.

Also, Alfred hasn’t been able to find some of the AppleScripts I used to run from LaunchBar, so I’ve started turning them into Workflows. These Workflows are embarrassingly simple—just a keyword and an AppleScript—but they work and take almost no time to make.

Alfred Expense template workflow

I start by clicking the + button at the bottom of the window and work my way through the nested menus until I find the Keyword to AppleScript item.

Alfred AppleScript template

Then its just a matter of filling the keyword and title into the Keyword panel and pasting my previously written AppleScript between the on alfred_script(q) and end alfred_script lines in the AppleScript panel. For the Expenses template workflow shown above, I used the script described in this post from a couple of months ago. The full script in Alfred is

applescript:
 1:  on alfred_script(q)
 2:    set today to current date
 3:    tell application "Numbers"
 4:      activate
 5:      make new document with properties {document template:template "Expenses"}
 6:      -- delay 2 -- might need to wait for Numbers to launch
 7:      tell document 1
 8:        tell sheet 1
 9:          tell table 1 to set value of cell 1 of column 2 to today
10:          tell table 2 to set selection range to range "A2:A2"
11:        end tell
12:      end tell
13:      activate
14:    end tell
15:  end alfred_script

One place where Alfred is clearly better than LaunchBar is in its clipboard history. Alfred shows several lines of each clipboard entry in its right panel, whereas LaunchBar shows only the beginning of the first line, much like Alfred’s left panel.

Alfred clipboard history

So far, the frustrations I’ve felt with Alfred have been fixed by tweaking its settings. If that remains the case, I’ll probably switch to it permanently. To get essentially the same functionality using much less memory is too good a deal to pass up.


  1. John Gruber wrote a post many years ago about how all the OS X updates (to that point) actually made his old hardware run better because the newer versions had more efficient code. That was my experience, too, until Lion. It ran fine on my MacBook Air, but the constant memory swapping caused no end of disk thrashing on my iMac. To be sure, that iMac was light on RAM—it had only 3 GB, but in my defense, that was the maximum it could address—but it was supposed to work with Lion. That was the first time Apple misled me, and I haven’t trusted them on system requirements since then. 


Documentation

Since Marco Arment tweeted a link to my post on Overcast, I’ve gotten about twice as much traffic as usual and an undeserved reputation as an expert on Overcast’s features. Because of the latter, I’ve been fielding questions about Overcast that should be going to Marco himself, the wily bastard.

Much as I enjoy acting as Marco’s unpaid support tech, I really wish overcast.fm had a couple of pages of explanation for its settings and use. Overcast is easy to use, but not every feature is obvious.1 This is not a bad thing. Even behavior that seems obvious in retrospect will not necessarily be obvious upon first use. When using a new app, users always bring with them expectations and habits from the apps they used before. A new app that doesn’t have features that are at least a little unfamiliar, that does nothing other apps don’t already do, has no reason for being.

Overcast is hardly the only app that could use a little documentation. There seems to be belief among software developers nowadays that providing instructions indicates a failure of design. It isn’t. Providing instructions is a recognition that your users have different backgrounds and different ways of thinking. A feature that’s immediately obvious to User A may be puzzling to User B, and not because User B is an idiot.

You may not believe this, but when the Macintosh first came out everything about the user interface had to be explained. How to click a button, how to select an item from a menu, how to drag an item from one place to another. Oh, it was way easier to use a Mac than any other computer, but the conventions we now take for granted weren’t conventions yet because no one had experience with mice, menus, windows, and icons. All of these features seem intuitive to today’s users because they learned them as children and don’t remember not knowing them.

Jef Raskin wrote about this long ago in a paper with the wonderfully summarizing title “Intuitive Equals Familiar.” Here’s Raskin saying it better than I have:

As an interface designer I am often asked to design a “better” interface to some product. Usually one can be designed such that, in terms of learning time, eventual speed of operation (productivity), decreased error rates, and ease of implementation it is superior to competing or the client’s own products. Even where my proposals are seen as significant improvements, they are often rejected nonetheless on the grounds that they are not intuitive. It is a classic “catch 22.” The client wants something that is significantly superior to the competition. But if superior, it cannot be the same, so it must be different (typically the greater the improvement, the greater the difference). Therefore it cannot be intuitive, that is, familiar. [emphasis mine]

Let’s think about it another way. Here’s the layout of cab controls in a Caterpillar D5 dozer. Every D5 comes with an operator’s manual, and no one would expect otherwise, even though Caterpillar’s designers do their best to make the controls discoverable, comfortable, and easy to use; even though equipment operators are expected to be trained; and even though there are conventions for control design among construction equipment manufacturers.

Cat D5 cab

A typical Mac app has more controls—buttons, sliders, menu items, whatever—than the cab of a D5. Anyone can buy an app and start using it with no training whatsoever. And yet somehow it shouldn’t have an operator’s manual?

The desire to make computer programs as easy to use as possible is admirable, and the success developers have had in doing so benefits us all. But a little instruction here and there wouldn’t hurt.


  1. John Siracusa’s comments about Overcast’s settings on the latest ATP episode match up with my experience.