<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Miblog</title><link>https://vyskocil.me/</link><description>Recent content on Miblog</description><generator>Hugo</generator><language>en-us</language><atom:link href="https://vyskocil.me/index.xml" rel="self" type="application/rss+xml"/><item><title>Gonix: Unix From Userspace</title><link>https://vyskocil.me/blog/gonix-unix-from-userspace/</link><pubDate>Fri, 24 Apr 2026 14:21:36 +0200</pubDate><guid>https://vyskocil.me/blog/gonix-unix-from-userspace/</guid><description>A while back, I watched the video where Dennis Ritchie was showing Unix command line. And I though to myself.&amp;#xA;How hard would it be to replicate the unix pipe in Go?&amp;#xA;Given the fact I began experimenting 4 years ago, it might be tempting to say yes. It is hard! However, …</description><content:encoded><![CDATA[<p>A while back, I watched the <a href="https://youtu.be/XvDZLjaCJuw?t=786">video where Dennis Ritchie was showing Unix command
line</a>. And I though to myself.</p>
<blockquote>
<p>How hard would it be to replicate the unix pipe in Go?</p>
</blockquote>
<p>Given the fact I began experimenting 4 years ago, it might be tempting to say
yes. It is hard!  However, implementing a Unix command and a pipe itself is
trivial in Go. All the necessary ingredients are already present in a standard
library.</p>
<ul>
<li><code>pipe</code> is <code>io.Pipe</code></li>
<li>the producer and consumer runs concurrently and Go has <code>goroutines</code></li>
</ul>
<h2 id="tldr">TL;DR;</h2>
<p>Let me introduce the project quicky.
<a href="https://codeberg.org/gonix">https://codeberg.org/gonix</a> provides a Unix
utilities and scripts as a (Go) programming library. Since the utilities and
shell interpreter itself themselves are Go - there&rsquo;s no <code>fork+execve</code>, no extra
syscalls, it is all standard programming in userspace. And thanks to people
working on reimplementation of <a href="https://www.gnu.org/software/coreutils/">GNU core
utilities</a> to
<a href="https://github.com/uutils/coreutils">Rust</a> GNU compatible coreutils are
available right now.</p>
<p>Consider this trivial example of the script.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>&gt; gonix/cat /etc/passwd | rust/wc -l
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">42</span></span></span></code></pre></div>
<p>While it does look like one and behave like a script, it is all pure Go all the way down.</p>
<ol>
<li><a href="https://github.com/mvdan/sh">https://github.com/mvdan/sh</a> implements the <code>shell</code> itself.</li>
<li><code>cat</code> is implemented by a gonix project itself.
<a href="https://codeberg.org/gonix/gonix/src/branch/main/cat">https://codeberg.org/gonix/gonix/src/branch/main/cat</a></li>
<li><code>wc</code> uses WebAssembly build of <a href="https://github.com/uutils/coreutils">https://github.com/uutils/coreutils</a> project.</li>
</ol>
<p>The names <code>gonix/cat</code> and <code>rust/wc</code> are <em>arbitrary</em> - scripts can be called by any name and
these names has been chosen in order to showcase the fact this is not a script in a traditional sense.</p>
<h2 id="motivation">Motivation</h2>
<p>Despite using Linux for almost two decades, I have never liked the strict separation
between scripting and a programming. There is no <code>libgrep</code> or <code>libsed</code> or <code>libawk</code> that combines
the higher level tools with a proper programming environment. Projects like <code>libcurl/curl</code>
do exist though. Even then - if a script solves the problem by calling a <code>curl</code>, the solution
must be written again using interfaces tailored to <code>C</code> or other real programming language.</p>
<p>So while command line tools and scripting languages themselves are written in C
(or Go or Rust or Python or Node). Consuming those back as a software package
is awkward (see <code>gpgme</code> as an infamous example). This project is an attempt to
merge both worlds together and keep scripting useful part and integrated part
of a programming.</p>
<p>The <code>cat | wc</code> example above can be implemented using a few lines of Go and
all runs <em>in userspace</em> inside the <em>same</em> process without a single additional <code>fork+exec</code> in sight.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// register commands</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">cmds</span> = <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">FilterBuilderFunc</span>{
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;gonix/cat&#34;</span>: <span style="color:#a6e22e">cat</span>.<span style="color:#a6e22e">FromArgs</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;rust/wc&#34;</span>:  <span style="color:#a6e22e">coreutils</span>.<span style="color:#a6e22e">Wc</span>(),
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#75715e">// parse the script</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">file</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">syntax</span>.<span style="color:#a6e22e">NewParser</span>().
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">Parse</span>(<span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">NewReader</span>(<span style="color:#a6e22e">script</span>), <span style="color:#e6db74">&#34;gonix/cat ${FILE} | rust/wc&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">runner</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">interp</span>.<span style="color:#a6e22e">New</span>(
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">interp</span>.<span style="color:#a6e22e">Env</span>(<span style="color:#a6e22e">expand</span>.<span style="color:#a6e22e">ListEnviron</span>(<span style="color:#e6db74">&#34;FILE=test.me&#34;</span>)),
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">interp</span>.<span style="color:#a6e22e">StdIO</span>(<span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>, <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>),
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">interp</span>.<span style="color:#a6e22e">ExecHandlers</span>(<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">ExecHandler</span>),
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>) = <span style="color:#a6e22e">runner</span>.<span style="color:#a6e22e">Run</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#a6e22e">file</span>)</span></span></code></pre></div>
<h2 id="shoulders-of-giants">Shoulders of giants</h2>
<p>The most fascinating aspect of this project is how little code I needed to
write. It is also impressive how much I can rely on work of others. The whole
entire project contains less <strong>10,500 lines</strong> of a code. This is a cool metric,
especially in this AI agentic era.</p>
<p>This project would not be possible without the creators of Unix and Go and</p>
<ul>
<li><a href="https://github.com/mvdan/sh">https://github.com/mvdan/sh</a> for an excellent implementation of a shell for Go</li>
<li><a href="https://github.com/benhoyt/goawk">https://github.com/benhoyt/goawk</a> for amazing implementation of awk. The <a href="https://codeberg.org/gonix/gonix/src/branch/main/cat">gonix implementation of cat</a> is implemented as awk scripts.</li>
<li><a href="https://pkg.go.dev/github.com/tetratelabs/wazero">https://pkg.go.dev/github.com/tetratelabs/wazero</a> which allowed me to use</li>
<li><a href="https://github.com/uutils/coreutils">https://github.com/uutils/coreutils</a> despite being written in Rust</li>
<li><a href="https://github.com/u-root/u-root">https://github.com/u-root/u-root</a> project for having a native implementation of some utilites too.</li>
</ul>
<h2 id="the-unix-architecture">The unix architecture</h2>
<p>Consider this very simple Unix shell pipeline.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>&gt; cat /etc/passwd | wc -l
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">42</span></span></span></code></pre></div>
<p>In a traditional unix architecture there are many components, which must be there for this to work.</p>
<ol>
<li>unix kernel</li>
<li><code>sh</code>, <code>cat</code> and <code>wc</code> binaries installed</li>
<li>kernel starts the <code>sh</code> process</li>
<li>sh must <code>open(2)</code> and <code>read(2)</code> the script</li>
<li>sh must call <code>pipe(2)</code> to create a pipe</li>
<li>sh must call <code>fork(2)</code> for each sub-process</li>
<li>sh must call <code>dup(2)</code> connecting the <code>stdout</code> of <code>cat</code> with <code>stdin</code> of <code>wc</code></li>
<li>sh must call <code>execve(2)</code> for both subprocesses</li>
<li>sh calls <code>waitpid(2)</code> to get a report from kernel when and how tools
ended</li>
<li>sh can finally <code>exit(2)</code> and report the status</li>
</ol>
<p>Do not forget this is simplified version of all the work computer does. It
ignores stuff like signal handling and all other stuff, dynamic linking,
locales, memory calls like <code>brk</code> and <code>mmap</code>, opening locale and other files and
so on. The GNU userspace executed as <code>sh test.sh</code> ran <code>567</code> syscalls.</p>
<p>Additionally, a script will only work if these are installed on the target
system. This can become a problem if</p>
<ul>
<li>You cannot modify the system.</li>
<li>The required tool is not easily available.</li>
<li>Your system don&rsquo;t have latest GNU utilities and all it has are outdated unix tools.</li>
<li>your system is alien enough, that even if a Unix environment has been ported, this
will never feel native.</li>
</ul>
<p>With gonix shell scripting becomes 100% portable.</p>
<h2 id="gonix-architecture">Gonix architecture</h2>
<p>The core of unix command is the standard io. The C abstraction is called a
<em>file descriptor</em> and is represented by a type <code>int</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-C" data-lang="C"><span style="display:flex;"><span><span style="color:#75715e">/* #include&lt;unistd.h&gt; */</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">/* File number of stdin; 0. */</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#define STDIN_FILENO 0
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#75715e">/* File number of stdout; 1. */</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#define STDOUT_FILENO 1
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#75715e">/* File number of stderr; 2. */</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#define STDERR_FILENO 2</span></span></span></code></pre></div>
<p>This concept can be easily expressed in Go.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">StandardIO</span> <span style="color:#66d9ef">interface</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">Stdin</span>() <span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">Reader</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">Stdout</span>() <span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">Writer</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">Stderr</span>() <span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">Writer</span>
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>Which is exactly what
<a href="https://codeberg.org/gonix/gio/src/commit/5177420949c5aba0f4aa08fb98c4eaf9dd6f389d/unix/unix.go#L17-L22">gio/unix</a>
does.</p>
<p>And while each C unix tool starts with a <code>main</code> function</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-C" data-lang="C"><span style="display:flex;"><span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;unistd.h&gt;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">int</span> <span style="color:#a6e22e">main</span>() {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">write</span>(STDOUT_FILENO, <span style="color:#e6db74">&#34;stdout</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, <span style="color:#a6e22e">strlen</span>(<span style="color:#e6db74">&#34;stdout</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>));
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">write</span>(STDERR_FILENO, <span style="color:#e6db74">&#34;stderr</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, <span style="color:#a6e22e">strlen</span>(<span style="color:#e6db74">&#34;stderr</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>));
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>For our purpose its better to define <a href="https://codeberg.org/gonix/gio/src/commit/5177420949c5aba0f4aa08fb98c4eaf9dd6f389d/unix/unix.go#L25-L27">an
interface</a></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Filter</span> <span style="color:#66d9ef">interface</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">Run</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">StandardIO</span>) <span style="color:#66d9ef">error</span>
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>and implement it</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Demo</span> <span style="color:#66d9ef">struct</span>{}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">Demo</span>) <span style="color:#a6e22e">Run</span>(<span style="color:#a6e22e">_</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">stdio</span> <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">StandardIO</span>) <span style="color:#66d9ef">error</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Fprintf</span>(<span style="color:#a6e22e">stdio</span>.<span style="color:#a6e22e">Stdout</span>(), <span style="color:#e6db74">&#34;stdout\n&#34;</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Fprintf</span>(<span style="color:#a6e22e">stdio</span>.<span style="color:#a6e22e">Stderr</span>(), <span style="color:#e6db74">&#34;stderr\n&#34;</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<h2 id="example-cat">Example: cat</h2>
<p>The <code>cat</code> without any arguments is literary an one-liner with <code>io.Copy</code> doing all the work.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// import codeberg.org/gonix/gio/unix</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">Cat</span>) <span style="color:#a6e22e">Run</span>(<span style="color:#a6e22e">_</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">stdio</span> <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">StandardIO</span>) <span style="color:#66d9ef">error</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">Copy</span>(<span style="color:#a6e22e">stdio</span>.<span style="color:#a6e22e">Stdout</span>(), <span style="color:#a6e22e">stdio</span>.<span style="color:#a6e22e">Stdin</span>())
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<h2 id="example-wc--l">Example: wc -l</h2>
<p>The <code>wc -l</code> is a bit more lines, but still not very complicated.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// import codeberg.org/gonix/gio/unix</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">Lines</span>) <span style="color:#a6e22e">Run</span>(<span style="color:#a6e22e">_</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">stdio</span> <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">StandardIO</span>) <span style="color:#66d9ef">error</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">count</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">scanner</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">bufio</span>.<span style="color:#a6e22e">NewScanner</span>(<span style="color:#a6e22e">stdio</span>.<span style="color:#a6e22e">Stdin</span>())
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">scanner</span>.<span style="color:#a6e22e">Scan</span>() {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">count</span><span style="color:#f92672">++</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">scanner</span>.<span style="color:#a6e22e">Err</span>()
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Fprintf</span>(<span style="color:#a6e22e">stdio</span>.<span style="color:#a6e22e">Stdout</span>(), <span style="color:#e6db74">&#34;%d\n&#34;</span>, <span style="color:#a6e22e">count</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<h2 id="command-line-arguments">Command line arguments</h2>
<p><code>Run</code> and <code>unix.Filter</code> interface can satisfy the easiest tools without a
command line arguments or any notion of an outer context like working
directory. The typical Unix tool expects and receives command line arguments
when executed. This is solved by <a href="https://codeberg.org/gonix/gio/src/commit/252204530e2b3156df2818cb036dea7b9a533c23/unix/builder.go#L31-L33">filter builder
pattern</a></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">command</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">args</span> []<span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">opts</span> <span style="color:#f92672">...</span><span style="color:#a6e22e">ShellContextOption</span>) (<span style="color:#a6e22e">Filter</span>, <span style="color:#66d9ef">error</span>)</span></span></code></pre></div>
<p>What way implementation can get more context than context and standard io only.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">CatWithArgs</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">command</span> <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">args</span> []<span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// import codeberg.org/gonix/gio/unix</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#a6e22e">CatWithArgs</span>) <span style="color:#a6e22e">Run</span>(<span style="color:#a6e22e">_</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">stdio</span> <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">StandardIO</span>) <span style="color:#66d9ef">error</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">parse</span>(<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">args</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;%s: %w&#34;</span>, <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">command</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">Copy</span>(<span style="color:#a6e22e">stdio</span>.<span style="color:#a6e22e">Stdout</span>(), <span style="color:#a6e22e">stdio</span>.<span style="color:#a6e22e">Stdin</span>())
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<h2 id="the-shell-context">The shell context</h2>
<p>Let&rsquo;s consider this example</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>FOO<span style="color:#f92672">=</span><span style="color:#ae81ff">42</span> printenv</span></span></code></pre></div>
<p>Unix tools expects environment variables too. This is achieved by <code>opts ...ShellContextOption</code> of a builder. Every tool can then get proper functions
which can be used to get an access. The key design concern here is to decouple
the interface for such functionality with an actual implementation</p>
<ol>
<li>interface is defined in <code>gio/unix</code></li>
<li>injection is a part of <code>unix.FilterBuilderFunc</code></li>
<li>actual implementation if done in <code>sh</code> package for <code>mvdan.cc/sh/v3</code>.</li>
</ol>
<p>Tools can register those helpers to obtain enough from the context as seen in a following example.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">b</span> <span style="color:#a6e22e">uroot</span>.<span style="color:#a6e22e">Builder</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">builders</span> = <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">FilterLookupMap</span>{
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;mktemp&#34;</span>: <span style="color:#a6e22e">b</span>.<span style="color:#a6e22e">Mktemp</span>(),
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">filters</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">sh</span>.<span style="color:#a6e22e">NewFilters</span>(
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">builders</span>.<span style="color:#a6e22e">Lookup</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">unix</span>.<span style="color:#a6e22e">WithLookupEnvFunc</span>(<span style="color:#a6e22e">sh</span>.<span style="color:#a6e22e">LookupEnvFunc</span>),
</span></span><span style="display:flex;"><span>  )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">source</span> = <span style="color:#e6db74">&#34;TMPDIR=/tmp42 mktemp -d t.XXXXXX&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">parsed</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">syntax</span>.<span style="color:#a6e22e">NewParser</span>().
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">Parse</span>(<span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">NewReader</span>(<span style="color:#a6e22e">source</span>), <span style="color:#e6db74">&#34;&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">require</span>.<span style="color:#a6e22e">NoError</span>(<span style="color:#a6e22e">t</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">got</span> <span style="color:#a6e22e">bytes</span>.<span style="color:#a6e22e">Buffer</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">runner</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">interp</span>.<span style="color:#a6e22e">New</span>(
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">interp</span>.<span style="color:#a6e22e">StdIO</span>(<span style="color:#66d9ef">nil</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">got</span>, <span style="color:#66d9ef">nil</span>),
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">interp</span>.<span style="color:#a6e22e">ExecHandlers</span>(<span style="color:#a6e22e">filters</span>.<span style="color:#a6e22e">ExecHandler</span>),
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span><span style="color:#75715e">// expect error as /tmp42 does not exists</span></span></span></code></pre></div>
<p>Available options are</p>
<ol>
<li>get working directory via <code>unix.GetDirFunc</code></li>
<li>lookup environment variable via <code>unix.LookupEnvFunc</code></li>
<li>get all environment via <code>unix.GetEnvFunc</code></li>
</ol>
<h2 id="what-did-the-gonix-did-for-us">What did the gonix did for us?</h2>
<p>Nothing at the moment. However I think this can improve an existing container tooling for example.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> <span style="color:#e6db74">scratch</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#75715e"># I need to create a directory</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#75715e"># but can&#39;t as mkdir is not there</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> mkdir /app</span></span></code></pre></div>
<p>or the debugging sessions</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span><span style="color:#75715e"># go away: cannot execute /bin/ls: file not found</span>
</span></span><span style="display:flex;"><span>docker exec scratch ls -lh /app</span></span></code></pre></div>
<p>At this moment all the tools are supposed to be installed inside the system
itself. And consider a hypothetical improved tooling</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> <span style="color:#e6db74">scratch</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#75715e"># mkdir is available as a part of Go runtime</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#75715e"># no need to install a thing</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>GONIX rust/mkdir /app</span></span></code></pre></div>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span><span style="color:#75715e"># ls is a part of runtime, so rust/ls will work</span>
</span></span><span style="display:flex;"><span>better-docker exec --gonix <span style="color:#e6db74">&#39;rust/ls -lh /app&#39;</span> scratch</span></span></code></pre></div>
<p>The other possible use cases are</p>
<ul>
<li>And monitoring probes</li>
<li>Tools which setups systems using scripts like <code>distrobox</code></li>
<li>Experimenting with more exotic tools like those written for Plan9</li>
<li>AI companions writing and executing scripts</li>
<li>MCP server</li>
<li>And many more</li>
</ul>
<h2 id="roadmap">Roadmap?</h2>
<p>There is any as this is a project of love. However the interesting areas to consider are</p>
<ol>
<li>context aware io - at this moment individual filters much implement context
checks individually - idea here is to wrap context and io together, so Read/Write methods
will stop working once context is canceled</li>
<li>busybox-like tool which packs as much other tools as possible</li>
<li>interactive shell - being in Go it can have nice things like native fuzzy search</li>
</ol>
<h2 id="limitations">Limitations</h2>
<p>There are more aspects to a real Unix system than just pipes and environment
variables. Some are fundamental limits of how the kernel works, while others
may be implemented later. These limitations include things like</p>
<h3 id="kernel-properties">Kernel properties</h3>
<p>The project is an <em>emulation</em> of a real Unix system completely bypassing kernel
and all its features. So process isolation, process tracking, signals, control
groups, security context, SELinux labels, user and groups, extended attributes
and so on - nothing does work inside gonix.</p>
<p>Unless the <code>fork+exec</code> is involved -
<a href="unix/exec.go">https://codeberg.org/gonix/gio/src/branch/main/unix/exec.go#L30</a>
is available.</p>
<p>In other words <code>gonix</code> is <strong>not</strong> a security layer. Additionally running inside
the same OS process, you will never get SUID binaries like <code>sudo</code> working in
userspace.</p>
<h3 id="cant-execve-by-default">Can&rsquo;t execve by default</h3>
<p>Other part are unit tools calling <code>fork+exec</code> themselves like <code>xargs</code>. There is
no good solution for a <em>reverse</em> wrapper which will hijack <code>exec.Command</code> and
covert it to <code>unix.Filter</code>. So there is GNU compatible implementation of
<a href="https://codeberg.org/gonix/gonix/tree/main/xargs/">https://codeberg.org/gonix/gonix/tree/main/xargs/</a>
which gets the <code>unix.FilterBuilderFunc</code> as a parameter and can execute other
userspace tools.</p>
<p>This design allows setups like the main shell having an access to
different filters than <code>xargs</code> itself. This is how it is unit tested by the way.</p>
<h3 id="signals-and-process-tracking">Signals and process tracking</h3>
<p>While this one can be emulated, at the moment individual utilities don&rsquo;t
respect <code>context.Context</code> by default unless specified elsewhere. The design of
Go context will most likely allow emulating the signals, but this is not yet
done. The relevant part is a process tracking, this is not implemented - if
something get stuck, you must kill a whole process.</p>
<h3 id="interactivity">Interactivity</h3>
<p>While interesting, neither <code>mvdan.cc/sh/v3</code> neither the utilities are developed
and tested with interactivity in mind.</p>
<h2 id="are-there-any-other-similar-projects">Are there any other similar projects?</h2>
<p>I haven&rsquo;t searched thoroughly and those are two examples I am aware of.</p>
<p><a href="https://github.com/ewhauser/gbash">https://github.com/ewhauser/gbash</a> seems to
be the closest. However it seems to be more AI driven and I expect that most of code there is
generated.</p>
<p>The <code>gbash</code> project itself references
<a href="https://github.com/vercel-labs/just-bash">https://github.com/vercel-labs/just-bash</a>
which seems to be the same, only in Typescript.</p>
<h2 id="ai">AI</h2>
<p>While code in the project was written by me, I must admit I would not be able
to progress so quickly without a help of LLMs.</p>
<ol>
<li>Proof of concept code including wazero integration has been provided by
various AI tools as well as a few test cases.</li>
<li>The ideas behind <code>unix.ShellContextOption</code> has been investigated by Claude. It was
nice to be able to see and reject all the <em>wrong</em> solutions it produced, which helped
me to figure out the proper design.</li>
<li>Experimental <code>sbase</code> web assembly port has been vibe-coded by Claude.</li>
<li>deepl.com/write reviewed this text as usual.</li>
<li>Google AI explained me how to use Inkscape to convert the png logo into svg.</li>
</ol>
<h2 id="logo">Logo</h2>
<p>Gopher by Rene French CC license <a href="https://upload.wikimedia.org/wikipedia/commons/2/2d/Go_gopher_favicon.svg">https://upload.wikimedia.org/wikipedia/commons/2/2d/Go_gopher_favicon.svg</a> and <a href="https://www.deviantart.com/dollarakshay/art/Unix-Terminal-Logo-735161243">Unix Terminal Logo</a> <a href="https://www.deviantart.com/dollarakshay/gallery">Dollar Akshay</a></p>
]]></content:encoded></item><item><title>Going Dark</title><link>https://vyskocil.me/blog/going-dark/</link><pubDate>Sun, 22 Mar 2026 13:48:37 +0100</pubDate><guid>https://vyskocil.me/blog/going-dark/</guid><description>While writing about the responsive design of ByteKit,I wanted to demonstrate the fact it has both light and a dark themes. At this exact moment I realised that this blog itself has no dark theme. When I setup to setup Hugo for it I simply used an existing theme named hestia-pure. …</description><content:encoded><![CDATA[<p>While writing about the <a href="https://vyskocil.me/blog/bytekit-responsive/">responsive design of ByteKit</a>,I wanted to demonstrate the fact it has both <em>light</em> and a <em>dark</em>
themes. At this exact moment I realised that this blog itself has no dark theme. When I setup to setup Hugo for it I simply used an existing theme named
<a href="https://github.com/diwao/hestia-pure">hestia-pure</a>. As it didn&rsquo;t support the dark theme, neither did my blog.</p>
<p>Meanwhile, it was necessary to
<a href="https://codeberg.org/vyskocilm/miblog-hestia-pure">fork it</a> it due growing needs. Most notably it rely on SCSS, where I want CSS written by hand without any Javascript post-processing. With modernish CSS features like variables supporting the color scheme is easy and this blog is based on <a href="https://github.com/morhetz/gruvbox">gruvbox</a>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-css" data-lang="css"><span style="display:flex;"><span><span style="color:#75715e">/* https://github.com/morhetz/gruvbox?tab=readme-ov-file#light-mode-1 */</span>
</span></span><span style="display:flex;"><span>:<span style="color:#a6e22e">root</span> {
</span></span><span style="display:flex;"><span>  --bg: <span style="color:#ae81ff">#fbf1c7</span>;
</span></span><span style="display:flex;"><span>  --red: <span style="color:#ae81ff">#cc241d</span>;
</span></span><span style="display:flex;"><span>  --green: <span style="color:#ae81ff">#98971a</span>;
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<h2 id="going-dark">Going dark</h2>
<p>With CSS variables in place, the dark theme simply redefines variables using
<code>prefers-color-scheme: dark</code> <code>@media</code> query.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-css" data-lang="css"><span style="display:flex;"><span>@<span style="color:#66d9ef">media</span> <span style="color:#f92672">(</span><span style="color:#f92672">prefers-color-scheme</span><span style="color:#f92672">:</span> <span style="color:#f92672">dark</span><span style="color:#f92672">)</span> {
</span></span><span style="display:flex;"><span>  :<span style="color:#a6e22e">root</span> {
</span></span><span style="display:flex;"><span>    --bg: <span style="color:#ae81ff">#282828</span>;
</span></span><span style="display:flex;"><span>    --red: <span style="color:#ae81ff">#cc241d</span>;
</span></span><span style="display:flex;"><span>    --green: <span style="color:#ae81ff">#98971a</span>;
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>And the page itself can advertise supported schemes and its priority.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-html" data-lang="html"><span style="display:flex;"><span>&lt;<span style="color:#f92672">meta</span> <span style="color:#a6e22e">name</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;color-scheme&#34;</span> <span style="color:#a6e22e">content</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;light dark&#34;</span> /&gt;</span></span></code></pre></div>
<h2 id="fixing-mastodon-comments">Fixing Mastodon comments</h2>
<p>Taking an inspiration from
<a href="https://carlschwan.eu/2020/12/29/adding-comments-to-your-static-blog-with-mastodon/">https://carlswchwan.eu</a>,
I added support for Mastodon comments. The problem was that I originally reused a CSS from a different blog and didn&rsquo;t actually used site&rsquo;s CSS variables. This has been fixed and styling for comments gor merged to main <code>style.css</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-css" data-lang="css"><span style="display:flex;"><span><span style="color:#75715e">/* Mastodon comments */</span>
</span></span><span style="display:flex;"><span>#load-comment {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">background-color</span>: <span style="color:#a6e22e">var</span>(<span style="color:#f92672">--</span><span style="color:#66d9ef">blue</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">color</span>: <span style="color:#a6e22e">var</span>(<span style="color:#f92672">--</span>bg);
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">border</span>: <span style="color:#ae81ff">1</span><span style="color:#66d9ef">px</span> <span style="color:#66d9ef">solid</span> <span style="color:#a6e22e">var</span>(<span style="color:#f92672">--</span>blue12);
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<h2 id="color-scheme-based-screenshots">Color scheme based screenshots</h2>
<p>One of the coolest things about this exercise is that HTML itself supports <code>@media</code> queries, making it easy to render different images based on a preferred color scheme. This means that page with a light style will display light screenshots. And vice versa.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-html" data-lang="html"><span style="display:flex;"><span>&lt;<span style="color:#f92672">picture</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">source</span> <span style="color:#a6e22e">srcset</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;screenshot-dark.png&#34;</span> <span style="color:#a6e22e">media</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;(prefers-color-scheme: dark)&#34;</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">source</span> <span style="color:#a6e22e">srcset</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;screenshot-light.png&#34;</span> <span style="color:#a6e22e">media</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;(prefers-color-scheme: light)&#34;</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">img</span> <span style="color:#a6e22e">src</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;screenshot-light.png&#34;</span> <span style="color:#a6e22e">alt</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Miblog screenshot&#34;</span>&gt;
</span></span><span style="display:flex;"><span>&lt;/<span style="color:#f92672">picture</span>&gt;</span></span></code></pre></div>
<picture>
  <source srcset="screenshot-dark.png" media="(prefers-color-scheme: dark)">
  <source srcset="screenshot-light.png" media="(prefers-color-scheme: light)">
  <img src="screenshot-light.png" alt="Miblog screenshot">
</picture>
<h2 id="color-scheme-thumbnails">Color scheme thumbnails</h2>
<p>There is another problem on the main index page. Unfortunately, most of article
thumbnails were not created with dark theme support in mind. The <a href="https://vyskocil.me/blog/bytekit-responsive/">responsive
ByteKit</a> is the first article to
feature the support for dark themed thumbnails and dark themed screenshots.</p>
]]></content:encoded></item><item><title>Bytekit Responsive</title><link>https://vyskocil.me/blog/bytekit-responsive/</link><pubDate>Wed, 18 Mar 2026 12:25:57 +0100</pubDate><guid>https://vyskocil.me/blog/bytekit-responsive/</guid><description>Bytekit: jq, awk and a responsive design I released version 0.3.0 of a bytekit.app, which is a privacy oriented AGPLv3 Web application for developers that I had been working on.&amp;#xA;There are a few new features to highlight:&amp;#xA;AWK, which is based on the impressive goawk package from …</description><content:encoded><![CDATA[<h1 id="bytekit-jq-awk-and-a-responsive-design">Bytekit: jq, awk and a responsive design</h1>
<p>I released version <a href="https://quay.io/repository/vyskocilm/bytekit?tab=tags">0.3.0</a> of a <a href="https://bytekit.app">bytekit.app</a>, which is a
privacy oriented AGPLv3 <a href="https://vyskocil.me/blog/introducing-bytekit/">Web application for developers</a> that I had been working on.</p>
<p>There are a few new features to highlight:</p>
<ul>
<li><a href="https://bytekit.app/goawk">AWK</a>, which is based on the impressive <a href="https://benhoyt.com/writings/goawk/">goawk</a> package from Ben Hoyt.</li>
<li><a href="https://bytekit.app/gojq">jq</a>, which is based on the <a href="github.com/itchyny/gojq">pure Go version of jq</a></li>
<li>new design, logo and responsive layout and a footer</li>
</ul>
<p>I must admit that ChatGPT helped me generate ideas for the logo and its color
scheme. My minuscule Inkscape skills handled the rest then.</p>
<img src="bytekit-stronger.svg" alt="Logo" style="width: 40%; max-width: 350px; height: auto;">
<h2 id="responsive-design">Responsive design</h2>
<p>However much bigger change than visual was a responsiveness. Bytekit is
intentionally light on elements. I wanted to have something, which just works
and to not overwhelm users or myself with UI options. Additionally writing UI
elements is not a funniest or most creative part of the work. So once I made
elements like working. Well then every other problem can be solved by adding a
new input box, right?</p>
<p>However, the most significant change was the responsiveness. Not the visuals. Bytekit intentionally uses only a few elements. I wanted something that just works and to not overwhelm users or myself with UI options. And I must admit that writing UI elements is not the most funnies part of the work. Quite the opposite. So once I made elements like the <a href="https://codeberg.org/vyskocilm/bytekit/src/branch/main/internal/kit/input_box.go">InputBox</a> work. Then every problem became a nail. Or something input box can solve.</p>
<p>Let&rsquo;s say the <a href="https://bytekit.app/goawk">awk page</a>.</p>
<ol>
<li>Command line parameters? InputBox!</li>
<li>Program itself? Well InputBox, of course.</li>
<li>Input?</li>
<li>Output?</li>
</ol>
<p>You get it. The Input Box has a consistent layout, the copy and paste buttons,
can display an error like a wrong AWK program being passed and can be bind to
<em>on text changed</em> event providing an interactivity. And as most of the pages
are based on this element, this helps a lot with a consistent user experience.</p>
<picture>
  <source srcset="bytekit-goawk-dark.png" media="(prefers-color-scheme: dark)">
  <source srcset="bytekit-goawk-light.png" media="(prefers-color-scheme: light)">
  <img src="bytekit-goawk-light.png" alt="ByteKit theme">
