Using Hooks to Smart Program Your Browser

Hooks are a great way to extend a workflow by triggering actions upon events. What hooks are is a mechanism to call a function at a given time, simply put, to "hook" into a function means to execute at that time.

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. These are not normally hookable, but in Next, they are.

Hooking into the events fired off by the user allows the creation of optimized workflows, and extendable macros written in Lisp. A few examples of user actions can be seen below:

By hooking into these actions we can fire off macros to extend the functionality of our browser.

A Practical Example: Bookmarks

Let's hook into the bookmark page event to trigger interesting behavior. First let's look at the default implementation of bookmark-current-page which does, as the docstring suggests "Bookmark the currently opened page in the active buffer."

(define-command bookmark-current-page ()
  "Bookmark the currently opened page in the active buffer."
  (with-result (url (buffer-get-url))
    (let ((db (sqlite:connect
           (truename (probe-file *bookmark-db-path*)))))
       db "insert into bookmarks (url) values (?)" url)
      (sqlite:disconnect db))))

In the above example, the code will open the bookmark database, grab the URL of the current active-buffer, write it into the database, and then close the database.

Downloading our bookmarks and appending them to our notes

That's great, now our bookmark is in a database of bookmarks. Wouldn't it be cool though if our bookmarks were automatically appended to our notes? Well, of course it would be! We could write a simple Lisp function just for that!

(defun download-web-page-to-notes ()
  (let* ((url (interface:web-view-get-url (view *active-buffer*)))
         (web-page-contents (dex:get url))
             (initialize web-page-contents) "body" (children) (serialize))))
    (str:to-file "~/Downloads/notes.txt" body-contents)))

The above function will load the web page into a string via dexador (an HTTP library). After loading the web page into a string, it'll be passed to lquery (a document parsing library). Finally, it will write the body of the document to a file, via str (a string manipulation library). Of course our function could be improved, but this is the most simple version.

Don't miss the Common Lisp Cookbook if you need a good resource.

Hooking in to the bookmark save process

Now that we've written our super short function, we should be able to hook into the bookmark-current-page command. This is done like this:

(add-function-to-hook  :bookmark-current-page

The arguments are hook-name, function-key, and function. The reason we have both a function-key and a function is because the key allows us to pass anonymous functions which we can later unhook.

Now that we've setup our hook, whenever the user executes the command bookmark-current-page, the page will automatically be downloaded and appended to their notes!

Neat, right? Now imagine, extending literally any command via hooks! Execute and do literally anything you want on your machine to create adapted, customized workflows to suit your needs.

The Next Hook System

Within Next, every hook is part of a global hash called *available-hooks*. This hash contains a mapping of :keyword to another hashtable, which contains a mapping of function-name to the actual function. To help make this a little more concrete let's tie it back to our original example.

In our original example *available-hooks* will have a key :bookmark-current-page, the value at this key will be another hashtable. This hashtable contains all of the functions executed whenever the :bookmark-current-page hook is ran.

This hashtable (located at :bookmark-current-page) will contain a key called download-web-page-to-notes. The value at download-web-page-to-notes will be the function download-web-page-to-notes. The values contained within the bookmark-current-page hashtable will all be executed wherever (run-hook :bookmark-current-page) appears.

All user commands have hooks

One feature that makes Next unique is the special form define-command, a macro which wraps defun. Most importantly, the macro places the following form within the body of the function:

(run-hook :bookmark-current-page)

This snippet of code executes all hooks registered with the symbol :bookmark-current-page. Therefore, any defined command has an associated hook that can be hooked into!

Tying it all together

If you tie all of this in together, you'll see that the hook system is extremely elegant and light. All of Next's hook code is a mere 19 lines.

(defun add-function-to-hook (hook-name function-key function)
  (let ((hook-functions-hash (alexandria:ensure-gethash
                              (make-hash-table :test #'equalp))))
    (setf (gethash function-key hook-functions-hash) function)))

(defun execute-entry (key value)
  (declare (ignore key))
  (funcall value))

(defun run-hook (hook-name)
  (let ((hook-functions-hash (gethash hook-name *available-hooks*)))
    (when hook-functions-hash
      (maphash 'execute-entry hook-functions-hash))))

(defun remove-hook (hook-name function-key)
  (let ((hook-functions-hash (gethash hook-name *available-hooks*)))
    (remhash function-key hook-functions-hash)))


Hooks can be a great way to extend your browser. There are of course downsides to hooks. For example, consider a hooked function that depends on the execution of another hooked function, how do we ensure that they execute in the correct order? This is a difficult problem, and outside the scope of the hook use case.

Despite this major drawback, hooks present a very simple and effective mechanism to chain behavior in your workflows. Thanks for reading!