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!