</picture>
<p>Despite using light HTML or using a CSS framework, the design was not well-suited
to the smaller screens. The biggest culprit was the <code>&lt;aside&gt;</code> menu, which got
rendered at the top of the screen every time, making the site next to useless on
mobile. Fortunately Bulma.css, the CSS framework behind the page, supports a
<a href="https://bulma.io/documentation/start/responsiveness/">responsiveness</a>.</p>
<h2 id="menu-is-twice">Menu is twice</h2>
<p>The problem is how to ensure that mobile users gets no menu and can show one,
while desktop users has their menu always displayed as they do have much free
space. The solution was to render the menu twice and use CSS to hide/show it
depending on a screen size.</p>
<p>Bigger screens are now defined with <code>is-hidden-touch</code> CSS class, which ensures
it gets hidden on a smaller screens.</p>
<p>The menu for smaller screens is inside the navbar and has the class
<code>is-hidden-desktop</code>. This means, that by default, the <code>aside</code> and <code>navbar</code>
menus are hidden. In order to display it, there is a button which on a click
pass the <code>is-active</code> class to the proper element. This is a few lines of Go and
this is basically what drives a lot of interactivity of the bytekit itself.</p>
<picture>
  <source srcset="bytekit-mobile-menu-dark.png" media="(prefers-color-scheme: dark)">
  <source srcset="bytekit-mobile-menu-light.png" media="(prefers-color-scheme: light)">
  <img src="bytekit-mobile-menu-light.png" alt="ByteKit mobile menu">
