emacs/var/elfeed/db/data/88/88549598126eea743ef1a193b64ab6f686c0da59
2022-01-03 12:49:32 -06:00

164 lines
13 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 John Mercouris" />
<title>Hooks in practice</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">Hooks in practice</h1>
<p class="author">By John Mercouris</p>
</header>
<p>Update: This article was updated for Nyxt 2 for which the hook system has been completely rewritten.</p>
<h1 id="using-hooks-to-smart-program-your-browser">Using hooks to smart-program your browser</h1>
<p>Hooks are a great way to extend a workflow by triggering actions upon events. Simply put, a hook is an object that holds a list of <em>handlers</em>, which are functions called when the hook is <em>run</em> at a precisely defined point in the program.</p>
<p>In the world of a web browser, there are a ton of events: page loaded, DOM available, page rendered, etc. In addition to the events fired off by normal processing of web pages there are a large set of events which include actions by the user: tab deleted, page bookmarked, command called, mode enabled, etc.</p>
<p>In Nyxt, all these events are <em>hookable</em>.</p>
<p>Hooking into the events fired off by the browser or by the user allows the creation of extendable and tailored workflows.</p>
<h1 id="the-nyxt-hook-system">The Nyxt hook system</h1>
<p>Many hooks are as such executed at different points in Nyxt:</p>
<ul>
<li><p><strong>Global</strong> hooks, such as the <code>*after-init-hook*</code> hook, which is run after the browser has been initialized.</p></li>
<li><p><strong>Window</strong> and <strong>buffer</strong> related hooks (before and after creation, before and after the page is loaded, etc.).</p></li>
<li><p><strong>Modes</strong> hooks (before and after a mode is enabled or disabled).</p></li>
<li><p>&quot;Before&quot; and &quot;after&quot; <strong>command</strong> hooks.</p></li>
</ul>
<p>For the full list, run <code>describe-variable</code> and <code>describe-slot</code> over the names ending with <code>-hook</code>.</p>
<h2 id="practical-examples">Practical examples</h2>
<p>Let's consider a practical example. If you want to force the redirection of a domain to another, you can use the <code>request-resource-hook</code> to change the URL and return a new one. In the example below, we make sure we always visit <code>old.reddit.com</code> instead of the new Reddit interface:</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">(<span class="kw">defun</span><span class="fu"> old-reddit-handler </span>(request-data)</a>
<a class="sourceLine" id="cb1-2" data-line-number="2"> (<span class="kw">let</span> ((uri (url request-data)))</a>
<a class="sourceLine" id="cb1-3" data-line-number="3"> (<span class="kw">setf</span> (url request-data)</a>
<a class="sourceLine" id="cb1-4" data-line-number="4"> (<span class="kw">if</span> (<span class="kw">search</span> <span class="st">&quot;reddit.com&quot;</span> (quri:uri-host uri))</a>
<a class="sourceLine" id="cb1-5" data-line-number="5"> (<span class="kw">progn</span></a>
<a class="sourceLine" id="cb1-6" data-line-number="6"> (<span class="kw">setf</span> (quri:uri-host uri) <span class="st">&quot;old.reddit.com&quot;</span>)</a>
<a class="sourceLine" id="cb1-7" data-line-number="7"> (log:info <span class="st">&quot;Switching to old Reddit: ~s&quot;</span> (object-display uri))</a>
<a class="sourceLine" id="cb1-8" data-line-number="8"> uri)</a>
<a class="sourceLine" id="cb1-9" data-line-number="9"> uri)))</a>
<a class="sourceLine" id="cb1-10" data-line-number="10"> request-data)</a>
<a class="sourceLine" id="cb1-11" data-line-number="11"></a>
<a class="sourceLine" id="cb1-12" data-line-number="12">(define-configuration buffer</a>
<a class="sourceLine" id="cb1-13" data-line-number="13"> ((request-resource-hook</a>
<a class="sourceLine" id="cb1-14" data-line-number="14"> (<span class="kw">reduce</span> #&#39;hooks:add-hook</a>
<a class="sourceLine" id="cb1-15" data-line-number="15"> (<span class="kw">mapcar</span> #&#39;make-handler-resource (<span class="kw">list</span> #&#39;old-reddit-handler))</a>
<a class="sourceLine" id="cb1-16" data-line-number="16"> <span class="bu">:initial-value</span> %slot-default%))))</a></code></pre></div>
<p>You can ask Nyxt to automatically enable or disable modes depending on the URL, for instance, you can toggle the proxy mode per domain, which can be very convenient if you would like to, say, disable Tor for some resource intensive domains.</p>
<p>Another cool example would be automatically downloading any YouTube video we visit:</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">(<span class="kw">defvar</span><span class="fu"> +youtube-dl-command+ </span><span class="st">&quot;youtube-dl&quot;</span></a>
<a class="sourceLine" id="cb2-2" data-line-number="2"> <span class="st">&quot;Path to the &#39;youtube-dl&#39; program.&quot;</span>)</a>
<a class="sourceLine" id="cb2-3" data-line-number="3"></a>
<a class="sourceLine" id="cb2-4" data-line-number="4">(<span class="kw">defun</span><span class="fu"> auto-yt-dl-handler </span>(request-data)</a>
<a class="sourceLine" id="cb2-5" data-line-number="5"> <span class="st">&quot;Download a Youtube URL asynchronously to /tmp/videos/.</span></a>
<a class="sourceLine" id="cb2-6" data-line-number="6"><span class="st">Videos are downloaded with `+youtube-dl-command+&#39;.&quot;</span></a>
<a class="sourceLine" id="cb2-7" data-line-number="7"> (<span class="kw">let</span> ((url (url request-data)))</a>
<a class="sourceLine" id="cb2-8" data-line-number="8"> (<span class="kw">if</span> (<span class="kw">and</span> url</a>
<a class="sourceLine" id="cb2-9" data-line-number="9"> (<span class="kw">member</span> (quri:uri-domain url) &#39;(<span class="st">&quot;youtube.com&quot;</span> <span class="st">&quot;youtu.be&quot;</span>)</a>
<a class="sourceLine" id="cb2-10" data-line-number="10"> <span class="bu">:test</span> #&#39;string=)</a>
<a class="sourceLine" id="cb2-11" data-line-number="11"> (<span class="kw">string=</span> (quri:uri-path url) <span class="st">&quot;/watch&quot;</span>))</a>
<a class="sourceLine" id="cb2-12" data-line-number="12"> (<span class="kw">progn</span></a>
<a class="sourceLine" id="cb2-13" data-line-number="13"> (echo <span class="st">&quot;Youtube: downloading ~a&quot;</span> url)</a>
<a class="sourceLine" id="cb2-14" data-line-number="14"> (uiop:launch-program (<span class="kw">list</span> +youtube-dl-command+ (quri:render-uri url) <span class="st">&quot;-o&quot;</span> <span class="st">&quot;/tmp/videos/%(title)s.%(ext)s&quot;</span>))</a>
<a class="sourceLine" id="cb2-15" data-line-number="15"> <span class="co">;; Return nil to cancel the page load.</span></a>
<a class="sourceLine" id="cb2-16" data-line-number="16"> <span class="kw">nil</span>)</a>
<a class="sourceLine" id="cb2-17" data-line-number="17"> request-data)))</a>
<a class="sourceLine" id="cb2-18" data-line-number="18"></a>
<a class="sourceLine" id="cb2-19" data-line-number="19">(define-configuration buffer</a>
<a class="sourceLine" id="cb2-20" data-line-number="20"> ((request-resource-hook</a>
<a class="sourceLine" id="cb2-21" data-line-number="21"> (<span class="kw">reduce</span> #&#39;hooks:add-hook</a>
<a class="sourceLine" id="cb2-22" data-line-number="22"> (<span class="kw">mapcar</span> #&#39;make-handler-resource (<span class="kw">list</span> #&#39;auto-yt-dl-handler))</a>
<a class="sourceLine" id="cb2-23" data-line-number="23"> <span class="bu">:initial-value</span> %slot-default%))))</a></code></pre></div>
<p>Adjust it to your taste!</p>
<h2 id="all-user-commands-have-hooks">All user commands have hooks</h2>
<p>One feature that makes Nyxt unique is the ability to extend commands exposed to the user.</p>
<p>Because the Common Lisp language allows it, one could replace a command definition by another function. This is however not the recommended approach, most notably because it could break the built-in behavior.</p>
<p>We can then use the &quot;before&quot; and &quot;after&quot; hooks to extend the built-in commands. It is as simple as defining a new function with no parameters:</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">defun</span><span class="fu"> post-bookmark-hook </span>()</a>
<a class="sourceLine" id="cb3-2" data-line-number="2"> <span class="co">;; Your bookmark synchronization code...</span></a>
<a class="sourceLine" id="cb3-3" data-line-number="3"> (log:info <span class="st">&quot;Let&#39;s sync the bookmarks!&quot;</span>))</a></code></pre></div>
<p>and adding it to the list of hooks:</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">(hooks:add-hook bookmark-url-after-hook (hooks:make-handler-void #&#39;post-bookmark-hook))</a></code></pre></div>
<p>And voila!</p>
<h1 id="conclusions">Conclusions</h1>
<p>In summary, hooks present a very simple and effective mechanism to chain behavior in your workflows. We are looking forward to seeing what you can create with them!</p>
<p>For more about details on the hook internals and advanced usage, see our <a href="https://nyxt.atlas.engineer/article/hooks-implementation.org">other article</a>.</p>
<p>Thanks for reading!</p>
</body>
</html>