Hooks

Hooks

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:

For the full list, see the manual.

Practical examples

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:

(defun post-bookmark-hook ()
  (log:info "let's sync the bookmarks"))

and adding it to the list of hooks:

(push #'post-bookmark-hook bookmark-url-after-hook)

and voila!

Conclusions

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!