</picture>
<p>There are still areas to improve, such as the aforementioned InputBox which has
too much padding on a mobile devices. In overall the site looks much better now.</p>
<h2 id="better-footer">Better footer</h2>
<p>Last but not least change is the new footer. Previously, it displayed a nerdy
sha256 hash to everyone, making it unpleasant and especially on a mobile. This
has been changed, it got a name, em dash, short description, links to source
code and hashes are hidden for those interested.</p>
<picture>
  <source srcset="bytekit-footer-dark.png" media="(prefers-color-scheme: dark)">
  <source srcset="bytekit-footer-light.png" media="(prefers-color-scheme: light)">
  <img src="bytekit-footer-light.png" alt="ByteKit footer">
</picture>
<p>Logo for the article is remixed <a href="https://github.com/egonelbre/gophers/blob/master/vector/science/power-to-the-linux.svg">https://github.com/egonelbre/gophers/blob/master/vector/science/power-to-the-linux.svg</a> with <a href="https://upload.wikimedia.org/wikipedia/commons/c/cd/IPhone_size_comparison.svg">https://upload.wikimedia.org/wikipedia/commons/c/cd/IPhone_size_comparison.svg</a>.</p>
]]></content:encoded></item><item><title>Bytekit: Generating Passwords</title><link>https://vyskocil.me/blog/bytekit-generating-passwords/</link><pubDate>Mon, 09 Feb 2026 23:41:03 +0100</pubDate><guid>https://vyskocil.me/blog/bytekit-generating-passwords/</guid><description>In my recent post introducing bytekit, I described a tiny little PWA designed to help me and other developers. Here I am going to introduce a new feature: generating passwords and secrets.&amp;#xA;It may sound odd to use an online tool for something as sensitive as password. The good …</description><content:encoded><![CDATA[<p>In my recent <a href="https://vyskocil.me/blog/introducing-bytekit/">post introducing bytekit</a>, I described a tiny little PWA designed to help me and other developers.
Here I am going to introduce a new feature: generating passwords and
secrets.</p>
<p>It may sound odd to use an <em>online</em> tool for something as sensitive as
password. The good thing is that <a href="https://bytekit.app/password-generator">https://bytekit.app/password-generator</a> has the necessary properties to make it trustworthy.</p>
<ol>
<li>It runs inside browser.</li>
<li>It does not send data anywhere - this can be verified.</li>
<li>It is <a href="https://codeberg.org/vyskocilm/bytekit">https://codeberg.org/vyskocilm/bytekit</a> Free Software, so the source code can be inspected.</li>
<li>Anyone can deploy the static binary or a container <a href="https://quay.io/repository/vyskocilm/bytekit">quay.io/vyskocilm/bytekit</a> himself.</li>
</ol>
<h1 id="how-to-generate-a-secure-password">How to generate a secure password</h1>
<p>In a not-so-good days of enterprise systems a few rules were made mandatory.</p>
<ol>
<li>Passwords must have a minimum length</li>
<li>Passwords must contains D digits and S special characters</li>
<li>One must change your password every M months</li>
<li>The system remembers all previous passwords, so it&rsquo;ll reject too similar new passwords. This is very secure, indeed.</li>
</ol>
<p>Xkcd made them obsolete.</p>
<p><a href="https://xkcd.com/936/"><img src="https://imgs.xkcd.com/comics/password_strength.png" alt="Password Strength"></a></p>
<p>In fact the sane rules are</p>
<ol>
<li>Passwords must be stored in a password manager</li>
<li>It prevents a phishing attempts among others</li>
<li>Passwords must be randomly generated (ie no <code>admin</code>, <code>12345</code>, or a <code>password</code> or a <code>football</code>)</li>
<li>Passwords must not be reused</li>
<li>The password manager is typically protected by a master password and this is where xkcd comes into a place.</li>
<li>Use <a href="https://haveibeenpwned.com/">https://haveibeenpwned.com/</a> and <a href="https://haveibeenpwned.com/Passwords">https://haveibeenpwned.com/Passwords</a> to verify your email and a password.</li>
</ol>
<h1 id="algorithms">Algorithms</h1>
<p>So to make the traditional enterprise systems happy as well as readers of xkcd and to enjoy a bit of coding,
<a href="https://bytekit.app/password-generator">https://bytekit.app/password-generator</a>
generates 4 types of passwords at once.</p>
<ol>
<li><strong>random</strong> is password which has uppercases, lowercases, digits and symbols.
Password rules are respected.</li>
<li><strong>wovel-consontant-wovel</strong> is a simplified version of <em>A Random Word Generator
For Pronounceable Passwords National Technical Information Service (NTIS)
AD-A-017676</em>, which aims to generate memorable passwords. Now is deprecated.
The wovel consonant tries to generate readable passwords using a simplified
algorithm. In a reality they&rsquo;re not if a password is long enough. Rules are
respected by placing digits and special characters to random places in a
generated string.</li>
<li><strong>koremutake</strong> generated passwords based on <a href="https://shorl.com/koremutake.php">algorithm from shorl</a> and <a href="https://github.com/wneessen/apg-go/blob/main/koremutake.go">apg-go</a> and its predefined syllables. Bytekit
randomly switches between all lowecase, all uppercase or first uppercase for each syllable. Rules are respected by placing digits and special characters to random places in a
generated string.</li>
<li><strong>xkcd</strong> - the recommended algorithm for passwords intended to be memorized. The
<code>CorrectHorseBatteryStaple</code> from <a href="https://xkcd.com/936/">xkcd: Password
Strength</a> is an example of great and safe password.
Except not <em>this</em> one, because it has been breached 150 times already (lower
case variant more than 4000 times) and today it is very likely be a part of a dictionary.
Rules related to digits and specials are ignored in this case.</li>
</ol>
<p>When in doubt use a <strong>random</strong> option or <strong>xkcd</strong> if special characters or digits does not matter.</p>
<h1 id="password-rules">Password Rules</h1>
<p>Many sites require passwords to follow specific rules. To avoid implementing
many HTML controls, I use a simple algorithm that tries to identify rules in
plain English and apply them. The best results are when each rule is on its own
line, like the placeholder.</p>
<p>It works by searching for numbers or well known word numbers like zero and then
within three words long window some keyword is searched. The &ldquo;Password must
have three digits&rdquo; will find a number 3 and a keyword <code>digits</code>, so minimal
number of digits is set to 3.</p>
<p>The <a href="https://codeberg.org/vyskocilm/bytekit/src/commit/58330d16c30f26e288e61940cdf897673c89ff7eb9f95e278fbc54a5e68e86dd/internal/app/apg/generate.go#L368">parseRules</a> and <a href="https://codeberg.org/vyskocilm/bytekit/src/commit/58330d16c30f26e288e61940cdf897673c89ff7eb9f95e278fbc54a5e68e86dd/internal/app/apg/generate_test.go#L94">TestParse</a> can shed more light to anyone interested.</p>
]]></content:encoded></item><item><title>Neovim as a Java Ide with Lombok support</title><link>https://vyskocil.me/blog/neovim-as-java-ide-lombok/</link><pubDate>Mon, 26 Jan 2026 09:12:43 +0100</pubDate><guid>https://vyskocil.me/blog/neovim-as-java-ide-lombok/</guid><description>I got recently switched to working on a Java codebase. This is a tutorial on how to setup neovim as a proper Java IDE, including a lombok support.&amp;#xA;Introduction After working with computers for almost two decades, vim&amp;amp;rsquo;s modal editing is wired into my brain and a muscle …</description><content:encoded><![CDATA[<p>I got recently switched to working on a Java codebase. This is a tutorial on
how to setup neovim as a proper Java IDE, including a
<a href="https://projectlombok.org/">lombok</a> support.</p>
<h1 id="introduction">Introduction</h1>
<p>After working with computers for almost two decades, vim&rsquo;s modal editing is
wired into my brain and a muscle memory. I do not consider myself as an
advanced user, but yanking/pasting, <code>%</code>, splits, textobjects, macros and a few
movements that I use are enough for me to stick with it. Not being forced to
use mouse is a nice bonus too.</p>
<p>Enter <a href="https://neovim.io">neovim</a>. As a fork of vim, it inherits all good parts
and adds dozens of advantages making it a plausible IDE. Among other is brings
tree-sitter, native LSP client and Lua as a scripting language.</p>
<p>Unlike int my favorite language Go, where vim/neovim sits proudly <a href="https://go.dev/blog/survey2025">in a 3rd
place</a> with 19% of respondents claiming use is.
The situation in Java world is most likely different. I could not find
any specific numbers for it. However, since its inception, the Java world has been
dominated by IDEs. Starting with
<a href="https://netbeans.apache.org/front/main/index.html">Netbeans</a> - which to my
surprise is still an active project under Apache Foundation - through <a href="https://eclipseide.org/">Eclipse
IDE</a>, and, most recently <a href="https://www.jetbrains.com/idea/">Intellij</a>, probably the most popular IDE today. Then there are Spring Tools Suite,
JBoss Developer studio as well as a Visual Studio Code. Java has never been short of IDEs.</p>
<h1 id="neovim-plugins">Neovim plugin(s)</h1>
<p>When I&rsquo;m checking out anew programming language, I check out the
<a href="https://github.com/neovim/nvim-lspconfig">nvim-lspconfig</a> first. However, in the case of
Java, this is the wrong place to look.</p>
<p><a href="https://codeberg.org/mfussenegger/nvim-jdtls">mfussenegger/nvim-jdtls</a> is the plugin I used. It integrates neovim with <a href="https://projects.eclipse.org/projects/eclipse.jdt.ls">Eclipse JDT
LS</a>. Yes, that is is the
<em>same</em> Eclipse, that produces the IDE mentioned earlier. There is also
<a href="https://github.com/nvim-java/nvim-java">nvim-java/nvim-java</a> too, which I
haven&rsquo;t tested. The <code>nvim-java</code> project itself says</p>
<blockquote>
<p>nvim-jdtls is a plugin that follows &ldquo;Keep it simple, stupid!&rdquo; approach.
If you love customizing things by yourself, then give nvim-jdtls a try.</p>
</blockquote>
<p>Which, to be honest, haven&rsquo;t shed any lights whenever I want to use this or that plugin.</p>
<h2 id="installation">Installation</h2>
<p>The installation string is a bit longer, because project is hosted on codeberg.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-lua" data-lang="lua"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;mfussenegger/nvim-jdtls&#34;</span>,
</span></span><span style="display:flex;"><span>  url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://codeberg.org/mfussenegger/nvim-jdtls&#34;</span>,
</span></span><span style="display:flex;"><span>  ft <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;java&#34;</span>,
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>Then configure it and enable.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-lua" data-lang="lua"><span style="display:flex;"><span>vim.lsp.config(<span style="color:#e6db74">&#34;jdtls&#34;</span>, {capabilities <span style="color:#f92672">=</span> capabilities,})
</span></span><span style="display:flex;"><span>vim.lsp.enable(<span style="color:#e6db74">&#34;jdtls&#34;</span>)</span></span></code></pre></div>
<p>And that&rsquo;s all folks. Or isn&rsquo;t?</p>
<h1 id="project-lombok">Project Lombok</h1>
<p>A typical Java class has a few things: the private attributes, constructor and the infamous getter and setter methods. Unlike in Go, making the class attributes public is considered a bad style. However as both languages emphasizes interfaces, you can&rsquo;t place an attribute to interface defintion - ignoring <code>final static</code> constants in Java. So having getter setters makes a sense.</p>
<p>So here is the typical Java code</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-java" data-lang="java"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Foo</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">int</span> bar;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">Foo</span>(<span style="color:#66d9ef">int</span> bar) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">bar</span> <span style="color:#f92672">=</span> bar;
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> <span style="color:#a6e22e">getBar</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> bar;
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">void</span> <span style="color:#a6e22e">setBar</span>(<span style="color:#66d9ef">int</span> bar) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">bar</span> <span style="color:#f92672">=</span> bar;
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>The getter/setter code is very repetitive. The original Java solution was to use IDE to generate the getter and setter code. <a href="https://projectlombok.org/">Project lombok</a> automates the whole thing via plugging into JVM and via special feature called annotation it generates needed methods in a background.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-java" data-lang="java"><span style="display:flex;"><span><span style="color:#f92672">import</span> lombok.AllArgsConstructor;
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> lombok.Data;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@Data</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@AllArgsConstructor</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Foo</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">int</span> bar;
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>It turns out the <code>nvim-jdtls</code> does not support this out of the box, so LSP compaints all the time about missing methods.</p>
<h1 id="the-howto">The HOWTO</h1>
<p>The TL;DR answer is to check <a href="https://codeberg.org/vyskocilm/dotfiles/src/branch/main/.config/nvim/lua/plugins/jdtls.lua">jdtls.lua in my
dotfiles</a>. It contains complete setup for it.</p>
<p>I do not want to use Mason, which can automate a lot of LSP related downloads.
So the process is a bit manual.</p>
<ol>
<li><a href="https://projectlombok.org/download">download the lombok.jar</a> and place it somewhere</li>
<li>define <code>lombok_path</code> variable pointing to the lombok.jar</li>
<li>pass it as <code>&quot;-javaagent:&quot; .. lombok_path</code> to the configuration.</li>
</ol>
<p>And that&rsquo;s it.</p>
<p>I used the Lua variable <code>lombok_path</code> to debug issues I
was facing while configuring neovim. Using the variable made it easier to write a proper notification. So now, when I open a Java project, the confirmation about properly found <code>lombok.jar</code> properly appears at the top of the messages.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-lua" data-lang="lua"><span style="display:flex;"><span><span style="color:#66d9ef">local</span> notify <span style="color:#f92672">=</span> vim.notify
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">local</span> ok, fidget <span style="color:#f92672">=</span> pcall(require, <span style="color:#e6db74">&#34;fidget&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> ok <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>  notify <span style="color:#f92672">=</span> fidget.notify
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- Verify lombok exists</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> vim.fn.filereadable(lombok_path) <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>  notify(<span style="color:#e6db74">&#34;Lombok jar not found at: &#34;</span> <span style="color:#f92672">..</span> lombok_path, vim.log.levels.ERROR)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>  notify(<span style="color:#e6db74">&#34;Lombok jar found at: &#34;</span> <span style="color:#f92672">..</span> lombok_path, vim.log.levels.INFO)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span></span></span></code></pre></div>
<h1 id="acknowledgments">Acknowledgments</h1>
<p>The following images has been hurt during the logo creation process. And ChatGPT
has been used to sketch the original idea.</p>
<ul>
<li><a href="https://cdn.jsdelivr.net/gh/neovim/neovim@f05a289/cmake.packaging/neovim.svg">neovim.svg</a></li>
<li><a href="https://www.vectorlogo.zone/logos/java/java-icon.svg">java-icon.svg</a></li>
<li><a href="https://avatars.githubusercontent.com/u/45949248?s=200&amp;v=4">project-lombook-avatar</a></li>
</ul>
]]></content:encoded></item><item><title>Introducing Bytekit.app: Privacy Oriented Web App for developers</title><link>https://vyskocil.me/blog/introducing-bytekit/</link><pubDate>Tue, 13 Jan 2026 17:31:39 +0100</pubDate><guid>https://vyskocil.me/blog/introducing-bytekit/</guid><description>I recently deployed my tiny Web application bytekit.app. This is tool for developers doing stuff like JWT, base64, hashes and so on. I wanted something easy to use, privacy-oriented, and fast.&amp;#xA;Bytekit is written in Go. That might sound an odd choice for a browser app, but Go …</description><content:encoded><![CDATA[<p>I recently deployed my tiny Web application <a href="https://bytekit.app">bytekit.app</a>.
This is tool for developers doing stuff like JWT, base64, hashes and so on. I
wanted something easy to use, privacy-oriented, and fast.</p>
<p>Bytekit is written in <a href="https://go.dev">Go</a>. That might sound an odd choice for a browser
app, but Go itself targets WebAssembly. Using
<a href="https://go.dev/wiki/WebAssembly">GOARCH=wasm</a> together with
<a href="https://go-app.dev/">go-app</a> let me run Go in the browser.</p>
<h1 id="the-motivation">The motivation</h1>
<p>I mostly use the command line, but I still rely on online tools from time to time. Many are available, yet most have annoying issues:</p>
<ul>
<li>no privacy - &ldquo;we and 635 trustworthy partners swear we won&rsquo;t share your data&rdquo;</li>
<li>dubious security - is data processing done client-side or server-side? Are
results shared with third parties?</li>
<li>no consistency - each tool uses a different UI, and some need extra clicks for simple actions</li>
<li>no source code</li>
</ul>
<p>My main motivation was a better JWT decoder. I personally find the UI of
<a href="https://jwt.io">https://jwt.iot</a> as confusing with regards to verifying of
signatures. So I wanted to have a cleaner and simpler interface. As a GNOME
user, I like the Adwaita and GNOME HIG in general. So the goal was to have
something similar to <a href="https://flathub.org/en/apps/me.iepure.devtoolbox">Dev
Toolbox</a> only on web.</p>
<h2 id="1-security-first">1. Security first</h2>
<p>Developer tools often handle sensitive data like auth tokens and API keys. Sending
that data through a web app can be risky. Bytekit runs entirely in the browser</p>
<ul>
<li>nothing is ever sent outside your computer. You can verify this easily with your
browser&rsquo;s developer tools.</li>
</ul>
<h2 id="2-privacy">2. Privacy</h2>
<p>The Privacy policy for <a href="https://bytekit.app">https://bytekit.app</a> is simple. I
run the service alone - there are no partners, no trackers, no ads and no
analytics. The HTTP requests are sent to <code>journald</code> and are placed to 100MB big
in-memory ring buffer together with all other services. I inspect them manually
from time to time for a debugging purposes.</p>
<p>The site links to its source code hosted on <a href="https://codeberg.org/vyskocilm/bytekit/">Codeberg</a> and the it runs from <a href="https://quay.io/repository/vyskocilm/bytekit?tab=tags&amp;tag=latest">quay.io/vyskocilm/bytekit</a> container image. So self-hosting is always an option.</p>
<h2 id="3-open-source--free-software">3. Open source / Free software</h2>
<p>I chose AGPLv3 for the main app because I want to encourage people to
self-host, reuse, or improve the project. At the same time, the license asks
that any changes to be shared back with the community.</p>
<h2 id="4-easy-to-use">4. Easy to use</h2>
<p>I avoided forcing clicks for simple tasks. The app runs locally in your browser
and processes data as you type. The UI is meant to be clear and minimal — no
unnecessary bells and whistles. One example is the hash/checksum page. Instead of making you choose an algorithm, it computes them all at once.</p>
<h1 id="using-go-on-a-frontend">Using Go on a frontend?</h1>
<p>Let&rsquo;s start with an elephant in the room first. Which is the artifacts size.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>$ ls -lh public/web/*.wasm*
</span></span><span style="display:flex;"><span>-rw-r--r--. <span style="color:#ae81ff">1</span> michal michal 9.1M Jan  <span style="color:#ae81ff">7</span> 16:14 public/web/app.wasm
</span></span><span style="display:flex;"><span>-rw-r--r--. <span style="color:#ae81ff">1</span> michal michal 2.4M Jan  <span style="color:#ae81ff">7</span> 16:14 public/web/app.wasm.gz
</span></span><span style="display:flex;"><span>-rw-r--r--. <span style="color:#ae81ff">1</span> michal michal 2.1M Jan  <span style="color:#ae81ff">7</span> 16:14 public/web/app.wasm.zst</span></span></code></pre></div>
<p>For a perspective, a main Javascript file from YouTube says 1,835kB (resource
size 9,446 kB). So given the fact this comes with full Go runtime, I consider
this as pretty small and acceptable. The wasm binary and other assets are
compressed to save bandwidth. Clients request the compressed version via
Content Negotiation (<code>Accept-Encoding</code>). Browsers do this by default, so users
get the smaller transfer automatically.</p>
<p>The server actually refuses to serve an uncompressed wasm file.</p>
<p>The application itself is just a directory of a static assets inside a
<code>public/</code> folder. I also included optional Go <code>net/http</code> server to serve those
files. That server has a few advantages over serving static files:</p>
<ul>
<li>it is compatible with OCI containers and can be placed behind a reverse proxy</li>
<li>use of <code>embed</code> means the app is a single executable which works from memory
<ul>
<li>it doesn&rsquo;t need external services, neither a filesystem and can be deployed anywhere</li>
</ul>
</li>
<li>it handles content negotiation for compressed assets and refuses to serve a plain wasm</li>
<li>it makes app cache friendly by handling Etags, <code>If-None-Match</code> and <code>Vary</code> HTTP headers</li>
<li>it ensures a correct <code>Content-Type</code> for all requests</li>
<li>it has a rewrite rules included - so paths never ends with <code>.html</code></li>
<li>it takes a care about <code>Content-Security-Policy</code> headers</li>
</ul>
<h1 id="how-the-frontend-go-look-like">How the frontend Go look like?</h1>
<p>This is not a tutorial, only a short showcase of a frontent Go. The example is
a base64 encoding/decoding page. All UI components are plain Go structs which
must embed the framework&rsquo;s <code>app.Compo</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">base64</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> (
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">bk</span> <span style="color:#e6db74">&#34;codeberg.org/vyskocilm/bytekit/internal/kit&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;github.com/maxence-charriere/go-app/v10/pkg/app&#34;</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Content</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">app</span>.<span style="color:#a6e22e">Compo</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">showError</span> <span style="color:#a6e22e">bk</span>.<span style="color:#a6e22e">ToastFunc</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">encoded</span>   <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">decoded</span>   <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">encoding</span>  <span style="color:#f92672">*</span><span style="color:#a6e22e">base64</span>.<span style="color:#a6e22e">Encoding</span>
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>To render a content as HTML one must implement the <code>Render</code> method from
<a href="https://pkg.go.dev/github.com/maxence-charriere/go-app@v1.0.1/pkg/app#Composer">app.Composer</a>.
Those components are HTML components defined by <code>go-app</code> or those can be other
structs implementing <code>app.Composer</code> interface. The app defines a few reusable
UI components this way, like and <code>InputBox</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Content</span>) <span style="color:#a6e22e">Render</span>() <span style="color:#a6e22e">app</span>.<span style="color:#a6e22e">UI</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">app</span>.<span style="color:#a6e22e">Div</span>().<span style="color:#a6e22e">Body</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">title</span>{
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">showError</span>:  <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">showError</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">onSelected</span>: <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">onEncodingChanged</span>,
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">bk</span>.<span style="color:#a6e22e">InputBox</span>().
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">SetText</span>(<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">encoded</span>).
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">ShowError</span>(<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">showError</span>).
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">Label</span>(<span style="color:#e6db74">&#34;Base64 encoded content&#34;</span>).
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">OnChangedText</span>(<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">onEncodedChange</span>),
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">bk</span>.<span style="color:#a6e22e">InputBox</span>().
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">SetText</span>(<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">decoded</span>).
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">ShowError</span>(<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">showError</span>).
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">Label</span>(<span style="color:#e6db74">&#34;Decoded text&#34;</span>).
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">OnChangedText</span>(<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">onDecodedChange</span>),
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>The interactivity is done via callbacks - the framework calls a defined
callback a particular event.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Content</span>) <span style="color:#a6e22e">onDecodedChange</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">app</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">decoded</span> <span style="color:#66d9ef">string</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Async</span>((<span style="color:#66d9ef">func</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">encoded</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">encoding</span>.<span style="color:#a6e22e">EncodeToString</span>([]byte(<span style="color:#a6e22e">decoded</span>))
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Dispatch</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">app</span>.<span style="color:#a6e22e">Context</span>) {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">encoded</span> = <span style="color:#a6e22e">encoded</span>
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">decoded</span> = <span style="color:#a6e22e">decoded</span>
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>  }))
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>In the browser, JavaScript runs on a single main thread. Heavy computations
block that thread and freeze the UI because no other JavaScript can run at the
same time. There is a concept of a
<a href="https://developer.mozilla.org/en-US/docs/WebAssembly/API/Worker">Worker</a> which
lets you run code off the main thread.</p>
<p>In <a href="https://go-app.dev/concurrency">Concurrency</a> model of <code>go-app</code> all things runs in a single UI goroutine which updates the UI and a state of the components. <code>Async</code> then runs the code inside another goroutine, which do not block the main UI. When code is finished running, the <code>Dispatch</code> call passes results safely back to the UI goroutine.</p>
<h2 id="javascript-interop">Javascript interop</h2>
<p>No frontend code can live without an ability to call Javascript. This is the only way for accessing browser APIs. The <code>app</code> itself exposes some of the DOM API.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span>  <span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Async</span>(<span style="color:#66d9ef">func</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">app</span>.<span style="color:#a6e22e">Window</span>().<span style="color:#a6e22e">Get</span>(<span style="color:#e6db74">&#34;navigator&#34;</span>).
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">Get</span>(<span style="color:#e6db74">&#34;clipboard&#34;</span>).
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">Call</span>(<span style="color:#e6db74">&#34;writeText&#34;</span>, <span style="color:#a6e22e">text</span>)
</span></span><span style="display:flex;"><span>  })</span></span></code></pre></div>
<p>An access to elements triggering an event is possible too.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span>  <span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Async</span>(<span style="color:#66d9ef">func</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">title</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">JSSrc</span>().<span style="color:#a6e22e">Get</span>(<span style="color:#e6db74">&#34;innerText&#34;</span>).<span style="color:#a6e22e">String</span>()
</span></span><span style="display:flex;"><span>  })</span></span></code></pre></div>
<p>As well as a support for
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promises</a>,
which is the only way of reading from a clipboard.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">clipboard</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">app</span>.<span style="color:#a6e22e">Window</span>().<span style="color:#a6e22e">Get</span>(<span style="color:#e6db74">&#34;navigator&#34;</span>).
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">Get</span>(<span style="color:#e6db74">&#34;clipboard&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">promise</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">clipboard</span>.<span style="color:#a6e22e">Call</span>(<span style="color:#e6db74">&#34;readText&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">thenFunc</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">app</span>.<span style="color:#a6e22e">FuncOf</span>(<span style="color:#f92672">...</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">catchFunc</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">app</span>.<span style="color:#a6e22e">FuncOf</span>(<span style="color:#f92672">...</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">promise</span>.<span style="color:#a6e22e">Call</span>(<span style="color:#e6db74">&#34;then&#34;</span>, <span style="color:#a6e22e">thenFunc</span>).<span style="color:#a6e22e">Call</span>(<span style="color:#e6db74">&#34;catch&#34;</span>, <span style="color:#a6e22e">catchFunc</span>)</span></span></code></pre></div>
<p>The frontend itself is generated by <a href="https://go-app.dev">https://go-app.dev</a> via</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>$ GOARCH<span style="color:#f92672">=</span>wasm GOOS<span style="color:#f92672">=</span>js go build -o <span style="color:#e6db74">&#34;public/web/app.wasm&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>codeberg.org/vyskocilm/bytekit/cmd/bytekit</span></span></code></pre></div>
<p>This populates the content of a <code>public</code> folder where all web assets are placed. The <code>go-app</code> framework is the one doing all the magic behind the scenes.</p>
<h1 id="so-hows-dev-experience-without-npm">So how&rsquo;s dev experience without npm?</h1>
<p>The <code>npm</code> (<code>pnpm</code>, <code>yarn</code>, &hellip;) is pervasive in a FE development world. Not
using the standard frontend toolbox means you often ends up solving problems
yourself. That said, using npm for parts of the workflow can be reasonable. For
example, bytekit uses <code>purgecss</code> to minify bulma.css by removing unused
declarations, making assets as lean as possible.</p>
<p>On the other hand sometimes writing a part of a build chain is easy and very
convenient. As an example code compressing assets with gzip and zstd turned out
to be about 100 lines of straightforward, idiomatic Go code. This is a tradeoff
worth making. Especially the build system used,
<a href="https://magefile.org/">https://magefile.org/</a>, is Go code itself.</p>
<p>So there are gophers all the way down.</p>
<p>Having complete codebase in a single language including the build system makes
the developer experience very pleasant. There is a little magic and build logic
is only a code inside <code>internal/mage</code> and can be inspected, changed or tested
exactly like the rest of the other code. The top level <code>mage.go</code> merely defines
a build targets.</p>
<p>And as go supports <code>go tool</code> the build system is a dev dependency listed in <code>go.mod</code>.</p>
<pre tabindex="0"><code class="language-gomod" data-lang="gomod">tool github.com/magefile/mage</code></pre>
<p>And a CI is a buch of calls of a form <code>go tool mage $target</code></p>
<ul>
<li> smaller ecosystem to pull from</li>
<li> coherent codebase - everything is written in one language</li>
<li> speed - once is built, it runs faster than npm tooling</li>
</ul>
<h2 id="closing-thoughts">Closing thoughts</h2>
<p>Bytekit started as a small experiment in running Go on the web. It stays small and focused on a few useful tools. It’s privacy-first, open source, and easy to self‑host if you prefer.</p>
<p>Try it at [https://bytekit.app]. The source is on Codeberg and the container image is on <code>quay.io</code>, so running your own copy is straightforward. Issues, ideas, and pull requests are welcome.</p>
<p>If you like the approach or find a rough edge, tell me - especially about the UI and privacy parts. Thanks for reading and for trying Bytekit.</p>
<p>Logo is remixed [https://github.com/egonelbre/gophers/blob/master/vector/science/power-to-the-linux.svg].</p>
]]></content:encoded></item><item><title>UnmarshalText in Go</title><link>https://vyskocil.me/blog/unmarshal-text-in-go/</link><pubDate>Wed, 12 Nov 2025 10:10:45 +0100</pubDate><guid>https://vyskocil.me/blog/unmarshal-text-in-go/</guid><description>I must admit that I tend to overlook the interfaces in the encoding package. Since I typically work with JSON or Yaml, so I tend to care more about methods like UnmarshalJSON. However, today I learned the trick of using TextUnmarshaler when I needed to use a custom type inside …</description><content:encoded><![CDATA[<p>I must admit that I tend to overlook the interfaces in the
<a href="https://pkg.go.dev/encoding#TextUnmarshaler">encoding</a> package. Since I typically
work with JSON or Yaml, so I tend to care more about methods like
<code>UnmarshalJSON</code>. However, today I learned the trick of using <code>TextUnmarshaler</code>
when I needed to use a custom type inside the struct.</p>
<h1 id="problem">Problem</h1>
<p>Consider this code</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> (
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;encoding/json&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;fmt&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;net/url&#34;</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Foo</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">BaseURL</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">url</span>.<span style="color:#a6e22e">URL</span> <span style="color:#e6db74">`json:&#34;base_url&#34;`</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">src</span> = <span style="color:#e6db74">`{&#34;base_url&#34;: &#34;https://example.com&#34;}`</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">aFoo</span> <span style="color:#a6e22e">Foo</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">Unmarshal</span>([]byte(<span style="color:#a6e22e">src</span>), <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">aFoo</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>    panic(<span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;%#+v\n&#34;</span>, <span style="color:#a6e22e">aFoo</span>.<span style="color:#a6e22e">BaseURL</span>.<span style="color:#a6e22e">String</span>())
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>The standard <code>*url.URL</code> can&rsquo;t do that. There&rsquo;s
<a href="https://pkg.go.dev/net/url#URL.UnmarshalBinary">UnmarshalBinary</a>, but no other
method available.</p>
<pre><code>panic: json: cannot unmarshal string into Go struct field Foo.base_url of type url.URL
</code></pre>
<h1 id="unmarshaltext">UnmarshalText</h1>
<p><a href="https://pkg.go.dev/encoding/json#Unmarshal">json.Unmarshal</a> documents how it works</p>
<blockquote>
<p>To unmarshal JSON into a value implementing [Unmarshaler],
Unmarshal calls that value&rsquo;s [Unmarshaler.UnmarshalJSON] method, including
when the input is a JSON null.
Otherwise, if the value implements [encoding.TextUnmarshaler]
and the input is a JSON quoted string, Unmarshal calls
[encoding.TextUnmarshaler.UnmarshalText] with the unquoted form of the string.</p>
</blockquote>
<p>The only thing needed to be implemented is <code>TextUnmarshaler</code> interface. Because URL is
a string in JSON.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">URL</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">*</span><span style="color:#a6e22e">url</span>.<span style="color:#a6e22e">URL</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">u</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">URL</span>) <span style="color:#a6e22e">UnmarshalText</span>(<span style="color:#a6e22e">text</span> []<span style="color:#66d9ef">byte</span>) <span style="color:#66d9ef">error</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">parsed</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">url</span>.<span style="color:#a6e22e">Parse</span>(string(<span style="color:#a6e22e">text</span>))
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">u</span>.<span style="color:#a6e22e">URL</span> = <span style="color:#a6e22e">parsed</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>And the output is <code>&quot;https://example.com&quot;</code>.</p>
<h1 id="marshal-it-back">Marshal it back</h1>
<p>When marshaled back to JSON, the URL looks like this: No one wants to write
URLs like this.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;u&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;Scheme&#34;</span>: <span style="color:#e6db74">&#34;https&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;Opaque&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;User&#34;</span>: <span style="color:#66d9ef">null</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;Host&#34;</span>: <span style="color:#e6db74">&#34;example.com&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;Path&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;RawPath&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;OmitHost&#34;</span>: <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;ForceQuery&#34;</span>: <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;RawQuery&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;Fragment&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;RawFragment&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>Obviously there is an interface for that</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">u</span> <span style="color:#a6e22e">URL</span>) <span style="color:#a6e22e">MarshalText</span>() ([]<span style="color:#66d9ef">byte</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">u</span>.<span style="color:#a6e22e">URL</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> []<span style="color:#66d9ef">byte</span>{}, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> []byte(<span style="color:#a6e22e">u</span>.<span style="color:#a6e22e">URL</span>.<span style="color:#a6e22e">String</span>()), <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{<span style="color:#f92672">&#34;base_url&#34;</span>:<span style="color:#e6db74">&#34;https://example.com&#34;</span>}</span></span></code></pre></div>
<h1 id="conclusion">Conclusion</h1>
<p>While the text encoding methods are somewhat less popular than their
<code>encoding/json</code> counterparts, they can be still useful.</p>
<p>See complete example <a href="https://go.dev/play/p/BKX9j-OOprk">on Go playground</a>.</p>
<p>Logo is remixed <a href="https://github.com/egonelbre/gophers/blob/master/vector/superhero/standing.svg">github.com/egonelbre/gophers</a> and is under <a href="https://creativecommons.org/publicdomain/zero/1.0/">CC0 license</a> like an original.</p>
]]></content:encoded></item><item><title>Ci Setup Which Never Worked</title><link>https://vyskocil.me/blog/ci-setup-which-never-worked/</link><pubDate>Thu, 23 Oct 2025 19:20:50 +0200</pubDate><guid>https://vyskocil.me/blog/ci-setup-which-never-worked/</guid><description>In an earlier post about giving up Kubernetes, I wrote that my server is a plain old virtual machine pet. While this works pretty well for a static web server use case, I wanted to explore ways how to automate this blog.&amp;#xA;This is a story about how I failed. I am writing this …</description><content:encoded><![CDATA[<p>In an <a href="/blog/back-to-vm/">earlier post about giving up Kubernetes</a>, I
wrote that my server is a plain old virtual machine pet. While this works
pretty well for a static web server use case, I wanted to explore ways
how to automate this blog.</p>
<p>This is a story about how I failed. I am writing this because I did my best to
make it work, but it turned out its impossible</p>
<p>At the same time it has a some good hints about user systemd, quadlets, rootless podman and what I managed to get working.</p>
<h1 id="the-problem">The problem</h1>
<p>When it comes to your own stuff, overengineering is always the preferred
option, isn&rsquo;t it? My problem was fairly simple. After finishing a Markdown
document in <a href="https://neovim.io/">neovim</a>, I have to run
<a href="https://gohugo.io/">hugo</a>, install it, or find a
(distrobox)[https://distrobox.it/] container with it installed. Then, I need to
push the source and built content to the
<a href="https://codeberg.org/vyskocilm/miblog/">Codeberg</a> repository. After that, I
need to log into the server, switch to the user, and run <code>git pull</code> to finally
<em>publish</em> the changes.</p>
<p>Except for the <code>git</code>, this is a pretty 90s approach. And as a popular saying goes</p>
<blockquote>
<p>Developer will rather spent 3 days writing script* rather than doing the
same 3 minutes of manual work twice.</p>
</blockquote>
<p>Which is again pretty 90s, isn&rsquo;t it? Nowadays developers would spent 3 months
deploying a multi-cloud multi-region k8s cluster with Terraform, Helm, ArgoCD
and dozens of microservices than writing a script.</p>
<h1 id="the-idea">The idea</h1>
<p>The solution exists and is called <a href="https://en.wikipedia.org/wiki/CI/CD">CI/CD</a>.
Because Codeberg itself is a public instance of a software called
<a href="https://forgejo.org/">Forgejo</a>, the most natural way to do it is with
<a href="https://forgejo.org/docs/latest/user/actions/reference/">actions</a> powered by
<a href="https://forgejo.org/docs/latest/admin/actions/#forgejo-runner">forgejo-runner</a>.
There are
<a href="https://jan.wildeboer.net/2024/08/Running-a-runner-codeberg/">others</a> who
successfully did that.</p>
<blockquote>
<p>Here I wanted to mention that both forgejo-runner and codeberg are wonderful
tools, which has nothing to do with why I failed.</p>
</blockquote>
<p>So the idea is to build roughly this.</p>
<ol>
<li>On every push, Hugo Build translates the content.</li>
<li>Things will be packaged into an OCI container and pushed to an internal repository.</li>
<li>The blog itself will be an OCI container running behind Caddy.</li>
<li>The whole server will be behind the front-end Caddy, which will be in charge of logging, TLS, and certificate management.</li>
</ol>
<p>It&rsquo;s pretty standard, isn&rsquo;t it? Given the state of the industry in 2025, it
doesn&rsquo;t sound too complicated.</p>
<h1 id="the-container-curse">The container curse</h1>
<p>I must say that I like containers. When used properly, they&rsquo;re amazing. For
example the <a href="https://github.com/openSUSE/cnf">openSUSE cnf</a>. This is a Rust
tool built inside
<a href="https://github.com/openSUSE/cnf/blob/main/.github/workflows/pr_check.yml#L13">ubuntu-latest</a>
image. In order to test that it works on openSUSE itself, the resulting binary
is <a href="https://github.com/openSUSE/cnf/blob/main/test/root.sh#L23">mounted</a> to a
tested openSUSE Tumbleweed container. This allows for <a href="https://github.com/openSUSE/cnf/actions/runs/17488932710/usage">a
fast</a>
integration test using a real system as well as testing of all supported
scenarios like <code>zypper</code>, <code>dnf5</code> or <code>dnf</code>.</p>
<p>What I don&rsquo;t like is Docker, specifically <code>dockerd</code>, and the fact that it runs
as <code>root</code>. While I understand why it is like that. And after this saga I
understand it more than ever. Its simplicity is what made it so popular in the
first place. Docker like <code>dockerd</code> is a great solution for developers. Not for
my server. This brings me to the real <em>curse</em> of containers: <em>rootless</em> ones.</p>
<p>Specifically, I dislike <code>dockerd</code> because it runs as root. I understand why it
is like that, though. After this saga, I understand it more than ever. Its
simplicity is what made it popular in the first place. Docker, the all might
<code>dockerd</code>, is a great solution for developers. But not for my server. This
brings me to the real curse of containers: rootless ones.</p>
<p>It&rsquo;s 2025, and I must say, you can really go far with them. Most of their
limitations are in the form of <code>/etc/subuid</code> allocations, and sometimes one
encounters a strange permission error. However, it&rsquo;s much better than it was in
2020 when I was experimenting with them.</p>
<h1 id="how-to-setup-a-rootless-container">How to setup a rootless container</h1>
<h2 id="1-create-dedicated-user">1. create dedicated user</h2>
<p>Create a new dedicated system user; <code>systemd-sysuser</code> is probably the best
option nowadays.</p>
<pre><code class="language-sh">&gt; cat /etc/sysusers.d/forgejo.conf
#Type Name    ID   GECOS               Home directory  Shell
u     actions -    &quot;forgejo actions&quot;   /home/actions   /sbin/nologin
r     -       1000-1999
m     actions systemd-journal
</code></pre>
<p>For rootless Podman containers, it is best to start an user instance via
systemd so that the containers will be managed by it like a regular services.
Note that <code>systemd-sysuser</code> does not create a home directory.</p>
<pre><code class="language-sh"># Create new user
&gt; systemd-sysusers /etc/sysusers.d/forgejo.conf
# Run systemd's user instance for a newly user
&gt; systemctl start user@1999.service
</code></pre>
<p><a href="https://www.redhat.com/en/blog/quadlet-podman">Quadlet</a> is a Red Hat technology
that integrates Podman with a systemd. By writing a <code>.container</code> file
that describes all of its properties, quadlet generates an
appropriate systemd unit.</p>
<p>This means that forgejo runner <em>itself</em> runs as a container. To do its job, it
needs a &ldquo;docker&rdquo; socket. In this case it is podman socket of a <code>actions</code> user
mounted to the proper place. This allows runner to pull and execute other
containers without having a chance to obtain root privileges.</p>
<p>This setup mounts <code>/home/actions/runner</code> into the runner container, ensuring cache is
saved between runs.</p>
<pre><code class="language-ini">[Unit]
Description=Forgejo runner

[Container]
Image=code.forgejo.org/forgejo/runner:11
ContainerName=forgejo-runner
User=0
Group=0
Environment=DOCKER_HOST=unix:///var/run/docker.sock
Exec=forgejo-runner --config /data/config/config.yml daemon
Volume=%h/runner:/data:Z
Volume=/run/user/%U/podman/podman.sock:/var/run/docker.sock:rw
Volume=/etc/localtime:/etc/localtime:ro
AutoUpdate=registry

[Service]
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=default.target
</code></pre>
<pre><code class="language-sh"># Check actions user services
&gt; systemctl --machine actions@.host --user status forgejo-runner
● forgejo-runner.service - Forgejo runner
     Loaded: loaded (/home/actions/.config/containers/systemd/forgejo-runner.container; generated)
     Active: active (running) since Thu 2025-10-23 18:37:46 CEST; 1h 35min ago
</code></pre>
<h2 id="selinux-intermezzo">SElinux intermezzo</h2>
<p>In the grand scheme of things, this was really a minor annoyance. I got a
&ldquo;permission denied&rdquo; on <code>/var/run/docker.sock</code> inside the container. The problem
was that openSUSE Leap default policy don&rsquo;t allow for <code>container_t</code> to use
<code>connectto</code>.</p>
<pre><code class="language-sh">$ ausearch -m AVC -ts recent

type=AVC msg=audit(1761167553.886:1210): avc:  denied  { connectto } for
pid=34382 comm=&quot;forgejo-runner&quot; path=&quot;/run/user/1999/podman/podman.sock&quot;
scontext=system_u:system_r:container_t:s0:c167,c401
tcontext=unconfined_u:unconfined_r:container_runtime_t:s0-s0:c0.c1023
tclass=unix_stream_socket permissive=0
</code></pre>
<pre><code class="language-sh">&gt; ausearch -m AVC -ts recent | audit2allow -M forgejorunner
&gt; semodule -i forgejorunner.pp
</code></pre>
<pre><code>module forgejorunner 1.0;

require {
        type container_t;
        type container_runtime_t;
        class unix_stream_socket connectto;
}

#============= container_t ==============
allow container_t container_runtime_t:unix_stream_socket connectto;
</code></pre>
<h2 id="running-own-registry">Running own registry</h2>
<p>So far I was making a good progress. The runner was registered against Codeberg
and was running a <code>printf</code> just fine. The second problem was decifing to store
the containers with blog. I did not want to store them in any external
registry. They are private artifact of the server itself, so why expose them?</p>
<p>As I got confident with systemd, quadlets and container, I deployed another
container file under <code>registry</code> user.</p>
<pre><code class="language-ini">[Unit]
Description=Zot containers registry

[Container]
Image=ghcr.io/project-zot/zot-linux-amd64:latest
ContainerName=zotregistry
User=0
Group=0
Exec=serve /data/config/config.json
Volume=%h/registry:/data:Z
Volume=/etc/localtime:/etc/localtime:ro
PublishPort=127.0.0.1:2999:2999
AutoUpdate=registry

[Service]
Restart=on-failure
RestartSec=30s

[Install]
WantedBy=default.target
</code></pre>
<p>Long story short. It is a good idea that does not work. In a hindsight, it
makes a sense. You can&rsquo;t connect two rootless Podmans running under a different
user accounts. Not without doing their networking equal to <code>host</code>. Which kinda
makes no sense. Why go through all the troubles isolating things and then let
everything bind everywhere?</p>
<p>One simple alternative would be to expose the registry under a public name, such as
<code>registry.vyskocil.me</code>. The container was exposed to the (localhost)host. However, I
never wanted to deal with yet another public service. And the intent was always to keep the registry internal to the VM.</p>
<p>The easiest solution was to
move <code>registry</code> under <code>actions</code> user and to define a Podman network.</p>
<pre><code class="language-sh">&gt; cat /home/actions/.config/containers/systemd/ci-net.network
[Network]
Driver=bridge
</code></pre>
<p>Add <code>Network=ci-net.network</code> to both container files. And a started containers must have an access to the same network. As this is managed by <code>systemd</code> it has a prefix.</p>
<pre><code class="language-yaml">container:
  network: &quot;systemd-ci-net&quot;
</code></pre>
<h1 id="the-dead-end">The dead end</h1>
<p>I must admit that, during this phase, I thought all the difficult problems had been solved. Permissions, SELinux, and networking were all configured and set up. However, item 3 of my plan turned out to be a show stopper.</p>
<p>You cannot build a container. Inside a container. Period! Altough there seems to
be workarounds like <code>gcr.io/kaniko-project/executor:latest</code> they always require
elevated privileges like access to <code>/dev/fuse</code> for overlayfs and possibly the
<code>--priviledged</code> flag to gain more capabilities. I was not
comfortable doing this. The purpose of using separate user accounts
and a rootless Podman containers was to make the system more secure and less susceptible to breaches.</p>
<h1 id="sync-like-its-1996">Sync like its 1996</h1>
<p>So, how did I solve the problem? <code>rsync</code>! Since runner runs on my own host, I can mount the host&rsquo;s file system write the results there.</p>
<pre><code class="language-yaml">container:
  image: docker.io/library/node:22-bookworm
  options: &gt;-
    -v /home/actions/runner/public:/public:Z

  steps:
  - name: Trigger a sync
    run: touch /public/.ready-for-sync
</code></pre>
<h1 id="or-rather-2010">Or rather 2010?</h1>
<p>Then how are we going to sync files? <code>systemd</code> comes to the rescue. The easiect way is to have unit watching <code>.ready-for-sync</code> and run the <code>rsync</code> script. It runs as a root as it needs to adapt the privileges once more. However <code>systemd</code> itself is great because t provides a lot of tunables that limit the permissions to an absolute minimum.</p>
<pre><code class="language-ini">[Service]
# Hardening
ProtectSystem=strict
ProtectHome=read-only
BindReadOnlyPaths=/home/actions/runner/public
ReadWritePaths=/run/lock/ /srv/www...
InaccessiblePaths=/root
PrivateTmp=yes
PrivateMounts=yes
NoNewPrivileges=yes
ProtectControlGroups=yes
...
</code></pre>
<h1 id="conclusion">Conclusion</h1>
<p>Altough I didn&rsquo;t reach my original goal, I can still call this journey as a
success. At least I can uninstall <code>git</code> from VM and <code>hugo</code> from laptop and can
enjoy at least a partially modern, fully automated setup.</p>
<p>Codeberg/Forgejo and Forgejo-runner are amazing pieces of a software. I was
surprised that I could deploy and run the software successfully, even with rather
unusual setup.</p>
<p>And rootless pPdman containers in 2025? They are super cool. The
integration to <code>systemd</code> via quadlets is amazing. I feel good knowing they are limited by real Unix privileges and not by namespace/container magic.</p>
<p>However as you give up all the privileges, you can&rsquo;t build a new OCI container.</p>
<h1 id="last-remark">Last remark</h1>
<p>This is dedicated to all the script kiddies bombing my <code>sshd</code> with <code>ubuntu</code>, <code>admin</code>,
<code>wordpress</code> and other non sense accounts. Good luck with <code>actions</code>!</p>
<pre><code class="language-sh">&gt; getent passwd actions
actions:x:1999:1999:forgejo actions:/home/actions:/sbin/nologin
</code></pre>
]]></content:encoded></item><item><title>Oneoflint: Exhaustive Lint for Proto Files</title><link>https://vyskocil.me/blog/oneoflint-exhaustive-lint-for-proto-files/</link><pubDate>Sat, 18 Oct 2025 12:17:56 +0200</pubDate><guid>https://vyskocil.me/blog/oneoflint-exhaustive-lint-for-proto-files/</guid><description>In an earlier post about sumlint I wrote about a linter that helps with exhaustive checks of type switches for interfaces with a particular naming pattern.&amp;#xA;After some time, I realized that this is essentially how protobuf&amp;amp;rsquo;s oneof is implemented by protoc and protoc-gen-go. …</description><content:encoded><![CDATA[<p>In an <a href="/blog/sumlint-having-sum-types-in-go/">earlier post about sumlint</a> I wrote about a linter that helps
with exhaustive checks of type switches for interfaces with a particular naming pattern.</p>
<p>After some time, I realized that this is essentially how <a href="https://protobuf.dev/programming-guides/proto3/#oneof">protobuf&rsquo;s
oneof</a> is implemented by
<code>protoc</code> and <code>protoc-gen-go</code>. Surprisingly, the naming convention it uses is
<em>almost</em> identical to mine. This protobuf definition</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-proto" data-lang="proto"><span style="display:flex;"><span><span style="color:#66d9ef">message</span> <span style="color:#a6e22e">Msg</span> {<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#66d9ef">oneof</span> payload {<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>    A a <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>    B b <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  }<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>}</span></span></code></pre></div>
<p>is implemented using nearly the same naming convention.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">isMsg_Payload</span> <span style="color:#66d9ef">interface</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">isMsg_Payload</span>()
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<h2 id="oneoflint">oneoflint</h2>
<p>Welcome <a href="https://gihub.com/gomoni/sumlint/cmd/oneoflint">oneoflint</a>. This
linter matches protobuf&rsquo;s <code>oneof</code> pattern and provides an exhaustive checks for code implementing the protobuf.</p>
<p>Because the naming convention is almost the same, the two linters
shares the same code (and bugs). They are exposed as two binaries, so everyone can be opinionated and there is not configuration. It is exactly how a proper Go tooling should be.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">Sum</span> = <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">analysis</span>.<span style="color:#a6e22e">Analyzer</span>{
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Name</span>: <span style="color:#e6db74">&#34;sumlint&#34;</span>,
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Run</span>:  <span style="color:#a6e22e">analyzer</span>{<span style="color:#a6e22e">prefix</span>: <span style="color:#e6db74">&#34;Sum&#34;</span>}.<span style="color:#a6e22e">run</span>,
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">Oneof</span> = <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">analysis</span>.<span style="color:#a6e22e">Analyzer</span>{
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Name</span>: <span style="color:#e6db74">&#34;oneoflint&#34;</span>,
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Run</span>:  <span style="color:#a6e22e">analyzer</span>{<span style="color:#a6e22e">prefix</span>: <span style="color:#e6db74">&#34;is&#34;</span>}.<span style="color:#a6e22e">run</span>,</span></span></code></pre></div>
<h2 id="be-honest">Be honest</h2>
<blockquote>
<p>There is no configuration at all.
Be honest!
I am being honest!
So what about CLI arguments?
Okay, there is a verbose flag.</p>
</blockquote>
<p>When developing a linter, there&rsquo;s nothing worse than starting with an empty output when it was supposed to work. The worst part is that, due to the clever caching of the Go toolchain, you may see your tool doing something, but then nothing at the subsequent step. The cache simply assumes that linters are bug-free, but that&rsquo;s not the case during development. After some back-and-forth with Copilot and the package documentation, I figured out how to properly add a flag.</p>
<p>This defines the <code>flag.FlagSet</code>. Each one has a proper name so that each linter can have its own configuration.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">init</span>() {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">verboseFlag</span> <span style="color:#66d9ef">bool</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">fs1</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">flag</span>.<span style="color:#a6e22e">NewFlagSet</span>(<span style="color:#a6e22e">Sum</span>.<span style="color:#a6e22e">Name</span>, <span style="color:#a6e22e">flag</span>.<span style="color:#a6e22e">ExitOnError</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">fs1</span>.<span style="color:#a6e22e">BoolVar</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">verboseFlag</span>, <span style="color:#e6db74">&#34;verbose&#34;</span>, <span style="color:#66d9ef">false</span>, <span style="color:#e6db74">&#34;enable verbose output&#34;</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Sum</span>.<span style="color:#a6e22e">Flags</span> = <span style="color:#f92672">*</span><span style="color:#a6e22e">fs1</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">fs2</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">flag</span>.<span style="color:#a6e22e">NewFlagSet</span>(<span style="color:#a6e22e">Oneof</span>.<span style="color:#a6e22e">Name</span>, <span style="color:#a6e22e">flag</span>.<span style="color:#a6e22e">ExitOnError</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">fs2</span>.<span style="color:#a6e22e">BoolVar</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">verboseFlag</span>, <span style="color:#e6db74">&#34;verbose&#34;</span>, <span style="color:#66d9ef">false</span>, <span style="color:#e6db74">&#34;enable verbose output&#34;</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Oneof</span>.<span style="color:#a6e22e">Flags</span> = <span style="color:#f92672">*</span><span style="color:#a6e22e">fs2</span>
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<blockquote>
<p>Detected shared state anomaly: both FlagSet instances bind to identical bool storage (verboseFlag)</p>
</blockquote>
<p>This is a valid comment from our AI overlord. In fact I asked it to output it in less human form for an effect. In this case the value is accessed using <a href="https://pkg.go.dev/flag#FlagSet.Lookup"><code>Lookup</code></a> method and not by reading the variable.</p>
<p>Usage on a command line is simple. The FlagSet name is a prefix of an argument.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>$ go vet -vettool<span style="color:#f92672">=</span>/path/to/sumlint -sumlint.verbose .</span></span></code></pre></div>
<h2 id="unit-tests">Unit tests</h2>
<p>Unit testing the code with <code>golang.org/x/tools/go/analysis/analysisTest</code> proved challenging. To use it, you need to put the Go source code into <code>testdata/src/$package</code> and let the analyzer compare the results with the expected data.</p>
<p>This is great as long as it works. However, if the data does not match, <code>analysistest</code> becomes very difficult to understand.</p>
<p>First, you need to assert the list of exported <a href="https://pkg.go.dev/golang.org/x/tools/go/analysis#Fact">Facts</a>. In the case of <code>oneoflint</code>, this is a list of structs that implement the isMsg_Payload interface.</p>
<p>The second assertion is the error message that is supposed to be printed by the <code>linter</code>. Which gave this test case.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">one_of</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">isMsg_Payload</span> <span style="color:#66d9ef">interface</span> { <span style="color:#75715e">//want isMsg_Payload:`one_of\.Msg_A,one_of\.Msg_B`</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">isMsg_Payload</span>()
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Msg_A</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">A</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">A</span> <span style="color:#e6db74">`protobuf:&#34;bytes,1,opt,name=a,proto3,oneof&#34;`</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Msg_B</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">B</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">B</span> <span style="color:#e6db74">`protobuf:&#34;bytes,2,opt,name=b,proto3,oneof&#34;`</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#f92672">*</span><span style="color:#a6e22e">Msg_A</span>) <span style="color:#a6e22e">isMsg_Payload</span>() {}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#f92672">*</span><span style="color:#a6e22e">Msg_B</span>) <span style="color:#a6e22e">isMsg_Payload</span>() {}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">process</span>(<span style="color:#a6e22e">msg</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Msg</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">msg</span>.<span style="color:#a6e22e">GetPayload</span>().(<span style="color:#66d9ef">type</span>) { <span style="color:#75715e">// want `non-exhaustive type switch on isMsg_Payload: missing cases for: one_of.Msg_B`</span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">case</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Msg_A</span>:
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">default</span>:
</span></span><span style="display:flex;"><span>	}</span></span></code></pre></div>
<h2 id="integration-testing">Integration testing</h2>
<p>To my surprise unit testing was not enough. While <code>sumlint</code> worked like a charm, the <code>oneoflint</code> failed and printed nothing. This is when the <code>verbose</code> flag goes handy. It allows to print just enough state of a linter to be able to diagnose a problem.</p>
<p>Much to my surprise, unit testing was not enough. <code>sumlint</code> worked like a charm, but <code>oneoflint</code> failed and printed nothing. This is when the <code>verbose</code> flag comes in handy. It is supposed to print enough information about the linter to diagnose a problem.</p>
<p>With enough logs, Copilot was able to suggest a <a href="https://github.com/gomoni/sumlint/commit/ab0f702c8b17c513fb8fde00a08854b2ed0da142#diff-c925c91196efe3f3ff9e0a21a2766cd48bab478f47e1cb0affee2dc3286a2f76R305">fix</a>. The issue was related to the fact that the proto interface was not exported, and Copilot suggested adding a new pass for this case.</p>
<p>However, my trust in unit tests was shaken. The first problem I had with this approach was that the analyzer test could not load the external package. The source code for the unit test was generated by <code>protoc</code>, but due to its inability to import non-standard library packages, it had to be manually changed. Of course, not being able to check a real world code is not good.</p>
<p>The <a href="https://github.com/gomoni/sumlint/tree/main/test">github.com/gomoni/sumlint/test</a> package contains realistic test data with a file generated by <code>protoc</code>. The test file then imports real Go code, tests calls to <code>go vet</code> via <code>os/exec</code>. And compares the results.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">test</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;github.com/gomoni/sumlint/test/one_of&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">oneofTest</span> <span style="color:#66d9ef">struct</span>{}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">oneofTest</span>) <span style="color:#a6e22e">good</span>(<span style="color:#a6e22e">msg</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">one_of</span>.<span style="color:#a6e22e">Msg</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">msg</span>.<span style="color:#a6e22e">GetPayload</span>().(<span style="color:#66d9ef">type</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">case</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">one_of</span>.<span style="color:#a6e22e">Msg_A</span>:
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">case</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">one_of</span>.<span style="color:#a6e22e">Msg_B</span>:
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">default</span>:
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">oneofTest</span>) <span style="color:#a6e22e">noDefault</span>(<span style="color:#a6e22e">msg</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">one_of</span>.<span style="color:#a6e22e">Msg</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">msg</span>.<span style="color:#a6e22e">GetPayload</span>().(<span style="color:#66d9ef">type</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">case</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">one_of</span>.<span style="color:#a6e22e">Msg_A</span>:
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">case</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">one_of</span>.<span style="color:#a6e22e">Msg_B</span>:
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">oneofTest</span>) <span style="color:#a6e22e">noB</span>(<span style="color:#a6e22e">msg</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">one_of</span>.<span style="color:#a6e22e">Msg</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">payload</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">msg</span>.<span style="color:#a6e22e">GetPayload</span>()
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">payload</span>.(<span style="color:#66d9ef">type</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">case</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">one_of</span>.<span style="color:#a6e22e">Msg_A</span>:
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">default</span>:
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>In other words, integration test</p>
<ol>
<li>build linters</li>
<li><code>cd test</code></li>
<li>run <code>go vet -vettool=/path/to/oneoflint .</code></li>
<li>compare the stderr with expected output</li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>$ <span style="color:#f92672">(</span>cd test; go vet -vettool<span style="color:#f92672">=</span>/path/to/oneoflint .<span style="color:#f92672">)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># github.com/gomoni/sumlint/test</span>
</span></span><span style="display:flex;"><span>./oneof.go:16:2: missing default <span style="color:#66d9ef">case</span> on isMsg_Payload: code cannot handle nil interface
</span></span><span style="display:flex;"><span>./oneof.go:24:2: non-exhaustive type switch on isMsg_Payload: missing cases <span style="color:#66d9ef">for</span>: github.com/gomoni/sumlint/test/one_of.Msg_B</span></span></code></pre></div>
]]></content:encoded></item><item><title>Sumlint: Having sum types in Go</title><link>https://vyskocil.me/blog/sumlint-having-sum-types-in-go/</link><pubDate>Mon, 13 Oct 2025 15:51:33 +0200</pubDate><guid>https://vyskocil.me/blog/sumlint-having-sum-types-in-go/</guid><description>A sum type (also known as enum / tagged union / one of / disjoint union) allows a type to be exactly one of several fixed alternatives. Functional languages treat this as a first-class concept with pattern matching and exhaustive checking. Go offers almost everything, but does …</description><content:encoded><![CDATA[<p>A sum type (also known as enum / tagged union / one of / disjoint union) allows a type to be
exactly one of several fixed alternatives. Functional languages treat this as a
first-class concept with pattern matching and exhaustive checking. Go offers <em>almost</em> everything, but does not. My little linting tool
<a href="https://github.com/gomoni/sumlint">sumlint</a> aims to bridge part of that gap
for Go.</p>
<h1 id="introduction">Introduction</h1>
<p>This concept exists in many functional languages. Take <a href="https://elm-lang.org/try">Elm</a>, for example</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-elm" data-lang="elm"><span style="display:flex;"><span><span style="color:#f92672">import </span><span style="color:#a6e22e">Html</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#66d9ef">Shape</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">=</span> <span style="color:#66d9ef">Circle</span> <span style="color:#66d9ef">Float</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">|</span> <span style="color:#66d9ef">Point</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>area <span style="color:#a6e22e">:</span> <span style="color:#66d9ef">Shape</span> <span style="color:#a6e22e">-&gt;</span> <span style="color:#66d9ef">Float</span>
</span></span><span style="display:flex;"><span>area shape <span style="color:#a6e22e">=</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> shape <span style="color:#66d9ef">of</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">Circle</span> r <span style="color:#a6e22e">-&gt;</span>
</span></span><span style="display:flex;"><span>            pi <span style="color:#a6e22e">*</span> r <span style="color:#a6e22e">*</span> r
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">Point</span> <span style="color:#a6e22e">-&gt;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">main </span><span style="color:#a6e22e">=</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">let</span> shape <span style="color:#a6e22e">=</span> <span style="color:#66d9ef">Point</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">in</span>
</span></span><span style="display:flex;"><span>  area shape
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">|&gt;</span> <span style="color:#66d9ef">Debug</span><span style="color:#a6e22e">.</span>toString
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">|&gt;</span> <span style="color:#66d9ef">Html</span><span style="color:#a6e22e">.</span>text</span></span></code></pre></div>
<p>In this case, an idiomatic Go implementation would be straightforward.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Areaer</span> <span style="color:#66d9ef">interface</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">Area</span>() <span style="color:#66d9ef">float64</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Point</span> <span style="color:#66d9ef">struct</span> {}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">p</span> <span style="color:#a6e22e">Point</span>) <span style="color:#a6e22e">Area</span>() <span style="color:#66d9ef">float64</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>Consider creating a new case in Elm.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-elm" data-lang="elm"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#66d9ef">Shape</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">=</span> <span style="color:#66d9ef">Circle</span> <span style="color:#66d9ef">Float</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">|</span> <span style="color:#66d9ef">Point</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">|</span> <span style="color:#66d9ef">Rectangle</span> <span style="color:#66d9ef">Float</span> <span style="color:#66d9ef">Float</span></span></span></code></pre></div>
<p>This results in a compile error that leads the developer to all places where this case must be handled. Additionally, using interfaces can be impractical when the underlying structs have different layouts.</p>
<p>This <code>case</code> does not account for all possibilities.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>10|&gt;    case shape of
</span></span><span style="display:flex;"><span>11|&gt;        Circle r -&gt;
</span></span><span style="display:flex;"><span>12|&gt;            pi * r * r
</span></span><span style="display:flex;"><span>13|&gt;        Point -&gt;
</span></span><span style="display:flex;"><span>14|&gt;            0
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Missing possibilities include:</span></span></code></pre></div>
<h2 id="what-have-go-ever-done-for-us">What have Go ever done for us?</h2>
<blockquote>
<p>Interfaces? Generics? Type switches?</p>
<p>All right, but apart of interfaces, generics and type switches?</p>
<p>Iterators?</p>
<p>Iterators? Oh shut up!</p>
</blockquote>
<p>Go itself seems to be very close to having sum types. The following code is valid Go code that looks almost exactly like Elm examples.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Shape</span> <span style="color:#66d9ef">interface</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">Circle</span> | <span style="color:#a6e22e">Point</span>
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<blockquote>
<p>Excellent proposal, sir! There are only two problems.
The first is this is a generic type constraint only.
And the second is this is only a generic type constraint.</p>
<p>Technically this is a single problem, but I considered it to be
so important, that I mentioned it twice!</p>
</blockquote>
<p>In other words, if you declare a variable of type Shape, the Go compiler will complain.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>cannot use type Shape outside a type constraint: interface contains type constraints</span></span></code></pre></div>
<p>However, since Go 1, there has been a nearly perfect construct in Go: a type switch. However, as this is Go, there is no way to perform advanced pattern matching; a switch can be based on types or values, but not in a single statement.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">shape</span> <span style="color:#a6e22e">Shape</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">area</span> <span style="color:#66d9ef">float64</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">x</span>.(<span style="color:#a6e22e">Shape</span>) {
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">case</span> <span style="color:#a6e22e">Point</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">area</span> = <span style="color:#ae81ff">0.0</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">case</span> <span style="color:#a6e22e">Circle</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">area</span> = <span style="color:#a6e22e">math</span>.<span style="color:#a6e22e">Pi</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">x</span>.<span style="color:#a6e22e">R</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">x</span>.<span style="color:#a6e22e">R</span>
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>Except for exhaustiveness. The Go compiler is unaware of which structs implement which interfaces, so adding a new case has no impact. The code will compile and crash at runtime. While possible, type switches are not idiomatic Go.</p>
<h2 id="burntsushigo-sumtype">BurntSushi/go-sumtype</h2>
<p>I am not the first person to try to solve this problem.Take
<a href="https://github.com/BurntSushi/go-sumtype">https://github.com/BurntSushi/go-sumtype</a>
as an example. BurntSushi is a Rust developer and the author of <code>ripgrep</code>, among other projects. So it&rsquo;s not surprising that he&rsquo;s interested in having at least one type of sum type available in Go.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">//go-sumtype:decl MySumType</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">MySumType</span> <span style="color:#66d9ef">interface</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">sealed</span>()
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>$ go-sumtype mysumtype.go
</span></span><span style="display:flex;"><span>mysumtype.go:18:2: exhaustiveness check failed <span style="color:#66d9ef">for</span> sum type <span style="color:#e6db74">&#39;MySumType&#39;</span>: missing cases f</span></span></code></pre></div>
<h2 id="can-we-do-it-better">Can we do it better?</h2>
<p>The Rust code looks like this, and this approach is considered normal and idiomatic.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#75715e">#[derive(Debug)]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">Rectangle</span> {
</span></span><span style="display:flex;"><span>    width: <span style="color:#66d9ef">u32</span>,
</span></span><span style="display:flex;"><span>    height: <span style="color:#66d9ef">u32</span>,
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>Not in Go. The magic comments commonly seen in Go source code are directives of the compiler itself. Such as <code>go:embed</code>, <code>go:build</code>, <code>go:tool</code>. The only exception is <code>nolint</code>, which is used to turn off false positives of a linter.</p>
<p>In Go world, there is actually a great precedent. The Test, Bench, and Fuzz prefixes are merely conventions that have an actual meaning for testing code. So let&rsquo;s have a linter that implements the following simple convention</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// SumFoo declares a sum type, which is recognized by sumlint</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">SumFoo</span> <span style="color:#66d9ef">interface</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">sumFoo</span>()
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>The rules are simple.</p>
<blockquote>
<p>The Sum interface must be public and have <code>Sum</code> as a prefix.</p>
<p>It must implement a private method with the same name and the <code>sum</code> prefix.</p>
</blockquote>
<p>Each definition of this type is considered a sum type. Sumlint detects all structs that implement the interface and enforces exhaustiveness in type switches. No magic comments are needed, as the special status is visible from the type name itself.</p>
<h2 id="an-example">An example</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// SumFoo declares a sum type, which is recognized by sumlint</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">SumFoo</span> <span style="color:#66d9ef">interface</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">sumFoo</span>()
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// A is an implementation of a SumFoo</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">A</span> <span style="color:#66d9ef">struct</span>{}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">A</span>) <span style="color:#a6e22e">sumFoo</span>() {}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// B is an implementation of a SumFoo</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">B</span> <span style="color:#66d9ef">struct</span>{}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">B</span>) <span style="color:#a6e22e">sumFoo</span>() {}</span></span></code></pre></div>
<p>There are three cases. The first one is ideal because all cases are covered. Unlike in other languages, Go requires a default case to handle the nil interface case. This particular property may clash with a nilness or nilaway analyzers.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">good</span>(<span style="color:#a6e22e">x</span> <span style="color:#a6e22e">SumFoo</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">x</span>.(<span style="color:#66d9ef">type</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">case</span> <span style="color:#a6e22e">A</span>, <span style="color:#a6e22e">B</span>:
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">default</span>:
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>Removing <code>B</code> case leads to an error.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// missing B</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">noB</span>(<span style="color:#a6e22e">x</span> <span style="color:#a6e22e">SumFoo</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">x</span>.(<span style="color:#66d9ef">type</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">case</span> <span style="color:#a6e22e">A</span>:
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">default</span>:
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>go vet -vettool<span style="color:#f92672">=</span><span style="color:#e6db74">${</span>HOME<span style="color:#e6db74">}</span>/go/bin/sumlint .
</span></span><span style="display:flex;"><span>./src.go:29:2: non-exhaustive type switch on SumFoo: missing cases <span style="color:#66d9ef">for</span>: github.com/gomoni/sumlint/tests.B</span></span></code></pre></div>
<p>As well as a missing default. This case is intended to handle the <code>nil</code> interface value, which can’t be easily work-arounded in a Go.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// missing default, is reported</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">noDefault</span>(<span style="color:#a6e22e">x</span> <span style="color:#a6e22e">SumFoo</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">x</span>.(<span style="color:#66d9ef">type</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">case</span> <span style="color:#a6e22e">A</span>, <span style="color:#a6e22e">B</span>:
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>go vet -vettool<span style="color:#f92672">=</span><span style="color:#e6db74">${</span>HOME<span style="color:#e6db74">}</span>/go/bin/sumlint .
</span></span><span style="display:flex;"><span>./src.go:23:2: missing default <span style="color:#66d9ef">case</span> on SumFoo: code cannot handle nil interface</span></span></code></pre></div>
<p>Edited on 2021-10-18: fix the grammar, improved wording.</p>
]]></content:encoded></item><item><title>Updating Hugo Templates</title><link>https://vyskocil.me/blog/updating-hugo-templates/</link><pubDate>Mon, 23 Jun 2025 15:43:42 +0200</pubDate><guid>https://vyskocil.me/blog/updating-hugo-templates/</guid><description>This is my personal log detailing how I updated the blog to support the latest version of Hugo.&amp;#xA;I rarely write content here. Perhaps I should work on making writing a habit. The blog itself does uses hugo to generate the HTML from Markdown input. This works well. However. Hugo is …</description><content:encoded><![CDATA[<p>This is my personal log detailing how I updated the blog to support the latest
version of Hugo.</p>
<p>I rarely write content here. Perhaps I should work on making
writing a habit. The blog itself does uses <a href="https://gohugo.io">hugo</a> to
generate the HTML from Markdown input. This works well. However. Hugo is
<em>huge</em> big, super flexible and makes incompatible changes from time to time.</p>
<pre><code>ERROR deprecated: .Site.Author was deprecated in Hugo v0.124.0 and subsequently
removed. Implement taxonomy 'author' or use .Site.Params.Author instead.
</code></pre>
<p>As I no expert on Hugo and only use it occasionally, I must admit that such
errors do not help me much. However, it turns out that this is a problem that
can be solver with AI. And in fact ChatGPT provided me with some useful answers
regaring taxonomy and <code>.Site.Params.Author</code>.</p>
<h2 id="siteauthor">.Site.Author</h2>
<p>I fixed an error in
<a href="https://codeberg.org/vyskocilm/miblog/commit/f5354a33037ffc9773ff15521b1f90cef83eebe0#diff-c2996dfd59d9c152da4b8148714e209d60f21ffa">config.toml</a>
by simply moving data around, because <em>taxonomy</em> is not needed for a single
user blog.</p>
<pre><code class="language-diff">- [author]
-   author = &quot;Michal Vyskocil&quot;
-   profile = &quot;I am software developer and a former field manager with 12+ years of experience, Prague, Czech Republic&quot;

[Params]
+   author = &quot;Michal Vyskocil&quot;
+   profile = &quot;I am software developer and a former field manager with 12+ years of experience, Prague, Czech Republic&quot;
</code></pre>
<p>Modifying the <code>config.toml</code> is only a half of the story. The way the Markdown is rendered is the responsibility
of the used template. This also needs to be updated. This blog uses
(miblog-hestia-pure)[https://codeberg.org/vyskocilm/miblog-hestia-pure], which
is a fork of
<a href="https://github.com/diwao/hestia-pure">https://github.com/diwao/hestia-pure</a>. Therefore with the change above, updating the template was <a href="https://codeberg.org/vyskocilm/miblog-hestia-pure/commit/2e0ddcb89ea6c2dd317064b810c99d8869c825c2">straightforward</a>.</p>
<pre><code class="language-diff">  &lt;div class=&quot;author__info pure-u-4-5 pure-u-md-7-8&quot;&gt;
-   {{ if and (.Site.Author.author) (ne .Site.Author.author &quot;&quot;) }}
-   &lt;h3&gt;{{ .Site.Author.author }}&lt;/h3&gt;
+   {{ if and (.Site.Params.author) (ne .Site.Params.author &quot;&quot;) }}
+   &lt;h3&gt;{{ .Site.Params.author }}&lt;/h3&gt;
    {{ else }}
</code></pre>
<h2 id="truncate-output">Truncate output</h2>
<p>Hugo added a <a href="https://gohugo.io/content-management/summaries/">new divider for
summaries</a>, which is HTML
comment `</p>
<p>`. This broke the index.html and the RSS output.
I only realised this once I put the blog online 🤦. As I had no intention of adding the marker to all the articles I had already written, the <a href="https://codeberg.org/vyskocilm/miblog-hestia-pure/commit/6cdab1202c885e322820720e59e185a4fad5eb19">solution</a> was to truncate each article to 280 characters.</p>
<pre><code class="language-diff">-   &lt;p &gt;{{ if .IsPage }}{{ .Summary }}...{{ else }}{{ .Site.Params.description }}{{ end }}&lt;/p&gt;
+   &lt;p &gt;{{ if .IsPage }}{{ .Summary | plainify | truncate 280 }}...{{ else }}{{ .Site.Params.description }}{{ end }}&lt;/p&gt;
</code></pre>
<h2 id="fixing-rss">Fixing RSS</h2>
<p>Changing the output of RSS was more work. This was because I basically had to fork the <a href="https://github.com/gohugoio/hugo/blob/master/tpl/tplimpl/embedded/templates/rss.xml">default RSS template</a>. And then I had to adapt it to <a href="https://codeberg.org/vyskocilm/miblog-hestia-pure/commit/9644b00fb47bd7401999860b5058a06372472eeb">my needs</a>.</p>]]></content:encoded></item><item><title>Blink.cmp</title><link>https://vyskocil.me/blog/blink.cmp/</link><pubDate>Thu, 08 May 2025 21:31:50 +0200</pubDate><guid>https://vyskocil.me/blog/blink.cmp/</guid><description>In the Lazy Neovim Configuration I wrote a bit about the neovim configuration I use. While most of my configuration has not changed, I learned about a new completion plugin blink.cmp. It got recommended by kulala-ls and got recently introduced in a kickstart.nvim.&amp;#xA;The …</description><content:encoded><![CDATA[<p>In the <a href="/blog/lazy-neovim-configuration.html">Lazy Neovim Configuration</a> I
wrote a bit about the neovim configuration I use. While most of my
configuration has not changed, I learned about a new completion plugin
<a href="https://cmp.saghen.dev/">blink.cmp</a>. It got recommended by
<a href="https://github.com/mistweaverco/kulala-ls?tab=readme-ov-file#configuration">kulala-ls</a>
and got recently introduced in a
<a href="https://github.com/nvim-lua/kickstart.nvim">kickstart.nvim</a>.</p>
<h2 id="the-installation">The installation</h2>
<p>Is easy with <a href="https://lazy.folke.io/">lazy.nvim</a>.</p>
<pre><code class="language-lua">{
  'saghen/blink.cmp',
  -- optional: provides snippets for the snippet source
  dependencies = { 'rafamadriz/friendly-snippets' },

  -- use a release tag to download pre-built binaries
  version = '1.*',
}
</code></pre>
<h2 id="the-configuration">The configuration</h2>
<p>While my complete configuration file
<a href="https://codeberg.org/vyskocilm/dotfiles/src/branch/main/.config/nvim/lua/plugins/blink.lua">blink.lua</a>
is a big one, the plugin is actually pretty easy to install and start with. What
is really nice is that it offers a <em>lot</em> of completions by default. The
<code>&quot;lsp&quot;</code>, <code>&quot;buffer&quot;</code>, <code>&quot;path&quot;</code> are all builtin. The <code>&quot;snippets&quot;</code> too, just
require an external plugin with defined snippets.</p>
<p>What I really like are the defaults. The <code>keymap = { preset = &quot;default&quot; }</code> uses
the <code>['&lt;C-y&gt;'] = { 'select_and_accept' },</code> so <code>Ctrl+y</code> accepts the completion
as every vim nerd expects.</p>
<p>However when needed, it is easy to configure the plugin to the linking. So I
enabled
<a href="https://cmp.saghen.dev/configuration/completion.html#documentation">documentation</a>
to be opened by default or to enable <code>ghost_text</code> so the completion will be
visible as a virtual text in the buffer. The documentation is simply amazing.</p>
<p>What really amazed me was a <a href="https://cmp.saghen.dev/configuration/completion.html#menu-draw">menu-draw</a> configuration. What was problem and I did not appreciated was it does not show the completion source by default. The configuration was pretty straightforward. First add a <code>source_name</code> into columns.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-lua" data-lang="lua"><span style="display:flex;"><span>columns <span style="color:#f92672">=</span> { { <span style="color:#e6db74">&#34;kind_icon&#34;</span> }, { <span style="color:#e6db74">&#34;label&#34;</span>, <span style="color:#e6db74">&#34;label_description&#34;</span>, gap <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span> }, { <span style="color:#e6db74">&#34;source_name&#34;</span> } },</span></span></code></pre></div>
<p>And then define how the <code>source_name</code> will look like. This define max width,
the highlight class and most importantly the content of the field like <code>[LSP]</code>
or <code>[Buffer]</code> or <code>[Copilot]</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-lua" data-lang="lua"><span style="display:flex;"><span>components <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>	source_name <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>		width <span style="color:#f92672">=</span> { max <span style="color:#f92672">=</span> <span style="color:#ae81ff">30</span> },
</span></span><span style="display:flex;"><span>		text <span style="color:#f92672">=</span> <span style="color:#66d9ef">function</span>(ctx)
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;[&#34;</span> <span style="color:#f92672">..</span> ctx.source_name <span style="color:#f92672">..</span> <span style="color:#e6db74">&#34;]&#34;</span>
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">end</span>,
</span></span><span style="display:flex;"><span>		highlight <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;BlinkCmpSource&#34;</span>,
</span></span><span style="display:flex;"><span>	},
</span></span><span style="display:flex;"><span>},</span></span></code></pre></div>
<p>Support for snippets is builtin, given the external plugin is installed. Either
luasnip or mini.snippets are supported.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-lua" data-lang="lua"><span style="display:flex;"><span>snippets <span style="color:#f92672">=</span> { preset <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;luasnip&#34;</span> },</span></span></code></pre></div>
<p>And besides documentation blink has a completion for function signatures, so it can provide a context help when using a function too.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-lua" data-lang="lua"><span style="display:flex;"><span>signature <span style="color:#f92672">=</span> { enabled <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span> },</span></span></code></pre></div>
<h2 id="to-rust-or-not-to-rust">To Rust or not to Rust</h2>
<p>The name blink does refer to the speed plugin. So except native engine written
in Lua, there is a Rust based one claiming a significant speedup. The Rust
based <a href="https://github.com/saghen/frizbee">frizbee</a> is a SIMD fuzzy matcher
which achieves ~6x the performance of fzf while ignoring typos.</p>
<ol>
<li><a href="https://github.com/Saghen/blink.cmp/tree/main/lua/blink/cmp/fuzzy/lua"><code>lua</code></a></li>
<li><a href="https://github.com/Saghen/blink.cmp/tree/main/lua/blink/cmp/fuzzy/rust"><code>rust</code></a></li>
</ol>
<p>I ended up using <code>fuzzy = { implementation = &quot;lua&quot; },</code> which is the one
Kickstart offers. It worked well for me - except the emoji and nerdfont
providers. But more specifically I do not want <code>neovim</code> plugins to download and
install random binaries from the Internet. And that&rsquo;s sadly the only one option
for a <code>frizbee</code> at the moment.</p>
]]></content:encoded></item><item><title>Lazy neovim plugins loading</title><link>https://vyskocil.me/blog/lazy-nvim/</link><pubDate>Mon, 26 Aug 2024 11:30:17 +0200</pubDate><guid>https://vyskocil.me/blog/lazy-nvim/</guid><description>A few months ago I changed my neovim configuration and started using the Lazy package manager. The thing is that Lazy is fantastic at installing plugins. LSP, neotest, lazydev, telescope and all the goodies from the wonderful neovim community are easy to install and add.&amp;#xA;So it is …</description><content:encoded><![CDATA[<p>A few months ago I changed my neovim configuration and
<a href="/blog/lazy-neovim-configuration/">started using the Lazy package manager</a>. The
thing is that Lazy is fantastic at installing plugins. LSP, neotest, lazydev,
telescope and all the goodies from the wonderful neovim community are easy to install and add.</p>
<p>So it is not surprising that I ended up with 37 plugins installed and loaded every time I run <code>nvim</code>. While none of the plugins are particularly heavy and the
startup time is below 100 ms. It is still interesting to find out why is the
package manager is named Lazy.</p>
<h2 id="make-lazynvim-lazy-again">Make Lazy.nvim lazy again</h2>
<p><a href="https://github.com/folke/lazy.nvim">folke/lazy.nvim</a> already advertises the lazy loading feature</p>
<ul>
<li>🔌 Automatic lazy-loading of Lua modules and lazy-loading on events, commands, filetypes, and key mappings</li>
</ul>
<p>The problem is that most of the configuration examples do not care. After all, loading on
startup doesn&rsquo;t break anything, so it is a good default after all. This leaves the exercise
of lazily loading stuff to the curious user.</p>
<p>Lazy itself provides all the necessary hints. Just type <code>:Lazy</code> and check the
output. In my case <code>nvim</code> loads 5 mandatory plugins, tht can&rsquo;t be postponed without breaking the editor.</p>
<pre><code>  Total: 37 plugins

  Loaded (5)
    ● gruvbox 3.57ms  start
    ● lazy.nvim 5.56ms  init.lua
    ● lualine.nvim 6.19ms  VeryLazy
    ● nvim-lastplace 0.57ms  start
    ● vim-sleuth 0.47ms  start
</code></pre>
<h2 id="verylazy">VeryLazy</h2>
<p>That is actually a cool name. In short, lazy allows you to start a plugin in a
case of an event. VeryLazy is specific one. Documentation says</p>
<blockquote>
<p>VeryLazy: triggered after LazyDone and processing VimEnter auto commands</p>
</blockquote>
<p>VeryLazy plugins are not
counted in the startup time. Just press <code>P</code> in <code>:Lazy</code> view to compare
<code>lualine.nvim 3.82ms  VeryLazy</code></p>
<pre><code>  Startuptime: 36.28ms

  Based on the actual CPU time of the Neovim process till UIEnter.
  This is more accurate than `nvim --startuptime`.
    LazyStart 14.69ms
    LazyDone  28.89ms (+14.2ms)
    UIEnter   36.28ms (+7.39ms)


    ●  startuptime 36.28ms
    ●  VeryLazy 5.33ms
      ➜  lualine.nvim 4.06ms
</code></pre>
<p>And as <code>lualine.nvim 4.11ms  start</code></p>
<pre><code>  Startuptime: 47.56ms

  Based on the actual CPU time of the Neovim process till UIEnter.
  This is more accurate than `nvim --startuptime`.
    LazyStart 12.86ms
    LazyDone  37.18ms (+24.32ms)
    UIEnter   47.56ms (+10.38ms)

    ●  startuptime 47.56ms
    ●  VeryLazy 1.56ms
</code></pre>
<p><code>VeryLazy</code> is then the simplest optimization. But the plugin will still load on every startup. So it is indeed a simple but lazy optimization. With a cool name.</p>
<h2 id="events">Events</h2>
<p><a href="https://lazy.folke.io/spec#spec-lazy-loading">Lazy Loading</a> offers an
excellent place to start. Unfortunately a bit short on examples.</p>
<blockquote>
<p>event:  Lazy-load on event. Events can be specified as BufEnter or with a pattern like BufEnter *.lua</p>
</blockquote>
<p>The value can be a string, an array of strings, or a function or a patterns that allows you to start the plugin after some action. Here are <code>gitsigns</code> and <code>nvim-lint</code>
plugins configured to get loaded after a buffer is read. As neither is language
dependent, there isn&rsquo;t much to do.</p>
<pre><code>  ○ gitsigns.nvim  BufReadPost 
  ○ nvim-lint  BufReadPost  InsertLeave  BufWritePost 
</code></pre>
<p><code>~/.config/nvim/lua/plugins/gitsigns.lua</code> contains</p>
<pre><code class="language-lua">return {
  &quot;lewis6991/gitsigns.nvim&quot;,
  event = { &quot;BufReadPost&quot; },
}
</code></pre>
<h2 id="file-types">File types</h2>
<p>I tend to use a language agnostic tools, so have a <code>lspconfig/neotest/conform</code>
combo instead of <code>go.nvim</code>. Nothing against <code>go.nvim</code>, it is a great plugin I use as a base for <a href="https://codeberg.org/vyskocilm/gists/src/branch/main/2024/go-treesitter-sql-injection.scm">my configuration</a>. This means that most of the plugins I have can&rsquo;t be restricted to a file type. There are notable exceptions though.</p>
<pre><code>  ○ kulala.nvim  http 
  ○ lazydev.nvim  lua 
  ○ luvit-meta 
</code></pre>
<p>Lazydev is for Lua files, and since it&rsquo;s a plugin from
<a href="https://github.com/folke">folke</a> himself, it would be odd to not to suggest a lazy loading by default.</p>
<pre><code class="language-lua">return {
  {
    &quot;folke/lazydev.nvim&quot;,
    -- only load on lua files
    ft = &quot;lua&quot;,
  },
  { -- optional `vim.uv` typings
    &quot;Bilal2453/luvit-meta&quot;,
    lazy = true
  },
}
</code></pre>
<p><code>lazy=true</code> options is handy for library plugins. Such plugin will be loaded when needed by other plugin.</p>
<h2 id="command">Command</h2>
<p>Since I use some commands only via neovim&rsquo;s command line, there&rsquo;s nothing easier
than loading the plugin after typing it on a command line. This is a case of
<code>vim-fugitive</code> and <code>:G</code> command. Lazy registers the command and loads the
plugin when it is really needed.</p>
<pre><code>  ○ vim-fugitive  G 
</code></pre>
<pre><code class="language-lua">return {
  &quot;tpope/vim-fugitive&quot;,
  cmd = &quot;G&quot;,
}
</code></pre>
<h2 id="keys">Keys</h2>
<p>Some other plugins have a defined shortcut(s) to activate. In my case it&rsquo;s the
powerful Telescope plugin. Lazy can also handle this scenario too. The plugin
can be activated with a command or a shortcut. This form expects the actual
keymap to be activated inside <code>config</code> function of a plugin.</p>
<pre><code>  ○ neotest  &lt;leader&gt;t 
  ○ telescope.nvim  Telescope  &lt;leader&gt;sr  &lt;leader&gt;s.
</code></pre>
<pre><code class="language-lua">return {
  {
    &quot;nvim-telescope/telescope.nvim&quot;,
    cmd = &quot;Telescope&quot;,
    keys = {
      {&quot;&lt;leader&gt;&lt;leader&gt;&quot;},
      {&quot;&lt;leader&gt;sh&quot;},
    },
    config = function()
      -- set &lt;leader&gt;sh here
      vim.keymap.set(&quot;n&quot;, &quot;&lt;leader&gt;sh&quot;, ...)
    end
  },
}
</code></pre>
<h2 id="conclusion">Conclusion</h2>
<p>Was it worth it? I reduced the startup time of an empty Neovim from 90ms to
~40ms, where opening a Go file increases the time to 80ms. So in terms of
numbers it was not. On the other hand, it was cool and fun, and I hope you&rsquo;ve
enjoyed reading this.</p>
<h2 id="fonts">Fonts</h2>
<p><a href="https://www.nerdfonts.com/font-downloads">FiraCode Nerd Font</a> under SIL OPEN
FONT LICENSE Version 1.1, converted to woff2 format by
<a href="https://cloudconvert.com/ttf-to-woff2">cloudconvert.com</a></p>
]]></content:encoded></item><item><title>It and New Go Iterators</title><link>https://vyskocil.me/blog/it-and-new-go-iterators/</link><pubDate>Wed, 24 Jul 2024 11:11:41 +0200</pubDate><guid>https://vyskocil.me/blog/it-and-new-go-iterators/</guid><description>As Go 1.23 reaches Release Candidate status the previous rangefunc experiment has been stabilized and iterators are a part of a compiler and a standard library.&amp;#xA;The iter package provides common type definitions. The slices package has several functions that work with iterators …</description><content:encoded><![CDATA[<p>As Go 1.23 reaches <a href="https://groups.google.com/g/golang-announce/c/8ciOP5ve_CM">Release Candidate
status</a> the previous
<code>rangefunc</code> experiment has been stabilized and
<a href="https://tip.golang.org/doc/go1.23#iterators">iterators</a> are a part of a
compiler and a standard library.</p>
<ul>
<li>The <code>iter</code> package provides common type definitions.</li>
<li>The <code>slices</code> package has several functions that work with iterators</li>
<li>The <code>maps</code> package has several functions that work with <code>iter.Seq2</code> and maps</li>
</ul>
<h2 id="installing-a-release-candidate-of-go">Installing a release candidate of Go</h2>
<p>Since Go 1.23 has not been released yet, you need to install a release candidate. With
recent changes and a <code>GOTOOLCHAIN</code> support, this is easier than ever.</p>
<pre><code class="language-sh"># Given an installed go compiler
$ go version
go version go1.22.5 linux/amd64
# Get the release candidate and set it in a go.mod
$ go get go@1.23rc2 toolchain@1.23rc2
# And make it a default
$ GOTOOLCHAIN=&quot;go1.23rc2&quot; go version
go version go1.23rc2 linux/amd64
$ cat go.mod
module github.com/gomoni/it

go 1.23rc2
</code></pre>
<p>A simple <code>export GOTOOLCHAIN=go1.23rc2</code> is all that is needed to make <code>go</code>
and a language server and other tools to work with a new release. See the <a href="https://go.dev/blog/toolchain">Forward
Compatibility and Toolchain Management in Go
1.21</a> from the Go Blog for more details.</p>
<h2 id="it-v010">it v0.1.0</h2>
<p>Changes to a standard library made most of
<a href="https://github.com/gomoni/it">github.com/gomoni/it</a> obsolete. And it is never
a good idea for third-party packages to compete and duplicate it.
Surprisingly neither <code>Filter</code>, neither <code>Map</code> helpers got there, making
<a href="https://pkg.go.dev/github.com/gomoni/it@v0.1.0"><code>it@v0.1.0</code></a> a perfect place
for them. To be closer to the standard library these helpers exists in a
respective subpackage</p>
<ul>
<li><a href="https://pkg.go.dev/github.com/gomoni/it@v0.1.0/islices">islices</a> provides filter and map on top of <code>iter.Seq</code></li>
<li><a href="https://pkg.go.dev/github.com/gomoni/it@v0.1.0/imaps">imaps</a> provides filter and map on top of <code>iter.Seq2</code></li>
</ul>
<p>The <a href="https://pkg.go.dev/github.com/gomoni/it@v0.1.0">it</a> itself provides a <code>Chain</code> and <code>Mappable</code> structs providing a
chainable API for a helper functions.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> (
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;fmt&#34;</span>
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;slices&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;github.com/gomoni/it&#34;</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">n</span> <span style="color:#f92672">:=</span> []<span style="color:#66d9ef">string</span>{<span style="color:#e6db74">&#34;aa&#34;</span>, <span style="color:#e6db74">&#34;aaa&#34;</span>, <span style="color:#e6db74">&#34;aaaaaaa&#34;</span>, <span style="color:#e6db74">&#34;a&#34;</span>}
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ch</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">it</span>.<span style="color:#a6e22e">NewMapable</span>[<span style="color:#66d9ef">string</span>, <span style="color:#66d9ef">int</span>](<span style="color:#a6e22e">slices</span>.<span style="color:#a6e22e">Values</span>(<span style="color:#a6e22e">n</span>))
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">slice</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ch</span>.
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">Filter</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">s</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">bool</span> { <span style="color:#66d9ef">return</span> len(<span style="color:#a6e22e">s</span>) <span style="color:#f92672">&gt;=</span> <span style="color:#ae81ff">2</span> }).
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">Map</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">s</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">int</span> { <span style="color:#66d9ef">return</span> len(<span style="color:#a6e22e">s</span>) }).
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">Collect</span>()
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">slice</span>)
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<h2 id="request-pkggodev-upgrade">Request pkg.go.dev upgrade</h2>
<p>As soon as the new release
<a href="https://github.com/gomoni/it/releases/tag/v0.1.0">v0.1.0</a> was tagged,
<a href="https://pkg.go.dev">pkg.go.dev</a> showed obsolete information. It turns out that
there is an easy process for requesting an update.</p>
<p>Let me use a hypothetical <code>v0.1.109</code> release, to make a link work even for the future releases.</p>
<ol>
<li>Go to the version which does not exists yet (<a href="https://pkg.go.dev/github.com/gomoni/it@v0.1.109">https://pkg.go.dev/github.com/gomoni/it@v0.1.109</a>)</li>
<li>Click on a button that says <em>Request &ldquo;<a href="mailto:github.com.../it@v0.1.109">github.com.../it@v0.1.109</a>&rdquo;</em></li>
<li>Profit!</li>
</ol>
]]></content:encoded></item><item><title>Back to the Virtual Machine</title><link>https://vyskocil.me/blog/back-to-vm/</link><pubDate>Wed, 03 Apr 2024 21:59:15 +0200</pubDate><guid>https://vyskocil.me/blog/back-to-vm/</guid><description>Recently I was playing with Kubernetes to power my tiny blog. I am back on a Virtual Machine powered by openSUSE Leap.&amp;#xA;The trigger The xz backdoor meant that I immediately nuked the server from the Internet. While MicroOS itself uses SELinux .. the Kubernetes is known for running …</description><content:encoded><![CDATA[<p>Recently I was playing with
<a href="https://vyskocil.me/blog/vme-kubernetes/">Kubernetes</a> to power my tiny blog.
I am back on a Virtual Machine powered by <a href="https://get.opensuse.org/leap">openSUSE Leap</a>.</p>
<h1 id="the-trigger">The trigger</h1>
<p>The <a href="https://news.opensuse.org/2024/03/29/xz-backdoor/">xz backdoor</a> meant that
I immediately nuked the server from the Internet. While MicroOS itself uses
SELinux .. the Kubernetes is known for running everything as a root by
default.</p>
<iframe
    src="https://witter.cz/@vyskocilm/111765038117396313/embed"
    class="mastodon-embed"
    style="max-width: 100%; border: 0"
    width="400"
    allowfullscreen="allowfullscreen">
</iframe>
<script src="https://witter.cz/embed.js" async="async"></script>

<h1 id="leap-forward">Leap forward</h1>
<p>Frankly speaking I am not a devops guy and Kubernetes is a hell of layers and
indirections. I was actually quite surprised when realizing how little vanilla
Kubernetes does and how many things you have to get from the broader ecosystem.</p>
<p>Even the Ingress - the way you assign public URLS to your services - is not
provided and you must select and learn an external Ingress controller. And
configuring it is no fun.</p>
<p>{{ &lt; highlight yaml &gt; }}
metadata:
annotations:
ingress.kubernetes.io/redirect-regex: &ldquo;^https://vyskocil.org/?(.*)&rdquo;
ingress.kubernetes.io/redirect-replacement: &ldquo;<a href="https://vyskocil.me/$1%22">https://vyskocil.me/$1&quot;</a>
{{ &lt; /highlight &gt; }}</p>
<p>And do not try to start with Helm. That is a horrible abstraction, that has become
an industry standard for some reason. All in all managing the Kubernetes is a full time job.
Which I do not enjoy doing.</p>
<h1 id="web-server">Web server</h1>
<p>You can use Apache httpd or nginx as your web server. I found the
<a href="https://caddyserver.com/">Caddy</a> to be much easier to configure and it comes
with some bells and whistles like a built-in <code>acme</code> protocol support, so https
via letsencrypt is automatically configured. Setting up this blog was as easy
as</p>
<pre><code>vyskocil.me {
        tls email@example.net
	root * /srv/www/htdocs/vyskocil.me/miblog/public/
	encode zstd gzip
	file_server
}
</code></pre>
<p>And <code>sudo systemctl reload caddy.service</code> and voila!</p>
<pre><code>curl --head https://vyskocil.me
HTTP/2 200 
accept-ranges: bytes
alt-svc: h3=&quot;:443&quot;; ma=2592000
content-type: text/html; charset=utf-8
etag: &quot;sb7i6ejr7&quot;
last-modified: Sun, 31 Mar 2024 09:54:14 GMT
server: Caddy
content-length: 25603
date: Wed, 03 Apr 2024 20:34:05 GMT
</code></pre>
]]></content:encoded></item></channel></rss>