<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang=""> <head> <meta charset="utf-8" /> <meta name="generator" content="pandoc" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" /> <meta name="author" content="By Pierre Neidhardt" /> <title>Customizing the prompt buffer</title> <style type="text/css"> code{white-space: pre-wrap;} span.smallcaps{font-variant: small-caps;} span.underline{text-decoration: underline;} div.column{display: inline-block; vertical-align: top; width: 50%;} </style> <style type="text/css"> a.sourceLine { display: inline-block; line-height: 1.25; } a.sourceLine { pointer-events: none; color: inherit; text-decoration: inherit; } a.sourceLine:empty { height: 1.2em; position: absolute; } .sourceCode { overflow: visible; } code.sourceCode { white-space: pre; position: relative; } div.sourceCode { margin: 1em 0; } pre.sourceCode { margin: 0; } @media screen { div.sourceCode { overflow: auto; } } @media print { code.sourceCode { white-space: pre-wrap; } a.sourceLine { text-indent: -1em; padding-left: 1em; } } pre.numberSource a.sourceLine { position: relative; } pre.numberSource a.sourceLine:empty { position: absolute; } pre.numberSource a.sourceLine::before { content: attr(data-line-number); position: absolute; left: -5em; text-align: right; vertical-align: baseline; border: none; pointer-events: all; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; padding: 0 4px; width: 4em; color: #aaaaaa; } pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa; padding-left: 4px; } div.sourceCode { } @media screen { a.sourceLine::before { text-decoration: underline; } } code span.al { color: #ff0000; font-weight: bold; } /* Alert */ code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */ code span.at { color: #7d9029; } /* Attribute */ code span.bn { color: #40a070; } /* BaseN */ code span.bu { } /* BuiltIn */ code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */ code span.ch { color: #4070a0; } /* Char */ code span.cn { color: #880000; } /* Constant */ code span.co { color: #60a0b0; font-style: italic; } /* Comment */ code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */ code span.do { color: #ba2121; font-style: italic; } /* Documentation */ code span.dt { color: #902000; } /* DataType */ code span.dv { color: #40a070; } /* DecVal */ code span.er { color: #ff0000; font-weight: bold; } /* Error */ code span.ex { } /* Extension */ code span.fl { color: #40a070; } /* Float */ code span.fu { color: #06287e; } /* Function */ code span.im { } /* Import */ code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */ code span.kw { color: #007020; font-weight: bold; } /* Keyword */ code span.op { color: #666666; } /* Operator */ code span.ot { color: #007020; } /* Other */ code span.pp { color: #bc7a00; } /* Preprocessor */ code span.sc { color: #4070a0; } /* SpecialChar */ code span.ss { color: #bb6688; } /* SpecialString */ code span.st { color: #4070a0; } /* String */ code span.va { color: #19177c; } /* Variable */ code span.vs { color: #4070a0; } /* VerbatimString */ code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */ </style> <!--[if lt IE 9]> <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script> <![endif]--> </head> <body> <header> <h1 class="title">Customizing the prompt buffer</h1> <p class="author">By Pierre Neidhardt</p> </header> <p>For an introduction to the prompt buffer features and background story, see <a href="./prompt-buffer.org">our other article</a>.</p> <p><img src="../static/image/article/prompt-buffer-set-url.png" /></p> <p>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.</p> <h1 id="api-and-customization">API and customization</h1> <h2 id="invocation">Invocation</h2> <p>A prompt buffer is invoked with the <code>prompt</code> command. For instance, in <code>set-url</code> we can find:</p> <div class="sourceCode" id="cb1" data-org-language="lisp"><pre class="sourceCode commonlisp"><code class="sourceCode commonlisp"><a class="sourceLine" id="cb1-1" data-line-number="1">(prompt</a> <a class="sourceLine" id="cb1-2" data-line-number="2"> :prompt <span class="st">"Open URL"</span></a> <a class="sourceLine" id="cb1-3" data-line-number="3"> <span class="bu">:input</span> (<span class="kw">if</span> prefill-current-url-p</a> <a class="sourceLine" id="cb1-4" data-line-number="4"> (render-url (url (current-buffer))) <span class="st">""</span>)</a> <a class="sourceLine" id="cb1-5" data-line-number="5"> :history history</a> <a class="sourceLine" id="cb1-6" data-line-number="6"> <span class="co">;; `actions' are defined elsewhere, more details below.</span></a> <a class="sourceLine" id="cb1-7" data-line-number="7"> :sources (<span class="kw">list</span> (<span class="kw">make-instance</span> 'new-url-or-search-source :actions actions)</a> <a class="sourceLine" id="cb1-8" data-line-number="8"> (<span class="kw">make-instance</span> 'global-history-source :actions actions)</a> <a class="sourceLine" id="cb1-9" data-line-number="9"> (<span class="kw">make-instance</span> 'bookmark-source :actions actions)</a> <a class="sourceLine" id="cb1-10" data-line-number="10"> (<span class="kw">make-instance</span> 'search-engine-url-source :actions actions)))</a></code></pre></div> <p>The only required parameters are <code>:prompt</code> and the <code>:sources</code>.</p> <h2 id="sources">Sources</h2> <p>Much of the prompt configuration actually happens in the sources themselves.</p> <p>A source is just a class that inherits from <code>prompter:source</code>. For instance the aforementioned <code>global-history-source</code> could be defined with</p> <div class="sourceCode" id="cb2" data-org-language="lisp"><pre class="sourceCode commonlisp"><code class="sourceCode commonlisp"><a class="sourceLine" id="cb2-1" data-line-number="1">(define-class global-history-source (prompter:source)</a> <a class="sourceLine" id="cb2-2" data-line-number="2"> ((prompter:name <span class="st">"Global history"</span>)</a> <a class="sourceLine" id="cb2-3" data-line-number="3"> (prompter:constructor (<span class="kw">lambda</span> (source)</a> <a class="sourceLine" id="cb2-4" data-line-number="4"> (<span class="kw">declare</span> (<span class="kw">ignorable</span> source))</a> <a class="sourceLine" id="cb2-5" data-line-number="5"> (history-initial-suggestions)))</a> <a class="sourceLine" id="cb2-6" data-line-number="6"> (prompter:filter-preprocessor <span class="kw">nil</span>) <span class="co">; Don't remove non-exact results.</span></a> <a class="sourceLine" id="cb2-7" data-line-number="7"> (prompter:actions '(buffer-load)))</a> <a class="sourceLine" id="cb2-8" data-line-number="8"> (:export-class-name-p <span class="kw">t</span>))</a></code></pre></div> <p>The required slots are <code>prompter:name</code> and <code>prompter:constructor</code>. 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.</p> <h2 id="actions">Actions</h2> <p>Actions are provided as a list of commands via the <code>prompter:actions</code> slot. The above source has one action by default. It's possible to locally extend sources when invoking a prompt. For instance, in <code>set-url</code> we have</p> <div class="sourceCode" id="cb3" data-org-language="lisp"><pre class="sourceCode commonlisp"><code class="sourceCode commonlisp"><a class="sourceLine" id="cb3-1" data-line-number="1">(<span class="kw">let</span> (<span class="co">;; ...</span></a> <a class="sourceLine" id="cb3-2" data-line-number="2"> (actions (<span class="kw">list</span> (make-unmapped-command buffer-load)</a> <a class="sourceLine" id="cb3-3" data-line-number="3"> (make-command new-buffer-load (suggestion-values)</a> <a class="sourceLine" id="cb3-4" data-line-number="4"> <span class="st">"Load a URL in a new buffer."</span></a> <a class="sourceLine" id="cb3-5" data-line-number="5"> (make-buffer-focus :url (url (<span class="kw">first</span> suggestion-values)))))))</a> <a class="sourceLine" id="cb3-6" data-line-number="6"> <span class="co">;; ...</span></a> <a class="sourceLine" id="cb3-7" data-line-number="7"> )</a></code></pre></div> <p>Then <code>actions</code> is passed to the various source instantiations as in:</p> <div class="sourceCode" id="cb4" data-org-language="lisp"><pre class="sourceCode commonlisp"><code class="sourceCode commonlisp"><a class="sourceLine" id="cb4-1" data-line-number="1">(<span class="kw">make-instance</span> 'global-history-source :actions actions)</a></code></pre></div> <p>An action is either a well-named command, as with buffer-load, or a locally-defined command like in the previous snippet.</p> <p>Some more details are needed here:</p> <ul> <li><p><code>make-command</code> is like <code>define-command</code> and returns a <code>command</code> object without defining it globally.</p> <p>Thus <code>new-buffer-load</code> above won't be listed in <code>execute-command</code>.</p></li> <li><p><code>make-unmapped-command</code> transform a function into another one that takes a list as argument and runs the original function over the first element only.</p> <p>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.</p></li> </ul> <h2 id="attributes">Attributes</h2> <p>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).</p> <p>This is where the <code>prompter:object-attributes</code> method comes into play. The <code>global-history-source</code> above lists suggestions of the <code>history-entry</code> type. To display something more meaninful than a bunch of</p> <div class="sourceCode" id="cb5" data-org-language="lisp"><pre class="sourceCode commonlisp"><code class="sourceCode commonlisp"><a class="sourceLine" id="cb5-1" data-line-number="1">#<HISTORY-ENTRY {100924F7F3}<span class="op">></span></a></code></pre></div> <p>in the prompt buffer, we can specialize the aforementioned method:</p> <div class="sourceCode" id="cb6" data-org-language="lisp"><pre class="sourceCode commonlisp"><code class="sourceCode commonlisp"><a class="sourceLine" id="cb6-1" data-line-number="1">(<span class="kw">defmethod</span><span class="fu"> prompter</span>:object-attributes ((entry history-entry))</a> <a class="sourceLine" id="cb6-2" data-line-number="2"> `((<span class="st">"URL"</span> ,(render-url (url entry)))</a> <a class="sourceLine" id="cb6-3" data-line-number="3"> (<span class="st">"Title"</span> ,(title entry))))</a></code></pre></div> <p>Now the suggestions are printed as</p> <pre><code>https://example.org Example Domain https://nyxt.atlas.engineer Nyxt ... </code></pre> <p>and all columns are automatically aligned for you!</p> <h2 id="filters-suggestion-processing">Filters (suggestion processing)</h2> <p>Whenever the user inputs a character in the prompt buffer, the suggestions of all sources are processed.</p> <ul> <li><p>The <code>prompter:filter-preprocessor</code> is called on the whole list of suggestions. In particular, this means that the preprocessor can remove duplicates.</p> <p>The source won't display anything until the filter-preprocessor is done.</p></li> <li><p>Then, the <code>prompter:filter</code> is run over the suggestions returned by the preprocessor, one after the other.</p> <p>This per-suggestion stepping allows the source view to update its display at regular intervals until all suggestions are filtered.</p> <p>This filter function is particularly useful to score suggestions by relevance.</p></li> <li><p>Finally, the <code>prompter:filter-postprocessor</code> is run of the result of the filter, this time again over the entire list at once.</p> <p>Once again, the source view is only updated when the postprocessor is done with the whole list.</p></li> </ul> <p>The prompter is said to be <em>ready</em> when all its sources are <em>ready</em>, that is, when the preprocessor, the filter and the postprocessor have terminated for a given user input.</p> <h2 id="suggestion-objects">Suggestion objects</h2> <p>Internally, when the prompter filters and sorts suggestions, it wraps the list of initial values (arbitrary objects) into <code>suggestion</code> objects.</p> <p>It could be defined as follows:</p> <div class="sourceCode" id="cb8" data-org-language="lisp"><pre class="sourceCode commonlisp"><code class="sourceCode commonlisp"><a class="sourceLine" id="cb8-1" data-line-number="1">(define-class suggestion ()</a> <a class="sourceLine" id="cb8-2" data-line-number="2"> ((value <span class="kw">nil</span></a> <a class="sourceLine" id="cb8-3" data-line-number="3"> <span class="bu">:type</span> <span class="kw">t</span>)</a> <a class="sourceLine" id="cb8-4" data-line-number="4"> (attributes '()</a> <a class="sourceLine" id="cb8-5" data-line-number="5"> :documentation <span class="st">"A non-dotted alist of attributes to structure the filtering.</span></a> <a class="sourceLine" id="cb8-6" data-line-number="6"><span class="st">Both the key and the value are strings."</span>)</a> <a class="sourceLine" id="cb8-7" data-line-number="7"> (match-data <span class="kw">nil</span></a> <a class="sourceLine" id="cb8-8" data-line-number="8"> <span class="bu">:type</span> <span class="kw">t</span></a> <a class="sourceLine" id="cb8-9" data-line-number="9"> :documentation <span class="st">"Arbitrary data that can be used by the `filter'</span></a> <a class="sourceLine" id="cb8-10" data-line-number="10"><span class="st">function and its preprocessors. It's the responsibility of the filter to ensure</span></a> <a class="sourceLine" id="cb8-11" data-line-number="11"><span class="st">the match-data is ready for its own use."</span>)</a> <a class="sourceLine" id="cb8-12" data-line-number="12"> (score <span class="fl">0.0</span></a> <a class="sourceLine" id="cb8-13" data-line-number="13"> :documentation <span class="st">"A score the can be set by the `filter' function and</span></a> <a class="sourceLine" id="cb8-14" data-line-number="14"><span class="st">used by the `sort-predicate'."</span>))</a> <a class="sourceLine" id="cb8-15" data-line-number="15"> (:documentation <span class="st">"Suggestions are processed and listed in `source'.</span></a> <a class="sourceLine" id="cb8-16" data-line-number="16"><span class="st">It wraps arbitrary object stored in the `value' slot.</span></a> <a class="sourceLine" id="cb8-17" data-line-number="17"><span class="st">The other slots are optional.</span></a> <a class="sourceLine" id="cb8-18" data-line-number="18"></a> <a class="sourceLine" id="cb8-19" data-line-number="19"><span class="st">Suggestions are made with the `suggestion-maker' slot from `source'."</span>))</a></code></pre></div> <p>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 <code>score</code> slot, which in turn is readily available to the <code>filter-postprocessor</code> for sorting and whatnot.</p> <h1 id="conclusion">Conclusion</h1> <p>We hope this inspires you to write awesome configurations and, why not, <a href="./the-thin-line-between-users-and-collaborators.org">extensions for Nyxt</a>!</p> <p>Thanks for reading :-)</p> </body> </html>