macOS URI Protocol Handler


Strangely over the past week I ran into the need for a URI protocol handler on three different occasions. Instead of looking for three separate existing handlers that might work, I decided to write a single generic handler. The solution is a simple URI protocol router that forwards requests to shell scripts that handle the protocol requests. Below I describe some of the details; you can also find the end result on github: uri-handler.

To simplify the solution I rely on a bit of convention:

  1. All handlers are placed in a single directory ~/.uri_handlers
  2. Each configured protocol will be handled by a shell script that has the same name as the protocol

The macOS App

Because it is a simple app, I chose to write it in AppleScript

uri-handler.scpt

  on open location this_URL
	set AppleScript's text item delimiters to ":"
	set uri_elements to every text item of this_URL
	set protocol to item 1 of uri_elements
	do shell script "cd ~/.uri_handlers;source .bash_profile;./" & protocol & " '" & this_URL & "' > /dev/null 2>&1 &"
  end open location

Adding a Handler

I wanted to make it easy to add a new handler by calling a shell script with the name of the protocol to be handled.

add-handler.py

  #!/usr/bin/env python
  import sys
  import subprocess

  if __name__ == "__main__":
      if len(sys.argv) == 2:
          protocol = sys.argv[1]
          f = open("/Applications/uri-handler.app/Contents/Info.plist", "r")
          contents = f.readlines()
          f.close()
          contents.insert(23, "              <string>" + protocol + "</string>\n")
          f = open("/Applications/uri-handler.app/Contents/Info.plist", "w")
          contents = "".join(contents)
          f.write(contents)
          f.close()
          subprocess.run("touch /Applications/uri-handler.app; cp handler_template " + protocol + ";", shell=True)
          print("created new handler: (" + protocol + "), and added " + protocol + " to Info.plist file.")
      else:
          print("please provide a handler protocol name")

Removing a handler

I also wanted to make it easy to remove a protocol handler.

remove_handler.py

  #!/usr/bin/env python
  import sys
  import subprocess

  if __name__ == "__main__":
      if len(sys.argv) == 2:
          protocol = sys.argv[1]
          f = open("/Applications/uri-handler.app/Contents/Info.plist", "r")
          contents = f.readlines()
          f.close()
          f = open("/Applications/uri-handler.app/Contents/Info.plist", "w")
          for line in contents:
              if line != "              <string>" + protocol + "</string>\n":
                  f.write(line)

          subprocess.run("touch /Applications/uri-handler.app; rm " + protocol, shell=True)
          print("deleted handler: (" + protocol + "), and removed " + protocol + " from Info.plist file.")
      else:
          print("please provide a handler protocol name")

Emacs Handler

My initial need was a URI protocol that would open a file in Emacs and position the cursor at a specific line number. This need is related to an Elixir macro I wrote to provide source code location in logged error and debug messages in the terminal. Clicking the link opens the file to the exact source line.

I run Emacs as a server, and thus the emacsclient application can open a file buffer in an already open Emacs session. The syntax to open a file to a specific line and column is:

emacsclient +{line}:{column} {filename}

First I created and registered the emacs handler by running the add_handler shell script.

cd ~/.uri_handler;./add_handler emacs

Next I modified the default handler source code for my needs.

emacs.py

  #!/usr/bin/env python
  import sys
  import subprocess
  from urllib.parse import urlparse, parse_qs

  if __name__ == "__main__":
      sys.stdout = open('emacs-log', 'w')
      print ("Edit Started...")
      if len(sys.argv) == 2:
          uri = sys.argv[1]
          print (uri)
          result = urlparse(uri)
          query = parse_qs(result.query, keep_blank_values=True)
          if "line" in query:
              line = query["line"][0]
          else:
              line = "0"
          if "column" in query:
              column = query["column"][0]
          else:
              column = "0"
          if "file" in query:
              filename = query["file"][0]
          else:
              filename = "*scratch*"
              line = "0"
              column = "0"

          print(filename)
          print(line)
          print(column)
          subprocess.Popen(["/usr/local/Cellar/emacs/26.1_1/bin/emacsclient", "+" + line + ":" + column, filename])
      else:
          print("Error: no url provided")

While I was at it, I also created an Org-mode protocol handler. This is very easy since Org already handles URIs. So the script can simply pass the entire URI to emacsclient.

org-protocol://capture?template=z&url={url}&title={title}&body={body}

Here is the code for the handler

./add_handler org-mode

org-mode.py

  #!/usr/bin/env python
  import sys
  import subprocess

  if __name__ == "__main__":
      if len(sys.argv) == 2:
          uri = sys.argv[1]
          print("url: " + uri)
          subprocess.run("emacsclient \"" + uri + "\"", shell=True)
      else:
          print("Error: no url provided")

With the Org-mode URI protocol in place, it is easy to add Chrome bookmarklets that can pull info out of a web page and insert it into an Org document using org-capture. For example the bookmarklet below copies the title, link, and whatever text I have highlighted into my Org-mode notes. The key part is the template=z which tells Org to use the custom capture template associated with the letter z. In Org this would be equivalent to C-c c z.

org capture bookmarklet

javascript:(function () { window.location.href='org-protocol://capture?template=z&url=' +encodeURIComponent(window.location.href)+'&title=' +encodeURIComponent(document.title)+'&body=' +encodeURIComponent(window.getSelection()); })();
comments powered by Disqus