2021-05-14, updated: 2024-03-12
Tested with Nyxt 2 Pre-release 7.
Customizing the prompt buffer
For an introduction to the prompt buffer features and background story, see our other article.
In this article, we will review the API of the prompt buffer and see how the user can leverage this to write a custom prompt buffer.
API and customization
Invocation
A prompt buffer is invoked with the prompt
command. For instance, in set-url
we can find:
(prompt
:prompt "Open URL"
:input (if prefill-current-url-p
(render-url (url (current-buffer))) "")
:history history
;; `actions' are defined elsewhere, more details below.
:sources (list (make-instance 'new-url-or-search-source :actions actions)
(make-instance 'global-history-source :actions actions)
(make-instance 'bookmark-source :actions actions)
(make-instance 'search-engine-url-source :actions actions)))
The only required parameters are :prompt
and the :sources
.
Sources
Much of the prompt configuration actually happens in the sources themselves.
A source is just a class that inherits from prompter:source
. For instance the aforementioned global-history-source
could be defined with
(define-class global-history-source (prompter:source)
((prompter:name "Global history")
(prompter:constructor (lambda (source)
(declare (ignorable source))
(history-initial-suggestions)))
(prompter:filter-preprocessor nil) ; Don't remove non-exact results.
(prompter:actions '(buffer-load)))
(:export-class-name-p t))
The required slots are prompter:name
and prompter:constructor
. The latter provides a list of initial suggestions. If a list, the suggestions are immediately available. If a function, the initial suggestions are computed asynchronously, which is useful to avoid delaying the prompt display in case the initial suggestions take time to compute.
Actions
Actions are provided as a list of commands via the prompter:actions
slot. The above source has one action by default. It's possible to locally extend sources when invoking a prompt. For instance, in set-url
we have
(let (;; ...
(actions (list (make-unmapped-command buffer-load)
(make-command new-buffer-load (suggestion-values)
"Load a URL in a new buffer."
(make-buffer-focus :url (url (first suggestion-values)))))))
;; ...
)
Then actions
is passed to the various source instantiations as in:
An action is either a well-named command, as with buffer-load, or a locally-defined command like in the previous snippet.
Some more details are needed here:
make-command
is likedefine-command
and returns acommand
object without defining it globally.Thus
new-buffer-load
above won't be listed inexecute-command
.make-unmapped-command
transform a function into another one that takes a list as argument and runs the original function over the first element only.Since prompts always return a list of suggestions (even when there is only one selected suggestion), this macro comes in handy to run existing functions over lists of 1 element.
Attributes
Suggestion values are arbitrary objects: strings, numbers, URLs, structures… For compound structures, it's interesting to display the various parts of the objects (for instance, display the slots of a class).
This is where the prompter:object-attributes
method comes into play. The global-history-source
above lists suggestions of the history-entry
type. To display something more meaninful than a bunch of
in the prompt buffer, we can specialize the aforementioned method:
(defmethod prompter:object-attributes ((entry history-entry))
`(("URL" ,(render-url (url entry)))
("Title" ,(title entry))))
Now the suggestions are printed as
https://example.org Example Domain
https://nyxt.atlas.engineer Nyxt
...
and all columns are automatically aligned for you!
Filters (suggestion processing)
Whenever the user inputs a character in the prompt buffer, the suggestions of all sources are processed.
The
prompter:filter-preprocessor
is called on the whole list of suggestions. In particular, this means that the preprocessor can remove duplicates.The source won't display anything until the filter-preprocessor is done.
Then, the
prompter:filter
is run over the suggestions returned by the preprocessor, one after the other.This per-suggestion stepping allows the source view to update its display at regular intervals until all suggestions are filtered.
This filter function is particularly useful to score suggestions by relevance.
Finally, the
prompter:filter-postprocessor
is run of the result of the filter, this time again over the entire list at once.Once again, the source view is only updated when the postprocessor is done with the whole list.
The prompter is said to be ready when all its sources are ready, that is, when the preprocessor, the filter and the postprocessor have terminated for a given user input.
Suggestion objects
Internally, when the prompter filters and sorts suggestions, it wraps the list of initial values (arbitrary objects) into suggestion
objects.
It could be defined as follows:
(define-class suggestion ()
((value nil
:type t)
(attributes '()
:documentation "A non-dotted alist of attributes to structure the filtering.
Both the key and the value are strings.")
(match-data nil
:type t
:documentation "Arbitrary data that can be used by the `filter'
function and its preprocessors. It's the responsibility of the filter to ensure
the match-data is ready for its own use.")
(score 0.0
:documentation "A score the can be set by the `filter' function and
used by the `sort-predicate'."))
(:documentation "Suggestions are processed and listed in `source'.
It wraps arbitrary object stored in the `value' slot.
The other slots are optional.
Suggestions are made with the `suggestion-maker' slot from `source'."))
This is useful to store information through the processing pipeline described in the previous section. For instance, the filter can calculate and store the score of each suggestion, in the score
slot, which in turn is readily available to the filter-postprocessor
for sorting and whatnot.
Conclusion
We hope this inspires you to write awesome configurations and, why not, extensions for Nyxt!
Thanks for reading :-)
Did you enjoy this article? Register for our newsletter to receive the latest hacker news from the world of Lisp and browsers!
- Maximum one email per month
- Unsubscribe at any time