Block indentation in Emacs

There are several small things Emacs could be doing to make it nicer to write code. One I was missing was making it possible to go with one press of the return key between braces in a C derived language from this:

if (test) { }

to this:

if (test) {
   <-- insertion point here
}

That is, pressing return before the closing parentheses, brace or bracket should move the closing character two lines down, indent it properly, and move the insertion point to the new empty line in the middle and indent it property. The way TextMate does it.

Here are a few of elisp functions to accomplish this:

(defun char-isws (c)
  "Is character c a whitespace character?"
  (or (char-equal c ?\ )
      (char-equal c ?\t)
      (char-equal c ?\n)))

(defun line-next-non-ws ()
  "Return the next non-whitespace character on the current line or nil."
  (let ((cc (char-after)))
    (if (and cc (char-isws cc))
        (save-excursion
          (if (re-search-forward "[^[:space:]]" (save-excursion     (end-of-line) (point)) t)
              (char-before)
            nil))
      cc)))

(defun newline-and-indent-extra-for-closing-paren ()
  "Insert a newline and indent. If the next non-whitespace character is a closing paren, insert two newlines and indent the two  new lines correctly, placing the point on the first of the two new lines."
  (interactive)
  (let ((nc (line-next-non-ws)))
    (if (or (null nc)
            (not (= (char-syntax nc) ?\))))
        (newline-and-indent)
      (progn
        (just-one-space)
        (newline-and-indent)
        (newline-and-indent)
        (previous-line)
        (indent-for-tab-command)))))

Now you can bind return to newline-and-indent-extra-for-closing-paren in a suitable language keymap. I've been using this with scala-mode and it works well there.

Article page

Exporting geolocation data from iPhoto with AppleScript

I recently transferred all my photos to iPhoto. I share them on Flickr, but I've been unhappy with iPhoto's built-in Flickr support — it has an arbitrary 500 photo limit on web album size, it's crashy, it does weird synchronizations that take ages when combined with lots of large photos and a slow internet connection, it has multiple times failed to send all the full-resolution images — so I've been exploring alternatives. There's at least FlickrExport and Flickr's own Uploadr.

Although the tools work, there's a downside compared to iPhoto's built-in Flickr support. iPhoto has wonderful geotagging support, as does Flickr, but iPhoto doesn't write the data to EXIF tags and that poses a problem for the tools. Uploadr reads just the files and so never sees the data, and apparently iPhoto doesn't provide the data to FlickrExport, either. The result is that Flickr won't know the locations of the photos.

There's a way to work around this problem. The solution is AppleScript. iPhoto exports photo objects that can tell you their location as set inside iPhoto. The downside to this approach is that it's AppleScript, but apparently the alternatives like Python or JSTalk aren't quite up to tasks like these without application support.

This script will write the locations of the selected photos relies on ExifTool. It will litter your photo directory with files ending with _original that should contain the unmodified images. You should make sure the modified files are ok before deleting the originals. The usual caveats apply: I'm no AppleScript expert and this has not been tested particularly rigorously. I'd be careful especially if you don't live in the NE hemisphere. And you might want to reduce the number of dialogs. Do what you want with it.

