Where parallels cross

Interesting bits of life

Emacs, Nyxt and Engine-mode: how to browse URLs via Nyxt and Slime

Too long; didn't read

Emacs can browse the web via Nyxt. Here we make a browse-url-nyxt function using Slime. We can use that with engine-mode to search the web! You can find the code at https://github.com/ag91/emacs-with-nyxt.

The problem

In my last blog, we saw how to leverage Guix to start using Nyxt. We also discovered how easy is to command Nyxt via Common Lisp. The problem is that I am too lazy to run this by hand every time. Also, I would rather focus on testing out how good Nyxt really is. (And pick up some Common Lisp in the process!)

My main issue is that all the setting up of Swank and Slime connections distracts me from what I really want. For a start, I would like Emacs to use Nyxt as any other browser. Emacs has a nice interface for this: the browse-url functions.

Still, the communication between Emacs and Nyxt is not the terminal, but a CL REPL! Is the browse-url-nyxt function doable?

It is a problem indeed

To have a browse-url-nyxt is useful. It is the standard Emacs way to search online. And there is a nice little mode that makes searches a breeze: engine-mode. This allows you to define your own search engines to use via Emacs. For example my DuckDuckGo search engine looks like the following.

(defengine duckduckgo
  "https://duckduckgo.com/?q=%s"
  :keybinding "D"
  :browser 'browse-url-chromium)

Here I specify the query url for DuckDuckGo. Can you see that %s at the end of the URL? When I use this, engine-mode will ask me for a search string and it will embed it there. The :keybinding defines the key I have to press to use this search. And the :browser defines what browser to use for it.

How cool would it be to simply add Nyxt in the browser section?! It would allow to do our searches in Common Lisp!

And there is a solution

Be ready for another hack of mine. The first challenge we face is to automate the start of the Nyxt browser. We want to call an Emacs command and have a Nyxt open and listen to us from a REPL. We need to run the browser, and to connect Swank and Slime. This is how it looks in Elisp.

(defun my/start-and-connect-to-nyxt (&optional no-maximize)
  "Start Nyxt with swank capabilities."
  (interactive)
  (async-shell-command (format "nyxt -e \"(nyxt-user::start-swank)\""))
  (sleep-for my/slime-nyxt-delay)
  (my/slime-connect "localhost" "4006")
  (unless no-maximize (my/slime-repl-send-string "(toggle-fullscreen)")))

First, we run asynchronously Nyxt with a Lisp expression that starts the Swank connection. Then we wait a little for the connection to be ready. At this point we can connect Slime. Finally, we command Nyxt to go full screen (unless we don't want to). Note by the way: we just used a CL namespace! We need that nyxt-user:: because Nyxt evaluates a Common Lisp but does not import its package. How cool are we?

We need the delays because both Nyxt and Slime take some time to run.

The my/slime-connect function is a bit ugly because my local version of Slime is different from the Swank version of Guix.

(defun my/slime-connect (host port)
  (defun true (&rest args) 't)
  (advice-add 'slime-check-version :override #'true)
  (slime-connect host port)
  (sleep-for my/slime-nyxt-delay)
  (advice-remove 'slime-check-version #'true))

You can see how I have to "advice" slime-check-version to stop asking me: "is it alright if versions mismatch?".

Similarly, my/slime-repl-send-string needs the same trick.

(defun my/slime-repl-send-string (sexp)
  (defun true (&rest args) 't)
  (advice-add 'slime-check-version :override #'true)  
  (if (slime-connected-p)
      (slime-repl-send-string sexp)
    (error "Slime is not connected to Nyxt. Run `my/start-and-connect-to-nyxt' first."))
  (sleep-for my/slime-nyxt-delay)
  (advice-remove 'slime-check-version #'true))

This function simply sends a string to the current Slime connection. For now I am using strings, but it would be much more Lispy to make this a macro. That way we could just input lists and make strings in the macro.

Next challenge is to browse the url we want. We just need to know the right command for this. Then we can send it via my/slime-repl-send-string.

(defun my/browse-url-nyxt (url &optional buffer-title)
  (interactive "sURL: ")
  (my/slime-repl-send-string
   (format
    "(buffer-load \"%s\" %s)"
    url
    (if buffer-title (format ":buffer (make-buffer :title \"%s\")" buffer-title) ""))))

buffer-load is the Nyxt function we need. The optional argument lets us open a new link in another "tab"/buffer of the browser.

At this point we have all we need to make browse-url-nyxt!

(defun browse-url-nyxt (url &optional new-window)
  (interactive "sURL: ")
  (unless (slime-connected-p) (my/start-and-connect-to-nyxt))
  (my/browse-url-nyxt url url))

This looks line any other browse-url function. If we miss the Slime connection to Nyxt, we create one. The we just try to browse through Nyxt.

Cherry on the cake: our new engine-mode definition!

(defengine duckduckgo
  "https://duckduckgo.com/?q=%s"
  :keybinding "n"
  :browser 'browse-url-nyxt)

You can find the entire code here.

Conclusion

This was our first integration of Emacs and Nyxt! You saw also that we learned some Common Lisp in the process. This is just great!

All you need to try this is to follow the installation steps for Nyxt in my previous post, and grab my code at https://github.com/ag91/emacs-with-nyxt.

Feel free to get in touch if you start Nyxting: we can learn together!

Happy browsing!

Comments