emacs/var/elfeed/db/data/c7/c7f129252db8c4365d019c9f0fe5b2b24b5aba84
2022-01-03 12:49:32 -06:00

191 lines
16 KiB
Plaintext

<!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">&quot;Open URL&quot;</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">&quot;&quot;</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&#39; 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> &#39;new-url-or-search-source :actions actions)</a>
<a class="sourceLine" id="cb1-8" data-line-number="8"> (<span class="kw">make-instance</span> &#39;global-history-source :actions actions)</a>
<a class="sourceLine" id="cb1-9" data-line-number="9"> (<span class="kw">make-instance</span> &#39;bookmark-source :actions actions)</a>
<a class="sourceLine" id="cb1-10" data-line-number="10"> (<span class="kw">make-instance</span> &#39;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">&quot;Global history&quot;</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&#39;t remove non-exact results.</span></a>
<a class="sourceLine" id="cb2-7" data-line-number="7"> (prompter:actions &#39;(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">&quot;Load a URL in a new buffer.&quot;</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> &#39;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">#&lt;HISTORY-ENTRY {100924F7F3}<span class="op">&gt;</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">&quot;URL&quot;</span> ,(render-url (url entry)))</a>
<a class="sourceLine" id="cb6-3" data-line-number="3"> (<span class="st">&quot;Title&quot;</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 &#39;()</a>
<a class="sourceLine" id="cb8-5" data-line-number="5"> :documentation <span class="st">&quot;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.&quot;</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">&quot;Arbitrary data that can be used by the `filter&#39;</span></a>
<a class="sourceLine" id="cb8-10" data-line-number="10"><span class="st">function and its preprocessors. It&#39;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.&quot;</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">&quot;A score the can be set by the `filter&#39; function and</span></a>
<a class="sourceLine" id="cb8-14" data-line-number="14"><span class="st">used by the `sort-predicate&#39;.&quot;</span>))</a>
<a class="sourceLine" id="cb8-15" data-line-number="15"> (:documentation <span class="st">&quot;Suggestions are processed and listed in `source&#39;.</span></a>
<a class="sourceLine" id="cb8-16" data-line-number="16"><span class="st">It wraps arbitrary object stored in the `value&#39; 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&#39; slot from `source&#39;.&quot;</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>