-- This applescript will geotag the selected photos with the
-- location information set in iPhoto.
--  
--   You must have exiftool installed; by default it's loaded from
-- /opt/local/bin, where MacPorts installs it from the package
-- p5-image-exiftool.
--
-- Author: Juri Pakaste (http://www.juripakaste.fi/)
--
-- Based on the Set Geo Data.scpt script by
-- Andrew Turner (http://highearthorbit.com)
-- 
property exifToolOriginal : "_original"
property exifToolPath : "/opt/local/bin/exiftool"

on extract_decimal(realnum)
   set res to realnum - (round realnum rounding down)
   res
end extract_decimal

on roundFloat(n, precision)
    set x to 10 ^ precision
    (((n * x) + 0.5) div 1) / x
end roundFloat

on d2s(degs)
    log "enter d2s"
    if the degs < 0 then
        set the degs to degs * -1
    end if

    set the degrees to round degs rounding down
    set the minssecs to extract_decimal(degs)

    log "minssecs: " & minssecs

    set the minssecs to minssecs * 60
    set the mins to round minssecs rounding down

    set the minssecs to extract_decimal(minssecs)
    log "minssecs 2: " & minssecs
    set the secs to minssecs * 60

    "" & degrees & "," & mins & "," & roundFloat(secs, 2)
end d2s

on exportCoords(image_file, lat, lng, alt)
    set the northSouth to "N"
    set the eastWest to "E"

    if the lat is less than 0 then
        set the northSouth to "S"
        set the lat to the lat * -1
    end if

    if the lng is less than 0 then
        set the eastWest to "W"
        set the lng to the lng * -1
    end if

    log "calling d2s on " & lat
    set the latstr to my d2s(lat)
    set the lngstr to my d2s(lng)

    set exifCommand to exifToolPath & " -GPSMapDatum=WGS-84     -gps:GPSLatitude='" & latstr & "' -gps:GPSLatitudeRef='" & northSouth     ¬
        & "' -gps:GPSLongitude='" & lngstr & "'     -gps:GPSLongitudeRef='" & eastWest ¬
        & "' -xmp:GPSLatitude='" & latstr & northSouth & "'     -xmp:GPSLongitude='" & lngstr & eastWest & "'     -xmp:GPSMapDatum='WGS-84'" & " -xmp:GPSVersionID='2.2.0.0'" & " " & quoted form of image_file
    display dialog of ("running: " & exifCommand)
    set output to do shell script exifCommand
    display dialog of output
    --do shell script "rm '" & image_file & "'" &     exifToolOriginal    
end exportCoords

tell application "iPhoto"
    activate
    try
        copy (my selected_images()) to these_images
        if these_images is false or (the count of     these_images) is 0 then ¬
            error "Please select one or more images."

        repeat with i from 1 to the count of these_images
            set this_photo to item i of these_images
            tell this_photo
                set the image_file to the image path
                set lat to the latitude
                set lng to the longitude
                set alt to the altitude
            end tell

            if lat < 90.1 and lng < 180.1 then
                my exportCoords(image_file, lat, lng,     alt)
            else
                display alert ("No location set for "     & name of this_photo)
            end if

            log "read image, lat: " & lat & ", lng: " &     lng

        end repeat
        display dialog "Geo Exif write complete."
    on error error_message number error_number
        if the error_number is not -128 then
            display dialog of ("failed on: " & image_file)
            display dialog error_message buttons     {"Cancel"} default button 1
        end if
    end try
end tell


on selected_images()
    tell application "iPhoto"
        try
            -- get selection
            set these_items to the selection
            -- check for single album selected
            if the class of item 1 of these_items is album     then error
            -- return the list of selected photos
            return these_items
        on error
            return false
        end try
    end tell
end selected_images

Update (2010-02-28): Thank you to @simonmark on Twitter, who pointed out the script had some issues. My version broke if file names had single quotes in them and Simon's version broke with double quotes (admittedly probably rarer in file names.) I was going to leave it as it was, but found the quoted form method of text objects in AppleScript Language Guide which, assuming it works correctly, should make the script always work properly (at least in terms of file name handling.) I also copied Simon's better error display code and replaced the extract_decimal implementation with something that isn't quite as silly as my previous version was.

Update (2010-06-02): I set up a Bitbucket repository for this and other scripts I've written for iPhoto.

Article page

Announcing Chipmunk Backup

I put up on Launchpad a backup utility I wrote called Chipmunk Backup. It's not extremely configurable nor does it have a huge set of features. It's a simple tool for maintaining a number of GnuPG encrypted full backups of a directory in a remote, rsync-accessible location.

There's no ready to download archive, but checking out lp:chipmunk-backup with bzr should give you a working version.

It's written in PLT Scheme and is known to work with version 4.1.4.

Article page

Emacs tips: Navigate CamelCase words

Emacs tip #0: Always search EmacsWiki when you think you might need something.

Emacs tip #1: To navigate studlyCapped words, M-x c-subword-mode, as found on the CamelCase page. I had to add the following lines to my .emacs to get it work with C-left/C-right, M-b/M-f worked right out of the box:

(define-key global-map [(control right)] 'forward-word)
(define-key global-map [(control left)] 'backward-word)

Before that, they were bound to the -nomark variants.

Article page

Inline admin forms with admin site links in Django

I have a somewhat difficult relationship with Django's admin site. It's a very useful feature, but I haven't really done enough with it to know when I'm going to hit a wall, if that wall's in the code or in my understanding, and how hard it's going to be to climb over the wall.

This time I wanted to have inline admin forms, except that I didn't actually want to have the forms there, I just wanted to have links to the objects — and not their views on the actual site, but on the admin site. As far as I can tell, there's no built-in support for this.

According to the admin docs, there are two subclasses of InlineModelAdmin: TabularInline and StackedInline. Looking at django/contrib/admin/options.py confirms this. And as the docs say, the only difference is the template they use. The stacked version comes pretty close when we add all the fields to an InlineModelAdmin subclass's exclude array, but it doesn't have the link.

To solve this we first create a new subclass:

class LinkedInline(admin.options.InlineModelAdmin):
    template = "admin/edit_inline/linked.html"

When you want to create inline links to a model, you subclass this new LinkedInline class. So to use a slightly contrived example, if we have a Flight with Passengers:

class PassengerInline(LinkedInline):
    model = models.Passenger
    extra = 0
    exclude = [ "name", "sex" ] # etc

class FlightAdmin(admin.ModelAdmin):
    inlines = [ PassengerInline ]

And yes, we have to exclude all the fields explicitly: an empty fields tuple or list is ignored.

The new template is easiest to create by cutting down aggressively the stacked template. Like this:

{% load i18n %}
<div class="inline-group">
  <h2>{{ inline_admin_formset.opts.verbose_name_plural|title}}</h2>
{{ inline_admin_formset.formset.management_form }}
{{ inline_admin_formset.formset.non_form_errors }}

{% for inline_admin_form in inline_admin_formset %}
<div class="inline-related {% if forloop.last %}last-related{% endif %}">
  <h3><b>{{ inline_admin_formset.opts.verbose_name|title }}:</b>&nbsp;{% if inline_admin_form.original %}{{ inline_admin_form.original }}{% else %} #{{ forloop.counter }}{% endif %}
    {% if inline_admin_formset.formset.can_delete and inline_admin_form.original %}<span class="delete">{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}</span>{% endif %}
  </h3>
  {{ inline_admin_form.pk_field.field }}
  {{ inline_admin_form.fk_field.field }}
</div>
{% endfor %}
</div>

The primary/foreign key fields are necessary to keep Django happy.

The result looks about right, it just lacks the links. It seems that Django doesn't give the template all the information we need to make them work: there's root_path that gives us /admin/, app_label contains the application's name and inline_admin_form.original.id contains the id of the inline object. What is lacking is the path component that names the model. I don't think it's available by default (is there a clean way to ask Django what's available in a template's context?), so we need to add it. Amend LinkedInline to look like this:

class LinkedInline(admin.options.InlineModelAdmin):
    template = "admin/edit_inline/linked.html"
    admin_model_path = None

    def __init__(self, *args):
        super(LinkedInline, self).__init__(*args)
        if self.admin_model_path is None:
            self.admin_model_path = self.model.__name__.lower()

Now inline_admin_formset.opts.admin_model_path will be bound to the lowercase name of the inline object's model, which is what the admin site uses in its paths.

With this, we can now replace the inline-related div in the template with this:

<div class="inline-related {% if forloop.last %}last-related{%  endif %}">
  <h3><b>{{ inline_admin_formset.opts.verbose_name|title  }}:</b>&nbsp;<a href="{{ root_path }}{{ app_label }}/{{ inline_admin_formset.opts.admin_model_path }}/{{ inline_admin_form.original.id }}/">{% if inline_admin_form.original %}{{ inline_admin_form.original }}{% else %} #{{ forloop.counter }}{% endif %}</a>
    {% if inline_admin_formset.formset.can_delete and inline_admin_form.original %}<span class="delete">{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}</span>{% endif %}
  </h3>
  {{ inline_admin_form.pk_field.field }}
  {{ inline_admin_form.fk_field.field }}
</div>

That's it. Now Flights get links to Passengers without big forms cluttering up the page.

Article page

iPhone shuffle

These days I use an iPhone as my mobile music device. I have a bit over 1800 songs on it. I usually use shuffle and had it stuck in a weird state a couple of weeks ago — it was constantly playing me just a few tracks. I usually listen for just half an hour to an hour at a time, so I don't know if it would have started looping or what, but those were basically always there for a week's worth of commutes. I finally restarted the phone and that seemed to help, but what do you know, a couple of weeks, several restarts and one operating system upgrade later, it's again playing me exactly the same tracks.

As much as I love the The Roots, honestly, at this points Adrenaline!'s "Once a-again, once a-gain..." start makes me mostly think "once again indeed."

Article page