Skip to main content

OS-X, 'Open With', and emacsclient

Because I've configured my GNU emacs to automatically run in daemon mode I wish to 'use' emacs by running emacsclient exclusively. But emacsclient isn't an OS-X 'app' which means that I can't double click on a file to open it with emacsclient. It also means that I can't use the 'Open With' menu to edit a file via emacsclient. Here is how to resolve those issues.

Create an emacsclient app

To create the app, I used Platypus. It is a truly great little application that enables one to put a wrapper around a shell script (or, I suppose, an a.out) to make it act like a 'real' application.

  1. Create a trivial shell script called ec which calls emacsclient. Make the script executable. I put it in my ~/bin/.

    #!/bin/bash
    # Start emacsclient and return immediately.
    /Applications/Emacs.app/Contents/MacOS/bin/emacsclient -n -c "$@"
    
  2. Optional: If you want your app to have the pretty GNU Emacs icon, copy Emacs.app/Contents/Resources/Emacs.icns to the temp dir.

  3. Launch Platypus. In the Platypus UI, do the following:

    App Name              -->  'emacsclient'
    Script Type           -->  'bash'
    Script Path           -->  '~/bin/ec'
    Output                -->  'None'
    Accepts dropped items -->  'checked'
    Everything else       -->  'unchecked'
    
    Optional: Click the gear under the icon and set icon to:  Emacs.icns
    
  4. In Platypus, click on 'create'.

    This will create a 'real' OS-X application named emacsclient.app in your temp dir. Copy this emacsclient.app to /Applications.

You now have a functioning emacsclient OS-X 'application'.

emacsclient.app and File Extensions

The app created by Platypus worked fine. However, nothing had been done to associate file extensions with the emacsclient.app. Without those associations it would not be possible to, for example, double click a Python file and have emacsclient open it.

It seems like Platypus would offer a mechanism for associating file extensions with the application it generates, but I could not find one. Here's what I did.

/Applications/emacsclient.app/Contents/Info.plist contains an array of file extensions that emacsclient knows how to open; they are stored under the CFBundleDocumentTypes key. The array was essentially empty.

I decided that I wanted to take the list of supported extensions from the Info.plist under Emacs.app and insert them into the plist for emacsclient.app. But how to do this?

The duti utility looked promising, but it would not compile on El Capitan. It may have just been a matter of updating a version string in the config, but I didn't feel like investing time in the necessary research.

I believe that I could have used plutil and PlistBuddy to convert both plists to XML and then manually edit them, but seemed tedious and error-prone.

I was surprised to discover that Python includes plistlib in its standard distribution. It made things ridiculously easy.

The following code will extract a key value from a source plist and insert that same key:value into a destination plist, saving the modified destination plist as a new plist.

#!/usr/bin/env python
# cppk.py -- This DOES NOT modify anything in /Applications

import sys, plistlib, os

(key, src, dest, new) = sys.argv[1:]

source = plistlib.readPlist(src)
destination = plistlib.readPlist(dest)

destination[key] = source[key]

plistlib.writePlist(destination, new)

Example usage:

$ E=/Applications/Emacs.app/Contents/Info.plist
$ EC=/Applications/emacsclient.app/Contents/Info.plist
$ ./cppk.py CFBundleDocumentTypes $E $EC ./Info.plist

This creates the desired plist and saves it as ./Info.plist. I then copied ./Info.plist to emacsclient.app/Contents/Info.plist (after making a backup copy).

To force OS-X to pick up the change, I ran lsregister, causing Launch Services to rebuild its database:

# Valid for OS-X 10.11.2.  If the path is invalid on your version,
# run 'locate lsregister' to find it.

% F=/System/Library/Frameworks/CoreServices.framework
% F+=/Versions/A/Frameworks/LaunchServices.framework
% F+=/Versions/A/Support/lsregister

% "$F" -lint -kill -r -domain local -domain system -domain user

This may take a while to complete.

Conclusion

After this point, emacsclient will appear in the 'Open With' menu. Additionally, it is now possible to globally associate emacsclient with file types so that it is the default application used to open them.

I can now double-click on a python file and have emacsclient open it.

I'd like to automate setting the global default application for certain file types (e.g., .py, .txt, etc.), but that will wait for another day.