# 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


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.

  #!/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")
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")
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()); })();