Nyxt is an extensible browser. But how does one write an extension for it? Given that there are few extensions (as of March 2021), there isn't much existing code to learn from. That's why I've written this step-by-step guide about how to extend Nyxt.
So, here's the process you can follow:
Nyxt is built in Common Lisp, so you need to follow the packaging conventions for Common Lisp programs:
nx-
prefix for discoverability.It's a lot, isn't it? Fortunately, all these routines can be automated with quickproject
. So, you can open up Lisp REPL and do:
(ql:quickload :quickproject) ;; Load quickproject
;; Create all the necessary things.
;; In my case, I ran something like:
(quickproject:make-project
;; Path to your extension. Remember the nx- prefix :)
"~/git/nx-search-engines/"
;; Your name.
:author "Artyom Bologov"
;; The license you want to distribute it under.
:license "BSD 2-clause"
;; You depend on Nyxt -- you're writing an extension for it, after all.
:depends-on '(:nyxt))
And most of the work will be done for you!
You'll likely need to refer to Nyxt symbols (be they names of functions, classes, or variables). Typically this can be done by using the nyxt:
prefix. To avoid that, you can import the frequently used symbols from the nyxt
package. In the case of nx-search-engines
, I needed define-class
, define-mode
, define-command
, and search-engine
class.
Importing can be done via the package definition, together with nickname setting, symbol exporting, and package documentation. In the case of nx-search-engines
, package definition looked like this:
;;;; package.lisp
(defpackage #:nx-search-engines
;; "Systematically importing all symbols with `:use #:nyxt` is
;; discouraged because it exposes your package to collisions upon
;; API changes when `nyxt` gets updated."
(:use #:cl)
;; Symbols I don't want to prefix.
(:import-from #:nyxt
#:define-class
#:define-mode
#:define-command
#:search-engine)
;; Symbols I want to be exported from nx-search-engines.
;; This is a traditional and not-so-flexible way to export things.
;; To export symbols from the extension files, use `serapeum:export-always'.
(:export #:duckduckgo
#:duckduckgo-images
#:google
#:google-images
#:bing-date
#:bing
#:bing-images
#:bing-videos
#:bing-maps
#:bing-news
#:bing-shopping
#:wordnet)
(:documentation "A collection of search engines for Nyxt browser."))
If you know Common Lisp, this step is straightforward – just write the extension relying on numerous Nyxt APIs (see next section about them).
If you don't know Lisp – no problem, you can always learn it and write a great extension in the process! We've put together a collection of resources that can help you in starting out: Nyxt Common Lisp Learning Recommendations.
There are lots of libraries Nyxt depends on. You can freely use them. Nyxt will guarantee that they are loaded. A non-exhaustive list of libraries you can rely on:
Nyxt APIs rely on the above libraries and allow you to shorten your code and extend Nyxt in a wink:
add-modes-to-auto-mode-rules
to associate modes with URLs that they need to be automagically enabled on.define-class
, define-user-class
, and define-configuration
as ways to make your extension as easily extensible as the Nyxt core is.nyxt:data-path
-persisted data using with-data-access
and query it unsafely with with-data-unsafe
.query-hints
for keyboard-only navigation.prompt-buffer
with the over-powered prompt
. Just use it with suitable source
-s and enjoy :)nyxt/web-mode
commands managing web navigation, be it history or page movement.user-interface
library to build your Lisp-powered extension interfaces from.Since we use the Common Lisp package system, you can guess the stability of the API by how it's used:
nyxt:
, then it's relatively stable and intended for extension use.nyxt::
prefix), then it may disappear someday.nyxt::%buffer
) – do not use it. It's an implementation detail that can change anytime and is intended for Nyxt-internal use.However, as it's all written in Lisp, no one restricts you from using anything you can get your hands on ;)
In the case of nx-search-engines
, I relied on the search-engine
class – after all, I needed to generate Nyxt-native search engines. This is quite a simple and boring API, and yet it's sufficient to allow Lisp-customizable search engines.
Another Nyxt API that I relied upon (particularly in search-engines-mode
) was element hints. The search-hint
command is a follow-hint
sibling. The difference is that it searches the class-dispatchable hints instead of following them. All at the cost of several method definitions and a search-hint
command call! That's how it looks:
;; One of the methods that search an element hint's contents.
;; This one uses the user-settable `image-search-engine' to search image URL.
(defmethod %search-hint ((hint nyxt/web-mode::image-hint))
(nyxt:buffer-load (nyxt::generate-search-query
(nyxt/web-mode::url hint)
(nyxt:search-url (image-search-engine
(nyxt:find-submode (nyxt:current-buffer)
'nyxt::search-engines-mode))))))
;;; More `%search-hint' definitions...
(define-command search-hint (&key annotate-visible-only-p)
"Search for the contents of the hint with default search engines.
In the case of links and input areas, a default search engine of Nyxt is
used (unless overridden by `engines:search-engine').
In case of images, `engines:image-search-engine' is used."
(nyxt/web-mode::query-hints "Search element" '%search-hint
:annotate-visible-only-p annotate-visible-only-p))
Nyxt uses the Common Lisp Object System (CLOS) for everything. There are Nyxt-specific macros to make any CLOS class configurable by the user. To make your extension customizable, you need to know only two of them: define-user-class
and define-mode
.
define-user-class
makes a class you've already defined (with defclass
or define-class
) configurable via define-configuration
. That's the only thing you need to write to make your classes customizable:
;; Define your class. `define-class' is used for brevity.
(define-class your-class ()
((slot-name nil
:type (or integer nil)
:documentation "Example slot."))
(:export-class-name-p t) ; Your class name will be exported with your package prefix.
(:export-accessor-names-p t) ; Slot names will be exported too.
(:export-predicate-name-p t) ; A your-class-p type-checking predicate will be exported.
(:accessor-name-transformer (hu.dwim.defclass-star:make-name-transformer name)))
(define-user-class your-class)
define-mode
relies on this same system with define-class
, define-user-class
, and define-configuration
. The difference is that modes are enableable and user-facing, while other classes usually aren't. A mode is the best place to store your extensions' configuration. I've relied on this with nx-search-engines
and defined search-engines-mode
:
(define-mode search-engines-mode ()
"A mode to search hints in the dedicated search engine and image search engine."
((search-engine (nyxt::default-search-engine
(nyxt:search-engines (nyxt:current-buffer)))
:type (or nyxt:search-engine null)
:documentation "The search engine to use when calling `search-hint'.")
(image-search-engine (google-images)
:type (or nyxt:search-engine null)
:documentation "The search engine to use when calling `search-hint' on images.")))
Search is the only thing nx-search-engines
is concerned about. Customization of search engines to use when searching element hints is the only reasonable configuration there.
Now one can change preferred search engines like this:
(define-configuration engines:search-engines-mode
((engines:search-engine (engines:duckduckgo))
(engines:image-search-engine (engines:duckduckgo-images))))
Now that you've written your extension, packaged it, and used all the necessary customizable APIs, you can share it with the world! Don't forget to let us know about your extension for it to be included in a list of Nyxt extensions.
Thanks for reading :3