Using Hooks to Smart Program Your Browser
Hooks are a great way to extend a workflow by triggering actions upon events. Simply put, a hook is a variable that holds a list of functions, which are called at a precisely defined point in the program.
In the world of a web browser, there are a ton of events: page loaded, DOM available, page rendered, etc. In addition to the events fired off by normal processing of web pages there are a large set of events which include actions by the user: tab deleted, page bookmarked, command called, etc.
All these are not normally hookable, but in Nyxt, they are.
Hooking into the events fired off by the browser or by the user allows the creation of extendable and optimized workflows.
The Nyxt Hook System
Many hooks are as such executed at different points in Nyxt:
- global hooks, such as the
load-hookhook, which is run after the URL to be visited was parsed, but not yet loaded,
- window and buffer related hooks (before and after creation, before and after the page is loaded, etc),
- modes hooks (before and after a mode is enabled or disabled),
- "before" and "after" command hooks.
For the full list, see the manual.
So what can we do with hooks?
If you want to force the redirection of a domain to another, you can use the
load-hook, change the url and return a new one. In the example below, we make sure we always visit
old.reddit.com instead of the new interface:
(defun old-reddit-handler (url) "Always redirect to old.reddit.com." (let ((uri (quri:uri url))) (if (search "www.reddit" (quri:uri-host uri)) (progn (setf (quri:uri-host uri) "old.reddit.com") (let ((new-url (quri:render-uri uri))) (log:info "Switching to old Reddit: ~a" new-url) new-url)) url))) (add-to-default-list #'old-reddit-handler 'buffer 'load-hook)
You can ask Nyxt to automatically enable or disable modes depending on the URL, for instance, you can toggle the proxy mode per domain, which can be very convenient if you would like to, say, disable Tor for some resource intensive domains:
(defvar *my-unproxied-domains* '("jit.si" "wikipedia.org")) (defun auto-proxy-handler (url) (let* ((uri (quri:uri url)) (domain (and uri (quri:uri-domain uri)))) (when domain (nyxt/proxy-mode:proxy-mode :activate (not (member-string domain *my-unproxied-domains*))))) url) (add-to-default-list #'auto-proxy-handler 'buffer 'load-hook)
Another cool example would be automatically downloading any YouTube video we see:
(defvar +youtube-dl-command+ "youtube-dl" "Path to the 'youtube-dl' program.") (defun auto-yt-dl-handler (url) "Download a Youtube URL asynchronously to /tmp/videos/. Videos are downloaded with `+youtube-dl-command+'." (let ((uri (quri:uri url))) (when (and uri (member-string (quri:uri-domain uri) '("youtube.com" "youtu.be")) (string= (quri:uri-path uri) "/watch")) (log:info "Youtube: downloading ~a" url) (uiop:launch-program (list +youtube-dl-command+ url "-o" "/tmp/videos/%(title)s.%(ext)s")))) url) (add-to-default-list #'auto-yt-dl-handler 'buffer 'load-hook)
Adjust it to your taste!
All user commands have hooks
One feature that makes Nyxt unique is the ability to extend commands exposed to the user.
Because the Common Lisp language allows it, one could replace a command definition by another function. This is however not the recommended approach, most notably because it could break the built-in behavior.
We can then use the before and after hooks to extend the built-in commands. It is as simple as defining a new function with no parameters:
and adding it to the list of hooks:
Hooks can be a great way to extend your browser. There are of course downsides to hooks. For example, consider a hooked function that gets its input from the execution of another hooked function, how do we ensure that they execute in the correct order?
In summary, hooks present a very simple and effective mechanism to chain behavior in your workflows. We are looking forward to seeing what you can create with them!
Thanks for reading!