<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Home on Sven Koschnicke</title><link>https://staging.sven.guru/</link><description>Recent content in Home on Sven Koschnicke</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Wed, 13 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://staging.sven.guru/index.xml" rel="self" type="application/rss+xml"/><item><title>Inline Preview for Source File Links in Emacs</title><link>https://staging.sven.guru/posts/emacs-file-link-preview/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://staging.sven.guru/posts/emacs-file-link-preview/</guid><description>&lt;p&gt;While working, I constantly take notes using org-mode. Because I often work with code, I extensively use the ability of org-mode to link directly into code files when I need to reference where something can be found.
Something like &lt;code&gt;[[file:~/src/foo/bar.el::42][the bit that parses the header]]&lt;/code&gt;. I can easily follow this link and have the file open in emacs, but in the note itself the link is just some underlined text. To remind myself what is behind the link I have to jump there, look, and jump back. That gets annoying.&lt;/p&gt;
&lt;p&gt;What I really wanted was for org to show me the relevant lines &lt;em&gt;right below
the link&lt;/em&gt;, the way it already inlines images. Here is what the same link
looks like before and after:&lt;/p&gt;
&lt;figure&gt;&lt;img src="https://staging.sven.guru/ox-hugo/emacs-link.9f37ce7d01468ce968c6f52f995729f2b79a5ff4c377d378eb9568eaa9cd8647.png" alt="An org-mode file link shown as plain underlined text, with no preview of the code it points to"&gt;
&lt;/figure&gt;
&lt;figure&gt;&lt;img src="https://staging.sven.guru/ox-hugo/emacs-link-shown.b7f9d1af8068fa7e7a7ffd8c211dd98bbb8c637dbf5346371bbc7857dc3857af.png" alt="The same org-mode file link with the referenced source lines inlined as a preview directly below it"&gt;
&lt;/figure&gt;
&lt;h2 id="the-hook-org-already-gives-us"&gt;The hook org already gives us&lt;/h2&gt;
&lt;p&gt;Org 9.7 ships &lt;code&gt;org-link-preview&lt;/code&gt; natively, but out of the box it only knows
how to preview images. The good news is that the mechanism is generic: every
link type can register a &lt;code&gt;:preview&lt;/code&gt; function via &lt;code&gt;org-link-set-parameters&lt;/code&gt;,
and org will call it with an overlay it has already placed on top of the
link. Whatever the function puts on that overlay is what the user sees.&lt;/p&gt;
&lt;p&gt;That is the entire trick. The rest of this post is just filling in the
preview function for &lt;code&gt;file:&lt;/code&gt; and &lt;code&gt;attachment:&lt;/code&gt; links so that, when the link
points at a specific line, we render a syntax-highlighted snippet around it.&lt;/p&gt;
&lt;h2 id="resolving-the-link-to-a-snippet"&gt;Resolving the link to a snippet&lt;/h2&gt;
&lt;p&gt;A file link in org can carry a &lt;em&gt;search option&lt;/em&gt; after &lt;code&gt;::&lt;/code&gt;. It can be a line
number (&lt;code&gt;foo.el::42&lt;/code&gt;) or a piece of text (&lt;code&gt;foo.el::defun my-thing&lt;/code&gt;) — org
uses it when following the link to jump to the right spot, and we want to
use it as the anchor for the preview.&lt;/p&gt;
&lt;p&gt;The helpers below do four things: turn a text search into a line number,
guess the source-block language from the file extension (so the snippet gets
the right fontification when re-inserted), apply font-lock to the extracted
text, and finally format the result with a 👉 marker on the target line so
it&amp;rsquo;s obvious which line the link actually points at.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-emacs-lisp" data-lang="emacs-lisp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defcustom&lt;/span&gt; &lt;span class="nv"&gt;my/org-link-preview-context-lines&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;Number of context lines to show before and after target line.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;:type&lt;/span&gt; &lt;span class="ss"&gt;&amp;#39;integer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;:group&lt;/span&gt; &lt;span class="ss"&gt;&amp;#39;org-link&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt; &lt;span class="nv"&gt;my/org-link-find-line-by-text&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;file&lt;/span&gt; &lt;span class="nv"&gt;search-text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;Find line number in FILE that contains SEARCH-TEXT.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;Returns line number or nil if not found.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;and&lt;/span&gt; &lt;span class="nv"&gt;file&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;file-exists-p&lt;/span&gt; &lt;span class="nv"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;search-text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;with-temp-buffer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;insert-file-contents&lt;/span&gt; &lt;span class="nv"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;goto-char&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;point-min&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;search-forward&lt;/span&gt; &lt;span class="nv"&gt;search-text&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="no"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;line-number-at-pos&lt;/span&gt;&lt;span class="p"&gt;)))))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt; &lt;span class="nv"&gt;my/org-link-get-language-from-extension&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;Get org-babel language identifier from FILE extension.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;let*&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;mode&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;assoc-default&lt;/span&gt; &lt;span class="nv"&gt;file&lt;/span&gt; &lt;span class="nv"&gt;auto-mode-alist&lt;/span&gt; &lt;span class="ss"&gt;&amp;#39;string-match&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;mode-name&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;and&lt;/span&gt; &lt;span class="nv"&gt;mode&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;symbol-name&lt;/span&gt; &lt;span class="nv"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;))))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;mode-name&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;let&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;lang&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;replace-regexp-in-string&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;-mode$&amp;#34;&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;mode-name&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;cond&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nf"&gt;assoc&lt;/span&gt; &lt;span class="nv"&gt;lang&lt;/span&gt; &lt;span class="nv"&gt;org-src-lang-modes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;string=&lt;/span&gt; &lt;span class="nv"&gt;lang&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;js&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;javascript&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;t&lt;/span&gt; &lt;span class="nv"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;or&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;file-name-extension&lt;/span&gt; &lt;span class="nv"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;text&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt; &lt;span class="nv"&gt;my/org-link-apply-syntax-highlighting&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt; &lt;span class="nv"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;Apply syntax highlighting to CONTENT based on FILE&amp;#39;s major mode.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;with-temp-buffer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt; &lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;let&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;mode&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;assoc-default&lt;/span&gt; &lt;span class="nv"&gt;file&lt;/span&gt; &lt;span class="nv"&gt;auto-mode-alist&lt;/span&gt; &lt;span class="ss"&gt;&amp;#39;string-match&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;and&lt;/span&gt; &lt;span class="nv"&gt;mode&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fboundp&lt;/span&gt; &lt;span class="nv"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ignore-errors&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;funcall&lt;/span&gt; &lt;span class="nv"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;font-lock-ensure&lt;/span&gt;&lt;span class="p"&gt;))))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;buffer-string&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt; &lt;span class="nv"&gt;my/org-link-format-preview-content&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt; &lt;span class="nv"&gt;start-line&lt;/span&gt; &lt;span class="nv"&gt;target-line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;Format CONTENT as preview with optional TARGET-LINE highlighting.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;with-temp-buffer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt; &lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;goto-char&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;point-min&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;let&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;current-line&lt;/span&gt; &lt;span class="nv"&gt;start-line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;result&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;eobp&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;let*&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;line-text&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;buffer-substring&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;point&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;line-end-position&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;is-target&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;and&lt;/span&gt; &lt;span class="nv"&gt;target-line&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;current-line&lt;/span&gt; &lt;span class="nv"&gt;target-line&lt;/span&gt;&lt;span class="p"&gt;))))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;setq&lt;/span&gt; &lt;span class="nv"&gt;result&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt; &lt;span class="nv"&gt;result&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;is-target&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;propertize&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;👉 &amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;line-text&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;\n&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ss"&gt;&amp;#39;face&lt;/span&gt; &lt;span class="ss"&gt;&amp;#39;hl-line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt; &lt;span class="s"&gt;&amp;#34; &amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;line-text&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;\n&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;setq&lt;/span&gt; &lt;span class="nv"&gt;current-line&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;1+&lt;/span&gt; &lt;span class="nv"&gt;current-line&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;forward-line&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt; &lt;span class="nv"&gt;my/org-link-get-file-preview&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;file&lt;/span&gt; &lt;span class="nv"&gt;target-line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;Get preview text for FILE centered around TARGET-LINE.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;Returns propertized string formatted as an org source block, or nil.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;and&lt;/span&gt; &lt;span class="nv"&gt;file&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;file-exists-p&lt;/span&gt; &lt;span class="nv"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;target-line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;with-temp-buffer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;insert-file-contents&lt;/span&gt; &lt;span class="nv"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;let*&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;language&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;my/org-link-get-language-from-extension&lt;/span&gt; &lt;span class="nv"&gt;file&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;start-line&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;target-line&lt;/span&gt; &lt;span class="nv"&gt;my/org-link-preview-context-lines&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;end-line&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;target-line&lt;/span&gt; &lt;span class="nv"&gt;my/org-link-preview-context-lines&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;goto-char&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;point-min&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;forward-line&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;1-&lt;/span&gt; &lt;span class="nv"&gt;start-line&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;let*&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;content-start&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;point&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;content-end&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;progn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;forward-line&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;end-line&lt;/span&gt; &lt;span class="nv"&gt;start-line&lt;/span&gt; &lt;span class="mi"&gt;-1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;point&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;buffer-substring&lt;/span&gt; &lt;span class="nv"&gt;content-start&lt;/span&gt; &lt;span class="nv"&gt;content-end&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;highlighted&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;my/org-link-apply-syntax-highlighting&lt;/span&gt; &lt;span class="nv"&gt;content&lt;/span&gt; &lt;span class="nv"&gt;file&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;formatted&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;my/org-link-format-preview-content&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;highlighted&lt;/span&gt; &lt;span class="nv"&gt;start-line&lt;/span&gt; &lt;span class="nv"&gt;target-line&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;line-info&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt; &lt;span class="s"&gt;&amp;#34; :file %s :line %d&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;file-name-nondirectory&lt;/span&gt; &lt;span class="nv"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;target-line&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;propertize&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;#+begin_src %s%s\n&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;language&lt;/span&gt; &lt;span class="nv"&gt;line-info&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="ss"&gt;&amp;#39;face&lt;/span&gt; &lt;span class="ss"&gt;&amp;#39;org-block-begin-line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;formatted&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;propertize&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;#+end_src&amp;#34;&lt;/span&gt; &lt;span class="ss"&gt;&amp;#39;face&lt;/span&gt; &lt;span class="ss"&gt;&amp;#39;org-block-end-line&lt;/span&gt;&lt;span class="p"&gt;)))))))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There&amp;rsquo;s nothing clever in there — the only thing worth pointing at is that
the snippet is wrapped in a fake &lt;code&gt;#+begin_src&lt;/code&gt; / &lt;code&gt;#+end_src&lt;/code&gt; pair with the
filename and line baked into the header. That way the rendered overlay looks
exactly like a normal org source block, getting nice syntax highlighting in the process.&lt;/p&gt;
&lt;h2 id="wiring-it-into-org-link-preview"&gt;Wiring it into org-link-preview&lt;/h2&gt;
&lt;p&gt;A &lt;code&gt;:preview&lt;/code&gt; function receives &lt;code&gt;(OV PATH LINK)&lt;/code&gt;. The contract is: configure
the overlay &lt;code&gt;OV&lt;/code&gt; however you like and return non-nil to keep it, or return
nil to have org throw the overlay away. We render the snippet via
&lt;code&gt;after-string&lt;/code&gt; so it appears &lt;em&gt;below&lt;/em&gt; the link instead of replacing it —
keeping the original link visible matters, because there might also be
information in the link description.&lt;/p&gt;
&lt;p&gt;The function only kicks in when the link has a search option. If there
isn&amp;rsquo;t one, there&amp;rsquo;s nothing to anchor the preview to, so we fall through to
org&amp;rsquo;s built-in image previewer. That way PNGs and friends still inline as
before, and we register the same dispatcher for both &lt;code&gt;file:&lt;/code&gt; and
&lt;code&gt;attachment:&lt;/code&gt; links so org-attach works too.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-emacs-lisp" data-lang="emacs-lisp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt; &lt;span class="nv"&gt;my/org-link-preview-source-file&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;ov&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt; &lt;span class="nv"&gt;link&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;Preview a source-file link as a snippet over overlay OV.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;Handles file links with a numeric (::42) or text (::needle) search
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;option. Returns non-nil on success, nil to let org fall back.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;let&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;search-option&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;org-element-property&lt;/span&gt; &lt;span class="nb"&gt;:search-option&lt;/span&gt; &lt;span class="nv"&gt;link&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;when&lt;/span&gt; &lt;span class="nv"&gt;search-option&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;let*&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;file&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;expand-file-name&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;line&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;string-match&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;\\`\\([0-9]+\\)\\&amp;#39;&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;search-option&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;string-to-number&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;match-string&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="nv"&gt;search-option&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;my/org-link-find-line-by-text&lt;/span&gt; &lt;span class="nv"&gt;file&lt;/span&gt; &lt;span class="nv"&gt;search-option&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;preview&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;my/org-link-get-file-preview&lt;/span&gt; &lt;span class="nv"&gt;file&lt;/span&gt; &lt;span class="nv"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;when&lt;/span&gt; &lt;span class="nv"&gt;preview&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;overlay-put&lt;/span&gt; &lt;span class="nv"&gt;ov&lt;/span&gt; &lt;span class="ss"&gt;&amp;#39;after-string&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;\n&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;preview&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;\n&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;overlay-put&lt;/span&gt; &lt;span class="nv"&gt;ov&lt;/span&gt; &lt;span class="ss"&gt;&amp;#39;face&lt;/span&gt; &lt;span class="ss"&gt;&amp;#39;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="no"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)))))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt; &lt;span class="nv"&gt;my/org-link-preview-file-dispatch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;ov&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt; &lt;span class="nv"&gt;link&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;Try source-file preview first, fall back to org&amp;#39;s image previewer.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;or&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;my/org-link-preview-source-file&lt;/span&gt; &lt;span class="nv"&gt;ov&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt; &lt;span class="nv"&gt;link&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;org-link-preview-file&lt;/span&gt; &lt;span class="nv"&gt;ov&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt; &lt;span class="nv"&gt;link&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;with-eval-after-load&lt;/span&gt; &lt;span class="ss"&gt;&amp;#39;ol&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;org-link-set-parameters&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;file&amp;#34;&lt;/span&gt; &lt;span class="nb"&gt;:preview&lt;/span&gt; &lt;span class="nf"&gt;#&amp;#39;&lt;/span&gt;&lt;span class="nv"&gt;my/org-link-preview-file-dispatch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;org-link-set-parameters&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;attachment&amp;#34;&lt;/span&gt; &lt;span class="nb"&gt;:preview&lt;/span&gt; &lt;span class="nf"&gt;#&amp;#39;&lt;/span&gt;&lt;span class="nv"&gt;my/org-link-preview-file-dispatch&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="one-small-papercut-links-with-descriptions"&gt;One small papercut: links with descriptions&lt;/h2&gt;
&lt;p&gt;There&amp;rsquo;s one last annoyance. By default &lt;code&gt;org-link-preview&lt;/code&gt; skips links that
have a description — which is, of course, most of the links I actually want
previewed, because I tend to write &lt;code&gt;[[file:foo.el::42][the parser]]&lt;/code&gt; rather
than dumping the raw path into the buffer. The fix is a one-line wrapper
that passes the &lt;code&gt;include-linked&lt;/code&gt; prefix argument so the link at point always
gets previewed, plus a Spacemacs binding under &lt;code&gt;SPC m l p&lt;/code&gt;. To clear a
preview, &lt;code&gt;C-u M-x org-link-preview&lt;/code&gt; still works as usual.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-emacs-lisp" data-lang="emacs-lisp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt; &lt;span class="nv"&gt;my/org-link-preview-here&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;Preview the link at point, including links with descriptions.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;interactive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;org-link-preview&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;with-eval-after-load&lt;/span&gt; &lt;span class="ss"&gt;&amp;#39;org&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;spacemacs/set-leader-keys-for-major-mode&lt;/span&gt; &lt;span class="ss"&gt;&amp;#39;org-mode&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;lp&amp;#34;&lt;/span&gt; &lt;span class="ss"&gt;&amp;#39;my/org-link-preview-here&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="another-step-towards-a-better-editor"&gt;Another step towards a better editor&lt;/h2&gt;
&lt;p&gt;There is room for improvement here. It doesn&amp;rsquo;t work if there are multiple
matching texts or no position in the link at all. But I can now read my notes
without context switching to the source file. It&amp;rsquo;s also satisfying to have used
an extension point that was designed for this kind of thing instead of creating
everything from scratch (which was what I had before). Just some small
customization making the editor I spend most of my day in a bit more at home.&lt;/p&gt;</description></item><item><title>Overengineering a static website</title><link>https://staging.sven.guru/posts/rust-static-site/</link><pubDate>Mon, 23 Feb 2026 00:00:00 +0000</pubDate><guid>https://staging.sven.guru/posts/rust-static-site/</guid><description>&lt;h2 id="the-boring-way"&gt;The Boring Way&lt;/h2&gt;
&lt;p&gt;In the beginning, there was static HTML served by Apache. Then everyone wanted dynamic content, so we got CGI, then PHP, and suddenly every website was dynamic — even the ones that didn&amp;rsquo;t need to be. Today, static site generators have brought us full circle. Write some Markdown (or org-mode, because it&amp;rsquo;s better), let Hugo convert it to HTML, push to GitHub, done. I did exactly that with the first versions of this homepage. It works well. The Hugo conversion can even run in a GitHub workflow.&lt;/p&gt;
&lt;h2 id="the-custom-server"&gt;The Custom Server&lt;/h2&gt;
&lt;p&gt;But it&amp;rsquo;s also a little bit boring. I thought about how a generic webserver like Nginx could never be optimized to such an extend for serving my static website because it needs to be usable for other use-cases, too. I wanted to try if it would be faster to keep all the pages in memory the way they need to be sent to the client. So I wrote a custom webserver in Rust which gets all the content compiled into the binary on build time. Then I did some benchmarks. The Rust server was over 16 times faster than a standard Nginx server.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Test Environment:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Hugo site: ~6.4MB, 89 files&lt;/li&gt;
&lt;li&gt;Benchmark tool: wrk (4 threads, 100 connections, 30 seconds)&lt;/li&gt;
&lt;li&gt;Rust server: Port 3000 (optimized release build)&lt;/li&gt;
&lt;li&gt;nginx: Port 8080 (Alpine, podman container, default config)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Results Summary&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test Case&lt;/th&gt;
&lt;th&gt;Rust Req/s&lt;/th&gt;
&lt;th&gt;nginx Req/s&lt;/th&gt;
&lt;th&gt;Speedup&lt;/th&gt;
&lt;th&gt;Rust Latency&lt;/th&gt;
&lt;th&gt;nginx Latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Homepage (HTML)&lt;/td&gt;
&lt;td&gt;122,519&lt;/td&gt;
&lt;td&gt;7,261&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;16.8x&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;754μs&lt;/td&gt;
&lt;td&gt;16.95ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSS (fingerprinted)&lt;/td&gt;
&lt;td&gt;129,547&lt;/td&gt;
&lt;td&gt;8,791&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14.7x&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;712μs&lt;/td&gt;
&lt;td&gt;15.92ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PNG Image (large)&lt;/td&gt;
&lt;td&gt;28,155&lt;/td&gt;
&lt;td&gt;10,494&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.7x&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2.15ms&lt;/td&gt;
&lt;td&gt;16.81ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;That was promising, so I added TLS support to the server. When comparing the deployed custom server with GitHub pages, Github pages still wins in overall response speed. That is because the most time of a web request is not spent waiting for the webserver but transferring the data from the server to the client, because the user is rarely sitting right next to the datacenter where the server is located.&lt;/p&gt;
&lt;p&gt;To make the distance between server and client as short as possible, one uses a content delivery network (CDN). The static content is copied to multiple datacenters all over the world and served from the location nearest to the user. GitHub pages distributes the content through a CDN by default and that makes it faster than any single webserver in one location (except when the user happens to be near this single server).&lt;/p&gt;
&lt;p&gt;But what a CDN can&amp;rsquo;t do is serve dynamic content. So I added a dynamic component to my homepage in the form of live statistics about requests to the server. It is nothing really useful, but it is something a CDN cannot do. You can see the statistics in action below each page. Click on &amp;ldquo;Show more&amp;rdquo; to see some diagrams. I made sure to only use data that cannot identify or track you. For now, the number of concurrent page requests (in a one second window) and the response time (that is, how long the server needs to respond to the request, not how long it takes response to reach the client) are measured. No IPs or other information are stored.&lt;/p&gt;
&lt;h2 id="going-full-unikernel"&gt;Going Full Unikernel&lt;/h2&gt;
&lt;p&gt;I could have stopped here. Deploy the Rust server to a virtual private server and be happy. But I didn&amp;rsquo;t want to stop. The server was already a single binary also containing the content it should serve. So why not skip the whole operating system and deploy the server as a unikernel directly to the hypervisor? The most difficult thing was actually finding a cloud provider that supported that and understanding the API for deployment. I chose Hetzner, because they are based in Europe, but it shows that unikernels are a not so often used technology.&lt;/p&gt;
&lt;p&gt;Using the API successfully needed some experimentation, and the &lt;code&gt;ops&lt;/code&gt; tool to build an unikernel image needs some polishing, but eventually it worked.&lt;/p&gt;
&lt;p&gt;TLS certificates from Let&amp;rsquo;s Encrypt couldn&amp;rsquo;t be stored on disk anymore, so I had to use a storage bucket for that (Hetzner provides these with a S3 compatible API). Storing the certificates only in memory would have meant that every deploy needs a new certificate, and that way I might have run into rate limits from Let&amp;rsquo;s Encrypt.&lt;/p&gt;
&lt;p&gt;But now everything works. This website is served from a custom server running directly on the hypervisor. No system updates I have to worry about (the provider cares about the hypervisor), I only have to update the server itself. And it is fast. I also spent some time reducing the amount of data transferred by optimizing the binary files (images, PDFs). The server supports brotli compression (in addition to the standard gzip compression).&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s still slower than using a CDN if you are not near the datacenter, but everything is properly optimized now and it was a fun learning experience.&lt;/p&gt;
&lt;h2 id="recap"&gt;Recap&lt;/h2&gt;
&lt;p&gt;Is this kind of deployment actually better? Speed and reduced attack surface are the advantages. I can also add fun little gimmicks like the statistics, because I control the whole webserver, but there are also not so nice things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;deployment is slow. The binary needs to be built, then the image needs to be built from the binary, then the image needs to be started in the cloud. Takes about 5 to 10 minutes until the new version is online. That&amp;rsquo;s a long time for small content changes.&lt;/li&gt;
&lt;li&gt;no easy debugging in production. I can&amp;rsquo;t SSH into the server or look at logs (there are no logs, currently). I&amp;rsquo;d need to create observability infrastructure like OpenTelemetry and some log consumer where the server could send data. Or I&amp;rsquo;d need to integrate that into the server, extending its attack surface again.&lt;/li&gt;
&lt;li&gt;the allocated server resources are way too many for a simple static website. I&amp;rsquo;m paying about 3.50 euros for the server (2 vCPUs, 4 GB RAM) plus about 6 euros for object storage (that&amp;rsquo;s basically Hetzner&amp;rsquo;s base fee, I&amp;rsquo;m only using a few kilobytes for the certificates).&lt;/li&gt;
&lt;li&gt;I can&amp;rsquo;t share the server resources to run other services like a Mastodon server or a Matrix server&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Should static sites be served like this? No, using something like GitHub pages is easier and faster. Maybe I will go from unikernel deployment back to a virtual private server where the binary runs and change the server so that it can dynamically update the in-memory content. That would make better use of the resources and reduce the time it takes to update the content drastically. But in the end, it&amp;rsquo;s just some fun experiment.&lt;/p&gt;</description></item><item><title>Magic Wormhole</title><link>https://staging.sven.guru/posts/magic-wormhole/</link><pubDate>Wed, 07 May 2025 00:00:00 +0000</pubDate><guid>https://staging.sven.guru/posts/magic-wormhole/</guid><description>&lt;p&gt;In a world where software gets more and more complex, and because of that also bloated, there are sometimes little gems which make your day a little bit better. They adhere to the initial goal of software and computers, helping people to get their tasks done. And when they just do their job and then get out of the way, it sometimes makes me smile.&lt;/p&gt;
&lt;p&gt;One if these programs I just had the pleasure to use again is Magic Wormhole. A small command line utility which lets you transfer files between two computers.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://staging.sven.guru/ox-hugo/2025-05-07_16-41-33_screenshot.7917851ed96f0899d6b7b8aa9170e81ece838e3a538aef01a0783cf3a05b5509.png" alt="Terminal showing Magic Wormhole sending a file and printing a short human-readable wormhole code"&gt;
&lt;a href="https://xkcd.com/949/"&gt;XKCD &amp;ldquo;File transfer&amp;rdquo;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So I wanted to give my laptop SSH access to a server I already had access to from my desktop computer. Of course, login through password is deactivated on the server. So I had to add the public SSH key of my laptop to the &lt;code&gt;authorized_keys&lt;/code&gt; file on the server.&lt;/p&gt;
&lt;p&gt;I could have sent my public key to myself by email or even put it on an USB stick and get it onto the desktop computer like this and then copy it to the server (using the very handy &lt;code&gt;ssh-copy-id&lt;/code&gt; program), but I remembered Magic Wormhole, which would let me transfer the key more easily to my desktop computer.&lt;/p&gt;
&lt;p&gt;I had it already installed, but it should be easy to install on almost any system (don&amp;rsquo;t ask me about Windows, though). Trying to remember how to use it, I entered &lt;code&gt;wormhole --help&lt;/code&gt; (actually I first tried &lt;code&gt;wormhole -h&lt;/code&gt;, but that doesn&amp;rsquo;t work):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ wormhole --help
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Usage: wormhole [OPTIONS] COMMAND [ARGS]...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Create a Magic Wormhole and communicate through it.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Wormholes are created by speaking the same magic CODE in two different
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; places at the same time. Wormholes are secure against anyone who doesn&amp;#39;t
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; use the same code.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Options:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --appid APPID appid to use
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --relay-url URL rendezvous relay to use
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --transit-helper tcp:HOST:PORT transit relay to use
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --dump-timing FILE.json (debug) write timing data to file
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --version Show the version and exit.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --help Show this message and exit.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Commands:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; help
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; receive Receive a text message, file, or directory (from &amp;#39;wormhole send&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; send Send a text message, file, or directory
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ssh Facilitate sending/receiving SSH public keys
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Okay, so executing &lt;code&gt;wormhole send .ssh/id_ed25519.pub&lt;/code&gt; should give me a code-word which I can use on the receiving machine by executing &lt;code&gt;wormhole receive&lt;/code&gt; and get the file securely transferred.&lt;/p&gt;
&lt;p&gt;But then the &lt;code&gt;ssh&lt;/code&gt; command caught my attention, as it seemed to be exactly my use-case:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ wormhole ssh --help
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Usage: wormhole ssh [OPTIONS] COMMAND [ARGS]...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Facilitate sending/receiving SSH public keys
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Options:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --help Show this message and exit.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Commands:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; accept Send your SSH public-key
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; invite Add a public-key to a ~/.ssh/authorized_keys file
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That was perfect. Just executing &lt;code&gt;wormhole ssh invite&lt;/code&gt; on the server:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ wormhole ssh invite
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Now tell the other user to run:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;wormhole ssh accept 1-embezzle-printer
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And then running &lt;code&gt;wormhole ssh accept 1-embezzle-printer&lt;/code&gt; on the laptop:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ wormhole ssh accept 1-embezzle-printer
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Sending public key type=&amp;#39;ssh-ed25519&amp;#39; keyid=&amp;#39;wayreth ssh key&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Really send public key &amp;#39;wayreth ssh key&amp;#39; ? [y/N]: y
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Key sent.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And on the server the program exits with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Appended key type=&amp;#39;ssh-ed25519&amp;#39; id=&amp;#39;wayreth&amp;#39; to &amp;#39;/home/sven/.ssh/authorized_keys&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s it. Now I could access the server from my laptop. Easy, secure, straight forward.&lt;/p&gt;</description></item><item><title>Hello World</title><link>https://staging.sven.guru/posts/hello-world/</link><pubDate>Thu, 02 Jan 2025 00:00:00 +0000</pubDate><guid>https://staging.sven.guru/posts/hello-world/</guid><description>&lt;p&gt;Shipping is a feature, and because I don&amp;rsquo;t have much time these days, I&amp;rsquo;ll start with a minimal viable product and get this homepage published as fast as possible. You&amp;rsquo;ll find my thoughts and insights here soon. Mostly about technology and software development. But I can&amp;rsquo;t say how often or when.&lt;/p&gt;</description></item><item><title>About</title><link>https://staging.sven.guru/about/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://staging.sven.guru/about/</guid><description>&lt;p&gt;Hey, I&amp;rsquo;m Sven Koschnicke, a computer nerd with over 25 years of hands-on
experience across the entire technology stack. From designing microchips and
programming FPGAs to developing high-performance kernel modules, I bring deep
technical expertise at every level.&lt;/p&gt;
&lt;p&gt;My unique strength lies in true full-stack mastery - seamlessly bridging
hardware, systems, and software. Whether it&amp;rsquo;s DevOps infrastructure,
sophisticated backend services, or polished frontend applications, I&amp;rsquo;ve
successfully delivered solutions across the spectrum.&lt;/p&gt;
&lt;p&gt;I specialize in tackling complex technical challenges that require innovative
thinking and optimization. With my broad knowledge I can successfully steer a
team to create a robust and scalable software solution. I focus on creating
high-performance, specialized systems that push boundaries and deliver
exceptional value.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re looking for someone who can not only understand your technical
challenges from every angle but also architect and implement solutions that
exceed expectations in both performance and quality, let&amp;rsquo;s connect.&lt;/p&gt;
&lt;p&gt;You can download my full &lt;a href="https://staging.sven.guru/cv-en.pdf"&gt;resume in English&lt;/a&gt; or &lt;a href="https://staging.sven.guru/cv-de.pdf"&gt;in German&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="contact"&gt;Contact&lt;/h2&gt;
&lt;p&gt;You can reach me by email: &lt;a href="mailto:s.koschnicke@gfxpro.com"&gt;s.koschnicke@gfxpro.com&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My postal address:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;GFXpro, z. Hd. Sven Koschnicke&lt;br /&gt;
Tannenstraße 16&lt;br /&gt;
24229 Schwedeneck&lt;br /&gt;
Germany&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Bank account:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;bunq&lt;br /&gt;
IBAN: NL02 BUNQ 2057 0845 58&lt;br /&gt;
BIC: BUNQNL2A&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="legal-imprint--in-german"&gt;Legal / Imprint (in german)&lt;/h2&gt;
&lt;p&gt;Anbieter und Verantwortlicher im Sinne des § 5 TMG und § 55 Absatz 2 RStV ist:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Sven Koschnicke (Einzelunternehmer)&lt;br /&gt;
Tannenstraße 16&lt;br /&gt;
24229 Schwedeneck&lt;br /&gt;
Germany&lt;br /&gt;
E-Mail: &lt;a href="mailto:s.koschnicke@gfxpro.com"&gt;s.koschnicke@gfxpro.com&lt;/a&gt;&lt;br /&gt;
USt-IdNr.: DE235896138&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Hinweis für Verbraucher:&lt;/p&gt;
&lt;p&gt;Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit: &lt;a href="https://ec.europa.eu/consumers/odr"&gt;https://ec.europa.eu/consumers/odr&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Ich bin nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.&lt;/p&gt;</description></item><item><title>Imprint</title><link>https://staging.sven.guru/imprint/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://staging.sven.guru/imprint/</guid><description>&lt;p&gt;This website is operated by me, Sven Koschnicke, and I am responsible in terms of data protection rights (see my full address on the &lt;a href="https://staging.sven.guru/about/"&gt;about page&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;When using this website, no personal information about you is stored by me.&lt;/p&gt;
&lt;p&gt;When your browser-software requests a part of this website, it sends some request information to the server providing the website. This request information includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;your current IP-Address over which you are connected to the internet&lt;/li&gt;
&lt;li&gt;name and version of your browser software and operating system&lt;/li&gt;
&lt;li&gt;website, from which you reached the requested website if using a hyperlink (the so called &lt;a href="https://en.wikipedia.org/wiki/HTTP_referer"&gt;referer-URL, it&amp;rsquo;s misspelling originated in the original proposal for the HTTP specification&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;date and time of the request&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The request information is required by the server to process your request of the website (Article 6.1(f) GDPR) and is discarded as soon as this request is fulfilled.&lt;/p&gt;
&lt;p&gt;Your privacy is very important to me. For this reason, I do not use any third party services on my website, which would receive your request information. I also do not set any cookies which would allow to track you navigating my website. My website is only available over &lt;a href="https://en.wikipedia.org/wiki/Transport_Layer_Security"&gt;TLS&lt;/a&gt;, the standard for encrypted HTTP (called HTTPS), preventing other from seeing or changing the requested sites (only the hostname, sven.guru, for the request is not encrypted, which is a feature of the &lt;a href="https://en.wikipedia.org/wiki/Server_Name_Indication"&gt;SNI extension&lt;/a&gt; to TLS and allows the server to serve more than one website over TLS).&lt;/p&gt;
&lt;p&gt;When you contact me, I store the personal information which is provided along with your message to me. I keep this data as long as necessary for our conversation (Article 6.1(b) GDPR) and as required by law (which is six years for business correspondence in germany, Article 257(4) german commercial code, HGB). Conversations over e-mail are not encrypted by default and you should use PGP to encrypt e-mails. You can find &lt;a href="https://keybase.io/skoschnicke"&gt;my PGP-key at keybase.io&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;When I work for you, I store your contact data in my invoice system, which is called &lt;a href="https://www.getharvest.com/privacy-policy"&gt;Harvest&lt;/a&gt; and is operated by Iridesco, LLC d/b/a Harvest, a New York (USA) limited liability company. Harvest is fully GDPR compliant (&lt;a href="https://www.getharvest.com/privacy-policy"&gt;see Harvest&amp;rsquo;s privacy policy&lt;/a&gt;). Invoices are stored ten years as required by german law (Article 257(4) german commercial code, HGB).&lt;/p&gt;
&lt;p&gt;Additionally to normal bank transfer you can pay me using PayPal and Stripe. When using one of these third party services, the service receives your payment data. You can consult the &lt;a href="https://www.paypal.com/de/webapps/mpp/ua/privacy-prev"&gt;PayPal privacy policy&lt;/a&gt; and the &lt;a href="https://stripe.com/de/privacy"&gt;Stripe privacy policy&lt;/a&gt; for further details.&lt;/p&gt;
&lt;p&gt;In any case, I follow industry standards of technical, physical and administrative security to safeguard your personal information.&lt;/p&gt;
&lt;p&gt;If you want to access or change your personal data that I store, you may &lt;a href="https://staging.sven.guru/about/"&gt;contact me&lt;/a&gt;. You have the right to disclosure, correction, deletion or constraint of your personal data, if permitted by law.&lt;/p&gt;
&lt;p&gt;If you are unhappy about how I handle your personal data, you may complain to any data privacy authority. For example to the &lt;a href="https://www.datenschutzzentrum.de/"&gt;Independent Centre for Privacy Protection Schleswig-Holstein&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you have any questions, feel free to &lt;a href="https://staging.sven.guru/about/"&gt;contact me&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Privacy Policy</title><link>https://staging.sven.guru/privacy/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://staging.sven.guru/privacy/</guid><description>&lt;p&gt;This website is operated by me, Sven Koschnicke, and I am the data controller (see my full address on the &lt;a href="https://staging.sven.guru/about/"&gt;about page&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;When using this website, no personal information about you is stored.&lt;/p&gt;
&lt;p&gt;When your browser-software requests a part of this website, it sends some request information to the server providing the website. This request information includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;your current IP-Address over which you are connected to the internet&lt;/li&gt;
&lt;li&gt;name and version of your browser software and operating system&lt;/li&gt;
&lt;li&gt;website, from which you reached the requested website if using a hyperlink (the so called &lt;a href="https://en.wikipedia.org/wiki/HTTP_referer"&gt;referer-URL, its misspelling originated in the original proposal for the HTTP specification&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;date and time of the request&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The request information is required by the server to process your request of the website (Article 6.1(f) GDPR). It is not written to any log and is discarded as soon as the request is fulfilled.&lt;/p&gt;
&lt;p&gt;Your privacy is very important to me. For this reason, I do not use any third party services on my website, which would receive your request information. I also do not set any cookies which would allow to track you navigating my website.&lt;/p&gt;
&lt;p&gt;The website does store a few small settings in your browser&amp;rsquo;s local storage (localStorage and sessionStorage), so that it remembers them from one page to the next: the progress of the adventure game, as soon as you click in it (your inventory, solved puzzles, the room you came from, and whether you switched the game off), the colour theme you picked (only until you close the tab), and whether the server metrics panel is expanded. None of this contains an identifier, and none of it is ever sent to the server or anyone else; it stays in your browser. Storing it is necessary for the features you use (Section 25(2) no. 2 TDDDG). You can delete it at any time by clearing this website&amp;rsquo;s data in your browser.&lt;/p&gt;
&lt;p&gt;My website is only available over &lt;a href="https://en.wikipedia.org/wiki/Transport_Layer_Security"&gt;TLS&lt;/a&gt;, the standard for encrypted HTTP (called HTTPS), preventing others from seeing or changing the requested pages (only the hostname, sven.guru, for the request is not encrypted, which is a feature of the &lt;a href="https://en.wikipedia.org/wiki/Server_Name_Indication"&gt;SNI extension&lt;/a&gt; to TLS and allows the server to serve more than one website over TLS).&lt;/p&gt;
&lt;p&gt;When you contact me, I store the personal information which is provided along with your message to me. I keep this data as long as needed to handle your request (Article 6.1(b) GDPR) and as required by law (which is six years for business correspondence in Germany, Article 257(4) German commercial code, HGB). Conversations over e-mail are not encrypted by default and you should use PGP to encrypt e-mails. You can find &lt;a href="https://keybase.io/skoschnicke"&gt;my PGP-key at keybase.io&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;When I work for you, I store your contact data in my invoice system, which is called &lt;a href="https://www.getharvest.com/privacy-policy"&gt;Harvest&lt;/a&gt; and is operated by Iridesco, LLC d/b/a Harvest, a New York (USA) limited liability company. Harvest states that it is GDPR compliant (&lt;a href="https://www.getharvest.com/privacy-policy"&gt;see Harvest&amp;rsquo;s privacy policy&lt;/a&gt;). Invoices are stored for ten years as required by German law (Article 6.1(c) GDPR, Article 257(4) German commercial code, HGB).&lt;/p&gt;
&lt;p&gt;In addition to normal bank transfer, you can pay me using PayPal and Stripe. When using one of these third party services, the service receives your payment data. You can consult the &lt;a href="https://www.paypal.com/de/webapps/mpp/ua/privacy-prev"&gt;PayPal privacy policy&lt;/a&gt; and the &lt;a href="https://stripe.com/de/privacy"&gt;Stripe privacy policy&lt;/a&gt; for further details.&lt;/p&gt;
&lt;p&gt;In any case, I follow industry standards of technical, physical and administrative security to safeguard your personal information.&lt;/p&gt;
&lt;p&gt;If you want to access or change your personal data that I store, you may &lt;a href="https://staging.sven.guru/about/"&gt;contact me&lt;/a&gt;. You have the right to disclosure, correction, deletion or constraint of your personal data, if permitted by law.&lt;/p&gt;
&lt;p&gt;If you are unhappy about how I handle your personal data, you may complain to any data privacy authority. For example to the &lt;a href="https://www.datenschutzzentrum.de/"&gt;Independent Centre for Privacy Protection Schleswig-Holstein&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you have any questions, feel free to &lt;a href="https://staging.sven.guru/about/"&gt;contact me&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Website Monitor</title><link>https://staging.sven.guru/monitoring/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://staging.sven.guru/monitoring/</guid><description>&lt;p&gt;If you found this page through your server logs: requests with the User-Agent
&lt;code&gt;sven.guru website monitor - https://sven.guru/monitoring&lt;/code&gt; come from a small,
personal, non-commercial uptime monitor run by me, Sven Koschnicke. It checks
whether a handful of websites I care about are up. It isn&amp;rsquo;t a search engine,
an archive, an SEO tool or a crawler collecting training data.&lt;/p&gt;
&lt;h2 id="how-to-opt-out"&gt;How to opt out&lt;/h2&gt;
&lt;p&gt;Add this to your robots.txt:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;User-agent: SvenGuruMonitor
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Disallow: /
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The monitor caches robots.txt for up to 24 hours, so it stops within about a
day. After that it only re-reads your robots.txt once a day to see whether it
has changed, and requests nothing else from your site.&lt;/p&gt;
&lt;h2 id="what-it-requests"&gt;What it requests&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Only the one configured URL per site, usually the homepage, with &lt;code&gt;GET&lt;/code&gt;. It
doesn&amp;rsquo;t crawl or follow links, doesn&amp;rsquo;t load images, CSS or JavaScript, and
doesn&amp;rsquo;t execute anything.&lt;/li&gt;
&lt;li&gt;It follows up to 5 redirects, reads at most the first 64 KB of the response
and gives up after 15 seconds.&lt;/li&gt;
&lt;li&gt;It records only the status code, the response time and the outcome (ok,
error, rate-limited). Page content isn&amp;rsquo;t stored. Detailed results are kept
for 30 days, daily uptime totals after that.&lt;/li&gt;
&lt;li&gt;The results only show up on a private dashboard on my home network. Nothing
is published.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="how-often"&gt;How often&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;While a site is healthy: once every 5 minutes (±10% jitter), so about 12
requests an hour, or 290 a day.&lt;/li&gt;
&lt;li&gt;After a failed request: one retry after about 1 minute. While a site is
down: retries after 1, 2 and 4 minutes, then every 5 minutes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="rate-limiting"&gt;Rate limiting&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;429 Too Many Requests&lt;/code&gt; is never treated as an outage. The monitor just
backs off.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Retry-After&lt;/code&gt; is honoured on any response, as seconds or as an HTTP date. If
it asks for a longer wait than the normal schedule, the monitor waits that
long (up to 1 hour per instruction). It never makes the monitor check more
often.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="robots-dot-txt-details"&gt;robots.txt details&lt;/h2&gt;
&lt;p&gt;The monitor follows &lt;a href="https://www.rfc-editor.org/rfc/rfc9309"&gt;RFC 9309&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Product token: &lt;code&gt;SvenGuruMonitor&lt;/code&gt;. The monitor obeys the group for that token
if there is one, otherwise the &lt;code&gt;*&lt;/code&gt; group.&lt;/li&gt;
&lt;li&gt;robots.txt is fetched per origin and cached for up to 24 hours. Every
redirect hop is checked against its own origin&amp;rsquo;s robots.txt before it&amp;rsquo;s
requested.&lt;/li&gt;
&lt;li&gt;If robots.txt disallows the URL, the monitor stops requesting the site
entirely and only re-reads robots.txt once a day.&lt;/li&gt;
&lt;li&gt;A missing robots.txt (404 or any other 4xx) means &amp;ldquo;allowed&amp;rdquo;, as the RFC
says. If robots.txt can&amp;rsquo;t be fetched (server error, timeout) and the monitor
has no earlier copy, it doesn&amp;rsquo;t request the site at all.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="contact"&gt;Contact&lt;/h2&gt;
&lt;p&gt;If the monitor misbehaves, or you can&amp;rsquo;t edit your robots.txt, write to me at
&lt;a href="mailto:s.koschnicke@gfxpro.com"&gt;s.koschnicke@gfxpro.com&lt;/a&gt;. I&amp;rsquo;ll then adjust the monitor for your site.&lt;/p&gt;</description></item></channel></rss>