Open macOS Finder Window in Emacs Dired

Quite often I have a directory open in macOS Finder that I want to open in Emacs dired, e.g. to mass-rename files, copy stuff to a remote machine via SSH, or what have you.

In my last post, for example, I used Gifox to create a GIF and then viewed the result in Finder. To add the GIF to my blog post, I needed to copy it over into the appropriate website project directory. Since I was writing that post in Emacs already, I wanted to quickly copy the file from my Downloads folder (my default scratchpad for output like screenshots and screen recordings, including GIFs) over to the blog post’s location.

Now there’s two ways to move the file you see in Finder to a directory that you have access to in Emacs:

  1. Either reveal the Emacs file/directory a new Finder window (I am using reveal-in-osx-finder.el for that) and then use Finder to copy stuff between the directories;
  2. or visit the Finder window’s directory in Emacs and use dired to copy or move the file from one pane to the other.

Up until today, I couldn’t do the latter, and I quite like the experience of copying files between two dired panes, so this post documents how I ended up using AppleScript to fetch the Finder window’s location and jump to the path via dired.

Get Finder window location via AppleScript

I have a helper function to cd to the frontmost Finder window in the terminal for ages, based on a 2013 post by Brett Terpstra (of course):

# Part of my bash/zsh aliases file:
cdf() {
  target=`osascript -e 'tell application "Finder" to if (count of Finder windows) > 0 then get POSIX path of (target of front Finder window as text)'`
  if [ "$target" != "" ]; then
    cd "$target"; pwd
  else
    echo 'No Finder window found' >&2
  fi
}

This calls AppleScript from the command line to ask Finder.app for the frontmost window’s path.

This can be called from within Emacs, too, and I wrote a helper for that. Initially, I did overcomplicate everything and reacted to the exit code in case of AppleScript command failure instead of using Brett’s approach to just return an empty string when there’s no Finder window.

I’ll be using the same approach to access the Finder window’s location from Emacs.

Use osascript in Emacs to talk to Finder

What follows is not onw but two versions of more or less the same thing: asking Finder for the location using a command line invocation from Emacs.

I share both with you so you (and I) learn some Elisp along the way, because although the approach I’ll show first is simpler, I found the stuff I learned in the approach I’ll share last very educating, too. (And I ended up adding a couple notes to my Zettelkasten to document what I learned.)

The simple approach: a literal translation of Brett’s code

I shared Brett’s original AppleScript-based shell function above already. It either produces an empty string when no Finder window is open, or a POSIX path to the window’s location.

To call the osascript command line utility from Emacs, I use call-process and pass the AppleScript as an argument:

