Circle service

Greg Scown of Smile Software wrote a blog post today in which he described a TextExpander group for writing ⓒⓘⓡⓒⓛⓔⓓ ⓛⓔⓣⓣⓔⓡⓢ. He was inspired by the excessive use of such letters in the tweets of Stephen Hackett.

ⓣⓗⓔ ⓐⓟⓟⓛⓔ ⓢⓣⓞⓡⓔ ⓘⓢ ⓓⓞⓦⓝ ⓙⓤⓢⓣ ⓛⓘⓚⓔ ⓔⓥⓔⓡⓨ ⓞⓣⓗⓔⓡ ⓐⓟⓟⓛⓔ ⓔⓥⓔⓝⓣ ⓑⓡⓑ ⓑⓛⓞⓖⓖⓘⓝⓖ
Stephen Hackett (@ismh) Oct 16 2014 7:56 AM

What’s good about Greg’s snippet group is that it works on both the Mac and iOS; what’s less good is the length of the abbreviations:

For example:

oooT gets you: Ⓣ
ooox gets you: ⓧ
ooo4 gets you: ④

Now it’s true that typing three o’s in a row isn’t much more time-consuming than typing just one, but I still prefer to type the text normally and then convert it to circled form. So I fired up Automator and made a Service to do it.

It took almost no time because I’d already made a few services that did similar things: s̸t̸r̸i̸k̸e̸t̸h̸r̸o̸u̸g̸h̸, dılɟ, and EBG13. The circled text service was most like the text flipper, so I copied it and did some quick editing.

In Automator, the circle service looks like this:

Circle service

The Python script that runs when the Service is invoked is this:

python:
1:  # coding: utf8
2:  
3:  from sys import stdin, stdout
4:  
5:  pchars = u"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
6:  cchars = u"ⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏ⓪①②③④⑤⑥⑦⑧⑨"
7:  circler = dict(zip(map(ord, pchars), cchars))
8:  stdout.write(stdin.read().decode('utf8').translate(circler).encode('utf8'))

Update 10/18/14
OK, this is kind of weird. There are apparently a couple of ways to get the Ⓜ character, and what I did originally caused the translation to fail with capital letters beyond M. The UTF-8 code for Ⓜ, in hex, is 24C2. But for some reason, when you insert it from the Mac Character Viewer, as I did when I first wrote this script, it inserts not just 24C2, but also FE0E, which is Variation Selector-15, a character of zero space.

Circled M in Character Viewer

That messed up the definition of the dictionary in Line 7 and caused all of the higher capital letters to point to a circled capital letter one lower than they should have. For example, S would turn into Ⓡ. To fix this problem, I opened IPython and gave it these two commands

m = unichr(9410)
print m

Because decimal 9410 is hex 24C2, this caused Ⓜ to print without the trailing zero-width character. I used it to clean up the cchars string, and now the service works as it should.

You can’t see any difference between the current script and what I originally posted, because the difference is an invisible character. This is the sort of thing that makes people hate computers.

What I should hate, though, is Apple for sticking an invisible character in where it doesn’t belong. I wonder if that bug has always been there.

The first line tells the Python interpreter that the source code is going to include UTF-8 characters. Lines 5–7 set up a dictionary in which the keys are the character codes of the regular characters and the values are the corresponding circled characters. This kind of dictionary is what the string translate method uses as its argument.1

Lines 8 looks more complicated than it is. Basically, it reads standard input, runs the translation, and writes the result to standard output. The messiness comes from the decode and encode methods, which are there to handle the non-ASCII characters. This arrangement is called the “Unicode sandwich,” and I learned about it by watching this excellent talk by Ned Batchelder.

The service is called Circle selection and it appears in the Services submenu when text is selected. But using the Services submenu is a pain in the ass, so I defined shortcuts for this and the other text conversion services.

Keyboard shortcuts for text translation

I’ve restricted the shortcuts to work only in Dr. Twoot, because Twitter is the only place2 I’ll use them.

The other services are structured the same way in Automator; the only differences are the Python scripts. The post from last year shows older versions of the scripts, which generally worked, but aren’t as robust as what I’m using now.

Here’s the script for flipping characters:

python:
 1:  # coding: utf8
 2:  
 3:  from sys import stdin, stdout
 4:  
 5:  pchars = u"abcdefghijklmnopqrstuvwxyz,.?!'()[]{}"
 6:  fchars = u"ɐqɔpǝɟƃɥıɾʞlɯuodbɹsʇnʌʍxʎz'˙¿¡,)(][}{"
 7:  flipper = dict(zip(map(ord, pchars), fchars))
 8:  a = list(stdin.read().decode('utf8').lower().translate(flipper))[:-1]
 9:  a.reverse()
10:  stdout.write(''.join(a).encode('utf8'))

The script for striking through characters is this:

python:
1:  from sys import stdin, stdout
2:  
3:  unstruck = stdin.read().decode('utf8')
4:  struck = u'\u0338'.join(unstruck)
5:  stdout.write(struck.encode('utf-8'))

Strikethrough works for both A̸S̸C̸I̸I̸ and most Ü̸ñ̸î̸ç̸ø̸ƌ̸é̸ characters, but it won’t work on Emoji. I guess Emoji don’t have whatever characteristics are necessary to function with combining characters.

The script for doing a ROT13 is this:

python:
1:  from string import maketrans
2:  from sys import stdin, stdout
3:  
4:  alpha = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
5:  rot13 = 'nopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM'
6:  r13table = maketrans(alpha, rot13)
7:  stdout.write(stdin.read().translate(r13table))

You’ll notice that I used maketrans in this script and that there’s no Unicode sandwich. That’s because the only characters that get converted are ASCII. Everything that isn’t an ASCII letter passes through untouched, so multi-byte characters don’t need to be decoded and encoded. That’s what what my testing shows, anyway.

Since all four of these services are basically just Python scripts, there’s probably some clever way to run them via Pythonista and a URL scheme on iOS. I haven’t looked into it. Frankly, I wrote these scripts mostly because they were fun, not because I expect to use them much. Unless I need to communicate with Stephen.


  1. In theory, you can use the string.maketrans function to create this kind of table, but it’s never worked for me when Unicode characters are involved. 

  2. Other than this post.