173 lines
15 KiB
Plaintext
173 lines
15 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>Emacs Hacks</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">Emacs Hacks</h1>
|
|
<p class="author">By Pierre Neidhardt</p>
|
|
</header>
|
|
<h1 id="nyxt-interfaces">Nyxt interfaces</h1>
|
|
<p>Nyxt can be controlled from an external program via SWANK. For instance, Emacs with <a href="https://github.com/slime/slime">SLIME</a> makes it possible to hack Nyxt while it's running.</p>
|
|
<p>But what about the other way around? Can we use Nyxt to manipulate other programs? Sure we can! Nyxt is a full-blown Common Lisp program that can leverage all of Common Lisp power to interact with foreign programs via Unix sockets, XML-RPC, you name it.</p>
|
|
<p>An interesting example is that of Emacs, since it can be hacked live with Elisp, a language very similar to Common Lisp. In the following article, Nyxt will generate Elisp code to hack Emacs live!</p>
|
|
<h1 id="youtube-dl-with-emacs">Youtube-dl with Emacs</h1>
|
|
<p><a href="https://yt-dl.org">Youtube-dl</a> is a great program for downloading audio and video files from a ridiculous number of websites.</p>
|
|
<p>That said, we would like to make it more convenient than constantly copy-pasting to a shell. Besides, naively calling <code>youtube-dl</code> from Nyxt is not so convenient since we lack feedback or interactivity with the running subprocess.</p>
|
|
<p>We could derive our own Youtube-dl extension for Nyxt so that we would have a buffer showing progress of multiple downloads and allowing for interactivity. However, until someone works on the extension, let's see what others have been doing so far.</p>
|
|
<p>Chris Wellon wrote <a href="https://github.com/skeeto/youtube-dl-emacs/">a great Emacs extension</a>, which sadly, only works for Youtube. This won't stop us! Here is our poor man's universal interface to Youtube-dl to supplement the aforementioned extension:</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"> youtube-dl-url </span>(&optional url)</a>
|
|
<a class="sourceLine" id="cb1-2" data-line-number="2"> <span class="st">"Run 'youtube-dl' over the URL.</span></a>
|
|
<a class="sourceLine" id="cb1-3" data-line-number="3"><span class="st">If URL is nil, use URL at point."</span></a>
|
|
<a class="sourceLine" id="cb1-4" data-line-number="4"> (interactive)</a>
|
|
<a class="sourceLine" id="cb1-5" data-line-number="5"> (<span class="kw">setq</span> url (<span class="kw">or</span> url (thing-at-point-url-at-point)))</a>
|
|
<a class="sourceLine" id="cb1-6" data-line-number="6"> (<span class="kw">let</span> ((eshell-buffer-name <span class="st">"*youtube-dl*"</span>)</a>
|
|
<a class="sourceLine" id="cb1-7" data-line-number="7"> (<span class="kw">directory</span> (cl-loop for dir in '(<span class="st">"~/Videos"</span> <span class="st">"~/Downloads"</span>)</a>
|
|
<a class="sourceLine" id="cb1-8" data-line-number="8"> <span class="kw">when</span> (file-directory-p dir)</a>
|
|
<a class="sourceLine" id="cb1-9" data-line-number="9"> <span class="kw">return</span> (expand-file-name dir)</a>
|
|
<a class="sourceLine" id="cb1-10" data-line-number="10"> finally <span class="kw">return</span> <span class="st">"."</span>))</a>
|
|
<a class="sourceLine" id="cb1-11" data-line-number="11"> (eshell)</a>
|
|
<a class="sourceLine" id="cb1-12" data-line-number="12"> (<span class="kw">when</span> (eshell-interactive-process)</a>
|
|
<a class="sourceLine" id="cb1-13" data-line-number="13"> (eshell <span class="kw">t</span>))</a>
|
|
<a class="sourceLine" id="cb1-14" data-line-number="14"> (eshell-interrupt-process)</a>
|
|
<a class="sourceLine" id="cb1-15" data-line-number="15"> (insert (<span class="kw">format</span> <span class="st">"cd '%s' && youtube-dl "</span> <span class="kw">directory</span>) url)</a>
|
|
<a class="sourceLine" id="cb1-16" data-line-number="16"> (eshell-send-input)))</a></code></pre></div>
|
|
<p>The above snippet opens a dedicated <code>*youtube-dl*</code> Eshell buffer so that we can track download progress from there, as well as stack multiple video downloads.</p>
|
|
<p>With that set up, now we can add the following snippet to our <code>nyxt/init.lisp</code>:</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">defun</span><span class="fu"> eval-in-emacs </span>(&<span class="kw">rest</span> s-exps)</a>
|
|
<a class="sourceLine" id="cb2-2" data-line-number="2"> <span class="st">"Evaluate S-EXPS with emacsclient."</span></a>
|
|
<a class="sourceLine" id="cb2-3" data-line-number="3"> (<span class="kw">let</span> ((s-exps-string (cl-strings:replace-all</a>
|
|
<a class="sourceLine" id="cb2-4" data-line-number="4"> (<span class="kw">write-to-string</span></a>
|
|
<a class="sourceLine" id="cb2-5" data-line-number="5"> `(<span class="kw">progn</span> ,@s-exps) <span class="bu">:case</span> :downcase)</a>
|
|
<a class="sourceLine" id="cb2-6" data-line-number="6"> <span class="co">;; Discard the package prefix.</span></a>
|
|
<a class="sourceLine" id="cb2-7" data-line-number="7"> <span class="st">"nyxt::"</span> <span class="st">""</span>)))</a>
|
|
<a class="sourceLine" id="cb2-8" data-line-number="8"> (<span class="kw">format</span> <span class="va">*error-output*</span> <span class="st">"Sending to Emacs:~%~a~%"</span> s-exps-string)</a>
|
|
<a class="sourceLine" id="cb2-9" data-line-number="9"> (uiop:run-program</a>
|
|
<a class="sourceLine" id="cb2-10" data-line-number="10"> (<span class="kw">list</span> <span class="st">"emacsclient"</span> <span class="st">"--eval"</span> s-exps-string))))</a>
|
|
<a class="sourceLine" id="cb2-11" data-line-number="11"></a>
|
|
<a class="sourceLine" id="cb2-12" data-line-number="12">(define-command youtube-dl-current-page ()</a>
|
|
<a class="sourceLine" id="cb2-13" data-line-number="13"> <span class="st">"Download a video in the currently open buffer."</span></a>
|
|
<a class="sourceLine" id="cb2-14" data-line-number="14"> (with-result (url (buffer-get-url))</a>
|
|
<a class="sourceLine" id="cb2-15" data-line-number="15"> (eval-in-emacs</a>
|
|
<a class="sourceLine" id="cb2-16" data-line-number="16"> (<span class="kw">if</span> (<span class="kw">search</span> <span class="st">"youtu"</span> url)</a>
|
|
<a class="sourceLine" id="cb2-17" data-line-number="17"> `(<span class="kw">progn</span> (youtube-dl ,url) (youtube-dl-list))</a>
|
|
<a class="sourceLine" id="cb2-18" data-line-number="18"> `(youtube-dl-url ,url)))))</a>
|
|
<a class="sourceLine" id="cb2-19" data-line-number="19"></a>
|
|
<a class="sourceLine" id="cb2-20" data-line-number="20">(define-key <span class="st">"C-c d"</span> 'youtube-dl-current-page)</a></code></pre></div>
|
|
<p>We define a helper function <code>eval-in-emacs</code> which sends a bunch of formatted s-expressions to Emacs. This requires the Emacs daemon, either by starting Emacs with <code>emacs --daemon</code> or by running <code>M-x server-start</code> from an Emacs instance.</p>
|
|
<p>The <code>youtube-dl-current-page</code> command tests whether the current URL is Youtube, in which case we use Chris Wellons' <code>youtube-dl</code> extension, or else we rely on the Eshell version we wrote in our Emacs config.</p>
|
|
<h1 id="org-mode-and-org-capture">Org-mode and Org-capture</h1>
|
|
<p><a href="https://orgmode.org/">Org-mode</a> is a great extension for Emacs for note taking (and so much more). It has a feature called "org-capture" which, on a key press, will store some contextual information to your agenda, so that later Org will remind you to refer to it again.</p>
|
|
<p>This can be useful for a web browser: You'd like to mark this page and let Org remind you to read it later? Nothing easier! And off we go to add the following snippet to our Emacs init file:</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">(add-to-list</a>
|
|
<a class="sourceLine" id="cb3-2" data-line-number="2"> 'org-capture-templates</a>
|
|
<a class="sourceLine" id="cb3-3" data-line-number="3"> `(<span class="st">"w"</span> <span class="st">"Web link"</span> entry (file+headline ,(<span class="kw">car</span> org-agenda-files) <span class="st">"Links"</span>)</a>
|
|
<a class="sourceLine" id="cb3-4" data-line-number="4"> <span class="st">"* %?%a</span><span class="sc">\n</span><span class="st">:SCHEDULED: %(org-insert-time-stamp (org-read-date nil t </span><span class="sc">\"</span><span class="st">+1d</span><span class="sc">\"</span><span class="st">))</span><span class="sc">\n</span><span class="st">"</span>))</a></code></pre></div>
|
|
<p>The above snippet does quite a few things, so let's analyze it carefully:</p>
|
|
<ul>
|
|
<li><p>The first <code>"w"</code> is the key binding to insert a web link when Org mode asks which capture to perform. You can add several capture templates with different key bindings to perform different actions.</p></li>
|
|
<li><p><code>(file+headline ,(car org-agenda-files) "Links")</code> tells Org where to insert to result. Here we chose the "Links" headline in our first agenda file.</p></li>
|
|
<li><p><code>%a</code> is the Org URL of the page we are going to pass from Nyxt.</p></li>
|
|
<li><p>The rest is arbitrary. Here we schedule the reading to the day after (<code>+1d</code> in the time-stamp).</p></li>
|
|
</ul>
|
|
<p>See <code>org-capture-templates</code> documentation for more details.</p>
|
|
<p>Now to our <code>nyxt/init.lisp</code>:</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">(define-command org-capture ()</a>
|
|
<a class="sourceLine" id="cb4-2" data-line-number="2"> <span class="st">"Org-capture current page."</span></a>
|
|
<a class="sourceLine" id="cb4-3" data-line-number="3"> (with-result* ((url (buffer-get-url))</a>
|
|
<a class="sourceLine" id="cb4-4" data-line-number="4"> (title (buffer-get-title)))</a>
|
|
<a class="sourceLine" id="cb4-5" data-line-number="5"> (eval-in-emacs</a>
|
|
<a class="sourceLine" id="cb4-6" data-line-number="6"> `(org-link-set-parameters</a>
|
|
<a class="sourceLine" id="cb4-7" data-line-number="7"> <span class="st">"nyxt"</span></a>
|
|
<a class="sourceLine" id="cb4-8" data-line-number="8"> :store (<span class="kw">lambda</span> ()</a>
|
|
<a class="sourceLine" id="cb4-9" data-line-number="9"> (org-store-link-props</a>
|
|
<a class="sourceLine" id="cb4-10" data-line-number="10"> <span class="bu">:type</span> <span class="st">"nyxt"</span></a>
|
|
<a class="sourceLine" id="cb4-11" data-line-number="11"> :link ,url</a>
|
|
<a class="sourceLine" id="cb4-12" data-line-number="12"> :description ,title)))</a>
|
|
<a class="sourceLine" id="cb4-13" data-line-number="13"> `(org-capture))))</a>
|
|
<a class="sourceLine" id="cb4-14" data-line-number="14"></a>
|
|
<a class="sourceLine" id="cb4-15" data-line-number="15">(define-key <span class="st">"C-c C-t"</span> 'org-capture)</a></code></pre></div>
|
|
<p>This is similar to the example in the previous section. Note that we are passing multiple s-expressions to Emacs, they will all be wrapped into a single <code>(progn ...)</code> and Emacs will evaluate everything in one go.</p>
|
|
<p>Happy hacking!</p>
|
|
</body>
|
|
</html>
|