(defun ct/finder-path ()
  "Return path of the frontmost Finder window, or the empty string.

Asks Finder for the path using AppleScript via `osascript', so
this can take a second or two to execute."
  (let ($applescript)
    (setq $applescript "tell application \"Finder\" to if (count of Finder windows) > 0 then get POSIX path of (target of front Finder window as text)")
    (with-temp-buffer
      ;; Produce a list of process exit code and process output (from the temp buffer)
      (call-process "/usr/bin/osascript" nil (current-buffer) nil "-e" $applescript)
      (string-trim (buffer-string)))))

The trick here is to put both the STDOUT (and STDERR) output into a buffer. Wrapped in with-temp-buffer, this effectively captures the command output in an invisible background buffer and then returns the trimmed result as a string.

This is apparently the most simple approach to getting the string output of a shell command. Both shell-command and call-process are designed to insert the result in a buffer, e.g. to write a log, or insert the result right where you are typing. That’s probably useful when you want to e.g. select text and then call a program to act on the selection (e.g. uniq or tr, although there’s Emacs Lisp equivalents of course) and finally replace the selected text with the output from that command. – This focus on interactivity makes capturing the output as a string instead a bit awkward, though.

Update 2021-07-12: I’ve updated my script to use ns-do-applescript instead, which performs the same much quicker. See the follow-up post.

To open the path that’s returned here in dired (aka “jump” to the path), the following interactive function does the real work and produces a “No Finder window found” message just like Brett’s original shell function:

(defun ct/dired-finder-path ()
  (interactive)
  (let (($path (ct/finder-path)))
    (if (string-equal "" $path)
        (message "No Finder window found.")
      (dired $path))))

Now let’s have a look at my original, more cumbersome approach for everyone’s education.

The more complex approach that handles error codes

I ended up here because my actual cdf command line helper function produced a different AppleScript code – one that didn’t default to the empty string when no Finder window was open, but produce an error instead. I actually don’t find this more but less user friendly than Brett’s code from 2013.

To look up the code, I used which cdf in my terminal. It turns out that some zsh plug-in I installed along the way overwrote the code by Brett, so I got this:

$ which cdf
cdf () {
	cd "$(pfd)"
}

$ which pfd
pfd () {
	osascript 2> /dev/null <<EOF
    tell application "Finder"
      return POSIX path of (insertion location as alias)
    end tell
EOF
}

There’s no error handling, so without a Finder window open, the AppleScript execution will fail with an error as we’ll see below.

Also interesting to note: The AppleScript execution here pipes in a heredoc string via STDIN. Note that Brett’s original, shorter version uses the -e parameter to specify the AppleScript code as a string.

Initially, I tried to use shell-command, which also supports the backwards-piping using a heredoc. On top, this Elisp function allows you to specify a STDOUT and a STDERR output buffer to capture success and failure in parallel.

But everyone (including the Emacs manual) told me to not use this for programmatic shell command executions, so I switched to the slightly more cumbersome call-process. Using a heredoc was awkward with call-process’s separation of command name and parameters, so Iended up passing the script as a parameter instead using -e, too.

An upside of call-process over shell-command is that it returns the exit code of the command execution. So (call-process ...) will return 0 for successful execution of the command, or the error code (e.g. 1) upon failure. That arguably can make reacting to success and failures a bit simpler since the exit code is available. But then you need to react to both the output and the error code, which adds complexity.

That’s why the resulting variant of my function ct/finder-path produces a tuple or list of values, e.g. (list 0 "/path/to/window") on success, or (list 1 "XYZ Error Message beep boop") on failure.

(defun ct/finder-path ()
  "Ask Finder for the path of the frontmost window using AppleScript
and return the exit code as the 1st list item and the path as
the 2nd list item of the result.

This uses `/usr/bin/osascript', so it can take a second or two.

The return value is of the following form:
`(list PROCESS-EXIT-CODE PROCESS-OUTPUT)'.

For non-zero exist codes, you should treat the output as an
error message."
  (let ($applescript)
    ;; This concatenates the multi-line AppleScript into a string:
    (setq $applescript (mapconcat 'identity
                                  '("tell application \"Finder\""
                                    "  return POSIX path of (insertion location as alias)"
                                    "end tell")
                                  "\n"))
    (with-temp-buffer
      ;; Produce a list of process exit code and process output (from the temp buffer)
      (list (call-process "/usr/bin/osascript" nil (current-buffer) nil "-e" $applescript)
            (string-trim (buffer-string))))))

As a consequence, the actual “go to Finder window in dired” command unwraps has to work with the tuple of values and unpack the two elements to conditionally display a failure message:

(defun ct/dired-finder-path ()
  (interactive)
  (let* (($result (ct/finder-path))
         ($exit-code (car $result))
         ($output (cadr $result)))
    (if (eq 0 $exit-code)
        (dired $output)
      (message "Fetching Finder window failed (maybe no window open?):\n%s" $output))))

The minibuffer then displays this when no Finder window is open at all:

"Fetching Finder window failed (maybe no window open?):
72:77: execution error: Finder got an error: AppleEvent handler failed. (-10000)"

Yes, including the quotation marks. Not the prettiest, but serviceable.

I got the idea to return both the exit code and the captured output in a list from StackOverflow. The original author there suggests to extract a wrapper for this kind of command line invocation that looks very useful:

(defun process-exit-code-and-output (program &rest args)
  "Run PROGRAM with ARGS and return the exit code and output in a list."
  (with-temp-buffer
    (list (apply 'call-process program nil (current-buffer) nil args)
          ;; Note: I'm trimming the output to drop trailing newline!
          (string-trim (buffer-string)))))

(process-exit-code-and-output "which" "bash")
;; => (0 "/usr/bin/bash")

Since I didn’t end up using this approach and don’t act on the exit code at all, I didn’t bother to introduce this generalized process helper.

But it’s a neat idea that might come in handy (and which I hopefully remember later).

Limit the scope to macOS

To limit the availability of these functions to macOS (in case you also run Emacs on Linux or Windows), wrap the function defuns in:

(when (memq window-system '(mac ns))
  ;; paste defun's here
  )

I have this in my init.el and when I’m on macOS, I can invoke ct/dired-finder-path just fine, but don’t even see this function on Linux.