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:
- All handlers are placed in a single directory
~/.uri_handlers
- 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()); })();