Friday, March 6, 2009

A tr.im TextExpander snippet that works for me

At the end of A TextExpander snippet to paste quoted text I gave myself a to-do to create (or find) a TextExpander snippet to create a tr.im shortened URL. tr.im is currently my favorite URL shortening service. Not only is the domain short and easy to remember, but (if you sign up for an account) they provide stats for each “tr.immed” URL.

A Google search found this post on the SmileOnMyMac Blog (from the makers of TextExpander), where you can download a snippet that works. But it’s unnecessarily complicated. After reading the tr.im API documentation I simplified it to this shell script:
#! /bin/bash
curl -u yacitus:xxxxx http://api.tr.im/api/trim_simple?url=`pbpaste`
Note that Blogger refuses to make this column any wider or use a scrollbar to show all of this code. But the text is there; just copy it and paste into your favorite text editor. (You'll need to use the same trick to see all of the code for the other bash one-liner and the Python code below.)

(You’ll of course want to replace “yacitus” with your tr.im username and replace “xxxxx” with your password.)

The problem with this is that it doesn’t work for me at work, where I use the Authoxy proxy server. I can make it work with:
#! /bin/bash
curl -u yacitus:xxxxx -x localhost:8080 http://api.tr.im/api/trim_simple?url=`pbpaste`
…but then it doesn’t work when I’m at home (and not using Authoxy). curl, unfortunately, doesn’t auto-detect proxy settings.

Once again, Python comes to the rescue. I read in “Fuzzyman’s” urllib2 - The Missing Manual that the Python urllib2 module will auto-detect proxy settings, so I wrote this script:
#!/usr/bin/env python
"""
This script writes to stdout a tr.im version of the indicated URL.

"""

import urllib2
import sys


TRIM_API_URL = 'http://api.tr.im/api'
USERNAME = 'yacitus'
PASSWORD = 'xxxxx'


def main():
"""The entry function."""
url_to_trim = sys.argv[1]
if not url_to_trim:
print "ERROR: one (and only one) argument accepted--the URL to GET"
return -1

response = urllib2.urlopen('%s/trim_simple?url=%s&username=%s&password=%s'
% (TRIM_API_URL,
url_to_trim,
USERNAME,
PASSWORD))
print response.read().strip()

print response.code

return 0



if __name__ == '__main__':
sys.exit(main())
Like in A TextExpander snippet to paste quoted text, I put this in a file called trim_url, did a chmod +x on it, created a symbolic link to it in /usr/local/bin/, and created my TextExpander snippet:










I could have saved myself a lot of time if I had stopped there. But I read on http://tr.im/api that basic HTTP authentication is preferred over the query string parameters I used above. So I figured it would be a learning opportunity to implement basic HTTP authentication in Python. The problem is, I’m not done learning yet! I have yet to get it to work. I posted my question on the BayPIGgies mailing list where I got some good advice on how to debug it (but no one saw the problem), and I also posted a question on stackoverflow.com where I got one answer that may be an improvement on the query string parameters, but again no one saw the problem. I guess I’ll have to take jj’s suggestion and look at what is being sent over the wire. When I figure it out I’ll post the answer on my PyPap blog (and of course on the BayPIGgies mailing list and stackoverflow.com).

Tuesday, March 3, 2009

A TextExpander snippet to paste quoted text

I prefer the inline replying posting style. So a frequent workflow for me is copying text, pasting it into BBEdit, applying "Increase Quote Level", "Rewrap quoted text...", "Select All", "Copy" and then pasting the text into (typically) Mailplane, where I finish composing my email. (Or if I'm running Outlook in Fusion, I may compose the email entirely in BBEdit and paste it as a whole back into Outlook.) Each time I do this, I get that "there's got to be a better way" itch. (But then I get back to what I'm working on and forget about it until next time.)

Yesterday I was reading Dan Frakes' "Plain Clip revisited" MacGem post, which explains how to use the free (donations excepted) Plain Clip utility with TextExpander to automatically paste whatever is in the clipboard with any formatting information removed when you type "ptp" (which is the abbreviation he chose to assign to this "snippet" in TextExpander). I need to do that occasionally (and again, I've been doing it by pasting into BBEdit and then copying back to the clipboard), so I downloaded Plain Clip and a free trial of TextExpander and began setting it up. Dan Frakes uses an AppleScript to run Plain Clip through the command-line. But I noticed I can define a shell script in TextExpander so I don't need to bother with an AppleScript "do shell script" wrapper. But I couldn't get it to work. I quickly found a link in the comments to Gordon Meyer's "A tip for using Plain Clip with TextExpander" blog post, which solved the problem for me.

And that's when a light bulb finally went off in (over?) my head. If I created a simple command-line utility to do word wrapping, I could create a TextExpander snippet to paste quoted text. And about 30 minutes later that's just what I had.

When I need to create a command-line utility, I almost always turn to Python. And as is so often the case with Python's "batteries included" nature, I found a textwrap module that provided almost everything I needed.
#!/usr/bin/env python
"""
This script wraps the textwrap module with a command-line interface.

"""

import optparse
import sys
from textwrap import TextWrapper


DEFAULT_WIDTH = 70
DEFAULT_QUOTE_STRING = '> '


def main():
"""The entry function."""
parser = optparse.OptionParser(description='Text wrapper', prog='wwrap')
parser.add_option('-w', '--width',
action='store',
metavar='NUM',
default='%d' % DEFAULT_WIDTH,
help='maximum length of wrapped lines; defaults to 70')
parser.add_option('-q', '--quote',
action='store_true',
default=False,
help="add '%s' to start of each wrapped line"
% DEFAULT_QUOTE_STRING)
parser.add_option('-r', '--remove',
action='store',
metavar='TXT',
help='remove TXT from start of each line prior to'
' wrapping')
parser.add_option('--removequotes',
action='store_true',
default=False,
help="remove '%s' from the start of each line prior"
" to wrapping" % DEFAULT_QUOTE_STRING)

options, arguments = parser.parse_args()

if not options.width:
print "I don't know how the width wasn't specified, since it's"
print "supposed to default to %d. Aborting." % DEFAULT_WIDTH
return -1

try:
width = int(options.width)
except ValueError:
print "ERROR: the width specified ('%s') is not a number" % (
options.width)
return -1

if options.remove and options.removequotes:
print "ERROR: cannot use both --remove and --removequotes options"
return -1

if options.removequotes:
options.remove = DEFAULT_QUOTE_STRING

wrapper = TextWrapper()
if options.quote:
wrapper.initial_indent = DEFAULT_QUOTE_STRING
wrapper.subsequent_indent = DEFAULT_QUOTE_STRING
wrapper.width = width

lines = sys.stdin.read().split('\n')
if options.remove:
for i, line in enumerate(lines):
if line.startswith(options.remove):
lines[i] = line[len(options.remove):]

print wrapper.fill('\n'.join(lines))

return 0




if __name__ == '__main__':
sys.exit(main())
Note that even though you can't see the code at the end of long lines, it's there. Just copy it all and paste it into your favorite text editor to read it all.

This code should be quite self-explanatory. As is (also) so often the case with Python, the "meat" of these 81 lines are the 7 lines starting with line 67 where I read in the text to be wrapped from stdin. All the preceeding lines are for handling the command-line options.

I put this in a file called "wwrap" and did a `chmod +x` on it. I then created a symbolic link to it in /usr/local/bin/, and I was ready to create my TextExpander snippet.

















Now I can just type 'qtp' and the quoted, wrapped form of whatever text is in the clipboard is pasted in.

I'm inspired to look for more workflows I can simplify with TextExpander and possibly Python. (I'm going to start by trying to figure out how to re-create the "Create tr.im shortened URL" in Dan Frakes' TextExpander screenshot.)