<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Hugo's weblog on WhyNotHugo (雨果)</title><link>https://whynothugo.nl/</link><description>Recent content in Hugo's weblog on WhyNotHugo (雨果)</description><generator>Hugo -- gohugo.io</generator><language>en</language><managingEditor>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</managingEditor><webMaster>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</webMaster><atom:link href="https://whynothugo.nl/posts.xml" rel="self" type="application/rss+xml"/><item><title>Deprecation plan for vdirsyncer</title><link>https://whynothugo.nl/journal/2026/04/09/deprecation-plan-for-vdirsyncer/</link><pubDate>Thu, 09 Apr 2026 10:43:45 +0200</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2026/04/09/deprecation-plan-for-vdirsyncer/</guid><description><![CDATA[<p>I&rsquo;m pleased to announce that NLNet has accepted my <a href="https://nlnet.nl/project/Pimsync/">grant proposal for
pimsync</a>. The main goal of this grant and associated work is to port all
features from vdirsyncer which are still missing from <a href="https://pimsync.whynothugo.nl/">pimsync</a>.</p>
<p>For my personal use case, pimsync works fine, and it&rsquo;s been a great &ldquo;vdirsyncer
version 2&rdquo;. In particular, I find the daemon mode of enormous value. In case a
server rejects a single calendar event or contact, pimsync will also continue
and sync everything else. These are the two main features which have made a
difference for me. I can mostly run it in the background and forget about it</p>
<p>For other use cases, some features are still missing, and I&rsquo;ll be working on
addressing these in the coming months.</p>
<p>By the end of this project, all users should be able to move to pimsync. Of
course, nobody&rsquo;s force to migrate, and users happy with vdirsyncer can continue
using it, although support will wind down substantially. That said, I hope the
improvements in pimsync are attractive for most use cases.</p>
<p>With the Python ecosystem being in a perpetual state of change and backwards
incompatibility, I suspect that in a few years vdirsyncer will gradually become
harder to run: functionality or libraries on which it relies will slowly stop
working on newer versions of Python. To be frank, I find this quite
disappointing, even if I&rsquo;m sun-setting the project.</p>
<p>The man page <code>pimsync-migration(7)</code> (also <a href="https://pimsync.whynothugo.nl/pimsync-migration.7.html">available online</a>) contains a
list of all missing features. If you&rsquo;re using any functionality which is missing
in pimsync and also missing from this list, please reach out by <a href="https://todo.sr.ht/~whynothugo/pimsync">opening an
issue</a> or <a href="https://lists.sr.ht/~whynothugo/vdirsyncer-devel">send an email to the list</a> so I can bring it into the
roadmap.</p>
<p>Beyond porting vdirsyncer features, I don&rsquo;t have any major plans for pimsync,
and I expect it to enter a long period of stability, with minor fixes. I do have
several other related projects (with good synergy but low coupling) in mind.</p>
]]></description></item><item><title>Status update 2026-03</title><link>https://whynothugo.nl/journal/2026/04/02/status-update-2026-03/</link><pubDate>Thu, 02 Apr 2026 15:13:29 +0200</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2026/04/02/status-update-2026-03/</guid><description><![CDATA[<h1 id="pimsync">Pimsync<div class="permalink">[<a href="#pimsync">permalink</a>]</div></h1>
<p>Pimsync 0.5.7 is out, with mostly minor improvements and bug fixes.</p>
<p>The <code>pimsync sync</code> command (which synchronises once and exits) now shows a
human-readable summary of operations being performed. <code>pimsync sync -i</code> allows
reviewing operations before they happen. This is especially useful during
initial setup (to ensure that everything is working as intended) and for
debugging unusual scenarios.</p>
<p>The <code>sync</code> command now exits with a non-zero status on error. An obvious
behaviour, but I overlooked this previously since my main focus was the <code>daemon</code>
command, which never exits.</p>
<p>I&rsquo;ve made good progress with <code>http_proxy</code> support and rewriting the buggy Unix
Domain Socket support to be much smoother. The previous implementation used a
library which rewrote URL domains to include the socket path. This is quirky,
and broke under conditions that are rather common. In Pimsync&rsquo;s case, the socket
path is declared in the configuration file and does not change. The new
implementation uses a newly written code for this, since no existing library fit
this (rather common) use case. I&rsquo;d like to upstream this Unix Domain Socket
support into <code>hyper-util</code>, but upstream is in an odd state of flux: the old HTTP
client which we&rsquo;re using is a legacy one, and the new one isn&rsquo;t ready yet.
Upstreaming support for legacy interfaces might not be the best time investment.</p>
<h1 id="sway">Sway<div class="permalink">[<a href="#sway">permalink</a>]</div></h1>
<p>I <a href="https://github.com/swaywm/sway/pull/9064">implemented support</a> for the <a href="https://wayland.app/protocols/ext-workspace-v1">ext-workspace-v1</a> protocol in sway,
which will be released as part of sway 1.12. This will enable using a standard
Wayland protocol to control workspaces, and should substantially improve
interoperability of status bars and similar shell components.</p>
<p>I also <a href="https://github.com/swaywm/sway/pull/9093">fixed a small bug</a> where fullscreen windows which didn&rsquo;t draw
a surface large enough didn&rsquo;t cover the whole screen. This is a niche edge case
which I only encountered once with an emulator, but the issue had been open for
a long time and it was actually a pretty straightforward fix.</p>
<h1 id="khal">Khal<div class="permalink">[<a href="#khal">permalink</a>]</div></h1>
<p>I&rsquo;ve tagged a release of <a href="https://github.com/pimutils/khal/blob/master/CHANGELOG.rst#0140">khal 0.14.0</a>, with many fixes and improvements mostly
made by contributors — I mostly reviewed, merged and tagged the release. Thanks
to everyone who contributed for this release.</p>
<h1 id="imapgoose">ImapGoose<div class="permalink">[<a href="#imapgoose">permalink</a>]</div></h1>
<p>I tackled a bug report in which the listener connection was not properly
re-established after a network error. This led me to iron out the entire error
handling for the listener connection. After applying the fixes, some paths to
improving the surrounding code became obvious, and the code for handling
disconnections is now much tidier, in essentially a single mini event loop which
handles all possible failure modes for the listener.</p>
<p>ImapGoose now also handles a server resetting its MODSEQ, a niche scenario which
isn&rsquo;t even mentioned in the specification (and I guess, technically allowed). I
did not expect this to actually happen, but the fix is the result of this
actually happening to someone and them having to work around it.</p>
<p>Detection of non-existent mailboxes was not always accurate. Sometimes ImapGoose
assumed an unrelated error, when in reality, the error was a missing mailbox.
ImapGoose now unconditionally tries to create a mailbox, which succeeds if the
mailbox does not exist, and will simply fail again if the error was something
else. Regrettably, error detection in IMAP is not very clean, but this has no
practical downsides.</p>
<p>That&rsquo;s all for <del>this</del> last month. Better late than never!</p>
]]></description></item><item><title>Creating an XDG_RUNTIME_DIR</title><link>https://whynothugo.nl/journal/2026/04/02/creating-an-xdg_runtime_dir/</link><pubDate>Thu, 02 Apr 2026 09:44:02 +0200</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2026/04/02/creating-an-xdg_runtime_dir/</guid><description><![CDATA[<p>Many applications rely on a runtime directory: a dedicated directory which
serves as a rendezvous point for different components and for runtime files.</p>
<p>The full documentation for it is in the <a href="https://specifications.freedesktop.org/basedir/latest/#variables">XDG base directory
specification</a>.</p>
<h1 id="prior-art">Prior art<div class="permalink">[<a href="#prior-art">permalink</a>]</div></h1>
<ul>
<li><a href="https://github.com/ifreund/dumb_runtime_dir">dumb_runtime_dir</a>: PAM module
with less than 100 lines of C code.</li>
<li><a href="https://github.com/jjk-jacky/pam_rundir/">pam_rundir</a>: PAM module,
functionally equivalent to the above, over 400 lines of code.</li>
<li><a href="https://github.com/systemd/systemd/">pam_systemd</a>: Tied to systemd. Relies on
PAM too.</li>
<li><a href="https://github.com/elogind/elogind">pam_elogind</a>: Tied to <code>elogind</code>, an
unmaintained fork of <code>systemd-logind</code>.</li>
<li><code>mkdir /tmp/run-$UID</code>: simple, but insecure (see below).</li>
<li><a href="https://git.sr.ht/~whynothugo/mkrundir">mkrundir</a>: Small binary written by
myself, less than 40 lines, but requires <a href="https://en.wikipedia.org/wiki/Setuid">setuid</a> (and I&rsquo;d like to address
this).</li>
</ul>
<h1 id="requirements">Requirements<div class="permalink">[<a href="#requirements">permalink</a>]</div></h1>
<p>Concurrent sessions for the same user should share the same runtime directory.
For example, when logging in via a physical seat on a host, and then starting an
SSH session, they both need to use the same runtime directory path.</p>
<p>A native approach might use <code>/tmp/run-$USER</code>, but this is vulnerable to another
user account creating a directory in that location, inadvertently controlling
ownership of sensitive files. Likewise, another user simply creating a file
there, blocking creating of the runtime dir. Using a deterministic location in a
world-writeable location is insecure.</p>
<h1 id="approaches">Approaches<div class="permalink">[<a href="#approaches">permalink</a>]</div></h1>
<p>All possible approaches have their caveats and compromises. We can only pick
which compromise we prefer.</p>
<ul>
<li><strong>PAM model</strong>: requires installing and relying on PAM, a huge amount of
complexity and attack surface for something so trivial. They also wouldn&rsquo;t
work when simply using <code>loging</code> on a tty.</li>
<li><strong>setuid/setgid binary</strong>: probably the simplest approach, but usage of
setuid/setgid is frowned upon from a security point of view.</li>
<li>Using a deterministic location in <code>/tmp</code> is vulnerable to races/hijacking.</li>
<li><strong>Somewhere in <code>$HOME</code></strong>: survives reboots. This lacks any proper clean-up
mechanism, but can also result in stale files and break application
expectations.</li>
<li><strong>Dedicated daemon</strong>: expose a socket to a specific group, and have the daemon
create the directory on request. Works, but having an additional daemon for
this seems like an overkill.</li>
<li><strong>Randomised location in <code>/tmp</code></strong>: requires coordination across concurrent
sessions.</li>
</ul>
<p>Additionally, PAM, setuid or a dedicated daemon all require root involvement in
configuring or initialising a user session after user login. This kind of
initialisation should be handled inside the user session: the root user should
only be involved if strictly necessary.</p>
<h1 id="randomised-location-in-tmp">Randomised location in <code>/tmp</code><div class="permalink">[<a href="#randomised-location-in-tmp">permalink</a>]</div></h1>
<p>Of all the above approaches, I opted for using a non-deterministic (i.e.,
unpredictable) location in <code>/tmp</code>.</p>
<p>A &ldquo;name file&rdquo; somewhere in <code>$HOME</code> keeps the location of the current runtime
directory. Upon login, a helper called via <code>~/.zprofile</code> checks this file. If it
contains the location of a directory owned by the current user with the right
permission, it is used as the current <code>$XDG_RUNTIME_DIR</code>. Otherwise, a lock is
taken, a new directory is created, and the name file is replaced. The lock
ensures that two concurrent executions don&rsquo;t overstep each other when replacing
the name file.</p>
<p>This is simple enough, and can be implemented in a small, simple binary.</p>
]]></description></item><item><title>Pacman syndrome</title><link>https://whynothugo.nl/journal/2026/03/26/pacman-syndrome/</link><pubDate>Thu, 26 Mar 2026 03:25:57 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2026/03/26/pacman-syndrome/</guid><description><![CDATA[<p>I used Arch Linux for over a decade. I like a lot of things about it. I like
more things about it than I dislike. Over these many years, I grew quite
proficient with <code>pacman</code>, Arch Linux&rsquo;s package manager. All its commands became
intuitive and natural:</p>
<ul>
<li>Install a package: <code>pacman -S $PKG_NAME</code>. <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></li>
<li>View dependencies of a package: <code>pacman -Qi $PKG_NAME</code>.</li>
<li>Which package owns a file: <code>pacman -Qo /path/to/file</code>.</li>
</ul>
<p>The list goes on. If you&rsquo;ve never used this package manager, these commands are
as obscure as it gets. You&rsquo;d never guess the right flags, and you&rsquo;d never guess
what they do just by reading them. But if you&rsquo;re proficient with <code>pacman</code>, those
commands feel obvious, and it&rsquo;s easy to think that anyone with doubts should
just read the documentation.</p>
<p>During the many years I used Arch and <code>pacman</code>, I never questioned this. It felt
normal and natural, just the way things are.</p>
<p>I&rsquo;ve now been using Alpine for a couple of years, and <code>apk</code> is quite a bit more
obvious:</p>
<ul>
<li>Install a package: <code>apk add $PKG_NAME</code>.</li>
<li>View dependencies of a package: <code>apk info --depends $PKG_NAME</code>.</li>
<li>Which package owns a file: <code>apk info --who-owns /path/to/file</code>.</li>
</ul>
<p>You might not guess the exact names of flags to use based on what you want to
do, but you&rsquo;ll definitely guess what these commands do just by reading them.
Looking back at my years using <code>pacman</code>, I ask myself how that ever felt natural
or intuitive.</p>
<p>It just takes stepping outside of our daily habits to notice how cryptic some of
them are. We often use tools for years until they become second nature, and
never question whether they make sense, if they truly are &ldquo;easy to use&rdquo; or we&rsquo;re
just used to them. Every once in a while, we need to step out of our bubble and
use a different tool for our daily tasks, so we can have a clearer, less biased
picture of the tools we rely upon.</p>
<p>I&rsquo;ve come to think of this as the Pacman syndrome: familiarity makes an
unintuitive interface feel natural.</p>
<h1 id="disclaimer">Disclaimer<div class="permalink">[<a href="#disclaimer">permalink</a>]</div></h1>
<p>I use <code>pacman</code> as an example because it&rsquo;s the program that made me realise this
so clearly. I look back and think &ldquo;yeah, this thing is weird and non-obvious, I
can understand why folks using something else see it the way they do&rdquo;.</p>
<p><code>pacman</code> is great. It&rsquo;s lightweight and fast. And compared to most package
managers, it&rsquo;s still among the better ones.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Fun fact: the initial version of this article wrongly claimed
<code>pacman -Si $PKG_NAME</code> was the right command.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>Status update 2026-02</title><link>https://whynothugo.nl/journal/2026/02/28/status-update-2026-02/</link><pubDate>Sat, 28 Feb 2026 14:40:02 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2026/02/28/status-update-2026-02/</guid><description><![CDATA[<p>It&rsquo;s been a busy couple of months. I had to take some unexpected time off for
personal reasons in January, followed by <a href="https://fosdem.org/2026/">FOSDEM</a> and the <a href="https://postmarketos.org/blog/2026/02/10/fosdem-and-hackathon/">postmarketOS
hackathon</a> which kept me plenty busy.</p>
<h1 id="pimsync">pimsync<div class="permalink">[<a href="#pimsync">permalink</a>]</div></h1>
<p>I&rsquo;ve implemented an &ldquo;interactive sync&rdquo; (via <code>pimsync sync -i</code>), which shows a
list of all operations to be performed and waits for confirmation before
executing. This is especially useful when storages are in some unusual state and
one wants to be certain that sync will perform the expected operations.</p>
<p>While releases of pimsync and related libraries always depend on stable versions
of dependencies, during development these often require the latest <code>main</code> commit
of some dependency. I&rsquo;ve switched to using submodules to track these, which
should greatly simplify compiling pimsync for anyone who wants to try out the
latest development version.</p>
<p>Pimsync now also ships with zsh completion scripts. Writing these was actually
more fun than I expected it to be. I&rsquo;ve kept some tidy notes and will publish a
short guide on how to write zsh completions in future. Completion scripts for
other shells are welcome.</p>
<p>Conflict resolution for properties is now implemented: when the name or
description of a collection has diverged on both storages, this conflict can now
be resolved. Pimsync displays a prompt with options to keep A, keep B, or
manually provide a new value.</p>
<p>The biggest change has been one-way sync. It is now possible to configure
pimsync to synchronise two storages one way, effectively making B a clone of A.
This required several internal refactors: I had to split out the &ldquo;analyse the
state of this pair&rdquo; logic from &ldquo;determine the new set of actions for this pair&rdquo;
logic. The latter is implemented via a <a href="https://docs.rs/vstorage/latest/vstorage/sync/trait.Mode.html"><code>vstorage::sync::Mode</code></a> trait, so
any library consumer could potentially implement their own custom logic (e.g.:
sync only deletions one way, but only new items the other way).</p>
<p>I&rsquo;m happy with how this last change turned out, but somehow feel that the code
can be simplified further at this point. Complexity is a necessary ingredient to
progress: sometimes we need to add some complexity to see the path forward and
then simplify. At this stage, I&rsquo;m looking to refine and simplify the general
implementation a bit.</p>
<p>I&rsquo;ve started some work on <code>sync-token</code> (<a href="https://datatracker.ietf.org/doc/html/rfc6578">RFC 6578</a>) support. This allows pimsync
to only query a server for items which have changed since the last
synchronisation process (instead of having to fetch a whole list and determine
this manually). This dramatically reduces network usage (and local processing)
for each synchronisation. Server support for this feature varies, so the current
behaviour will remain as fallback. Several of the foundational pieces are there,
but the full feature is not yet done.</p>
<h2 id="davcli">davcli<div class="permalink">[<a href="#davcli">permalink</a>]</div></h2>
<p><a href="https://git.sr.ht/~whynothugo/davcli/"><code>davcli</code></a> now uses the latest version of
<code>libdav</code>. While this is mostly a debugging/inspection tool, it also serves as an
example of using the library.</p>
<h1 id="matridge">Matridge<div class="permalink">[<a href="#matridge">permalink</a>]</div></h1>
<p>The postmarketOS team relies on Matrix a lot for communication and coordination.
While I&rsquo;m not a fan of the latest VC-funded hype, it&rsquo;s true that Matrix is
widely used in the FLOSS ecosystem, and better than the usual variety of
proprietary silos. Even so, I didn&rsquo;t want to deal with yet-another messaging
client, so I set up a local XMPP-&gt;Matrix gateway using <a href="https://codeberg.org/slidge/matridge">matridge</a> with the
intent of using the Matrix network from an existing client.</p>
<p>The gateway had a lot of missing essential features, so I had no choice but to
set out and implement them, and fix a variety of issues. Patches were quickly
merged upstream, and we even had an opportunity to sync up with the lead
developer at FOSDEM.</p>
<p>At this point, the gateway is fully usable with a new Matrix account without
having to bootstrap it on some other client. It now properly handles invites,
joining rooms and even implements commands like changing the account password.
All this is doable from your favourite XMPP client!</p>
<p>Both the XMPP and Matrix ecosystems are richer thanks to this.</p>
<h1 id="hare-dbus">hare-dbus<div class="permalink">[<a href="#hare-dbus">permalink</a>]</div></h1>
<p><a href="https://sr.ht/~whynothugo/hare-dbus/">hare-dbus</a> was already in a pretty solid working state since last year, but
lacked support for non-blocking use. I&rsquo;ve now implemented support for
non-blocking mode and integration with <a href="https://sr.ht/~sircmpwn/hare-ev/">hare-ev</a>. The current non-blocking
implementation works, but serialising and parsing messages is still a bit
tedious since it requires manually using the mid-level message API.</p>
<p>The pending work here is to adapt to the code generator: it can currently
generate stubs for using hare-dbus&rsquo;s blocking API. The message types, parsers
and serialises which it currently generates are fine to use with the
non-blocking API, but the functions used to send the messages and callbacks are
tied to the blocking API. These two need to be split (making the blocking stubs
optional), and I need to implement stubs for the non-blocking API.</p>
<p>This has been shaping up really nicely. Working with hare is really nice, it&rsquo;s
simple, lean and doesn&rsquo;t add extra abstraction layers between application code
and the native OS APIs.</p>
<h1 id="hare-lsp">hare-lsp<div class="permalink">[<a href="#hare-lsp">permalink</a>]</div></h1>
<p>While I do enjoy writing code in Hare, I really missed the extremely useful IDE
integration which Go offers via <a href="https://go.dev/gopls/">gopls</a>. I wrote <a href="https://sr.ht/~whynothugo/hare-lsp/">hare-lsp</a> last year to fill in
that gap, but while working on hare-dbus, I found lots of scenarios where
go-to-definition didn&rsquo;t work. I kept around samples and one-line explanations of
each scenario, and later circled back to implement tests for them and finally
fix hare-lsp. It should now provide a much more reliable experience, although a
lot of scenarios can&rsquo;t be fixed until the hare stdlib implements the type
inference used by the compiler.</p>
<h1 id="imapgoose">ImapGoose<div class="permalink">[<a href="#imapgoose">permalink</a>]</div></h1>
<h2 id="kqueueinotify-re-implementation">kqueue/inotify re-implementation<div class="permalink">[<a href="#kqueueinotify-re-implementation">permalink</a>]</div></h2>
<p>ImapGoose&rsquo;s <a href="https://man.openbsd.org/kqueue.2">kqueue</a> implementation was essentially unusable. This was mostly
due to a limitation in the general-purpose library used to track files:
<code>fsnotify</code>.</p>
<p>When monitoring a directory for file creations or deletions, kqueue needs one
file descriptor per directory. When monitoring for file <em>changes</em>, kqueue needs
one file descriptor per file in said directories. Due to the nature of Maildir,
files are only created or deleted (or moved), but never changed in-place. So
ImapGoose doesn&rsquo;t need to track file changes, and only needs one file descriptor
per directory (or two per mailbox).</p>
<p><code>fsnotify</code> doesn&rsquo;t have an API to detect only file creation or deletion. This
also seems undesirable upstream and didn&rsquo;t fit its general-purpose use case.</p>
<p>I dropped this dependency, re-implemented the inotify backend (which is now
optimised for this specific use case), and implemented a new kqueue backend
which is also optimised for ImapGoose. <code>fsnotify</code>&rsquo;s kqueue backend also kept a
list of files in-memory to determine which files changed, but this duplicates
the logic already implemented by ImapGoose using a status repository (which
persists when the process exits too).</p>
<h2 id="pending-work-on-imapgoose">Pending work on ImapGoose<div class="permalink">[<a href="#pending-work-on-imapgoose">permalink</a>]</div></h2>
<p>The main issue in ImapGoose which is blocking a v1.0.0 release is [recovering
from sleep]: when a system goes to sleep and wakes up again, ImapGoose isn&rsquo;t
detecting a timeout. Sometimes it takes tens of minutes to detect this, even
though it has a timer every 3 minutes. I still can&rsquo;t fathom what&rsquo;s going on, and
have been unable to reproduce the issue in a controlled test environment.</p>
]]></description></item><item><title>Entrenching American corporations into local European payments</title><link>https://whynothugo.nl/journal/2026/02/25/entrenching-american-corporations-into-local-european-payments/</link><pubDate>Wed, 25 Feb 2026 18:39:29 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2026/02/25/entrenching-american-corporations-into-local-european-payments/</guid><description><![CDATA[<p>In the Netherlands, local online payments are done using iDEAL, a Dutch online
payment system which works surprisingly well. When I pay online, I&rsquo;m redirected
to my bank&rsquo;s website<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> where I authorise the operation. My bank then sends
the money directly to the merchant&rsquo;s account.</p>
<p>iDEAL has a few notable advantages: it&rsquo;s a purely local system (avoiding any
foreign intermediaries or fees), payments flow directly through the banking
system, and I don&rsquo;t have to share sensitive card information with merchants. I
instruct my bank to send money: I don&rsquo;t give a merchant permission to extract
it.</p>
<p>Sadly, <a href="https://epicompany.eu/media-insights/ideal-to-phase-into-wero">iDEAL is being phased away by Wero</a>. Wero is marketed as a
&ldquo;truly European&rdquo; payment method, touted as an improvement. In reality, it is
quite the opposite: it introduces strong dependency on Google and Apple, two
foreign companies with terrible track records on user rights.</p>
<p>Wero supports only apps that run on Google&rsquo;s and Android proprietary locked-down
operating systems. It does not support using a regular browser on a regular
computer. These facts are <a href="https://support.wero-wallet.eu/hc/en-us/articles/25599074240401-What-devices-support-the-Wero-app">confirmed in their FAQ</a>.</p>
<p>The choice is clear: to continue using online payments, one must use a mobile
device entirely under the control of Google or Apple and agree to their
contractual terms, or stop participating altogether. Wero is legally European,
but in practice it locks users into foreign ecosystems.</p>
<p>What really drives me crazy is the shameless propaganda around the initiative.
Wero is presented a European, independent, future-proof payment system. But it&rsquo;s
an American lock-in disguised as progress. The system relies entirely on foreign
corporations subject to laws and rulings outside Europe — jurisdictions
currently campaigning against European data sovereignty.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Some banks allow using their own application on some devices as well as the
browser.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>They and them as singular pronouns</title><link>https://whynothugo.nl/journal/2026/02/25/they-and-them-as-singular-pronouns/</link><pubDate>Wed, 25 Feb 2026 17:31:11 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2026/02/25/they-and-them-as-singular-pronouns/</guid><description><![CDATA[<p>&ldquo;They&rdquo; and &ldquo;them&rdquo; can be used as singular gender-neutral pronouns in English. A
common everyday example is:</p>
<blockquote>
<p>Person 1: I went to see my dentist today.<br>
Person 2: What did they say?</p>
</blockquote>
<p>I&rsquo;ve heard variants of the above since the nineties, but this is not at all a
modern trend. The following is example from William Shakespeare (1594):</p>
<blockquote>
<p>There&rsquo;s not a man I meet but doth salute me<br>
As if I were their well-acquainted friend.</p>
</blockquote>
<p>Note the usage of &ldquo;their&rdquo;, referring to the singular noun &ldquo;a man&rdquo;.</p>
<p>The following example is from Jane Austen (1813):</p>
<blockquote>
<p>To be sure you knew no actual good of me —<br>
but nobody thinks of that when they fall in love.</p>
</blockquote>
<p>The key is that &ldquo;nobody&rdquo; is grammatically singular: we say &ldquo;nobody is here&rdquo;, not
&ldquo;nobody are here&rdquo;.</p>
<p>The following Biblical example (James 2:15–16) uses &ldquo;them&rdquo; in singular:</p>
<blockquote>
<p>Suppose a brother or a sister is without clothes and daily food.<br>
If one of you says to them, &ldquo;Go in peace; keep warm and well fed,&rdquo; but does
nothing about their physical needs, what good is it?</p>
</blockquote>
<p>Examples of this usage go back at least as far back as the 14th century.</p>
]]></description></item><item><title>You don't need a Neovim plugin manager</title><link>https://whynothugo.nl/journal/2026/01/08/you-dont-need-a-neovim-plugin-manager/</link><pubDate>Thu, 08 Jan 2026 11:38:14 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2026/01/08/you-dont-need-a-neovim-plugin-manager/</guid><description><![CDATA[<p>This article is a resurrected draft from 2022, and its core argument remains
unchanged. Lua-specific portions apply only to Neovim, and everything else
applies to both Neovim and Vim.</p>
<p>Let&rsquo;s first understand the base mechanism which everything else builds upon: how
Vim discovers and loads plugins…</p>
<h1 id="the-runtimepath-list">The <code>runtimepath</code> list<div class="permalink">[<a href="#the-runtimepath-list">permalink</a>]</div></h1>
<p>Vim defines a <code>runtimepath</code> variable, a set of directories inside which it will
search for plugins. For my Neovim installation, <code>runtimepath</code> includes the
following:</p>
<ul>
<li><code>$HOME/.config/nvim</code></li>
<li><code>/etc/xdg/nvim</code></li>
<li><code>$HOME/.local/share/nvim/site</code></li>
<li><code>$HOME/.local/share/nvim/site/pack/*/start/*</code></li>
<li><code>/usr/local/share/nvim/site</code></li>
<li><code>/usr/share/nvim/site</code></li>
<li><code>/usr/share/nvim/runtime</code></li>
<li><code>/usr/share/nvim/runtime/pack/dist/opt/netrw</code></li>
<li><code>/usr/share/nvim/runtime/pack/dist/opt/matchit</code></li>
<li><code>/usr/lib/nvim</code></li>
<li><code>$HOME/.local/share/nvim/site/pack/*/start/*/after</code></li>
<li><code>/usr/share/nvim/site/after</code></li>
<li><code>/usr/local/share/nvim/site/after</code></li>
<li><code>$HOME/.local/share/nvim/site/after</code></li>
<li><code>/etc/xdg/nvim/after</code></li>
<li><code>$HOME/.config/nvim/after</code></li>
</ul>
<p>The above list is, in part, defined as subdirectories of <code>XDG_DATA_HOME</code> and
<code>XDG_DATA_DIRS</code>. Use <code>:echo &amp;runtimepath</code> to view the exact values on your
setup, or run <code>:h runtimepath</code> to see the documentation.</p>
<p>Inside any of these directories, Vim gives special treatment to a few special
subdirectories:</p>
<ul>
<li><code>plugin/</code>: is loaded automatically.</li>
<li><code>after/plugin/</code>: is loaded automatically and last, overriding any defaults.</li>
<li><code>ftplugin/</code>: is lazy-loaded and executed when a file of a matching filetype is
opened.</li>
<li><code>autoload/</code>: is lazy-loaded on demand. More on this below.</li>
<li><code>lua/</code>: its contents are loaded via <code>require('modulename')</code>. More on this
below.</li>
</ul>
<h2 id="vims-autoload-mechanism">Vim&rsquo;s autoload mechanism<div class="permalink">[<a href="#vims-autoload-mechanism">permalink</a>]</div></h2>
<p>When Vim tries to execute a Vimscript function called
<code>autoload#filename#functionname()</code>, Vim will search for a function called
<code>functionname</code> in the file <code>autoload/filename.vim</code>. Vim will search for this
file in any of the directories defined by <code>runtimepath</code>.</p>
<p>Essentially, placing files in the correct path in any of these directories will
result in Vim lazy-loading the file when the function is first called, so
placing code here is enough to lazy load it.</p>
<p>These are only loaded once, so if you have custom code that needs to be executed
for HTML and XML files, you can include an <code>ftplugin/html.vim</code> and
<code>ftplugin/xml.vim</code> which both autoload the same common file.</p>
<h2 id="neovims-lua-search-paths">Neovim&rsquo;s lua search paths<div class="permalink">[<a href="#neovims-lua-search-paths">permalink</a>]</div></h2>
<p>When using <code>require('mymodule')</code> in lua code, Neovim will search for the file
<code>mymodule</code> in the <code>lua</code> directory inside any of the directories defined in
<code>runtimepath</code>. Note that Lua support is entirely Neovim-specific.</p>
<h2 id="the-packadd-command">The <code>packadd</code> command<div class="permalink">[<a href="#the-packadd-command">permalink</a>]</div></h2>
<p>Files in <code>$HOME/.local/share/nvim/site/pack/*/start/*</code> are loaded automatically.
On the other hand, files in <code>$HOME/.local/share/nvim/site/pack/*/opt/*</code> are
loaded manually via <code>packadd</code>. Generally, this is only needed for plugins which
initialise pre-emptively (e.g.: by using <code>plugin</code> rather than <code>ftplugin</code> or
<code>autoload</code>), but you only want to initialise them in specific scenarios.</p>
<h1 id="installing-a-plugin">Installing a plugin<div class="permalink">[<a href="#installing-a-plugin">permalink</a>]</div></h1>
<p>Given the mechanisms described above, installing a plugin is just a matter of
placing its source files into the correct directory, and plugins already include
files in the correct layout.</p>
<p>Sometimes you want specific plugins to only initialise if you open a file of a
given type. In the case of plugins which require explicit initialisation (e.g.:
<code>require('myplugin').setup()</code>), the ideal place to do this is in
<code>ftplugin/python.lua</code> (replacing <code>python</code> with whichever filetype you care
about).</p>
<p>Vim plugins can be configured <em>and</em> lazy loaded with greater ease: they
typically take configuration via global variables (e.g.:
<code>vim.g.myplugin_use_monochrome = 1</code> or <code>let g:myplugin_use_monochrome = 1</code>), and
then lazy-load, for example, on a matching filetype. Such plugins only load when
a matching file has been opened, lazy initialise, but allow configuration to be
trivially defined in <code>init.vim</code> or <code>init.lua</code>. Neovim Lua plugins typically use
a <code>.setup()</code> function to initialise, requiring additional scaffold if you want
to delay loading them until necessary.</p>
<h1 id="plugin-managers">Plugin managers<div class="permalink">[<a href="#plugin-managers">permalink</a>]</div></h1>
<p>Plugin managers basically fetch plugins (e.g.: <code>git clone</code> into the correct
location). Some will place plugins into <code>opt</code> instead of <code>start</code>. Some plugin
managers implement lazy-loading of plugins on demand, but this should not be
necessary for plugins which properly implement lazy initialisation (e.g.: a Go
plugin should ensure that it is auto-loaded by having its main entry point in
<code>ftplugin/go.vim</code> or <code>ftplugin/go.lua</code>).</p>
<p>Plugin managers do provide certain conveniences, and there&rsquo;s nothing wrong with
using them, but it&rsquo;s important to understand that they&rsquo;re not a strict technical
requirement.</p>
<p>In particular, when dealing with plugins which do not contemplate lazy-loading,
plugin managers can help work around their initialisation and lazy-load them
even if they&rsquo;re not designed with this in mind. This can be achieved by using
<code>packadd</code>, but requires additional manual setup.</p>
<p>Modern plugin managers have grown into orchestration frameworks. Most of that
functionality compensates for plugin design, not editor limitations.</p>
<p>There are a few large alternatives to using plugin managers…</p>
<h1 id="distribution-package-manager">Distribution package manager<div class="permalink">[<a href="#distribution-package-manager">permalink</a>]</div></h1>
<p>The same distribution channel which shipped Neovim likely has some plugins which
can be installed the same way. Availability of plugins varies per distribution.
The AUR and Nixpkgs in particular are known to include a large variety of
plugins.</p>
<h1 id="tracking-plugins-with-dotfiles">Tracking plugins with dotfiles<div class="permalink">[<a href="#tracking-plugins-with-dotfiles">permalink</a>]</div></h1>
<p>I track my dotfiles (including all my Neovim configuration) via git, so tracking
plugins as git submodules has some natural synergy. Git itself can fetch and
track plugins, and also gives me a clear overview if any plugins have local
modifications which I may have forgotten about. Plugins are pinned to an exact
version, and I can upgrade a plugin and its configuration in the same commit.
Initial setup is also trivial.</p>
<p>The only manual step required is running <code>:TSUpdate</code> each time that the
tree-sitter plugin is updated. Ideally, this shall change in future as the
ecosystem starts to stabilise and we can rely on tree-sitter binaries installed
through the usual channels.</p>
<p>In my dotfiles repository, I use the following command to add new plugins as
submodules, installing them into the <code>start</code> directory so they are loaded
automatically:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>git submodule add <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    https://github.com/ibhagwan/fzf-lua <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    home/.local/share/nvim/site/pack/plugins/start/fzf-lua
</span></span><span style="display:flex;"><span>git submodule add <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    https://github.com/lewis6991/gitsigns.nvim/ <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    home/.local/share/nvim/site/pack/plugins/start/gitsigns.nvim
</span></span></code></pre></div><p>This is the core of what a plugin manager ultimately does: place plugin sources
into <code>runtimepath</code>. Having understood this, using a plugin manager becomes a
matter of preference rather than necessity.</p>
]]></description></item><item><title>Working with Git inside Vim</title><link>https://whynothugo.nl/journal/2026/01/03/working-with-git-inside-vim/</link><pubDate>Sat, 03 Jan 2026 15:52:16 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2026/01/03/working-with-git-inside-vim/</guid><description><![CDATA[<p>My workflow since time immemorial has been to use a code editor (Vim/Neovim) to
edit code, and then the <code>git</code> command line tool to operate on the repository.
This flow included switching to the command line for <code>git add -p</code> or <code>git diff</code>.</p>
<p>Each time I made a small tweak on an existing change, I had to switch back to
the terminal and <code>git add -p</code>, skip irrelevant changes, and stage the one I
want.</p>
<p>At some point I thought to myself &ldquo;you can do better&rdquo;. In recent months, I added
two sets of keyboard mappings which have dramatically changed how I work.</p>
<h1 id="jump-to-changes">Jump to changes<div class="permalink">[<a href="#jump-to-changes">permalink</a>]</div></h1>
<p>The set of mappings is:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-vim" data-lang="vim"><span style="display:flex;"><span><span style="color:#a6e22e">nnoremap</span> &lt;<span style="color:#a6e22e">expr</span>&gt; ]<span style="color:#a6e22e">c</span> &amp;<span style="color:#a6e22e">diff</span> ? <span style="color:#e6db74">&#39;]c&#39;</span> : <span style="color:#e6db74">&#39;&lt;Cmd&gt;Gitsigns next_hunk&lt;CR&gt;&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">nnoremap</span> &lt;<span style="color:#a6e22e">expr</span>&gt; [<span style="color:#a6e22e">c</span> &amp;<span style="color:#a6e22e">diff</span> ? <span style="color:#e6db74">&#39;[c&#39;</span> : <span style="color:#e6db74">&#39;&lt;Cmd&gt;Gitsigns prev_hunk&lt;CR&gt;&#39;</span>
</span></span></code></pre></div><p>These two are symmetrical: <code>]c</code> jumps to the next change in the current file and
<code>[c</code> jumps to the previous change in the current file (using <a href="https://github.com/lewis6991/gitsigns.nvim/"><code>gitsigns.nvim</code></a>).
The mapping is a bit long/complex because it intentionally avoids overriding the
default behaviour when in <code>diff</code> mode.</p>
<p>This makes reviewing and potentially operating on changes in a file fast and
straightforward.<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
<h1 id="stage-changes">Stage changes<div class="permalink">[<a href="#stage-changes">permalink</a>]</div></h1>
<p>The second set of mappings is:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-vim" data-lang="vim"><span style="display:flex;"><span><span style="color:#a6e22e">nnoremap</span> &lt;<span style="color:#a6e22e">buffer</span>&gt; &lt;<span style="color:#a6e22e">Leader</span>&gt;<span style="color:#a6e22e">ga</span> &lt;<span style="color:#a6e22e">Cmd</span>&gt;<span style="color:#a6e22e">Gitsigns</span> <span style="color:#a6e22e">stage_hunk</span>&lt;<span style="color:#a6e22e">CR</span>&gt;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">xnoremap</span> &lt;<span style="color:#a6e22e">buffer</span>&gt; &lt;<span style="color:#a6e22e">Leader</span>&gt;<span style="color:#a6e22e">ga</span> :<span style="color:#a6e22e">Gitsigns</span> <span style="color:#a6e22e">stage_hunk</span>&lt;<span style="color:#a6e22e">CR</span>&gt;
</span></span></code></pre></div><p>The first of these mappings stages the current hunk (contiguous range of changed
lines) to be committed. The second mapping applies only in visual mode (e.g.:
when text is selected) and only stages the selection.</p>
<p>This lets me easily stage changes to be committed from the editor, without
having to jump to the terminal. The main annoyance of jumping to the terminal
and using <code>git add -p</code> was that I have to search for these changes again, while
I already have my cursor on them in the editor!</p>
<p>I also still use <code>git add -p</code> when staging large amounts of changes in bulk. I
use whichever would be faster, given the state of changes.</p>
<h1 id="other-operations">Other operations<div class="permalink">[<a href="#other-operations">permalink</a>]</div></h1>
<p>I only really use those two mappings for git operations inside the code editor.
It also shows lines which have been added/changed/removed on the left next to
sign numbers. Everything else I still do via the <code>git</code> command line tool.</p>
<p>For my own usage, I don&rsquo;t see much value in doing things like <code>git checkout</code>,
<code>git commit</code>, <code>git fetch</code>, <code>git rebase</code>, etc in the editor. I&rsquo;ve seen plenty of
full-blown git integration in code editors, and I do not find these more
convenient than my current approach. In addition, they typically lack support
for a lot of the non-basic features that I&rsquo;ve come to rely upon over the years.</p>
<p>Perhaps there are more specific git operations which would be convenient inside
the code editor. At this time, I don&rsquo;t know of any and can&rsquo;t think of any. A few
more years of use might tell.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><code>[</code> and <code>]</code> are somewhat common for jumping to previous and next instances
of something. For example <code>[d</code> and <code>]d</code> jump to the previous or next
diagnostic, <code>[s</code> and <code>]s</code> jump across spelling mistakes, etc.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>Resolve .local mDNS domains via avahi using unbound</title><link>https://whynothugo.nl/journal/2025/12/16/resolve-.local-mdns-domains-via-avahi-using-unbound/</link><pubDate>Tue, 16 Dec 2025 02:08:49 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/12/16/resolve-.local-mdns-domains-via-avahi-using-unbound/</guid><description><![CDATA[<p>I want local applications (e.g.: SSH, Firefox, etc) to resolve <code>.local</code> domains
using mDNS, so that devices can find each other via multicast.<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
<p>I run Unbound as a DNS resolve, and I will configure it to delegate the <code>.local</code>
to a avahi2dns. avahi2dns uses D-Bus to talk to Avahi for the actual resolution.
There are several layers and processes involved due to the lack of a dedicated
solution: an mDNS proxy which just exposes a local DNS interface.</p>
<h1 id="setup">Setup<div class="permalink">[<a href="#setup">permalink</a>]</div></h1>
<p>Configure unbound to resolve <code>.local</code> domains using mDNS via avahi.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>apk add avahi avahi2dns
</span></span><span style="display:flex;"><span>rc-update add avahi-daemon
</span></span><span style="display:flex;"><span>rc-update add avahi2dns
</span></span><span style="display:flex;"><span>service avahi-daemon start
</span></span><span style="display:flex;"><span>service avahi2dns start
</span></span></code></pre></div><p>Add the following to <code>/etc/unbound/unbound.conf.d/avahi-local.conf</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;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>forward-zone:
</span></span><span style="display:flex;"><span>	name: &#34;local&#34;
</span></span><span style="display:flex;"><span>	forward-addr: 127.0.0.1@5354
</span></span><span style="display:flex;"><span>server:
</span></span><span style="display:flex;"><span>	do-not-query-localhost: no
</span></span><span style="display:flex;"><span>	domain-insecure: &#34;local&#34;
</span></span></code></pre></div><p>Finally, reload unbound:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>service unbound reload
</span></span></code></pre></div><div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>This can be insecure on networks with untrusted hosts. It is important that
both parties properly authenticate each other: by using known SSH host keys,
a trusted CA for TLS, etc.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>Programmatically provision Alpine VMs with tiny-cloud</title><link>https://whynothugo.nl/journal/2025/12/02/programmatically-provision-alpine-vms-with-tiny-cloud/</link><pubDate>Tue, 02 Dec 2025 10:12:37 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/12/02/programmatically-provision-alpine-vms-with-tiny-cloud/</guid><description><![CDATA[<p>My goal is to programmatically provision a VM. I want to use a script and
upstream image as input, and produce a customised, reproducible VM as output
with zero manual intervention.</p>
<p>I&rsquo;ve written before on setting up <a href="/journal/2022/07/01/quick-and-simple-vms-with-qemu/">quick and simple VMs with qemu</a>. That
article covered manually setting up VMs, which then need to be further
customised via serial console or SSH.</p>
<p>Automating provisioning of a VM via serial console or SSH is non-trivial. I&rsquo;d
need a mechanism on the host to determine when the VM is ready to receive
commands, but there&rsquo;s no obvious way to do so, other than parsing the output of
the serial port, waiting for a <code>login:</code> prompt, and then feeding the command.
This approach is brittle and error-prone.</p>
<h1 id="tiny-cloud">tiny-cloud<div class="permalink">[<a href="#tiny-cloud">permalink</a>]</div></h1>
<p>I opted for using images with <a href="https://gitlab.alpinelinux.org/alpine/cloud/tiny-cloud/"><code>tiny-cloud</code></a>. <code>tiny-cloud</code> is a tool for
auto-configuring VMs, and it&rsquo;s intended for hosting providers. These VM images
start up, and automatically seeks a configuration file on how to provision
itself. There&rsquo;s essentially two approaches to providing a configuration
mechanism: via a well-known endpoint (a static URL at <code>[fd00:ec2::254]</code> or
<code>169.254.169.254</code>) or a CD image with label <code>cidata</code>.</p>
<p>Surprisingly, the CD image approach is much easier for a development/testing
setup with static configuration. I use the <a href="https://alpinelinux.org/cloud/">official upstream images</a>,
the <code>nocloud</code> variant being the one that properly auto-detects a CD. I opted for
the variant with properties Release=3.22.2, Arch=x86_64, Firmware=UEFI,
Bootstrap=&ldquo;Tiny Cloud&rdquo; and Machine=Virtual.</p>
<h1 id="preparing-metadata">Preparing metadata<div class="permalink">[<a href="#preparing-metadata">permalink</a>]</div></h1>
<p>Preparing the metadata is quite straightforward.</p>
<p>First I prepared a <code>meta-data</code> file with a machine identification profile:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">instance-id</span>: <span style="color:#ae81ff">dev-vm-001</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">hostname</span>: <span style="color:#ae81ff">alpine-dev.whynothugo.nl</span>
</span></span></code></pre></div><p>And a <code>user-data</code> file with user configuration data (for a VPS provider, this is
data that varies depending on the tenant&rsquo;s requested settings):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e">#cloud-config</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">users</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">hugo</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ssh_authorized_keys</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">ssh-ed25519</span>
</span></span><span style="display:flex;"><span>        <span style="color:#ae81ff">AAAAC3NzaC1lZDI1NTE5AAAAIJiHg+cQwtqA7Ay+Fsimh9O5QnALlt6561TEu/Z67mU1</span>
</span></span><span style="display:flex;"><span>        <span style="color:#ae81ff">hugo@hyperion.whynothugo.nl</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">groups</span>: <span style="color:#ae81ff">wheel</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">shell</span>: <span style="color:#ae81ff">/bin/sh</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">packages</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">git</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">runcmd</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">echo &#34;Setup complete&#34;</span>
</span></span></code></pre></div><div class="notice errata">
  <header>Errata 2025-12-02</header>
  <section>
  I removed <code>- default</code> from <code>users</code>. My user was not properly added to the
<code>wheel</code> group and the <code>doas</code> rule was not written if the default user was
present. It is unclear if this is a bug or a misinterpretation on my part. I
have <a href="https://gitlab.alpinelinux.org/alpine/cloud/tiny-cloud/-/issues/82">reported this upstream</a> in search of further insight.
  </section>
</div>
<p>Most of this should be quite self-explanatory, and <code>runcmd</code> can be used for
further programmatic configuration of the guest.</p>
<h1 id="creating-the-vm">Creating the VM<div class="permalink">[<a href="#creating-the-vm">permalink</a>]</div></h1>
<p>I prepared a script which creates the VM itself:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span><span style="color:#75715e"># Fetch the image once, manually.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># curl -LO https://dl-cdn.alpinelinux.org/alpine/v3.22/releases/cloud/nocloud_alpine-3.22.2-x86_64-uefi-tiny-r0.qcow2</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Keep the original image clean.</span>
</span></span><span style="display:flex;"><span>cp nocloud_alpine-3.22.2-x86_64-uefi-tiny-r0.qcow2 alpine.qcow2
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>cloud-localds seed.iso user-data meta-data
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># tiny-cloud auto-grows the partition, but the disk itself needs to be larger.</span>
</span></span><span style="display:flex;"><span>qemu-img resize alpine.qcow2 +5G
</span></span><span style="display:flex;"><span>qemu-system-x86_64 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>	-drive file<span style="color:#f92672">=</span>alpine.qcow2,format<span style="color:#f92672">=</span>qcow2 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>	-drive file<span style="color:#f92672">=</span>seed.iso,format<span style="color:#f92672">=</span>raw <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>	-drive <span style="color:#66d9ef">if</span><span style="color:#f92672">=</span>pflash,format<span style="color:#f92672">=</span>raw,readonly<span style="color:#f92672">=</span>on,file<span style="color:#f92672">=</span>/usr/share/OVMF/OVMF_CODE.fd <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>	-drive <span style="color:#66d9ef">if</span><span style="color:#f92672">=</span>pflash,format<span style="color:#f92672">=</span>raw,readonly<span style="color:#f92672">=</span>on,file<span style="color:#f92672">=</span>/usr/share/OVMF/OVMF_VARS.fd <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>	-nic user,hostfwd<span style="color:#f92672">=</span>tcp:127.0.0.1:2222-:22 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>	-m 2G <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>	-smp cores<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>	-monitor unix:monitor.sock,server,nowait <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>	-nographic
</span></span></code></pre></div><p>A brief explanation of the above:</p>
<ul>
<li>I copy the original image since the bootstrap process mutates it; using a copy
allows me to quickly start over.</li>
<li><code>cloud-localds</code> prepares the previously mentioned ISO image with filename
<code>seed.iso</code>.</li>
<li>I resize (grow) the image because the default is full. I&rsquo;ll need extra space
to install my own packages.</li>
<li><code>-drive if=pflash</code> arguments configure the UEFI firmware; the VM won&rsquo;t start
without those.</li>
<li><code>-nic user</code> sets up an interface using user-space networking (this allows
running the VM without needing to elevate privileges for any network setup.
<ul>
<li><code>hostfwd=tcp:127.0.0.1:2222-:22</code> forwards port <code>localhost:2222</code> from the
host to port 22 on the VM. This allows me to SSH into the VM. When running
other services, I&rsquo;ll forward the appropriate port as necessary.</li>
</ul>
</li>
<li><code>-monitor unix:monitor.sock,server,nowait</code> sets up a monitor socket at
<code>./monitor.sock</code>. I can use this one to send commands to qemu, instructing it
to shut down the VM, put it to sleep, resume, etc.</li>
</ul>
<h1 id="first-run">First run<div class="permalink">[<a href="#first-run">permalink</a>]</div></h1>
<p>During start-up, the VM logs:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span> * Tiny Cloud - boot phase ...
</span></span><span style="display:flex;"><span>   ++ expand_root: starting
</span></span><span style="display:flex;"><span>GPT PMBR size mismatch (278527 != 10764287) will be corrected by write.
</span></span><span style="display:flex;"><span>The backup GPT table is not on the end of the device. This problem will be corrected by write.
</span></span><span style="display:flex;"><span>Re-reading the partition table failed.: Resource busy
</span></span><span style="display:flex;"><span>resize2fs 1.47.2 (1-Jan-2025)
</span></span><span style="display:flex;"><span>Filesystem at /dev/sda2 is mounted on /; on-line resizing required
</span></span><span style="display:flex;"><span>old_desc_blocks = 1, new_desc_blocks = 21
</span></span><span style="display:flex;"><span>The filesystem on /dev/sda2 is now 5381100 (1k) blocks long.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>   ++ expand_root: done
</span></span><span style="display:flex;"><span>   ++ set_ephemeral_network: starting
</span></span><span style="display:flex;"><span>   ++ set_ephemeral_network: done
</span></span><span style="display:flex;"><span>   ++ set_network_interfaces: starting
</span></span><span style="display:flex;"><span>   ++ set_network_interfaces: done
</span></span><span style="display:flex;"><span>   ++ enable_sshd: starting
</span></span></code></pre></div><p>It also auto-configures the network interface (via RA and DHCP).</p>
<p>Near the end of the start-up sequence, it logs:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span> * Tiny Cloud - early phase ...
</span></span><span style="display:flex;"><span>   ++ save_userdata: starting
</span></span><span style="display:flex;"><span>   ++ save_userdata: done
</span></span><span style="display:flex;"><span> [ ok ]
</span></span><span style="display:flex;"><span> * Tiny Cloud - main phase ...
</span></span><span style="display:flex;"><span>   ++ userdata_user: starting
</span></span><span style="display:flex;"><span>   ++ userdata_user: done
</span></span><span style="display:flex;"><span>   ++ create_default_user: starting
</span></span><span style="display:flex;"><span>create_default_user: already exists
</span></span><span style="display:flex;"><span>   ++ create_default_user: done
</span></span><span style="display:flex;"><span>   ++ set_hostname: starting
</span></span><span style="display:flex;"><span>   ++ set_hostname: done
</span></span><span style="display:flex;"><span>   ++ userdata_bootcmd: starting
</span></span><span style="display:flex;"><span>   ++ userdata_bootcmd: done
</span></span><span style="display:flex;"><span>   ++ userdata_groups: starting
</span></span><span style="display:flex;"><span>   ++ userdata_groups: done
</span></span><span style="display:flex;"><span>   ++ userdata_users: starting
</span></span><span style="display:flex;"><span>chpasswd: password for &#39;hugo&#39; changed
</span></span><span style="display:flex;"><span>   ++ userdata_users: done
</span></span><span style="display:flex;"><span>   ++ userdata_write_files: starting
</span></span><span style="display:flex;"><span>   ++ userdata_write_files: done
</span></span><span style="display:flex;"><span>   ++ userdata_ntp: starting
</span></span><span style="display:flex;"><span>   ++ userdata_ntp: done
</span></span><span style="display:flex;"><span>   ++ userdata_package_update: starting
</span></span><span style="display:flex;"><span>   ++ userdata_package_update: done
</span></span><span style="display:flex;"><span>   ++ userdata_package_upgrade: starting
</span></span><span style="display:flex;"><span>   ++ userdata_package_upgrade: done
</span></span><span style="display:flex;"><span>   ++ userdata_packages: starting
</span></span><span style="display:flex;"><span>fetch https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/APKINDEX.tar.gz
</span></span><span style="display:flex;"><span>fetch https://dl-cdn.alpinelinux.org/alpine/v3.22/community/x86_64/APKINDEX.tar.gz
</span></span><span style="display:flex;"><span>(1/9) Installing brotli-libs (1.1.0-r2)
</span></span><span style="display:flex;"><span>(2/9) Installing c-ares (1.34.5-r0)
</span></span><span style="display:flex;"><span>(3/9) Installing nghttp2-libs (1.65.0-r0)
</span></span><span style="display:flex;"><span>(4/9) Installing libpsl (0.21.5-r3)
</span></span><span style="display:flex;"><span>(5/9) Installing libcurl (8.14.1-r2)
</span></span><span style="display:flex;"><span>(6/9) Installing libexpat (2.7.3-r0)
</span></span><span style="display:flex;"><span>(7/9) Installing pcre2 (10.46-r0)
</span></span><span style="display:flex;"><span>(8/9) Installing git (2.49.1-r0)
</span></span><span style="display:flex;"><span>(9/9) Installing git-init-template (2.49.1-r0)
</span></span><span style="display:flex;"><span>Executing busybox-1.37.0-r19.trigger
</span></span><span style="display:flex;"><span>OK: 99 MiB in 95 packages
</span></span><span style="display:flex;"><span>   ++ userdata_packages: done
</span></span><span style="display:flex;"><span>   ++ set_ssh_keys: starting
</span></span><span style="display:flex;"><span>set_ssh_keys: no ssh key found
</span></span><span style="display:flex;"><span>   ++ set_ssh_keys: done
</span></span><span style="display:flex;"><span>   ++ ssh_authorized_keys: starting
</span></span><span style="display:flex;"><span>   ++ ssh_authorized_keys: done
</span></span><span style="display:flex;"><span> [ ok ]
</span></span><span style="display:flex;"><span>ssh-keygen: generating new host keys: RSA ECDSA ED25519
</span></span><span style="display:flex;"><span> * Starting sshd ... [ ok ]
</span></span><span style="display:flex;"><span> * Tiny Cloud - final phase ...
</span></span><span style="display:flex;"><span>   ++ userdata_runcmd: starting
</span></span><span style="display:flex;"><span>Setup complete
</span></span><span style="display:flex;"><span>   ++ userdata_runcmd: done
</span></span><span style="display:flex;"><span>   ++ bootstrap_complete: starting
</span></span><span style="display:flex;"><span>   ++ bootstrap_complete: done
</span></span><span style="display:flex;"><span> [ ok ]
</span></span></code></pre></div><p>I can now log into the VM via <code>ssh -p 2222 hugo@localhost</code>.</p>
<h1 id="controlling-the-vm">Controlling the VM<div class="permalink">[<a href="#controlling-the-vm">permalink</a>]</div></h1>
<p>Commands can be sent over <code>monitor.sock</code> using
<code>echo &quot;$COMMAND_HERE&quot; | socat - UNIX-CONNECT:monitor.sock</code>. An &ldquo;interactive
shell&rdquo; can be obtained with <code>socat stdio UNIX-CONNECT:monitor.sock</code>. Some
example commands:</p>
<ul>
<li><code>info status</code>: show a brief summary of the VM status</li>
<li><code>system_powerdown</code>: send ACPI shutdown signal (graceful shutdown)</li>
<li><code>quit</code>: immediately stop the VM</li>
<li><code>stop</code>: pause/freeze VM execution</li>
<li><code>cont</code>: continue VM execution</li>
<li><code>system_wakeup</code>: wake up from sleep</li>
<li><code>help</code>: show all available commands</li>
</ul>
<h1 id="further-steps">Further steps<div class="permalink">[<a href="#further-steps">permalink</a>]</div></h1>
<p>This is the basis for programmatically providing VMs using an upstream image as
a base. With further customisation of <code>user-data</code>, I can configure VMs for
specific purposes, and use them in scripted environments.</p>
]]></description></item><item><title>Status update 2025-11</title><link>https://whynothugo.nl/journal/2025/11/27/status-update-2025-11/</link><pubDate>Thu, 27 Nov 2025 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/11/27/status-update-2025-11/</guid><description><![CDATA[<h1 id="responses-api-in-libdav">Responses API in libdav<div class="permalink">[<a href="#responses-api-in-libdav">permalink</a>]</div></h1>
<p><a href="https://git.sr.ht/~whynothugo/libdav"><code>libdav</code></a> exposed functionality via
methods where all parameters must be specified. 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;-webkit-text-size-adjust:none;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span>caldav.create_collection(
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;/calendars/tasks/&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&amp;</span>[<span style="color:#f92672">&amp;</span>names::<span style="color:#66d9ef">CALENDAR</span>],
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&amp;</span>[<span style="color:#f92672">&amp;</span>prop] <span style="color:#75715e">// These are properties in XML form.
</span></span></span><span style="display:flex;"><span>).<span style="color:#66d9ef">await</span><span style="color:#f92672">?</span>;
</span></span></code></pre></div><p>The problem with this approach is adding new functionality. For example, I want
to allow specifying a display name, colour or other attributes of a calendar
when creating it. Adding extra arguments doesn&rsquo;t scale well: all call sites need
to be updated each time a new argument is added, and all call sites need to
specify several <code>None</code> values for properties which they&rsquo;re not using. It becomes
verbose and unreadable.</p>
<p>I&rsquo;ve finally refactored <code>libdav</code> to use a new &ldquo;Requests API&rdquo;, where each
functionality is exposed via a custom Request type implementing a <code>DavRequest</code>
trait. This trait has two methods: <code>prepare_request</code> and <code>parse_response</code>. As
you&rsquo;ve hopefully guessed, these methods prepare a new request and parse the
response respectively. The <code>WebDavClient</code> has a new <code>request()</code> method which
takes an instance of a <code>DavRequest</code> and executes it.</p>
<p>These separate <code>prepare_request</code> and <code>parse_response</code> methods result in a
cleaner layout and make testing much simpler by virtue of being able to test
both of these without any network I/O. On this topic, I strongly recommend
reading <a href="https://sans-io.readthedocs.io/">Network protocols, sans I/O</a>.</p>
<p><code>DavRequest</code> implementations like <code>CreateCalendar</code> have methods to specify
additional fields using a builder pattern. Here&rsquo;s an example using all the
fields for this request type:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">let</span> create_calendar <span style="color:#f92672">=</span> CreateCalendar::new(<span style="color:#e6db74">&#34;/calendars/work/&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// All of these calls are optional.
</span></span></span><span style="display:flex;"><span>    .with_display_name(<span style="color:#e6db74">&#34;Work Calendar&#34;</span>)
</span></span><span style="display:flex;"><span>    .with_colour(<span style="color:#e6db74">&#34;#FF0000&#34;</span>)
</span></span><span style="display:flex;"><span>    .with_order(<span style="color:#ae81ff">10</span>)
</span></span><span style="display:flex;"><span>    .with_components(<span style="color:#f92672">&amp;</span>[CalendarComponent::VEvent, CalendarComponent::VTodo]);
</span></span><span style="display:flex;"><span>webdav.request(create_calendar);
</span></span></code></pre></div><p>The resulting code is a greater total number of lines of code, but each
functionality is nicely split into its own file, making code easier to read,
follow and maintain. I recently wrote about <a href="/journal/2025/11/19/too-much-less-is-not-too-much-more/">the virtues of having a greater
amount of simpler code</a>.</p>
<p>I&rsquo;m pleased with the result of the implementation, and I will add additional
features (which were already planned) without having to add new arguments to
functions for each new feature introduced.</p>
<h1 id="streaming-api-for-vstorage">Streaming API for vstorage<div class="permalink">[<a href="#streaming-api-for-vstorage">permalink</a>]</div></h1>
<p>I&rsquo;ve refactored the synchronisation portion of vstorage/pimsync to operate on a
&ldquo;stream of operations&rdquo;. The general principle is that a plan with the actions to
perform during synchronisation is a <a href="https://docs.rs/futures-util/0.3.31/futures_util/stream/trait.Stream.html"><code>futures_util::stream::Stream</code></a> of
<code>Operation</code> objects, which each encode all the information required for their
execution.</p>
<p>The executor now also takes a Stream of operations too, executing them one at a
time. The main goal of this refactor was to improve how the daemon mode
operates: after the initial &ldquo;full&rdquo; synchronisation, the daemon will monitor for
individual changes, and send only operations for those individual changes to be
executed — instead of the current approach which just performs another full
sync. This Stream-based design enables doing exactly this, and also leaves room
for a lightweight implementation of <code>DAV:sync-token</code> from <a href="https://datatracker.ietf.org/doc/html/rfc6578">RFC 6578</a>.
<code>DAV:sync-token</code> allows querying the server for exactly which changes have
happened since the last sync without listing all items from all collections for
local comparison over and over.</p>
<p>The new Stream-based executor also allows improved concurrency control. At
present, up to eight concurrent operations execute at once, but I&rsquo;ll fine tune
this further in future. At present, the underlying <code>hyper</code> library <a href="https://github.com/hyperium/hyper/issues/3849">needs some
improvements for fine-grained control of connection pool</a>.</p>
<p>The new implementation probably does a few more memory indirections or CPU
cycles, but nothing which is practically noticeable. Due to the streaming nature
of the new design, memory usage should remain a bit lower, since a single
collection is enumerated at once now, rather than all of them beforehand.</p>
<p>The <code>sync</code> command, which performs a single synchronisation allowed viewing the
whole plan before its execution. This functionality remains: the whole plan is
simply collected into a huge array for display purposes, and then executed. This
usage is an edge case, so I don&rsquo;t consider the extra cost has any meaningful
impact.</p>
<h2 id="pre-fetched-items-during-planning">Pre-fetched items during planning<div class="permalink">[<a href="#pre-fetched-items-during-planning">permalink</a>]</div></h2>
<p>I also refactored the sync algorithm to simplify its logic. Previously, it would
pre-fetch all items that were new or had changed, then enter a planning phase
where it would again identify new or changed items by comparing their ETags and
hashes. I realise that if an item wasn&rsquo;t pre-fetched, that already implies it
hasn&rsquo;t changed; only pre-fetched items can differ from their previous versions
(even if some may end up unchanged because their edits cancel out).</p>
<p>At the same time, items which <em>have</em> changed are guaranteed to be pre-fetched.
As a result, the old code path that performed a late, lazy fetch was both
unnecessary and effectively unreachable — but still needed to be implemented
because of the earlier structure. With the refactor, that branch is gone, which
makes reading the code more straightforward.</p>
<h1 id="auto-conflict-resolution">Auto-conflict resolution<div class="permalink">[<a href="#auto-conflict-resolution">permalink</a>]</div></h1>
<p>I&rsquo;ve implemented automatic conflict resolution. Pimsync will automatically
resolve conflicts by keeping the item from whichever side is specified in the
configuration file, if any (there is no default value for this and the
functionality is disabled unless explicitly configured). This feature was
present in vdirsyncer and is now available in Pimsync too.</p>
<h1 id="imapgoose">ImapGoose<div class="permalink">[<a href="#imapgoose">permalink</a>]</div></h1>
<p>Since my last status update, I&rsquo;ve also been working a lot of ImapGoose. I&rsquo;ve
written about it on separate posts (<a href="/journal/2025/10/15/introducing-imapgoose/">intro</a>, <a href="/journal/2025/10/31/imapgoose-status-update-v0.3.2/">update</a>) over the last month. It&rsquo;s
currently close to a v1.0.0 release, with two issues blocking the release:</p>
<ol>
<li><a href="https://todo.sr.ht/~whynothugo/ImapGoose/17">Slow reconnection after sleep/resume</a></li>
<li><a href="https://todo.sr.ht/~whynothugo/ImapGoose/23">Initial import does not pre-process corrupt newlines</a></li>
</ol>
<p>ImapGoose focuses on a single use case: synchronising an IMAP server with a
local Maildir. Its narrower focus has made development much faster and much
smoother. Pimsync on the other hand, tries to cover all use cases in the same
domain, with a variety of storage backends. In the case of IMAP, there are
plenty of other tools for legacy servers and different scenarios. In the case of
Calendar and Contacts, we have a shortage of tools, so I&rsquo;ll continue trying to
cover all the usual scenarios.</p>
]]></description></item><item><title>Too much less is not too much more</title><link>https://whynothugo.nl/journal/2025/11/19/too-much-less-is-not-too-much-more/</link><pubDate>Wed, 19 Nov 2025 13:04:59 +0800</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/11/19/too-much-less-is-not-too-much-more/</guid><description><![CDATA[<p>I code with a &ldquo;less is more&rdquo; mindset. Typically, adding more lines of code
doesn&rsquo;t make something better, it&rsquo;s just more noise. I prefer to keep things
clear and concise. I also have a &ldquo;keep it simple&rdquo; mentality, preferring to write
straightforward code, and avoid clever abstractions.</p>
<p>Old programming wisdom tells us that to debug code, you need to be smarter than
the person who wrote it. If I use all my smarts to write code then I am, by
definition, not smart enough to debug it.</p>
<p>During the past months, I&rsquo;ve been changing how I write code a bit. I&rsquo;ve
identified that I focus too much on keeping the total amount of lines low.
Keeping code concise is great, but obsessing too much with total lines of code
is impairing my ability to write better software.</p>
<p>I&rsquo;ve recently been writing more code, but shorter functions; a larger amount of
simple abstractions. A function with 90 lines is pretty hard for a human to
follow. Splitting it into multiple functions with clearly defined scopes is
typically easier to understand and maintain. These multiple functions might be
30% more code in total, but I only ever need to keep in mind a single one of
them. Ideally, what each function does is explained in a short brief sentence.
When reading the code for any one of these functions, I only need to remember
what the others do, but don&rsquo;t need to worry about their internal implementation
details (i.e. I don&rsquo;t need to remember <em>how</em> they do it).</p>
<p>Too much code is not good, but too little code is also not good. Try to find the
sweet spot. Don&rsquo;t be scared of adding more code, especially if it removes code
from the already-complicated places.</p>
<hr>
<p>Most of what I&rsquo;m saying today is obvious, and some of it are lessons that we
learn during the first couple of years of programming. Yet it&rsquo;s still wisdom
that we should all remember to reflect upon, even after a few decades of coding.</p>
]]></description></item><item><title>Translation models between English and Chinese</title><link>https://whynothugo.nl/journal/2025/11/02/translation-models-between-english-and-chinese/</link><pubDate>Sun, 02 Nov 2025 19:31:26 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/11/02/translation-models-between-english-and-chinese/</guid><description><![CDATA[<p>I performed some tests comparing the results from different translation models
translating between English and Chinese. All of these models are available for
local offline use.</p>
<p>The goal is to find an ideal model to use on my own devices for translating this
language pair.</p>
<h1 id="overview">Overview<div class="permalink">[<a href="#overview">permalink</a>]</div></h1>
<p>I ran this in batches, with different sets of models each. I supplied all models
in each batch a set of texts. A native Chinese speaker proficient in English
evaluated the translations and ranked them from best to worst.</p>
<h1 id="first-batch-of-tests">First batch of tests<div class="permalink">[<a href="#first-batch-of-tests">permalink</a>]</div></h1>
<p>My first set of subjects were <a href="https://github.com/LibreTranslate/LibreTranslate">LibreTranslate</a> (with its current default model),
<a href="https://github.com/Helsinki-NLP/Opus-MT">Opus-MT</a> and <a href="https://huggingface.co/facebook/nllb-200-distilled-600M">NLLB-600M</a>. All of these run perfectly fine on consumer hardware.</p>
<p>Below is the verdict of the first batch of tests (I didn&rsquo;t keep more granular
results for the first batch).</p>
<h2 id="english-to-chinese">English to Chinese:<div class="permalink">[<a href="#english-to-chinese">permalink</a>]</div></h2>
<ul>
<li>LibreTranslate: good quality, unnaturally spoken, but clear.</li>
<li>Opus-MT: more naturally spoken, but results not necessarily as accurate.</li>
<li>NLLB: worst of the three.</li>
</ul>
<p>Based on these results, the recommendations are:</p>
<ul>
<li>For communicating with a Chinese speaker, use Opus-MT.</li>
<li>For language learning, use LibreTranslate (more literal).</li>
</ul>
<h2 id="chinese-to-english">Chinese to English<div class="permalink">[<a href="#chinese-to-english">permalink</a>]</div></h2>
<ul>
<li>LibreTranslate: not as good as the others, tense also wrong, misses a word.</li>
<li>Opus-MT: mixes up tense, but otherwise good.</li>
<li>NLLB: is very literal (not in a bad way).</li>
</ul>
<p>Based on these results, the recommendations are:</p>
<ul>
<li>For interpreting text in Chinese, use Opus-MT.</li>
<li>For communicating with an English speaker, use NLLB.</li>
</ul>
<h1 id="second-batch-of-tests">Second batch of tests<div class="permalink">[<a href="#second-batch-of-tests">permalink</a>]</div></h1>
<p>For my second batch of comparison, I used some newer (and also heavier) models:</p>
<ul>
<li>Opus-MT: same as before.</li>
<li>NLLB-600M: same as before.</li>
<li><a href="https://huggingface.co/facebook/nllb-200-3.3B">NLLB-3.3B</a>: heavier (and slower) than NLLB-600M.</li>
<li><a href="https://huggingface.co/docs/transformers/main/model_doc/madlad-400">MADLAD-400</a>: substantially heavier, and trickier to run on consumer hardware.</li>
<li><a href="https://huggingface.co/Unbabel/TowerInstruct-7B-v0.2">Tower</a>: Technically an LLM trained specially for translations, much heavier.</li>
</ul>
<p>For this second batch of tests, I translated from Chinese to English, and kept
more detailed notes of the results.</p>
<h2 id="test-21">Test 2.1<div class="permalink">[<a href="#test-21">permalink</a>]</div></h2>
<ul>
<li><strong>Input</strong>: 但说真的，一想到总有一天我也会把这个故事忘诸脑后，也是有点感伤呢。</li>
<li><strong>Opus-MT (6.70s)</strong>: But really, it&rsquo;s kind of sad to think that someday I&rsquo;m
going to forget about it.</li>
<li><strong>NLLB (7.93s)</strong>: But honestly, it&rsquo;s a bit painful to think that one day I&rsquo;ll
forget this story too.</li>
<li><strong>NLLB-3.3B (16.82s)</strong>: But to be honest, it&rsquo;s a little sad to think that one
day I&rsquo;ll forget this story too.</li>
<li><strong>MADLAD-400 (30.37s)</strong>: But, honestly, it&rsquo;s a little sad to think that one
day I&rsquo;ll forget this story.</li>
<li><strong>Tower (331.99s)</strong>: But to be honest, when I think about the fact that
someday I will forget this story, I feel a little sad.</li>
</ul>
<p>Opus-MT omits the subject in the second part, which really washes out the
meaning of the sentence. MADLAD omits &ldquo;too&rdquo;, which was considered as changing
the original meaning too much. Tower was considered the best since it retained
the original order of the statements, which in turn maintains its melancholic
tone.</p>
<p>The ranking for these is (from best to worst):</p>
<ul>
<li>Good results: Tower, NLLB-3.3B, NLLB.</li>
<li>Failed results: MADLAD-400, Opus-MT.</li>
</ul>
<h2 id="test-22">Test 2.2<div class="permalink">[<a href="#test-22">permalink</a>]</div></h2>
<p>The translation by deekseek.com is included here for reference.</p>
<ul>
<li><strong>Input</strong>: 也就是一刹那的事，一种说不清道不明的疲倦的伤怀爬上他的心头。天边的乌云正在散去，几缕阳光挟着风的轻巧和雨的温柔钻进窗棂，吻了吻桌前那盆已开始枯萎的雏菊，春末的气息就这样被若无其事地搅乱了。</li>
<li><strong>Opus-MT</strong> (8.06s): That is, at a moment, an inexplicable tired wound that
climbs over his heart. The clouds of the sky are dissipating, with the light
of the wind and the tenderness of the rain rushing into the window, kissing
the moist daisies that have begun to fade in front of the table, and the
breath of the end of the spring is disturbed by nothing.</li>
<li><strong>NLLB</strong> (10.22s): And it came to pass that a sore wound of fatigue, unknown
to the way, came upon his head. And the clouds of the sky were scattered, and
the light wind and the gentle rain of the sun drove into the curtain, and
kissed the basin before the table, which was beginning to wither, and the
breath of the end of spring was disturbed.</li>
<li><strong>NLLB-3.3B</strong> (28.12s): It was a moment, a weary sorrow, which he could not
explain, that climbed up his heart. The clouds of the sky were dissipating,
and the light of the sun, with the gentle wind and the gentle rain, penetrated
into the curtains, kissing the dew that had begun to wither in the pot before
the table, and the breath of late spring was thus disturbed without cause.</li>
<li><strong>MADLAD-400</strong> (58.45s): At that moment, a sort of unspeakable fatigue and
sadness climbed up to his heart. The dark clouds were dissipating, and a few
rays of sunlight, caught between the lightness of the wind and the gentleness
of the rain, penetrated the window, kissed the daisies in the pot before the
kiss table, which had begun to wither, and the air of the end of spring was
thus disturbed as if nothing had happened.</li>
<li><strong>Tower</strong> (621.65s): It was over in a flash, a kind of fatigue that was hard
to describe crept into his heart. The clouds on the horizon were dispersing,
and a few rays of sunlight, carried by the wind and the rain, crept through
the window and kissed the dying chrysanthemum</li>
<li><strong>Deepseek.com</strong>: In the blink of an eye, an indescribable, weary melancholy
crept into his heart. The dark clouds on the horizon were dispersing, and a
few strands of sunlight, carrying the lightness of the wind and the tenderness
of the rain, slipped through the window lattice. They kissed the pot of
withering daisies on the desk, quietly unsettling the late spring atmosphere.</li>
</ul>
<p>Tower&rsquo;s results here are truncated, and I later discovered that this is due to
how I configured my test service. This model is much less straightforward to run
than the others, and requires a lot of tuning and understanding of the domain
space which I lack.</p>
<ul>
<li>Ranking for passing results: MADLAD-400, NLLB-3.3B.</li>
<li>Ranking for failing results: Opus-MT, NLLB.</li>
</ul>
<h2 id="test-23">Test 2.3<div class="permalink">[<a href="#test-23">permalink</a>]</div></h2>
<ul>
<li><strong>Input</strong>: 澄澈而晴朗的苍穹万里无云，暖得有些过分的阳光仿佛给远方的风景草木都蒙上一层微微颤动的空气薄纱。这是故乡难得的好天气。周围很是安静，除了夏风轻轻掠过树木的声音和虫鸟的鸣叫，什么也听不见。他的马低头靠了过来，在他脸上嗅了嗅，喷出一个响鼻。</li>
<li><strong>Opus-MT</strong> (7.79s): The clear and clear sky is cloudless, and some of the
excessive sun warms up as if it were covered with a small tremor of air in
distant landscapes. This is a fine weather for home. The surroundings are very
quiet, and nothing can be heard except the sound of the summer breezes over
the trees and the sound of the bugbirds. His horse leans down, sniffs in his
face and snorts a loud nose.</li>
<li><strong>NLLB</strong> (11.98s): The clear and clear cloudless, warm and overly sunny
grassland was covered with a thin layer of air that gave the distant landscape
a slightly creeping tinge. It was a strange weather in his hometown. It was
very quiet, and nothing could be heard except the sound of the summer breeze
gently sweeping through the trees and the cries of insect birds. His horse&rsquo;s
lower head leaned over, sniffed in his face, and sprayed a whistling nose.</li>
<li><strong>NLLB-3.3B</strong> (30.70s): The clear and sunny sky was cloudless, the sun was
warm and a little too much, as if the air were thin and slightly shimmering
over the distant grass. This was the rare good weather in my hometown. It was
very quiet, nothing was heard around except the sound of the summer wind
gently sweeping through the trees and the chirping of insect birds. His horse
leaned over, sniffed on his face, and spat out a rattling nose.</li>
<li><strong>MADLAD-400</strong> (93.79s): The sky was clear and clear, and the sun was warm
enough to cover the distant landscape with a slightly fluttering veil of air.
It was a rarely good weather in his homeland. It was quiet, and nothing was
heard except the sound of the summer wind blowing through the trees and the
chirping of insects and birds. His horse leaned over, sniffed his face, and
gave a sniff.</li>
<li><strong>Tower</strong> (737.99s): The sky was clear and bright, with not a cloud in the
vast expanse of blue. The warm sunshine seemed to cast a light veil over the
distant scenery and vegetation. This was a rare good day in the homeland. The
surroundings were quiet, except for the sound of the</li>
</ul>
<p>Again, Tower&rsquo;s result is truncated, due to the configuration I used for its
output. Its results would have been considered the best of all five had it not
been truncated.</p>
<p>Ranking for passing results: MADLAD-400, NLLB-3.3B. Failed: NLLB, Opus-MT.</p>
<h2 id="test-24">Test 2.4<div class="permalink">[<a href="#test-24">permalink</a>]</div></h2>
<ul>
<li><strong>Input</strong>: 这大概是一条颠扑不破的真理，一段平淡的旧事总有戏剧化的开端。</li>
<li><strong>Opus-MT</strong> (6.45s): This is probably an intransigent truth, and there is
always a dramatic beginning to a flat old story.</li>
<li><strong>NLLB</strong> (7.93s): This is probably an unflinching truth, a plain old thing
that always begins dramatically.</li>
<li><strong>NLLB-3.3B</strong> (17.41s): It&rsquo;s probably an unbreakable truth that a plain old
thing always has a dramatic beginning.</li>
<li><strong>MADLAD-400</strong> (26.26s): It is probably an incontrovertible truth that a dull
old story always has a dramatic beginning.</li>
<li><strong>Deepseek.com</strong>: This is likely an unshakable truth: a mundane old tale
always possesses a dramatic beginning.</li>
</ul>
<p>At this point I removed Tower from the list. It&rsquo;s clear that it consistently
provides the best results, but requires an excessive amount of RAM (~25GB), and
over 10 minutes to process these samples. Ideally, Tower needs to be run on GPU
rather than CPU, and much more fine tuning than I was interested in doing for
this experiment. It can technically run on consumer hardware, but not on most of
my devices.</p>
<p>Ranking:</p>
<ul>
<li>Ranking for passing results: MADLAD-400</li>
<li>Ranking for failing results: NLLB-3.3B, Opus-MT, NLLB.</li>
</ul>
<h2 id="test-25">Test 2.5<div class="permalink">[<a href="#test-25">permalink</a>]</div></h2>
<ul>
<li><strong>Input</strong>: 旅游签证的发放对象是以观光旅游为目的的申请人。因此，除观光旅游外的短期访问目的都不属于此范围（除了部分签证）。</li>
<li><strong>Opus-MT</strong> (1.23s): Tourism visas are granted to applicants for tourist
purposes.</li>
<li><strong>Opus-MT</strong> (6.92s): Tourism visas are granted to applicants for tourist
purposes. Thus, short-term visits, with the exception of tourist tours, do not
fall within this scope (with the exception of partial visas).</li>
<li><strong>NLLB</strong> (8.59s): The object of the tourist visa is the applicant for tourist
purposes. Therefore, no short-term visit purposes other than tourist tourism
fall within this scope (except for some visas).</li>
<li><strong>NLLB-3.3B</strong> (33.25s): Tourist visas are issued to applicants for the purpose
of sightseeing tourism. Therefore, short-term visits for purposes other than
sightseeing tourism are not covered (except for some visas).</li>
<li><strong>MADLAD-400</strong> (32.56s): Tourist visas are issued to applicants for the
purpose of tourism. Therefore, short-term visits for purposes other than
tourism are not covered (except for some visas).</li>
</ul>
<p>During the initial run Opus-MT only translated the first sentence. It turns out
that this model only supports translating a single sentence at a time. I worked
around this by updating my test script to split sentences, translate them
individually, and then join them again.</p>
<p>All models passed this test, from best to worst: NLLB-3.3B, MADLAD-400, NLLB,
Opus-MT.</p>
<h2 id="test-26">Test 2.6<div class="permalink">[<a href="#test-26">permalink</a>]</div></h2>
<ul>
<li><strong>Input</strong>: 对于已注册用户，可以点击页面右上角的“登录”按钮，并输入自己的账号和密码进行登录。登录成功后，用户即可享受网页版提供的各项功能，如浏览商品、下单购买、查看订单等。</li>
<li><strong>Opus-MT</strong> (7.08s): For registered users, you can click on the &quot; Login &quot;
button at the top right corner of the page and enter your own account number
and password to log in. After a successful login, users will be able to enjoy
the various functions offered on the web page, such as browsing commodities,
purchase orders, viewing orders, etc.</li>
<li><strong>NLLB</strong> (8.93s): For registered users, you can click the login button in the
top right corner of the page and enter your account and password to log in.
After successful log-in, you can enjoy the features of the web version, such
as browsing goods, ordering, ordering, etc.</li>
<li><strong>NLLB-3.3B</strong> (22.84s): For registered users, you can click on the login
button in the upper right corner of the page and enter your account and
password to log in. After successful login, you can enjoy the features offered
by the web page, such as browsing products, ordering purchases, viewing
orders, etc.</li>
<li><strong>MADLAD-400</strong> (47.52s): For registered users, you can click the &ldquo;Login&rdquo;
button in the upper right corner of the page and enter your account number and
password to log in. After successful login, users can enjoy the various
functions offered by the web version, such as browsing products, placing
purchase orders, viewing orders, etc.</li>
</ul>
<p>All models passed this test, from best to worst: MADLAD-400, Opus-MT/NLLB-3.3B
(draw), NLLB.</p>
<h2 id="test-27">Test 2.7<div class="permalink">[<a href="#test-27">permalink</a>]</div></h2>
<ul>
<li><strong>Input</strong>: 身后有余忘缩手，眼前无路想回头。</li>
<li><strong>Opus-MT</strong> (6.84s): Behind them, there is no way to look back.</li>
<li><strong>NLLB</strong> (7.65s): I have a shrinking hand behind me, and I can&rsquo;t look back.</li>
<li><strong>NLLB-3.3B</strong> (16.79s): The people who are left behind are those who have
forgotten and have no way of looking back.</li>
<li><strong>MADLAD-400</strong> (35.74s): There is no way to turn back.</li>
<li><strong>Human translation</strong>: Forget to withdraw while there is still space behind,
and only think of turning back when no path is seen in front.</li>
</ul>
<p>All models failed this test.</p>
<h1 id="third-batch-of-tests">Third batch of tests<div class="permalink">[<a href="#third-batch-of-tests">permalink</a>]</div></h1>
<p>The third and final batch of tests focuses on the same models as the second
batch, but translating English to Chinese.</p>
<h1 id="test-31">Test 3.1<div class="permalink">[<a href="#test-31">permalink</a>]</div></h1>
<ul>
<li><strong>Input</strong>: The database was built from scratch. It did not have this many
functions. And guess what? People are now asking for more! There is no end to
their greed.</li>
<li><strong>Opus-MT</strong>
(1.67s): 数据库是从头建立起来的。 它没有这么多职能。 你猜怎么着? 现在人们在要求更多! 他们的贪婪是没有尽头的。</li>
<li><strong>NLLB</strong>
(8.32s): 据说数据库从头开始,它没有这么多功能. 猜猜什么?人们现在要求更多!他们的贪无止境.</li>
<li><strong>NLLB-3.3B</strong>
(19.62s): 数据库是从头开始建立的. 它没有这么多功能. 猜猜吧? 人们现在要求更多! 他们的贪没有尽头.</li>
<li><strong>MADLAD-400</strong>
(35.68s): 数据库是从零开始构建的,它没有这么多的功能。 猜猜看? 人们现在要求更多! 他们的贪婪没有止境。</li>
</ul>
<p>Ranking (all pass): Opus-MT, MADLAD, 3.3B, NLLB</p>
<h1 id="test-32">Test 3.2<div class="permalink">[<a href="#test-32">permalink</a>]</div></h1>
<p><strong>Input</strong>: This man is completely out of his sense. He speaks madness. Nonsense!</p>
<ul>
<li><strong>Opus-MT</strong> (7.30s): 这个男人完全疯了。 他讲疯话。 胡说八道!。</li>
<li><strong>NLLB</strong> (8.00s): 这个人完全失去了头脑,他说话是疯狂的.</li>
<li><strong>NLLB-3.3B</strong> (16.22s): 这个人完全失去了理智.他说疯狂.胡说八道!</li>
<li><strong>MADLAD-400</strong> (24.70s): 这个人完全失去理智了 他说的疯了 胡说八道</li>
</ul>
<p>Ranking:</p>
<ul>
<li>Passing: Opus-MT</li>
<li>Failed: MADLAD / NLLB-3.3B (draw), NLLB</li>
</ul>
<h1 id="test-33">Test 3.3<div class="permalink">[<a href="#test-33">permalink</a>]</div></h1>
<p><strong>Input</strong>: It is hard to tell which one he is quietly and deeply gazing at, the
bloody hue of the sinking sun at the horizon, or that pale azure moon hanging in
the early summer sky. Translation: en → zh</p>
<ul>
<li><strong>Opus-MT</strong>
(6.72s): 很难分辨他静静地、深深地注视着谁, 地平线下沉的太阳的血腥光芒, 或夏日初的苍蓝的月亮。</li>
<li><strong>NLLB</strong>
(8.61s): 很难说他静静地深地看着哪个,即向平线下沉的阳光的血色,还是在夏天早些时候的蓝色月亮.</li>
<li><strong>NLLB-3.3B</strong>
(22.13s): 很难说他静静地深深地凝视着哪一个, 血的阴影的日落在地平线上, 或那个白的蓝色的月亮悬挂在夏天的天空.</li>
<li><strong>MADLAD-400</strong>
(35.45s): 很难分辨他是静静地、深深地凝视着哪一个,是地平线上落下的血色太阳,还是初夏天空中那颗苍白的蓝月亮。</li>
</ul>
<p>Ranking:</p>
<ul>
<li>Passing: MADLAD, Opus-MT.</li>
<li>Failing: NLLB-3.3B, NLLB.</li>
</ul>
<h1 id="conclusions">Conclusions<div class="permalink">[<a href="#conclusions">permalink</a>]</div></h1>
<p>Tower consistently seems to provide the best translations, but is also in
another category of resource requirements and speed. I find it unsuitable for
modern consumer hardware.</p>
<p>Of the remaining models, all failed at least two tests. There is also a clear
asymmetry depending on the direction of the translation. When translating simple
or technical text (e.g.: not metaphors or poetry), all the models provide useful
results.</p>
<p>Opus-MT stands out positively when translating English to Chinese, and does a
generally okay job translating Chinese to English. It is by far the lightest and
fastest of the models, and quite suitable for average consumer devices.</p>
<p>MADLAD-400 stands out in the opposite direction, when translating Chinese to
English, but can also yield imperfect results. This model is noticeably slower
than Opus-MT, and uses a huge amount of RAM (between 30 and 40GB).</p>
<p>NLLB-3.3B does a pretty good job translating Chinese to English, but did rather
poorly in all tests in the opposite direction. NLLB-3.3B uses around 13–16GB,
while NLLB-600M uses around 2.5–3GB. This makes the former somewhat unsuitable
for most devices (especially phones), while the latter can run on much more
diverse hardware.</p>
]]></description></item><item><title>ImapGoose status update: v0.3.2</title><link>https://whynothugo.nl/journal/2025/10/31/imapgoose-status-update-v0.3.2/</link><pubDate>Fri, 31 Oct 2025 14:49:57 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/10/31/imapgoose-status-update-v0.3.2/</guid><description><![CDATA[<p>Plenty has happened since my <a href="/journal/2025/10/15/introducing-imapgoose/">initial announcement of ImapGoose</a>.
First of all, I implemented pipelining of uploads and downloads, which
dramatically improved speeds (initial tests yielded ~1GB messages per minute on
my host).</p>
<h1 id="scanning-and-task-queue">Scanning and task queue<div class="permalink">[<a href="#scanning-and-task-queue">permalink</a>]</div></h1>
<p>An important bug quickly came to light: the local maildirs are never fully
re-scanned. If any changes happened while ImapGoose <em>was not</em> running, those
changes would not be detected the next time it ran— or ever!</p>
<p>My first approach at fixing this introduced substantial complexity into the task
dispatching mechanism. There were no obvious faults with it, but it was too
complex for any obvious faults to be visible. I redesigned the task queue
keeping in mind all possible scenarios.</p>
<p>The resulting design is also quite simple: the IMAP listeners emits events
indicating which mailbox has changed (this is all the information we get). The
filesystem watcher emits events indicating which mailbox and file changed. In
case of the initials start-up event, no file is specified, and this implies that
the entire directory needs to be scanned for new files, deleted files, or
changed flags.</p>
<p>The dispatcher accumulates events received and executes sync tasks based on
this. Sync tasks are basically three:</p>
<ul>
<li>Scan a single message in filesystem (and sync change to IMAP).</li>
<li>Scan entire filesystem (and sync changes to IMAP).</li>
<li>Scan IMAP (and sync changes to local filesystem).</li>
</ul>
<p>There is no longer a distinction between &ldquo;Full Sync&rdquo; and &ldquo;Incremental Sync&rdquo; when
deciding <em>what to do</em>. When performing an IMAP scan, the workers use
<code>CONDSTORE/QRESYNC</code> (which returns a list of messages that have changed since
last time), or fetch all UIDs depending on whether the mailbox has been seen
before or not. This is a decision on <em>how</em> to scan the remote, and not a
decision on <em>what</em> to do, so it&rsquo;s one layer down.</p>
<p>A noticeable change of the refactor is that scanning the remote mailbox for
changes is no longer implied in all sync events: if only a local file (or files)
changed, we can determine what to do based on that file (or those files) and the
status repository. Since we&rsquo;ve been monitoring the remote mailbox, we know its
latest state already and can decide what to do without any network I/O.</p>
<p>All these changes brought around another simplification. Previously, during the
start-up sequence, both the listener and watcher would trigger full sync events
for all known mailboxes. This implied duplicate sync events for mailboxes on
both sides. I had implemented some special treatment for these to be properly
deduplicated. Now queued sync events are <em>directional</em>, so while both the
listener and watcher queue events, they&rsquo;re complementary events: one syncs local
changes to the IMAP server and the other syncs remote changes to the local
filesystem. We don&rsquo;t need special de-duplication for them, and this simplifies
the start-up sequence of the dispatcher.</p>
<h1 id="nested-mailboxes">Nested mailboxes<div class="permalink">[<a href="#nested-mailboxes">permalink</a>]</div></h1>
<p>Some issues surfaced relating to nested mailboxes. My initial design mapped the
hierarchy separation to a period in Maildir names, inspired by the same
behaviour in offlineimap. The mailbox <code>lists/mine</code> was stored on disk as
<code>lists.mine</code>, but the mailbox <code>lists.mine</code> would also be stored on disk with the
same name— a collision.</p>
<p>Eventually, I settled on fully supporting nested mailboxes. <code>notmuch</code> supports
these just fine, and there&rsquo;s realistically no problem with this. It&rsquo;s likely
what users of nested mailboxes expect too. The only limitation is that the
mailbox names <code>cur</code>, <code>new</code> and <code>tmp</code> are prohibited, since these names have
special meanings in Maildir structures.</p>
<p>Because directory mapping changed, installations running a version prior to
v0.2.0 need to migrate. The new <code>-m</code> command executes a migration, moving
directories and updating the status database accordingly.</p>
<h1 id="single-sqlite-connection">Single sqlite connection<div class="permalink">[<a href="#single-sqlite-connection">permalink</a>]</div></h1>
<p>Some users reported an occasional &ldquo;database is locked&rdquo; error from sqlite. This
was caused by Go using multiple connections for sqlite by default. While one
connection was writing, the other tried to write the same row and failed due to
the row being locked. Fixing this was as simple as adding a call to
<code>db.SetMaxOpenConns(1)</code>. For unknown reasons, I was never able to personally
reproduce this issue, despite having synchronised hundreds of thousands of
messages.</p>
<h1 id="placing-new-messages-in-new">Placing new messages in <code>new/</code><div class="permalink">[<a href="#placing-new-messages-in-new">permalink</a>]</div></h1>
<p>Some MUAs make a distinction between the <code>new/</code> and <code>cur/</code> directories inside a
maildir. ImapGoose now places new unseen messages in <code>new/</code> and other messages
in <code>cur/</code>. This should also help writing scripts to notify of new recent unseen
messages.</p>
<h1 id="concurrent-edits-on-flags">Concurrent edits on flags<div class="permalink">[<a href="#concurrent-edits-on-flags">permalink</a>]</div></h1>
<p>It&rsquo;s possible for a message to have conflicting flag edits, and this is now
handled appropriately. For example: a message is synchronised initially with no
flags. The IMAP version is externally edited to add the Replied flag and the
local message is edited to add the Seen flag. When synchronising after these two
events, we need to detect which flags changed exactly. When ImapGoose reads the
IMAP message with only the Replied flag, it will compare it with the status. The
message had no flag last time, which implies that the Replied flag is new. This
is then added to the local copy, but this operation needs to only <em>append</em> a
flag without overwriting others (in this case, the new Seen flag which was added
locally).</p>
<p>Additionally, when saving the result into the status, we don&rsquo;t record the Seen
flag, because that one is not in sync. This particular operation synced
remote-to-local flags only, and the status now records that the last time items
were in sync, they all had the Replied flag.</p>
<p>In this particular scenario, we&rsquo;ll always have another pending task to sync
local-to-remote, which will detect the Seen flag that was added locally, and
replicate that to the remote server, eventually having both sides with the same
view.</p>
<p>It took me several iterations to get the algorithm that was just right for this.
My initial approach was to do a two-way sync of flags, but this introduced some
added complexity and was somewhat non-obvious to follow. The final design is
solid and simple to reason about.</p>
<h1 id="ignoring-flags-in-filename-matches">Ignoring flags in filename matches<div class="permalink">[<a href="#ignoring-flags-in-filename-matches">permalink</a>]</div></h1>
<p>When looking for a file with an expected name, it&rsquo;s possible that the file does
not exist, but the message hasn&rsquo;t been deleted— it&rsquo;s been renamed. This is
because every time that flags change in a Maildir message, the message has to be
renamed (the flags are reflected in the filename). This caused some issues in
cases where ImapGoose tries to read a file and its flags have changed (and the
change hasn&rsquo;t been processed yet). To work around this, when looking for a file,
ImapGoose needs to find any other file in the same directory with the same
prefix and different flags. This is a computationally expensive operation, since
it requires reading all the directory entries.</p>
<p>The extra cost doesn&rsquo;t seem to have a noticeable impact on performance. This
operation cannot be skipped, because otherwise when a message&rsquo;s flags change,
ImapGoose would delete the remote message, and then re-upload it with the new
flags.</p>
<h1 id="user-service-files">User service files<div class="permalink">[<a href="#user-service-files">permalink</a>]</div></h1>
<p>v0.3.0 includes service files for running ImapGoose as a user service under
OpenRC and systemd (thanks <a href="https://craftyguy.net/">Clayton</a>).</p>
<h1 id="initial-index-import">Initial index import<div class="permalink">[<a href="#initial-index-import">permalink</a>]</div></h1>
<p>I added support for converging an existing setup. That is, an IMAP account and a
set of local Maildirs which are already in-sync. An additional condition is
required for this to work: messages in the Maildir must have a <code>U=71931</code> portion
in their filename, where <code>71931</code> is the UID of its counterpart on the IMAP
server. <a href="https://www.offlineimap.org/">OfflineIMAP</a> encodes filenames this way,
and I&rsquo;ve heard that <a href="https://isync.sourceforge.io/mbsync.html">mbsync</a> does too.</p>
<p>This initial import is implemented as <code>imapgoose -i</code>, which scans all local
messages and compares them to the remote message with the same UID. If they are
the same, ImapGoose stores into its status database that these two messages are
the same and in-sync. No changes are made during this operation. Its output is
as follows:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span># Repeated for every mailbox:
</span></span><span style="display:flex;"><span>Mailbox indexed mailbox=INBOX identical=433 mismatched=1 missing=6
</span></span><span style="display:flex;"><span># And finally:
</span></span><span style="display:flex;"><span>Account indexed account=personal identical=83084 mismatched=4 missing=44
</span></span></code></pre></div><p><code>mismatched</code> indicates that a file has a <code>U=</code> flag, but doesn&rsquo;t match the other
copy byte-to-byte or that its flags have diverged. The latter case is the usual
one. <code>missing</code> indicates a file has a <code>U=</code>, but no message exists on the server
with that UID.</p>
<p>This initial import feature allows quickly migrating to ImapGoose from other
tools &ldquo;importing&rdquo; the existing configuration and syncing only newer changes in
future. Such migrations are one-way. After ImapGoose has imported a mailbox like
this, running it again without <code>-i</code> will start synchronising messages. Switching
back to the previous tool at this point might be problematic, since its own
internal status is ignorant of the changes done by ImapGoose.</p>
<p><code>imapgoose -i</code> is safe to run at a later time, and is essentially idempotent. It
should never be used while another instance is already running.</p>
<h1 id="simplicity-through-constraints">Simplicity through constraints<div class="permalink">[<a href="#simplicity-through-constraints">permalink</a>]</div></h1>
<p>Much of ImapGoose&rsquo;s simplicity comes from supporting only one specific use case.</p>
<p>I&rsquo;ll use vdirsyncer as a counter-example: it can sync filesystem to caldav, or
caldav to caldav, or webdav to filesystem, etc. Due to this permutation of
choices, all these &ldquo;backends&rdquo; are implemented using a common API. This somewhat
constrains the design in some ways. We can&rsquo;t have one storage implement a
feature that others do not. Or we need others to provide fallback, or to make
the feature &ldquo;optional&rdquo;, and have call sites work around a feature&rsquo;s potential
non-availability.</p>
<p>On the other hand, in ImapGoose, the IMAP listener and Mailbox watcher return
entirely different event types:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// Listener sends: &#34;remote changed&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">RemoteEvent</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Mailbox</span> <span style="color:#66d9ef">string</span> <span style="color:#75715e">// Just mailbox name</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">// Watcher sends: &#34;local changed&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">LocalEvent</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Mailbox</span> <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">AbsPath</span> <span style="color:#66d9ef">string</span> <span style="color:#75715e">// Empty = full mailbox, non-empty = specific file</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>When scanning an IMAP remote, we can ask it for a list including only items
which have changed, when we scan a filesystem, we need to read all file entries
ourselves. This is why ImapGoose can&rsquo;t sync one IMAP server to another: it&rsquo;s
designed around this asymmetry.</p>
<h1 id="service-discovery">Service discovery<div class="permalink">[<a href="#service-discovery">permalink</a>]</div></h1>
<p>The <code>server</code> configuration parameter is now optional. If no server is specified,
it is determined by using DNS-based service discovery. As a reminder, your local
DNS server MUST be a validating DNSSEEC server in order to avoid MITM attacks.</p>
<h1 id="performance-improvements">Performance improvements<div class="permalink">[<a href="#performance-improvements">permalink</a>]</div></h1>
<p>Large synchronisation operations sometimes took quite long. It turns out that I
had lots of N+1 queries. I&rsquo;m aware of the usual wisdom against these, but I
wrote them thinking about sqlite&rsquo;s article titled <a href="https://www.sqlite.org/np1queryprob.html">Many Small Queries Are
Efficient In SQLite</a>. Ultimately, one query with all results is
still dramatically faster.</p>
<p>With that particular issue fixed, performance is good enough at this point that
I&rsquo;m not particularly inclined to tinker with it further. I am aware of places
where there is room for theoretical improvements, but ImapGoose can max out my
network uplink, so it&rsquo;s pointless.</p>
<h1 id="crlf-normalisation">CRLF normalisation<div class="permalink">[<a href="#crlf-normalisation">permalink</a>]</div></h1>
<p>Messages transmitted over the network are always transmitted with <code>\r\n</code>
(carriage return, newline) at the end of each line. When we download messages,
they&rsquo;re saved with these line endings. When we upload messages, they are
expected to have the same line ending. In theory, any tool that writes to a
Maildir SHOULD respect this convention. In practice, they don&rsquo;t. <code>notmuch</code> even
removes a single <code>\r</code> from a single line when a message is moved across
mailboxes.</p>
<p>In theory, changing any lines of a message might invalidate any signature which
it may contain. In practice, if the message was missing CR, it couldn&rsquo;t have
been transmitted anywhere without being changed anyway. Since messages can&rsquo;t be
uploaded without fixing a missing CR, they&rsquo;re now automatically fixed (and a
warning is logged).</p>
<p>Messages which we download are guaranteed to end in CRLF (because of how the
IMAP protocol encodes them), so messages which were downloaded and moved around
are not being tampered in any way.</p>
<h1 id="keeping-connections-alive">Keeping connections alive<div class="permalink">[<a href="#keeping-connections-alive">permalink</a>]</div></h1>
<p>Ideally, the connection with the <code>NOTIFY</code> listener remains permanently open.
Keeping the connection open is relatively cheap. Through it, we can receive
updates immediately, react instantly and fetch any updates that happened
remotely.</p>
<p>If the connection is dropped, ImapGoose reconnects and sets up a new <code>NOTIFY</code>
listener, but also needs to ask for the new status of all mailboxes. This adds
some network overhead which is not ideal (but necessary) during each
reconnection.</p>
<p>Many servers close idle connections after a while, so ImapGoose sends a <code>NOOP</code>
(basically a ping) to the server every 15 minutes to keep the connection alive.
Initial development showed that servers commonly close connections after 30
minutes, so this worked well to keep those connections alive. However, many
servers use a much shorter timeout (as short as 5 minutes). When connected to
these servers, ImapGoose would need to reconnect every 5 minutes, adding
unnecessary overhead.</p>
<p>Making the interval configurable isn&rsquo;t practical since people don&rsquo;t have a
convenient way to determine the right value. I changed the interval to 3
minutes, which works for servers with short timeouts while adding minimal burden
for those with longer timeouts.</p>
<p>I&rsquo;m still considering implementing auto-detection of how long a server takes to
disconnect an idle connection. It&rsquo;s possible to distinguish between a clean
disconnect and a network glitch. ImapGoose could record the interval after which
the server closes the connection, store this timeout, and only send pings every
half a period.</p>
<h1 id="development-reflections">Development reflections<div class="permalink">[<a href="#development-reflections">permalink</a>]</div></h1>
<h2 id="purity-of-git-history">Purity of git history<div class="permalink">[<a href="#purity-of-git-history">permalink</a>]</div></h2>
<p>I&rsquo;ve been iterating on how I code with these new projects.</p>
<p>I did a lot of short micro-commits, then reviewing, keeping notes of bugs, and
iterating with the model. When I&rsquo;m done implementing a feature, I have a large
amount of commits, many of which have changes known to be faulty in some way,
and some introducing hundreds of lines of code which are removed in the
following commit. All these commits are steps in the right direction, but are a
huge amount of noise. They all get squashed into one commit.</p>
<p>This results in larger commits, some a bit harder to follow since they mix a new
feature with a refactor of another function which I noticed was necessary along
the way. These types of commits are harder to review, and not ideal when sending
somebody else patches. But when working on a new project where nobody else is
looking at changes, these details don&rsquo;t really matter.</p>
<p>By removing this burden of &ldquo;keep small tidy commits&rdquo; I&rsquo;ve also moved faster,
since I can focus on fixing the task without focusing on keeping clean commits.
When sending patches to someone else, I sometimes need to split a large change
into multiple commits, sometimes in a shuffled order. This kind of rebasing
takes time, and is only meaningful if someone else will be reviewing commits, in
order, and understanding the full context.</p>
<h2 id="multiple-flags-in-go">Multiple flags in Go<div class="permalink">[<a href="#multiple-flags-in-go">permalink</a>]</div></h2>
<p>Go likes to be special and different in some unique aspects. One of these is
command line arguments and flags. Go doesn&rsquo;t support the classic single-letter
flags (like <code>ls -lt</code>). It only supports single-dash full-word flags like
<code>-list -time</code>.</p>
<p>ImapGoose has <code>-m</code> and <code>-v</code>, but using <code>-mv</code> didn&rsquo;t work: Go interprets that as
a single <code>mv</code> flag rather than two separate flags.</p>
<p>I could have written a paragraph in the documentation warning about this, but it
seems cleaner to just ignore Go&rsquo;s flag parsing abilities and parse these flags
on my own. It&rsquo;s honestly less than 50 lines of code anyway.</p>
<h1 id="current-status">Current status<div class="permalink">[<a href="#current-status">permalink</a>]</div></h1>
<p>The latest releases contain mostly minor fixes and optimisations. Work on
ImapGoose has slowed down, with it being stable at this point. I&rsquo;ll gather
feedback on the current version for some days, and then tag a v1.0.0.</p>
]]></description></item><item><title>Introducing ImapGoose</title><link>https://whynothugo.nl/journal/2025/10/15/introducing-imapgoose/</link><pubDate>Wed, 15 Oct 2025 14:10:09 +0200</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/10/15/introducing-imapgoose/</guid><description><![CDATA[<p>ImapGoose is a small program to keep local mailboxes in sync with an IMAP
server. The wording &ldquo;keep […] in sync&rdquo; implies that it does so continuously,
rather than a one-time sync. ImapGoose is designed as a daemon, monitoring both
the IMAP server and the local filesystem, and immediately synchronising changes.
When the IMAP server receives an email, it shows up in the filesystem within a
second. When an email is deleted on another email client, it is removed<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>
from the filesystem within a second.</p>
<p>ImapGoose is highly optimised to reduce the amount of network traffic and tasks
performed. To do so, it relies on a few modern IMAP extensions and only supports
modern email servers. &ldquo;Modern servers&rdquo; in the context of email means <em>servers
which support extensions which were standardised between 2005 and 2009</em>.</p>
<p>ImapGoose uses the CONDSTORE extension (<a href="https://www.rfc-editor.org/rfc/rfc4551">standardised in 2006</a>),
which basically allows it to tell the server &ldquo;I last saw this mailbox when it
was in state XYZ, please tell me what&rsquo;s new&rdquo;. This avoids the need to download
an entire message list (which can be tens of thousands of emails), making
incremental syncs much more efficient. It also uses the QRESYNC extension
(<a href="https://www.rfc-editor.org/rfc/rfc5162">standardised in 2008</a>) so that the server includes a list of deleted
messages too (i.e. <code>VANISHED</code>). Finally, ImapGoose uses the NOTIFY extension
(<a href="https://www.rfc-editor.org/rfc/rfc5465">standardised in 2009</a>), which allows an IMAP client to tell the server
&ldquo;please let me know when there are changes to these mailboxes&rdquo;, and then leave a
connection open. <code>NOTIFY</code> has two nice consequences: (1) the client doesn&rsquo;t need
to ask the server if there have been any changes at regular intervals, and (2)
the client is informed of any changes immediately, so they can be processed
without delay. Unlike the older IDLE extension (from 1996), NOTIFY (from 2009)
allows monitoring multiple mailboxes per connection, rather than just one.</p>
<p>In this article, I&rsquo;ll cover some of the general design details, inner workings
and other development details.</p>
<h1 id="general-mode-of-operation">General mode of operation<div class="permalink">[<a href="#general-mode-of-operation">permalink</a>]</div></h1>
<p>First off, ImapGoose keeps a small status database with some minor metadata
about the last-seen status of both the server and local Maildirs. This includes
the mapping between server UIDs and filesystem filenames. Its general design is
strongly inspired by <a href="https://blog.ezyang.com/2012/08/how-offlineimap-works/">how OfflineIMAP works</a>.</p>
<p>At start-up, ImapGoose lists all mailboxes in the server and in the local
filesystem. It then starts monitoring them (the server via NOTIFY, the client
via inotify/kqueue), so we receive notifications of any changes that may happen
after our initial listing. This ensures that, for example, if we receive a new
email while performing the initial sync, we get a notification for it.</p>
<p>Once monitoring is set up, ImapGoose queues a task to perform a full sync of
each mailbox. Initially, we determine if this is the first time we see this
mailbox by its absence in the status database. If this mailbox has not been seen
before, then we request all messages. The server returns all of these along with
a <code>HIGHESTMODSEQ</code>, which we store in the status database. This <code>HIGHESTMODSEQ</code>
is a numeric property of each mailbox and increases every time a change occurs
inside that mailbox. If a mailbox has been seen before, then we can ask the
server for changes since that <code>HIGHESTMODSEQ</code>, which delivers only the minimal
amount of data which we need, and nothing else about all the other thousands of
unchanged messages.</p>
<p>When a message is present in the server and absent in the filesystem (or vice
versa), we need to determine whether it is a new message, or if it is a message
that was previously present in both and deleted from the local filesystem. To
determine this, we use the status database and apply the exact same algorithm as
offlineimap. It&rsquo;s simple and well tested.</p>
<p>At times, ImapGoose may disconnect from the server (for example, due to a laptop
disconnecting from Wi-Fi, or going into sleep mode). It will try to re-connect
automatically using an exponential back-off: after 1 second, then after 2
seconds, 4 seconds, 8 seconds, 16 seconds, 32 seconds,… all the way up to 17
minutes. Then it will continue retrying every 17 minutes. This means users don&rsquo;t
really have to worry about ImapGoose&rsquo;s current state, whether it&rsquo;s still
working, etc. It knows how to back-off when there&rsquo;s no network and how to get
back to work when it is feasible again.</p>
<p>As mentioned above, ImapGoose &ldquo;queues&rdquo; sync tasks. Internally, it uses a task
queue; when changes are detected on the server, a task to sync that entire
mailbox is queued. A worker picks this up from the queue, asks for changes in
that mailbox, and synchronises them. When changes are detected in the
filesystem, a task to sync that particular message is queued. It may happen that
multiple messages arrive in quick succession for the same mailbox. In this case,
we don&rsquo;t want to trigger multiple syncs of the same mailbox, and we especially
don&rsquo;t want two workers to sync the same mailbox concurrently: this would quickly
lead to duplicate emails.</p>
<p>To work around concurrent syncs and redundant mailbox updates, ImapGoose uses a
&ldquo;dispatcher&rdquo;, which hands off sync tasks to workers. When a task to sync a
specific mailbox is handed to a worker, that mailbox is marked as &ldquo;busy&rdquo;, and we
don&rsquo;t process other tasks for that queue until that worker notifies that it has
finished its work on that mailbox. While a worker is synchronising a mailbox, we
may receive several notifications that changes have happened to that mailbox.
These changes could be the result of the changes made by the worker, or they
could be new emails being delivered, so we have to queue another task to sync
that mailbox. These tasks are kept in queue until the worker frees up the
mailbox, and the dispatcher additionally de-duplicates them: synchronising a
mailbox just once after the last change notification is enough to synchronise
the changes in all the notifications.</p>
<p>When a message changes in the filesystem, ImapGoose receives an inotify event.
This doesn&rsquo;t trigger a sync of the full mailbox, but instead a &ldquo;targeted&rdquo; sync,
which focuses only on that email message. We know that a single message has
changed, so there&rsquo;s no point in re-scanning the thousands of messages in the
mailbox. These targeted syncs are taken into account in deduplication; they only
get de-duplicated if the path for them is the same.</p>
<p>While the connection which is listening for changes from the server is kept
alive by sending periodic NOOP commands, the connections for workers are allowed
to time out. If no activity is happening, these connections simply time out, but
a connection is re-established once a worker needs it again. Great care has been
taken to avoid unnecessary churn in all possible aspects.</p>
<h1 id="prior-art">Prior art<div class="permalink">[<a href="#prior-art">permalink</a>]</div></h1>
<p>Before developing ImapGoose, I studied prior art in the field. In particular,
offlineimap does a great job at synchronising mailboxes. However, it doesn&rsquo;t
&ldquo;keep in sync&rdquo; in the same way; offlineimap needs to execute periodic syncs,
doesn&rsquo;t rely on modern extensions, and tends to &ldquo;hang&rdquo; when there are network
time-outs. ImapGoose is new and has no existing users, so it can just require
modern extensions or declare other scenarios as unsupported. Existing tools have
to maintain compatibility for existing users, which might rely on some legacy
email server. If I couldn&rsquo;t rely on NOTIFY, implementing ImapGoose in such a
clean efficient way would not have been possible. If I couldn&rsquo;t rely on
<code>CONDSTORE</code> and <code>QRESYNC</code>, I would have had to download lists of thousands of
emails each time even a single one changes. Thanks to <code>UIDPLUS</code>, the server
returns the UID of a newly uploaded message, and we don’t need any ugly
workarounds to retrieve it.</p>
<p>If someone needs to sync data from legacy servers, plenty of tools are still out
there, providing the best experience which those servers can offer.</p>
<h1 id="development">Development<div class="permalink">[<a href="#development">permalink</a>]</div></h1>
<p>When working on ImapGoose, I focused exactly on my needs for my particular use
case: keep my local mailboxes in sync with an IMAP server. There&rsquo;s no other
supported scenario, there&rsquo;s no fallback for legacy servers, and there&rsquo;s no
support for alternative email backends. All these constraints allowed me to
focus on making a tool that&rsquo;s great for a single use case: it does one thing and
does it well.</p>
<p>I strongly believe that my keeping tight constraints (e.g.: focusing on just one
use case, ignoring support for legacy servers, keeping things as simple as
possible) helped develop this much faster and with much cleaner results.</p>
<p>I started with a very clear picture of how the whole thing would work. I was
also familiar with <a href="https://github.com/emersion/go-imap">go-imap</a>, and knew it to be a well designed and well
implemented IMAP library. My immense appreciation goes to <a href="https://emersion.fr/">emersion</a> and the
contributors who&rsquo;ve worked on it. I didn&rsquo;t need to worry about the inner details
of talking to an IMAP server, parsing responses, tracking connection state, etc.
go-imap provides a simple idiomatic Go interface for IMAP commands and their
responses.</p>
<p>go-imap was lacking two features which I needed: support for the NOTIFY command
and for VANISHED (<a href="https://www.rfc-editor.org/rfc/rfc5162#section-3.6">rfc5162</a>). While still standing on the shoulders of giants, I
implemented both of these and sent patches for both of them
(<a href="https://github.com/emersion/go-imap/pull/718">NOTIFY</a>, <a href="https://github.com/emersion/go-imap/pull/720">VANISHED</a>). Until those are merged, ImapGoose is built
using my own (temporary) fork which has those two patches applied.</p>
<h2 id="configuration">Configuration<div class="permalink">[<a href="#configuration">permalink</a>]</div></h2>
<p>For configuration, I opted for the very simple and straightforward scfg
configuration format. The configuration file looks something like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>account example {
</span></span><span style="display:flex;"><span>    server imap.example.com:993
</span></span><span style="display:flex;"><span>    username hugo@example.com
</span></span><span style="display:flex;"><span>    password-cmd pass show email/example
</span></span><span style="display:flex;"><span>    local-path ~/mail/example
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="naming">Naming<div class="permalink">[<a href="#naming">permalink</a>]</div></h2>
<p>I wanted something easy to remember, easy to pronounce and that won&rsquo;t yield
thousands of unrelated search engine results. There&rsquo;s also room for an obvious
mascot/logo: a goose wearing a postman&rsquo;s hat carrying an envelope, using the
colour palette from the Go ecosystem. Please reach out if you are an illustrator
willing to contribute with artwork.</p>
<h2 id="open-source">Open source<div class="permalink">[<a href="#open-source">permalink</a>]</div></h2>
<p>ImapGoose is open source and distributed under the terms of <a href="https://git.sr.ht/~whynothugo/ImapGoose/tree/main/item/LICENCE">the ISC
licence</a>. The <a href="https://git.sr.ht/~whynothugo/ImapGoose">source code is available via git</a>. Feedback is
welcome, including bug reports.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Typically, another client moves a message to Trash, and ImapGoose replicates
the same operation, but the general idea still stands.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>Status update 2025-10</title><link>https://whynothugo.nl/journal/2025/10/09/status-update-2025-10/</link><pubDate>Thu, 09 Oct 2025 15:11:53 +0200</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/10/09/status-update-2025-10/</guid><description><![CDATA[<h1 id="jmap-support-in-pimsync">JMAP support in pimsync<div class="permalink">[<a href="#jmap-support-in-pimsync">permalink</a>]</div></h1>
<p>I&rsquo;ve finalised support for JMAP in pimsync, and tagged a release with it. It&rsquo;s
still experimental. While preliminary testing shows it works fine, it still
hasn&rsquo;t had extensive long-term testing. Feedback for it is most welcome, but
<strong>please</strong> keep frequent back-ups of your data while testing it. This is still
in an early state.</p>
<p>On my <a href="/journal/2025/09/20/status-update-2025-09/">previous status update</a> I mentioned that I&rsquo;d be switching to the
<a href="https://github.com/stalwartlabs/calcard">calcard</a> library for converting iCalendar to and from JSCalendar. calcard uses
serde for serialisation and deserialisation, and that resulted in a mismatch
with my own <a href="https://git.sr.ht/~whynothugo/libjmap/">libjmap</a>, which (at the time) used the <a href="https://github.com/maciejhirsz/json-rust">json</a> library. I therefore
ported libjmap to use serde instead. The resulting code is much clearer. After a
few iterations, it is also much more reliable.</p>
<p>With libjmap and calcard using the same serialisation representations, they fit
together much nicer. I then moved to port the <code>JmapStorage</code> implementation to
use this.</p>
<h1 id="etag-and-state">Etag and State<div class="permalink">[<a href="#etag-and-state">permalink</a>]</div></h1>
<p>The most complex part of the storage implementation was handling the mismatch
between <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag"><code>Etag</code></a> and <code>State</code>. In CalDAV, each item has an <code>Etag</code> property, a
version identifier which changes each time an item changes: if an item has
changed, its <code>Etag</code> has changed. Pimsync stores an <code>Etag</code> value on a per-item
basis, and tracks it each time an item is updated. This happens internally in
the synchronisation logic, quite detached from individual storage
implementations. Whenever we update an item, we tell the server to only update
it if its <code>Etag</code> matches the one last seen. If it doesn&rsquo;t match, then the item
has been modified by some other client, and a conflict needs to be resolved.</p>
<p>For the filesystem storage, we use a combination of inode number + mtime as an
<code>Etag</code>, and this matches the same conditions just fine.</p>
<p>JMAP doesn&rsquo;t have an equivalent to <code>Etag</code>. It has a <code>State</code> value, but that
changes whenever <em>any</em> item changes. We still use <code>State</code> as an <code>Etag</code>
internally, since we don&rsquo;t have anything else on which we can rely, but this
brings about several complications.</p>
<p>As soon as we update a single item, we&rsquo;ve invalidated the <code>Etag</code>/<code>State</code> for all
other items in that storage. My early prototypes simply omitted the usage of
these, but this could easily result in conflicts being ignored and data being
overwritten. I.e.: losing changes made by some other client. This is
unacceptable for anything beyond an early prototype.</p>
<p>In order to use the <code>State</code> value while keeping within the constraints of both
pimsync and JMAP, I had to change the process of updating an item to the
following sequence:</p>
<ul>
<li>Try to update the item passing the last known State.</li>
<li>If the process completes, then all is good.</li>
<li>If the process fails due to a State mismatch, then:
<ul>
<li>Fetch the current State.</li>
<li>Request all changes since our item&rsquo;s previously recorded State and the
current State.</li>
<li>Check whether this particular item has changed between those two States.</li>
<li>If the item has not been modified, retry updating it, passing this new State
as conditional.</li>
<li>If the item <em>has</em> been modified, then we have a real conflict — someone else
has modified it since we last saw it.</li>
</ul>
</li>
</ul>
<p>This works, but has a really ugly side: uploading a single item can end up
causing four network round trips. We can do better.</p>
<h2 id="tracking-state-locally">Tracking state locally<div class="permalink">[<a href="#tracking-state-locally">permalink</a>]</div></h2>
<p>In order to avoid continuously querying the server for state changes, I moved to
caching states and their changes inside the <code>JmapStorage</code> implementation.</p>
<p>Essentially, it keeps a mapping of states which show up, and items which were
modified between that state and the previous one. Each time that an item is
uploaded (or deleted), we pass an &ldquo;expected state&rdquo; to the server and it returns
a new state. We record in the cache that this new state transition only modifies
the items which we&rsquo;ve modified.</p>
<p>When we later update (or delete) some other item, we&rsquo;ll have a <code>State</code> for the
last time we&rsquo;ve seen that item from the server. We can check our local cache,
and if there&rsquo;s a full path of transitions between that state and the current
one, we can determine whether any of the intermediate states modified this item.
If none of them did, then we can send the request telling the server to update
the item assuming that it has not changed since the most recently seen state.</p>
<p>This implementation has mixed results. When continuously running (e.g.:
<code>pimsync daemon</code>) there are plenty of cache hits and it saves a lot of network
queries. When running ad-hoc (e.g.: <code>pimsync sync</code>) the cache usually doesn&rsquo;t
have enough updates to make any meaningful difference.</p>
<p>All of this is, essentially, a band aid. Execution of pimsync&rsquo;s synchronisation
algorithm currently expects to update items one at a time, but JMAP allows us to
send a single request updating any amount of items. Using this API, we could
<em>dramatically</em> reduce the amount of queries required. This requires large
changes to the executor, which could also benefit the <code>singlefile</code> storage
(which saves N events into a single iCalendar file). This is somewhat of an
invasive change, which I&rsquo;d like to address at some point in future, but there
are higher priorities at the moment.</p>
<h2 id="concurrency">Concurrency<div class="permalink">[<a href="#concurrency">permalink</a>]</div></h2>
<p>Due to how state changes occur, JMAP is incapable of dealing with concurrent
writes. If two clients send a write operation, they&rsquo;ll instruct the server to
only apply the changes if no other change has occurred. But because the state
tracking is for all items, if <em>any</em> other item changes, then the operation
fails. Because of this, the <code>JmapStorage</code> implementation only sends a single
write query at a time. Any more than that would, by definition, always fail.
Again, this will be ironed out once we simply write all changes in a single
query, but it&rsquo;s also an issue for concurrent clients trying to operate on
entirely different items.</p>
<h2 id="final-bits">Final bits<div class="permalink">[<a href="#final-bits">permalink</a>]</div></h2>
<p>The final bits of the implementation were integrating the <code>JmapStorage</code> into
pimsync. Mostly, allowing configuration blocks for storages with <code>type jmap</code>. At
this time, these storages need to specify <code>jmap/icalendar</code> or <code>jmap/vcard</code>. This
is somewhat of an annoyance, since the same storage can actually handle both. In
future, I&rsquo;ll <a href="https://todo.sr.ht/~whynothugo/pimsync/204">move the declaration of item type into <code>pair</code> blocks</a>, so a
single storage declaration can be re-used for both.</p>
<h1 id="collection-ids-and-url-segments">Collection IDs and URL segments<div class="permalink">[<a href="#collection-ids-and-url-segments">permalink</a>]</div></h1>
<p>Unrelated from JMAP: it is now possible to configure which URL segment is used
for collection identifiers. A few months ago, I <a href="/journal/2025/03/04/design-for-google-caldav-support-in-pimsync/#calendar-paths">explained why this is
needed</a> on some unusual configurations. This includes Google&rsquo;s CalDAV
implementation.</p>
<p>The new <code>collection_id_segment</code> configuration directive allows using the
second-to-last segment as a collection id. See the <a href="https://pimsync.whynothugo.nl/pimsync.conf.5.html#collection_id_segment">manual page</a> for
reference documentation.</p>
<p>While native OAuth support is still on the road map, this change allows using a
proxy such as <a href="https://github.com/jfly/google-dav-proxy">google-dav-proxy</a><sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>
with pimsync. It requires a bit more setup, but should be feasible to use today.
If you do try it out, please let me know the results.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>I have neither tested nor reviewed this proxy.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>Status update 2025-09</title><link>https://whynothugo.nl/journal/2025/09/20/status-update-2025-09/</link><pubDate>Sat, 20 Sep 2025 15:21:56 +0200</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/09/20/status-update-2025-09/</guid><description><![CDATA[<h1 id="how-not-to-write-a-jscontact--icalendar-converter">How not to write a JSContact &lt;&gt; iCalendar converter<div class="permalink">[<a href="#how-not-to-write-a-jscontact--icalendar-converter">permalink</a>]</div></h1>
<p>As part of my work to implement JMAP support for <a href="https://pimsync.whynothugo.nl/">pimsync</a>, I need to convert
<a href="https://www.rfc-editor.org/rfc/rfc8984.html">JSCalendar</a> entries to and from iCalendar (and likewise, <a href="https://www.rfc-editor.org/rfc/rfc9553.html">JSContact</a> entries to
and from vCard).</p>
<p>Fortunately, there were draft RFCs documenting how to do all these
conversions<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>. Essentially, all fields in iCalendar format have an
equivalent field in JSCalendar format and there&rsquo;s special syntax to keep around
unsupported fields in a lossless way so that they can be recovered when
converting back.</p>
<p>While reading the RFC I paid special attention to recurrence rules, which I
feared might be a great issue. Fortunately, recurrence rules are represented in
more or less the same format, so I don&rsquo;t need to <em>parse</em> them. The same applies
for other recurrence-related fields.</p>
<p>iCalendar events can have either (1) a start datetime<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> and an end
datetime, or (2) a start datetime and a duration. JSCalendar only supports start
datetime and duration, so anything in the first format needs to be converted. I
didn&rsquo;t think much of this in the two times that I read the entire RFC. This was
a grave</p>
<p>mistake.</p>
<h1 id="my-general-design">My general design<div class="permalink">[<a href="#my-general-design">permalink</a>]</div></h1>
<p>The general approach to my design was as follows:</p>
<p>When reading an iCalendar/vCard file, read each event/todo/contact and once we
reach the end of it, return a JSON object with its equivalent on the other type.</p>
<p>When reading a JSCalendar/JSContact file, there&rsquo;s a function to convert each
JSON object into its counterpart, and these functions call each other, forming a
sort of tree in the call stack.</p>
<h1 id="vcard--jscontact">vCard &lt;&gt; JSContact<div class="permalink">[<a href="#vcard--jscontact">permalink</a>]</div></h1>
<p>My simple design worked wonders for vCard &lt;&gt; JSContact, and I managed to
implement a rough prototype for pimsync<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>.</p>
<p>I also had unit tests converting JSContact into vCard and back, and a few
JSCalendar items into iCalendar and back. Everythiing worked wonders (for the
fields that were implemented, a few were just left as &ldquo;pending&rdquo;).</p>
<p>At this point, my design seemed solid, so I started building more around it, and
slowly working on the iCalendar&lt;&gt;JSCalendar aspect of it. Again, I noticed that
converting &rsquo;end datetime&rsquo; into durations requires a bit more thinking and left
it for later, without giving it a second thought.</p>
<h2 id="stable-uids">Stable UIDs<div class="permalink">[<a href="#stable-uids">permalink</a>]</div></h2>
<p>Potentially, vCard files may lack a UID, and conversion needs to add one. When
converting the same input multiple times, the process SHOULD add the same UID.
This is something which didn&rsquo;t entirely fit well with my design, but pimsync
should take care of this beforehand anyway: all its synchronisation process
requires that all entries have a UID anyway.</p>
<h1 id="timezones-are-nightmares">Timezones are nightmares<div class="permalink">[<a href="#timezones-are-nightmares">permalink</a>]</div></h1>
<p>This isn&rsquo;t the first time that timezones sprung up to haunt me in what seemed
like a perfectly good design for something that was going to work fine.</p>
<p>It turns out that if we have the start datetime for an event and an end
datetime, calculating the duration isn&rsquo;t that trivial. To do so, we need to take
the timezone into account. Does an event start on the day before a daylight
transition and end the following day at the same time? Then its duration is 23
or 25 hours, but not 24 hours!</p>
<p>Given how my entire converter converted and serialised each component in
isolation, I didn&rsquo;t have the timezone in scope when converting an event. In
fact, the timezone might be present further down in the same file, so it hasn&rsquo;t
even been parsed yet.</p>
<p>Now, it&rsquo;s far from impossible to re-implement things in a different way to take
this into account, but it did imply that I had to scrap most of my code and
start over, with a design that keeps some form of &ldquo;partially converted&rdquo;
representation in memory until all necessary information is found further down
in the same file.</p>
<h1 id="motivation">Motivation<div class="permalink">[<a href="#motivation">permalink</a>]</div></h1>
<p>I admit that being hit by this situation strongly reduced my motivation. Work on
JMAP support along with JSCalendar and JSContact has been taking far more than
anticipated, and at this stage I mostly want to get it over with and move on to
other more pressing features.</p>
<p>As I was dealing with having to re-start the same work with an entirely new
approach, the folks from Stalwart Labs posted an email to the JMAP mailing list
announcing <a href="https://github.com/stalwartlabs/calcard">calcard</a>: a project to
convert iCalendar/JSCalendar and vCard/JSContact.</p>
<p>I peeked at the code and was impressed. It&rsquo;s has mostly minimal dependencies,
and takes into account all possible fields. They first convert items into an
intermediate representation, and then into the other type. I wasn&rsquo;t a fan of
this approach at first, but then realised it&rsquo;s the only thing that really works
out if you need to work around the timezone issue that blocked me.</p>
<p><code>calcard</code> bundles <a href="https://github.com/fmeringdal/rust-rrule/"><code>rust-rrule</code></a>, a library to parse recurrence rules which I
have used and contributed to in the past. I&rsquo;m happy that they&rsquo;ve reused an
existing implementation that works, and I also have some fixes for <code>rrule</code> which
can hopefully trickle down too.</p>
<h1 id="moving-on">Moving on<div class="permalink">[<a href="#moving-on">permalink</a>]</div></h1>
<p>At this point, it makes more sense to adopt calcard for pimsync’s JMAP backend.
I’ll contribute fixes or improvements upstream as needed, which should be a
better use of time and result in a more robust solution.</p>
<p>This is a strange status update: usually I share something that works, or at
least an experiment that demonstrates that an idea does not. In this case, I
failed to implement a converter due to a flawed design. There is no greater
lesson to be learnt from it either.</p>
<!--
If you’ve ever spent weeks on something only to throw it all away, you know the
feeling. If not, congrats — you’ve dodged one of the less fun parts of being a
developer.
-->
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>While I worked on this project, one of these drafts became a proper
standard, <a href="https://datatracker.ietf.org/doc/html/rfc9555">RFC 9555</a>.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>&ldquo;datetime&rdquo; in this context refers to a date with an optional time.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>I&rsquo;m serious about this being a rough prototype; it entirely ignores race
conditions since it&rsquo;s more of a proof of concept than a version that&rsquo;s ever
supposed to be used.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>Unaccountable systems</title><link>https://whynothugo.nl/journal/2025/09/12/unaccountable-systems/</link><pubDate>Fri, 12 Sep 2025 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/09/12/unaccountable-systems/</guid><description><![CDATA[<p>My mum visited not long ago. I paid for her flight and a few other expenses and
she refunded me for that in US dollars in cash. I needed to convert these into
Euros before I could use them. Today I went to the currency exchange, &ldquo;GWK
Travelex&rdquo;, to exchange these dollars into Euros.</p>
<p>The lady at the desk was friendly and polite. She asked me for the amount, gave
me a quote (which, honestly, was pretty bad, but it&rsquo;s not like there&rsquo;s any
better choice in the Netherlands). She then inquired about the funds, requested
my ID, address and a few other bits of personal information. She then started a
phone call, asked me to wait for a bit. I waited for twenty-something minutes
while she herself was on hold. Eventually she told me that she could not proceed
with the operation. She explained that the phone call is part of the regular
procedure, where she calls their offices in the UK, sends all my personal
information, and the folks from the UK then approve or reject the operation. In
this case, they&rsquo;d told her that she was not authorised to offer me any exchange
or any transaction. They refused me service.</p>
<p>The next people in line were there for the exact same transaction as I was, and
completed their operation swiftly. I approached the desk again and asked the
same lady more details about my situation. She was not given a reason, even
after asking further, and was simply the messenger here: this faceless
corporation had decided I was on a &ldquo;do not serve&rdquo; list and I was not welcome as
a client there. As I inquired further, she indicated that I should not come
back, since they would refuse to provide me any sort of service, for any amount,
in future.</p>
<p>Considering that this is the only currency exchange office in the city (and in
this province), I&rsquo;m appalled and unsettled that they would simply refuse to
serve me, without explanation of recourse. Why? The employee explain that she
had no way of knowing, and she did not believe that I could determine why
either.</p>
<p>We can only guess what might be the reason. Are some of my friends of the wrong
ethnicity? Maybe those close to me are voting for the wrong parties? Your guess
is as good as mine. There&rsquo;s no accountability here, and financial institutions
can simply decide that any of us are not welcome and that&rsquo;s the end of it.</p>
<p>This isn&rsquo;t my first encounter with such arbitrary systems…</p>
<h1 id="dangerous-ethnicities">Dangerous ethnicities<div class="permalink">[<a href="#dangerous-ethnicities">permalink</a>]</div></h1>
<p>Back in 2019 I met this girl at a friend&rsquo;s birthday party. She worked at a bank
in the software development sector, and told me about the project on which she&rsquo;d
been working. Their system collected data from various sources on people
applying for financial services (e.g.: loans) and would indicate if someone was
eligible, or raise a red flag. In the latter case they would have to deal with
substantial additional bureaucracy, and often times would not be able to access
these services anyway.</p>
<p>She seemed quite proud of her work, and told me her team had demoed it the
previous week in front of the whole office. They&rsquo;d shown the report that the
system generated for each person in the team. In her case, the system flagged
her as &ldquo;dangerous&rdquo;, and she was not eligible for a loan. Her grandmother was
from Iran, and because there&rsquo;s frequently cases of money laundering or other
irregularities in Iran, she&rsquo;s immediately flagged too. Despite her being a Dutch
citizen, born and raised in the Netherlands, and working in this Dutch financial
institution, her bloodline was &ldquo;too risky&rdquo; for her employer to lend her
money.<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
<p>I was confused, and honestly, heartbroken. This person was telling me how proud
they were of their work, building a system that discriminates them based on
their bloodline. I questioned what she thought about it, and she explained
&ldquo;well, the rules are there…&rdquo; —she paused a few times— &ldquo;the rules are there…&rdquo;. I
remained silent and she eventually finished her sentence after repeating it a
few times &ldquo;the rules are there to protect us&rdquo;. Even though this was six years
ago, every time I remember this situation it brings me great sadness. Here was a
person who&rsquo;d worked hard to build a system which would discriminate against
them, and yet stood proudly defending their work.</p>
<p>I don&rsquo;t think this situation is a complete outlier either. In this
corporatocracy that is the modern world, a large proportion of the population
works hard to sustain and enforce the same system that oppresses them.
Generally, because there&rsquo;s no other choice. Either work doing that, or starve.
Frequently, it&rsquo;s not even obvious that this is the case, given the layers of
indirection.</p>
<p>It&rsquo;s hard not to think of The Matrix (1999). Anyone in this society is
unknowingly an agent which enforces its rules, despite it being the system that
keeps them trapped. When an outlier tries to change the system, the men in suits
will come after them — men who don’t have to follow the rules themselves</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>The irony that they trust her to work on the system which decides if you get
a loan, but don&rsquo;t trust her with a loan itself is still not lost on me.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>I'm really happy with my Pinetime</title><link>https://whynothugo.nl/journal/2025/08/09/im-really-happy-with-my-pinetime/</link><pubDate>Sat, 09 Aug 2025 17:37:09 +0200</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/08/09/im-really-happy-with-my-pinetime/</guid><description><![CDATA[<p>I&rsquo;ve been using my <a href="https://pine64.org/devices/pinetime/">Pinetime</a> with <a href="https://github.com/InfiniTimeOrg/InfiniTime">InfiniTime</a> since 2021. It&rsquo;s not perfect,
but it&rsquo;s definitely great. I&rsquo;m quite happy with it, and wouldn&rsquo;t trade it for
any other existing watch or smartwatch.</p>
<p>I have few requirements for a watch. It should: (1) keep the time, (2) show the
time in a clear, readable fashion, and (3) track steps taken each day, (4) have
a battery duration of no less than a week. Heart rate monitor is a nice plus,
and the Pinetime includes one.</p>
<p>Aside from meeting these requirements, the Pinetime has the upsides of being
(really) cheap, running open source software, and using standard 20mm straps.
These straps are the same as plenty of other regular watches, there’s immense
variety and they’re available in countless shops.</p>
<h1 id="keeping-track-of-time">Keeping track of time<div class="permalink">[<a href="#keeping-track-of-time">permalink</a>]</div></h1>
<p>This sounds like an obvious requirement for a watch. But my last two Fitbit
smartwatches didn&rsquo;t meet this requirement. After having my Fitbit watches for
over a year, the time would start being off. It sounds like a silly issue and
one that would be easy to fix. That was far from being the truth. The watches
didn&rsquo;t allow manually adjusting the time — instead they sync the time
automatically with the iOS app. Syncing with the app worked fine, and it would
reflect the amount of steps tracked and other measurements. But the time
remained wrong. It didn&rsquo;t sync. Their support staff always said &ldquo;maybe the
timezone is wrong on your profile, check that&rdquo;. The timezone was fine — if it
had been off, the time would have been off by an exact amount of hours, not by
twenty-something or thirty-something minutes.</p>
<p>When this happened to the first Fitbit, I thought it was a fluke. When this
happened to the second one, I started to see a pattern. Some time later, a
friend had the same issue and stopped wearing hers too.</p>
<p>So while &ldquo;it keeps the time&rdquo; is an obvious requirement for a watch, be aware
that not all smartwatches make it this far.</p>
<h1 id="showing-the-time-clearly">Showing the time clearly<div class="permalink">[<a href="#showing-the-time-clearly">permalink</a>]</div></h1>
<p>The Pinetime has only a few watch faces. One of them shows the time in clear
large numbers, with good contrast on a backlit display and easily readable in
the dark. It also shows the date in smaller font underneath. There&rsquo;s no need to
convert angles into hours and minutes, no obscuring the time with implicit <em>ante
meridiem</em> and <em>post meridiem</em>, just obvious numbers. Sure it&rsquo;s easy to convert
post meridiem times to 24hs time, but I hate waking up, seeing 07:00 on a clock,
and having to wonder if it&rsquo;s seven in the morning or seven in the evening. I see
no reason for which my watch should show anything other than the exact time,
clear and straight. Infinitime can do exactly this.</p>
<p>The Pinetime has a few other watch faces. They look cool, but are not of my
interest. It&rsquo;s also possible to develop your own, although it seems that you
need to actually know C++ to write a watch face. That&rsquo;s definitely a downside,
but not one that affects me.</p>
<h1 id="tracking-steps">Tracking steps<div class="permalink">[<a href="#tracking-steps">permalink</a>]</div></h1>
<p>InfiniTime tracks how many steps I take each day quite accurately. It&rsquo;s
interesting trivia which I care about, since it gives me a vague reference of
whether I&rsquo;ve been too sedentary or not.</p>
<p>Sadly, it only tracks steps taken today. There&rsquo;s <a href="https://github.com/InfiniTimeOrg/InfiniTime/pull/2120">a patch</a> to also
record steps taken the previous day. I mostly only care about this when I&rsquo;m up
past midnight. I don&rsquo;t have any interest in keeping historical records of steps.
I understand others do want this. If you do, you might need a companion app on a
phone or laptop which syncs the data off the device. I don&rsquo;t use any such thing,
so can&rsquo;t comment on them.</p>
<!-- TODO: maybe this is a new section, with a good segway? -->
<p>Sometimes if I take a single step, the step doesn&rsquo;t register immediately.
Instead, when I take three more, the total of four steps register at once. I
suspect that the device needs several steps worth of readings to identify false
positives, or might do processing in tiny batches. To be frank, I haven&rsquo;t cared
enough to dig into it, but a fascinating aspect of this device is that
development was and is done entirely in the open. Not only is the source code
available, but discussion of development is also public, so if you&rsquo;re really
curious you can peek at why things work this way and how the algorithms work.</p>
<h1 id="battery-duration">Battery duration<div class="permalink">[<a href="#battery-duration">permalink</a>]</div></h1>
<p>Battery lasts about 30 days.</p>
<p>There&rsquo;s an &ldquo;always on&rdquo; mode for the display which reduces that to about three.
The display is not OLED, nor optimised for this. I don&rsquo;t use this &ldquo;always on&rdquo;
mode, but do wish that I could <a href="https://github.com/InfiniTimeOrg/InfiniTime/discussions/2226">configure the device to automatically enable
this mode while it&rsquo;s charging</a>.</p>
<p>Leaving Bluetooth on also dramatically reduces battery. I think to around a
week? I only enable Bluetooth when I want to upgrade the software, or sync the
time. The time only needs to be synced after I let the battery get down to zero
percent.</p>
<p>The heart rate monitor also reduces battery duration substantially. I seldom use
it, so I&rsquo;m not sure how long the battery lasts when using it continuously.</p>
<!-- TODO: leave it on for a while. -->
<h1 id="its-cheap">It&rsquo;s cheap<div class="permalink">[<a href="#its-cheap">permalink</a>]</div></h1>
<p><a href="https://pine64.com/product-category/pinetime-smartwatch/">USD 26.99</a></p>
<h1 id="accidental-taps">Accidental taps<div class="permalink">[<a href="#accidental-taps">permalink</a>]</div></h1>
<p>An annoying issue is accidental taps. Any skin touching the watch screen
triggers a tap (or swipe). Some activities (including sleep) can trigger the
occasional accidental tap. A few times I&rsquo;ve woken up to find that some random
menu had been opened and some toggle changed, or the watch face was changed.
This only happens a couple of times per month, and typically it&rsquo;s just a menu
having been opened without any changes made. But it&rsquo;s still annoying.</p>
<p>There&rsquo;s <a href="https://github.com/InfiniTimeOrg/InfiniTime/pull/1359">a patch</a> which only unlocks the screen when the button is pressed
and not when tapping the screen. In theory an algorithm could detect all false
taps, and that would be the ideal solution. The patch works in the meantime. I&rsquo;m
not sure we&rsquo;ll ever have completely perfect detection for false taps.</p>
<h1 id="community-maintained-software">Community maintained software<div class="permalink">[<a href="#community-maintained-software">permalink</a>]</div></h1>
<p>InfiniTime is open source and community maintained software. There&rsquo;s a lot of
people working on improving it, and I really like the spirit behind it.</p>
<p>Both the code itself and discussions on feature implementations are public, and
some issues have really interesting discussion on how the algorithms were
implemented. This includes non-obvious algorithms like counting steps,
optimising the heart rate monitor, and reducing the detection rate of false taps
(this last one still not being quite there yet).</p>
<p>If you&rsquo;re interested in data analysis in resource constrained environments,
you&rsquo;re going to love this project. Personally, that&rsquo;s a bit out of my area of
expertise, so I can only provide feedback on whether things are an improvement
or not.</p>
<p>The open source nature of the project also helps me test patches made by others.
Some of these patches, like the ones pointed out above, fix issues which affect
me directly. My Pinetime actually runs a build of InfiniTime which includes both
patches. Building my own image is actually really easy using a docker container
which ships all the required build tools.</p>
<h1 id="unique-charger">Unique charger<div class="permalink">[<a href="#unique-charger">permalink</a>]</div></h1>
<p>The charging cradle is its own unique thing. You can&rsquo;t re-use the charging cable
for anything else but a Pinetime. I kinda get why they did it — a USB-C port
would accumulate dirt or liquid easily and be a pain to maintain. <a href="https://en.wikipedia.org/wiki/Qi_(standard)">Qi
charging</a> would likely have made the design much more complex, but would
still be a great improvement, and I&rsquo;d love to see this in a second iteration of
this device. The design <em>is</em> open source though, so a capable enough hacker
could design a variation with Qi charging.</p>
<p>The unique charging cradle isn&rsquo;t an issue because of the great battery duration.
I only really need to bring the charger with me if I&rsquo;m travelling somewhere more
than four weeks, which isn&rsquo;t often at all. For anything shorter, I can just
charge the watch before I leave.</p>
<h1 id="build-quality">Build quality<div class="permalink">[<a href="#build-quality">permalink</a>]</div></h1>
<p>The build quality is great. The watch is no larger than a standard watch and the
casing is solid metal. It&rsquo;s sturdy, durable and water-resistant (rated at IP67).</p>
]]></description></item><item><title>GPTs and feeling left behind</title><link>https://whynothugo.nl/journal/2025/08/06/gpts-and-feeling-left-behind/</link><pubDate>Wed, 06 Aug 2025 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/08/06/gpts-and-feeling-left-behind/</guid><description><![CDATA[<p>Every time that I read some blog post about &ldquo;coding with AI&rdquo;, or how cool new
models write entire libraries by themselves, I feel like I&rsquo;m lagging behind,
like I&rsquo;m missing out on some big, useful tool, and my skills are about to become
obsolete very soon.</p>
<p>So I try different models and tools, and it&rsquo;s all incredibly underwhelming. It&rsquo;s
honestly hard to believe that people get work done using these tools, because I
can spend a few hours on them (without getting even close to finishing the task
at hand) and realise that I could have done it myself in 25 minutes.</p>
<p>I tell myself <em>&ldquo;learning to use Vim took a long time, but then it paid off&rdquo;</em>
eventually. But I could (slowly) write text with Vim the first day. I can spend
an entire day with a GPT and produce nothing of value.</p>
<p>GPTs work great for finding the exact word to complete a sentence. They&rsquo;re
surprisingly good at finding the exact type annotation for a Python function.
They can find nuanced bugs in a single function which I copy-paste into the GPT.
But anything beyond writing a simple function always leads to useless junk.
Often times, they solve big problems by just importing a library that does not
exist, and calling a function which does the bulk of the logic. ChatGPT told me
the other day &ldquo;if you don&rsquo;t want any dependencies, you&rsquo;re going to have to
implement it yourself&rdquo;. But couldn&rsquo;t actually implement the necessary code.
Large portions of code have lots of hidden logic bugs, and when they fix one
they introduce another.</p>
<p>And then I see another post on Hacker News, about somebody using GPTs and how
they achieved great results on this and that. Part of me wants to think that
those articles are fake to generate hype, but the reality is that several of
them are written by well-known developers who&rsquo;ve been around for over a decade.
Some of the results are actually publicly available online.</p>
<p>I&rsquo;m in a state where I can&rsquo;t reconcile my own results with other people&rsquo;s
results. I hear people saying <em>&ldquo;this hammer is indestructible&rdquo;</em>, but when I pick
it up, it&rsquo;s just origami: made of paper, intricate, delicate, very cool-looking
but I can&rsquo;t even hammer a tomato with it.</p>
]]></description></item><item><title>Status update 2025-07</title><link>https://whynothugo.nl/journal/2025/07/30/status-update-2025-07/</link><pubDate>Wed, 30 Jul 2025 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/07/30/status-update-2025-07/</guid><description><![CDATA[<h1 id="fuse-for-unprivileged-users">fuse for unprivileged users<div class="permalink">[<a href="#fuse-for-unprivileged-users">permalink</a>]</div></h1>
<p>Early this year I signed up to finish my university degree, a <a href="https://en.wikipedia.org/wiki/Licentiate_(degree)">licentiate</a> in
computer science. I finished all the coursework years ago, and only my
dissertation was missing. That absorbed a lot of time during May and June, but
it&rsquo;s now finally complete.</p>
<p>My research (<a href="https://mirror.whynothugo.nl/t/tfg.pdf">PDF in Spanish</a>) went into mounting <a href="https://en.wikipedia.org/wiki/Filesystem_in_Userspace">fuse</a> filesystems
without any form of privilege escalation (some platforms use setuid, others
require root). At a high level, a service runs with elevated privileges, and
allows authorised users to mount filesystems via the kernel&rsquo;s fuse interface.
The bulk of the work was implementing the proof of concept.</p>
<p>Clients wishing to mount a new filesystem connect to the service via a socket,
then specify a mount path, and finally exchange the file descriptor used for
communicating fuse operations with the kernel.</p>
<p>The proof of concept allows not only mounting filesystems without setuid
binaries, but it also allows handing over the file descriptor to a process
inside a sandbox (even inside a docker container), so that a fuse service can
run in highly confined environments—a notable departure from requiring
privileges to run setuid binaries.</p>
<p>The source is an ugly cannibalisation of the <a href="https://github.com/libfuse/libfuse">libfuse</a> project (and therefore
under the LGPLv2/GPLv2 licences), and I hope to be able to release it at some
point, even if only as a prototype. In theory, the general design should be
usable in BSDs as well.</p>
<h1 id="pimsync--davmail">pimsync + davmail<div class="permalink">[<a href="#pimsync--davmail">permalink</a>]</div></h1>
<p>Pimsync has now been confirmed to work with <a href="https://davmail.sourceforge.net/">DavMail</a>. DavMail exposes a pretty
standard CalDAV server, but a couple of early adopters found a bug in pimsync
which prevented it from completing the discovery process.</p>
<p>Fixing the bug was straightforward, and pimsync v0.4.4 has now been reported to
work fine with DavMail.</p>
<h1 id="jmap-and-libjmap">JMAP and libjmap<div class="permalink">[<a href="#jmap-and-libjmap">permalink</a>]</div></h1>
<p>My other point of focus these past weeks has been JMAP support for pimsync.</p>
<h2 id="status-quo">Status quo<div class="permalink">[<a href="#status-quo">permalink</a>]</div></h2>
<p>Existing libraries for interacting with JMAP focus on email, and have no support
for Calendars or Contacts (although both of these JMAP extensions are
experimental).</p>
<p>The main contender was <a href="https://docs.rs/jmap-client/"><code>jmap-client</code></a>. Adding support for Calendars and
Contacts is possible, but the general design doesn&rsquo;t seem to be a good fit for
pimsync. The main blocker is that this library brings its own I/O (see: <a href="https://sans-io.readthedocs.io/">sans
I/O</a>) and (high level) HTTP client. This makes it a poor fit for pimsync, which
already has its own I/O and HTTP client implementations. Pimsync also needs a
lower-level HTTP client due to the configuration options exposed.</p>
<h2 id="cyrus-as-a-jmap-server">Cyrus as a JMAP server<div class="permalink">[<a href="#cyrus-as-a-jmap-server">permalink</a>]</div></h2>
<p>In order to work on a JMAP client, I needed a JMAP server. The only
implementation which currently supports calendars and contacts is <a href="https://www.cyrusimap.org/">Cyrus
IMAP</a>.</p>
<p>I tried to build Cyrus from source in an isolated environment, but came across
some build issues on Alpine. I <a href="https://github.com/cyrusimap/cyrus-imapd/issues/5435">reported these upstream</a>. The
developers provided fixes for each issue along the way. Cyrus IMAP now builds
and works fine on Alpine, and likely on other musl-based distributions too.</p>
<p>While iterating on the above, I used <a href="https://github.com/cyrusimap/cyrus-docker-test-server">cyrus-docker-test-server</a> to continue on
my own work. Running a server is as simple as:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>docker build -t cyrus-testserver .
</span></span><span style="display:flex;"><span>docker run -it <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -p 8080:8080 -p 8143:8143 -p 8110:8110 -p 8024:8024 -p 8001:8001 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  cyrus-testserver
</span></span></code></pre></div><h2 id="hacking-on-a-jmap-client">Hacking on a JMAP client<div class="permalink">[<a href="#hacking-on-a-jmap-client">permalink</a>]</div></h2>
<p>My first prototype code worked, but was quite ugly, since I learnt quite a few
quirks about JMAP while writing it. It doesn&rsquo;t matter how many times you read
the RFCs, you&rsquo;ll still run into surprises when writing the client. The current
code is a second prototype.</p>
<p>Some of the design choices in <code>jmap-client</code> make a bit of sense to me now, but
I&rsquo;m still not entirely convinced by others. I&rsquo;ve <a href="https://github.com/stalwartlabs/jmap-client/discussions/15">reached out</a> asking if they&rsquo;d
consider moving to a sans I/O approach. If so, I believe it&rsquo;s best to
collaborate with them and attempt to mould their client library to fit my
requirements (even though I&rsquo;d have to use a fork in the short term in order to
properly support Calendars and Contacts until they are fully standardised).</p>
<p>After having written the current JMAP implementation, I believe that requests
would be best designed with a builder pattern. It would make writing JMAP-based
applications a lot more pleasant for downstream consumers. Such an
implementation would be quite time consuming, and it&rsquo;s hard to justify it for
pimsync&rsquo;s more basic needs.</p>
<h2 id="libjmap">libjmap<div class="permalink">[<a href="#libjmap">permalink</a>]</div></h2>
<p>I&rsquo;ve extracted my JMAP client implementation into a new library, <a href="https://git.sr.ht/~whynothugo/libjmap/"><code>libjmap</code></a>. It
exposes the few basic functions required by pimsync:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">impl</span><span style="color:#f92672">&lt;</span>C<span style="color:#f92672">&gt;</span> JmapClient<span style="color:#f92672">&lt;</span>C<span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">where</span>
</span></span><span style="display:flex;"><span>    C: <span style="color:#a6e22e">Service</span><span style="color:#f92672">&lt;</span>http::Request<span style="color:#f92672">&lt;</span>String<span style="color:#f92672">&gt;</span>, Response <span style="color:#f92672">=</span> Response<span style="color:#f92672">&lt;</span>Incoming<span style="color:#f92672">&gt;&gt;</span> <span style="color:#f92672">+</span> Sync <span style="color:#f92672">+</span> Send <span style="color:#f92672">+</span> &#39;static,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;</span>C <span style="color:#66d9ef">as</span> Service<span style="color:#f92672">&lt;</span>http::Request<span style="color:#f92672">&lt;</span>String<span style="color:#f92672">&gt;&gt;&gt;</span>::Error: <span style="color:#a6e22e">std</span>::error::Error <span style="color:#f92672">+</span> Send <span style="color:#f92672">+</span> Sync,
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">discover_session_url</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">mut</span> client: <span style="color:#a6e22e">C</span>,
</span></span><span style="display:flex;"><span>        scheme: <span style="color:#a6e22e">Scheme</span>,
</span></span><span style="display:flex;"><span>        domain: String,
</span></span><span style="display:flex;"><span>    ) -&gt; Result<span style="color:#f92672">&lt;</span>Uri, DiscoverError<span style="color:#f92672">&gt;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">create_collection</span><span style="color:#f92672">&lt;</span>T: <span style="color:#a6e22e">Collection</span><span style="color:#f92672">&gt;</span>(<span style="color:#f92672">&amp;</span>self, name: <span style="color:#66d9ef">&amp;</span><span style="color:#66d9ef">str</span>) -&gt; Result<span style="color:#f92672">&lt;</span>Record<span style="color:#f92672">&gt;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">create_record</span><span style="color:#f92672">&lt;</span>T: <span style="color:#a6e22e">Collection</span><span style="color:#f92672">&gt;</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&amp;</span>self,
</span></span><span style="display:flex;"><span>        calendar_id: <span style="color:#66d9ef">&amp;</span><span style="color:#66d9ef">str</span>,
</span></span><span style="display:flex;"><span>        record: <span style="color:#66d9ef">&amp;</span><span style="color:#a6e22e">JsonValue</span>,
</span></span><span style="display:flex;"><span>    ) -&gt; Result<span style="color:#f92672">&lt;</span>Record<span style="color:#f92672">&gt;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">delete_collection</span><span style="color:#f92672">&lt;</span>T: <span style="color:#a6e22e">Collection</span><span style="color:#f92672">&gt;</span>(<span style="color:#f92672">&amp;</span>self, collection_id: <span style="color:#66d9ef">&amp;</span><span style="color:#66d9ef">str</span>) -&gt; Result<span style="color:#f92672">&lt;</span>String<span style="color:#f92672">&gt;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">delete_record</span><span style="color:#f92672">&lt;</span>T: <span style="color:#a6e22e">Collection</span><span style="color:#f92672">&gt;</span>(<span style="color:#f92672">&amp;</span>self, record_id: <span style="color:#66d9ef">&amp;</span><span style="color:#66d9ef">str</span>) -&gt; Result<span style="color:#f92672">&lt;</span>String<span style="color:#f92672">&gt;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">get_collections</span><span style="color:#f92672">&lt;</span>T<span style="color:#f92672">&gt;</span>(<span style="color:#f92672">&amp;</span>self) -&gt; Result<span style="color:#f92672">&lt;</span>Vec<span style="color:#f92672">&lt;</span>T<span style="color:#f92672">&gt;&gt;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">get_records</span><span style="color:#f92672">&lt;</span>T: <span style="color:#a6e22e">Collection</span><span style="color:#f92672">&gt;</span>(<span style="color:#f92672">&amp;</span>self, calendar_id: <span style="color:#66d9ef">&amp;</span><span style="color:#66d9ef">str</span>) -&gt; Result<span style="color:#f92672">&lt;</span>Vec<span style="color:#f92672">&lt;</span>Record<span style="color:#f92672">&gt;&gt;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// …
</span></span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The <code>get_collections</code> function also fetches properties (display name, colour,
etc), but a function to update properties is still missing.</p>
<p>I&rsquo;m still missing the support for the <code>state</code> parameter. Each time that the
server returns any data, it returns a <code>state</code>, an opaque value which changes any
time that data on the server changes. This allows sending requests specifying
operations which are only executed if the data has not changed on the server
since we last saw it. This mainly avoids race conditions. It&rsquo;s closely related
to HTTP&rsquo;s <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag">Etag header</a>, with the biggest difference being that the <code>state</code>
changes any time any item changes, while an <code>Etag</code> applies only to individual
items.</p>
<p>As such, the current (incomplete) JMAP backend in pimsync is subject to race
conditions if multiple clients operate on the same server at the same time.</p>
<h1 id="pimsync-jmap-support">pimsync JMAP support<div class="permalink">[<a href="#pimsync-jmap-support">permalink</a>]</div></h1>
<p>JMAP support in pimsync is still incomplete, and unreleased.</p>
<p>I need to convert data in JSCalendar format into iCalendar and vice versa, for
which I&rsquo;ve been following the <a href="https://datatracker.ietf.org/doc/draft-ietf-calext-jscalendar-icalendar/">draft RFC</a>. The code at this point works, but
mostly handles the basic cases. There is no public release of this yet.</p>
<p>Finally, there is a bit of a mismatch between how JMAP handles a global <code>state</code>
and pimsync handles a per-item <code>ETag</code>. I have a plan on how to work around this,
although this unfortunately adds a bit of extra network overhead.</p>
<p>I believe that <em>some</em> refactors can be done to pimsync which would improve its
integration with JMAP (and also with the missing <code>singlefile</code> storage), but I
have no plans to execute on these any time soon.</p>
<h1 id="upcoming-work">Upcoming work<div class="permalink">[<a href="#upcoming-work">permalink</a>]</div></h1>
<p>In the coming weeks I&rsquo;ll continue with the JMAP work. Once that is finished,
I&rsquo;ll move onto <a href="/journal/2025/03/04/design-for-google-caldav-support-in-pimsync/">OAuth and Google support</a>. After that, I intend to
implement any missing features which are present in vdirsyncer, so that pimsync
can fully replace it for all use cases.</p>
]]></description></item><item><title>Boycotting is about supporting an alternative</title><link>https://whynothugo.nl/journal/2025/06/30/boycotting-is-about-supporting-an-alternative/</link><pubDate>Mon, 30 Jun 2025 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/06/30/boycotting-is-about-supporting-an-alternative/</guid><description><![CDATA[<p>Boycotting an organisation isn&rsquo;t just about not giving them your money. It&rsquo;s
about funding an alternative.</p>
<p>Some folks who dislike an organisation still buy from it anyway. They argue that
their individual purchase is just another grain of sand in the desert—too small
to make a difference. While this is true, it&rsquo;s only half of the picture. When we
decide to boycott one organisation, we also buy the same goods or services from
another organisation.</p>
<p>When we decide to boycott one unethical organisation, it&rsquo;s not just about
reducing their profits, but also about funding their competition, however small
they may be. For small, honest businesses, every Euro counts. Every sale makes a
difference. Every customer helps them stay alive.</p>
]]></description></item><item><title>Connecting to Bluetooth hotspot</title><link>https://whynothugo.nl/journal/2025/06/08/connecting-to-bluetooth-hotspot/</link><pubDate>Sun, 08 Jun 2025 23:58:56 +0200</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/06/08/connecting-to-bluetooth-hotspot/</guid><description><![CDATA[<p>For some unknown reason, IPv4 routing doesn&rsquo;t work when using my iPhone as a
personal Wi-Fi hotspot. I thought I&rsquo;d try using it as a Bluetooth hotspot
instead. Spoiler: Internet works flawlessly over Bluetooth!</p>
<p>Here&rsquo;s a summary of the steps.</p>
<h1 id="connecting-to-the-device">Connecting to the device<div class="permalink">[<a href="#connecting-to-the-device">permalink</a>]</div></h1>
<p>First, on the iOS device, open the Bluetooth settings. The device is
discoverable only while this screen is visible. Then, on the Alpine Linux host,
open <code>bluetuith</code> (or any other program used to connect to Bluetooth devices),
scan for devices, and connect to the iPhone. A PIN will be shown on both
devices. Ensure that it matches, and press OK.</p>
<h1 id="enable-the-hotspot">Enable the hotspot<div class="permalink">[<a href="#enable-the-hotspot">permalink</a>]</div></h1>
<p>On the iOS device, go to <code>Settings &gt; Personal Hotspot</code>, and enable the toggle.
Something equivalent is likely possible on devices with other operating systems.</p>
<h1 id="connecting-to-the-bluetooth-pan">Connecting to the Bluetooth PAN<div class="permalink">[<a href="#connecting-to-the-bluetooth-pan">permalink</a>]</div></h1>
<p>Install <code>bluez-tools</code> (I <a href="https://gitlab.alpinelinux.org/alpine/aports/-/merge_requests/85377">submitted a package recipe to the aports repo</a>
as part of this work).</p>
<p>Use <code>bt-device -l</code> to list all devices. Find the MAC address of the iOS device,
and then run <code>bt-device -i XX:XX:XX:XX:XX:XX</code> to ensure that the iOS device
shows <code>NAP</code>. If it doesn&rsquo;t, then the personal hotspot is off.</p>
<p>Finally, run <code>bt-network -c XX:XX:XX:XX:XX:XX nap</code>, using the MAC address of
the device found above.</p>
<p>The first time both devices reported that the connection was established, but no
traffic flowed. After disconnecting and reconnecting twice, everything worked.</p>
<p>Not only was the IPv4 issue resolved, but the hotspot works perfectly, better
than the Wi-Fi hotspot has ever worked on an iOS device.</p>
<h1 id="usability">Usability<div class="permalink">[<a href="#usability">permalink</a>]</div></h1>
<p>When using the Wi-Fi hotspot, after the last device disconnects, the iOS device
stops broadcasting the access point. Reconnecting to it requires unlocking the
device and opening the Personal Hotspot screen again. It&rsquo;s often necessary to
toggle it off and on again.</p>
<p>When using the Bluetooth hotspot, my laptop can connect to the iOS device via
Bluetooth at any time, and the <code>bt-network…</code> command requests access to the
network. I don&rsquo;t need to reach for the iPhone every time.</p>
<p>Bluetooth is likely less power-hungry than Wi-Fi (at least for my typical use
case). I admit I haven&rsquo;t actually measured this. If you do measure this, please
send me your data.</p>
]]></description></item><item><title>Status update 2025-04</title><link>https://whynothugo.nl/journal/2025/04/22/status-update-2025-04/</link><pubDate>Tue, 22 Apr 2025 11:59:24 +0200</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/04/22/status-update-2025-04/</guid><description><![CDATA[<p>I submitted a <a href="https://lists.sr.ht/~sircmpwn/sr.ht-dev/patches/58679">patcheset to allow renaming projects in sourcehut</a>, which
was recently merged. This enabled me to updated the project URL to
<a href="https://sr.ht/~whynothugo/pimsync/">https://sr.ht/~whynothugo/pimsync/</a>. Previously this was called
<code>vdirsyncer-rs</code>, a leftover from the early prototype days.</p>
<p>This patchset follows on a <a href="https://lists.sr.ht/~sircmpwn/sr.ht-dev/patches/56262">similar previous patchset which implemented renaming
of issue trackers</a> from a few of months back.</p>
<p>Both of these were pretty basic features which were missing. Implementing them
was not that tricky: the codebase for sourcehut is well-organised and quite
legible (despite there still being a few quirks lingering here and there). I&rsquo;m
happy to be using a forge where the community is empowered to contribute with
such ease.</p>
<p>I&rsquo;m in the process of rewriting the pimsync manual pages from <a href="https://man.openbsd.org/man.7">man(7)</a> into
<a href="https://man.openbsd.org/mdoc.7">mdoc(7)</a>. I&rsquo;ve so far rewritten <a href="https://pimsync.whynothugo.nl/pimsync.1.html">pimsync(1)</a> and <a href="https://pimsync.whynothugo.nl/pimsync.conf.5.html">pimsync.conf(5)</a>. I&rsquo;m not a
huge fan of having the raw format in the repository (it&rsquo;s not the easiest for
newcomers to edit), but I&rsquo;m quite pleased with the end result. The renders
produced by a man pager are much tidier and lots of little quirks have been
fixed. The same is true of the html pages, which now also include proper links,
including links to other sections on the same page.</p>
<p>I&rsquo;ve implemented support for a <code>--socket-path</code> flag in <a href="https://git.sr.ht/~whynothugo/way-secure">way-secure</a> and tagged a
new 0.2.0 version. This new flag should make way-secure much easier to use in
shell scripts, and for simple experimentation.</p>
<p>I&rsquo;ve tagged <a href="https://github.com/pimutils/khal/blob/v0.13.0/CHANGELOG.rst">new releases of khal</a>. v0.11.4 includes a large accumulation
of fixes and smaller changes, but mostly retains backwards compatibility. 0.13.0
supports the <code>icalendar==6.0.0</code>, which had produced some major breakage, but
won&rsquo;t work with older dependencies or older, unmaintained versions of Python. To
be clear, I&rsquo;ve mostly tagged these new releases and published the new version:
most of the work was done by other contributors (thanks for all the patches!).</p>
<p>There hasn&rsquo;t been any major progress on the pimsync side. I&rsquo;ve been writing
prototype code to interact with JMAP servers, but this needs a few more
iterations before it&rsquo;s in a presentable state. Once this starts shaping up, I&rsquo;ll
start working on JMAP support for pimsync.</p>
]]></description></item><item><title>Man pages are great, man readers are the problem</title><link>https://whynothugo.nl/journal/2025/04/09/man-pages-are-great-man-readers-are-the-problem/</link><pubDate>Wed, 09 Apr 2025 14:29:42 +0200</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/04/09/man-pages-are-great-man-readers-are-the-problem/</guid><description><![CDATA[<p><em>TLDR: man pages support links, but man page readers neither display nor allow
allow following them.</em></p>
<p>Frequent criticisms of man pages are &ldquo;they don&rsquo;t have links to each other&rdquo; and
&ldquo;they don&rsquo;t re-flow if I make the window narrower&rdquo;. These are perfectly valid
complains, but the reality is that man page do support these features: it&rsquo;s the
programs that we use to read man pages that don&rsquo;t implement them.</p>
<h1 id="the-format">The format<div class="permalink">[<a href="#the-format">permalink</a>]</div></h1>
<p>Man pages are stored in the <a href="https://man.openbsd.org/mdoc.7">mdoc(7)</a> format and in the legacy <a href="https://man.openbsd.org/man.7">man(7)</a>
format<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>. The actual format for these files isn&rsquo;t the easiest to read in
the world, but it&rsquo;s definitely not worse than XML or JSON. mdoc has purely
semantic markup. A portion of an mdoc man page looks something like the
following<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>.Sh NAME
</span></span><span style="display:flex;"><span>.Nm openrc
</span></span><span style="display:flex;"><span>.Nd stops and starts services for the specified runlevel
</span></span><span style="display:flex;"><span>.Sh SYNOPSIS
</span></span></code></pre></div><p>In this case <code>.Sh</code>, <code>.Nm</code> and <code>.Nd</code> are macros which mark the following text as
&ldquo;section header&rdquo;, &ldquo;document name&rdquo; and &ldquo;document description&rdquo; respectively. If
you try to manually read or edit these files you&rsquo;ll need the to consult the
<a href="https://man.openbsd.org/mdoc.7#MACRO_OVERVIEW">macro overview</a>.</p>
<h1 id="references-links">References (&ldquo;links&rdquo;)<div class="permalink">[<a href="#references-links">permalink</a>]</div></h1>
<p>Two macros are of particular interest:</p>
<ul>
<li><a href="https://man.openbsd.org/mdoc.7#Xr"><code>.Xr</code></a>: &ldquo;Cross reference&rdquo;: link to another manual page.</li>
<li><a href="https://man.openbsd.org/mdoc.7#Sx"><code>.Sx</code></a>: Reference to another section in the same manual page.</li>
</ul>
<p>Both of these can be rendered as links. In fact, when <a href="https://man.openbsd.org/mdoc.7">mdoc(7)</a> pages are
converted into HTML, both of these macros are converted into actual links. Both
of the above linked manual pages include examples of this. The <code>.Sh</code> (section
header) macro renders as an anchor, so links can point to it directly.</p>
<p>Ironically, the documentation for <a href="https://man.openbsd.org/mdoc.7">mdoc(7)</a> renders links when displayed as HTML
on a browser, but not when displayed in a terminal via the <a href="https://man.openbsd.org/man.1">man(1)</a> command.</p>
<h1 id="conclusion">Conclusion<div class="permalink">[<a href="#conclusion">permalink</a>]</div></h1>
<p>We need better man page readers. Ones that let us follow references as links.</p>
<p>Currently <a href="https://man.openbsd.org/man.1">man(1)</a> formats a man pages and pipes it to <a href="https://man.openbsd.org/less.1">less(1)</a>. We can&rsquo;t
easily add supports for links here: we need a pager which understands man pages
natively.</p>
<p>While we&rsquo;re at it, we could also re-flow text when the terminal window is made
narrower.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>When I say legacy here, I really mean it. Documentation for <a href="https://man.openbsd.org/man.7">man(7)</a>
indicates that it was used from 1979 to 1989, and <a href="https://man.openbsd.org/mdoc.7">mdoc(7)</a> first appeared
in the early 1990s, in 4.4BSD.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>This sample was copy-pasted from <a href="https://github.com/OpenRC/openrc/blob/e56171b2a1815560e03b9e9b2ef507a02c59e288/man/openrc.8">OpenRC&rsquo;s man pages</a>. See the
repository for a full example and licence details.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>HDR on the Steam Deck</title><link>https://whynothugo.nl/journal/2025/04/06/hdr-on-the-steam-deck/</link><pubDate>Sun, 06 Apr 2025 19:57:30 +0200</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/04/06/hdr-on-the-steam-deck/</guid><description><![CDATA[<p>After reboot my Steam Deck yesterday evening, the external display to which it
is connected showed an &ldquo;HDR mode&rdquo; notification in the middle of the screen for a
moment. From that point on, SteamOS rendered way too bright, especially for an
evening setting. We wanted to turn down the brightness. The display doesn&rsquo;t
allow reducing brightness when in HDR mode; brightness is defined by the device
sending the video stream. I tried to use the brightness slider on the Steam
Deck&rsquo;s settings menu. It was disabled.</p>
<p>If adjusting brightness is not possible, then the next obvious thing to do was
to turn off HDR mode<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>. I set out to find the toggle for this in settings, and
right above it was what I really needed: an &ldquo;SDR content in HDR mode&rdquo; slider.</p>
<p>The slider worked exactly as expected. Great, no need to disable HDR.</p>
<p>But the experience hadn&rsquo;t been great. This slider probably shouldn&rsquo;t be hidden
down at the end of the Display settings. In fact, <strong>the typical &ldquo;brightness&rdquo;
slider should be repurposed to perform exactly this function</strong>. This slider
won&rsquo;t control brightness for game which support HDR; those games have an in-game
slider. So the OS&rsquo;s brightness slider, once adapted to control &ldquo;SDR brightness
in HDR mode&rdquo; would need a little clarification underneath: &ldquo;Does not apply to
HDR games; see in-game menu&rdquo;.</p>
<p>The first game we played did support HDR, and the first screen which is showed
was one to properly adjust brightness for it. HDR is a really cool feature, but
if brightness control is not a first-class feature, then the value of HDR
quickly drops to a negative one.</p>
<h1 id="addendum">Addendum<div class="permalink">[<a href="#addendum">permalink</a>]</div></h1>
<p>Huge thanks for everyone involved in getting HDR working on this software stack;
I&rsquo;m really pleased to see the improvements to the graphics stack that have been
happening on Linux in recent times.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>At this point, my eyes were already starting to hurt from starting at
excessive brightness, so leaving HDR mode on was not an option.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>Status update 2025-03</title><link>https://whynothugo.nl/journal/2025/03/31/status-update-2025-03/</link><pubDate>Mon, 31 Mar 2025 18:55:47 +0200</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/03/31/status-update-2025-03/</guid><description><![CDATA[<p>March has been a busy month, wrapping up a lot of ongoing work on pimsync.</p>
<p>I&rsquo;ve implemented a <a href="https://pimsync.whynothugo.nl/">full documentation website</a>. It provides guides on
setting up pimsync, common scenarios, the security model as well as HTML renders
of the manual pages. The man pages continue to be the canonical reference
documentation, whereas the website includes more usage guides and explanations.
It also includes instructions for building and hints for packages. If you&rsquo;re
interested in packaging pimsync for your distributions, please check out the
<a href="https://pimsync.whynothugo.nl/install.html#packaging">packaging instructions</a>.</p>
<p>I tagged a v0.4.1 release of pimsync. The <a href="https://pimsync.whynothugo.nl/changelog.html">changelog</a> is also available in the
documentation website.</p>
<p>If you find some gap in the documentation, or some detail is not clear, please
reach out. At this stage, feedback is needed in order to continue refining it.</p>
<h1 id="the-vdir-storage-format">The vdir storage format<div class="permalink">[<a href="#the-vdir-storage-format">permalink</a>]</div></h1>
<p>The <a href="https://pimutils.org/specs/vdir/"><code>vdir specification</code></a> is now published on
a page of its own. Previously this format was documented in vdirsyncer&rsquo;s
documentation, but the format itself was designed so that different tools can
operate on the same set of files avoiding conflicts or races as much as
possible.</p>
<p>The <code>vdir</code> storage format is a simply convention on how to store calendar and
address book data locally, as stand-alone files. It is easy to implement and
allows interoperability across various tools. It also puts people in full
control of their data, since all of it is stored in standardised formats easily
accessible in the local filesystem.</p>
<p>Publishing the specification as a stand-alone document should better reflect
it&rsquo;s intended purpose: being a specification which other tools can follow in
order to improve interoperability.</p>
<p>Tools such as <a href="https://pimsync.whynothugo.nl/">pimsync</a>, <a href="https://lostpackets.de/khal/">khal</a>, <a href="https://todoman.readthedocs.io/en/stable/">todoman</a>, <a href="https://kkga.me/projects/tdx/">tdx</a>, <a href="https://git.sr.ht/~whynothugo/ab-bday">ab-bday</a>, <a href="https://git.sr.ht/~whynothugo/ab-tidy">ab-tidy</a>,
<a href="https://vdirsyncer.pimutils.org/en/stable/">vdirsyncer</a>, and others already rely on this format, and I hope that more
tools will join in future.</p>
<h1 id="repairing-items">Repairing items<div class="permalink">[<a href="#repairing-items">permalink</a>]</div></h1>
<p>pimsync implements a <code>repair</code> command, which repairs invalid items in storages.
Typically, these are items with a missing UID or with duplicate UIDs.</p>
<p>If additional common scenarios of invalid items surface, I&rsquo;ll extend this
command to handle them as well (assuming that there is a clear / unambiguous
path to fixing them).</p>
<h1 id="content-line-writer">content-line-writer<div class="permalink">[<a href="#content-line-writer">permalink</a>]</div></h1>
<p>I&rsquo;ve published a new release of the <a href="https://docs.rs/content-line-writer/0.2.2/"><code>content_line_writer</code></a> crate. This library
implements escaping of content lines for iCalendar and vCard files, and is
usable in applications that need to generate either of these and don&rsquo;t want to
deal with the nuances of folding lines.</p>
<p>It&rsquo;s not terribly interesting yet, but it&rsquo;s one of the building block to start
working on JSCalendar and JMAP support.</p>
]]></description></item><item><title>Unemployment is not the problem</title><link>https://whynothugo.nl/journal/2025/03/27/unemployment-is-not-the-problem/</link><pubDate>Thu, 27 Mar 2025 18:40:59 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/03/27/unemployment-is-not-the-problem/</guid><description><![CDATA[<p>Modern society faces a paradox: the more efficient we become, the fewer workers
we need to sustain ourselves. Yet we still demand that people work long hours
just to earn survival. If 50 people working full-time can produce enough for
100, why do we deny the other 50 access to those goods—instead of letting all
100 work half the hours and share the benefits?</p>
<p>Critics might argue this would collapse productivity or disincentivise work. But
if we already produce enough, why must we keep labouring like we don&rsquo;t? The real
disincentive is forcing unemployment on some while overworking others in a
system that, by its own efficiency, has made so many jobs unnecessary.</p>
<p>The solution isn&rsquo;t to invent useless busywork or cling to outdated 40-hour
weeks—it’s to distribute necessary labor fairly. A shorter work week wouldn&rsquo;t
mean less gets done; it would mean what must get done is made by the work of
all, freeing time for life beyond work. The obstacle isn&rsquo;t economics, but our
insistence that survival must be earned through toil, even when toil is no
longer needed.</p>
]]></description></item><item><title>Goodbye mutetab</title><link>https://whynothugo.nl/journal/2025/03/27/goodbye-mutetab/</link><pubDate>Thu, 27 Mar 2025 16:06:00 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/03/27/goodbye-mutetab/</guid><description><![CDATA[<p>For the last several years, I&rsquo;ve been using <a href="https://addons.mozilla.org/en-GB/firefox/addon/mutetab/">mutetab</a>, a Firefox extension which
mutes new tabs by default. This changes the typical paradigm from &ldquo;tabs can play
audio by default&rdquo; to &ldquo;tabs need to be un-muted to play audio&rdquo;.</p>
<p>I mostly needed this because of how many crappy websites around the web
automatically play audio when opened, even in background. American news and
media sites are a common offender (but definitely not the only ones): try to
read an article and they&rsquo;ll start automatically playing some unrelated
video<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.</p>
<p>Firefox has blocked auto-playing media for many releases now, and the feature
has grown reliable enough that I no longer need mutetab. It&rsquo;s definitely nice
for a browser to provide this as a first-class feature, but I still wish we
didn&rsquo;t need to work around crap like this in the first place.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Playing videos with people talking on top of a new article is one of those
things that make absolutely no sense to me: if a reader is reading your
article, why would you make reading harder by playing a video of people
talking about something? Whoever even considered this was a good idea?&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>Change in GTK's default font</title><link>https://whynothugo.nl/journal/2025/03/23/change-in-gtks-default-font/</link><pubDate>Sun, 23 Mar 2025 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/03/23/change-in-gtks-default-font/</guid><description><![CDATA[<p>After a system update last night, I noticed that Firefox and several other
applications were using a different font for their UI. The new font renders
larger glyphs, making all UIs look cramped, fit less text, and it&rsquo;s somehow not
as comfortable to read as the previous default (this last might be due to
familiarity, but I do find the new default font too wide).</p>
<p>I used my laptop (which hadn&rsquo;t been updated) as a reference to compare what&rsquo;s
changed and why a different font is being used. Not new fontconfig rules were
installed, so fontconfig should behave the same.</p>
<p>GTK&rsquo;s default font is configured via <a href="https://en.wikipedia.org/wiki/Dconf">dconf</a>, and is stored under the key
<code>org.gnome.desktop.interface.font-name</code> (this is true even if my desktop isn&rsquo;t
GNOME; GTK configuration keys often start with <code>org.gnome…</code>). Checking this
configuration value revealed that the default font had changed.</p>
<p>This value can be changed interactively using the <code>dconf-editor</code> GUI, or via the
command line. The command line incantation to restore the previous default is:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>dconf write /org/gnome/desktop/interface/font-name <span style="color:#e6db74">&#39;&#34;Cantarell 11&#34;&#39;</span>
</span></span></code></pre></div><p>The double quotes are required because <code>dconf</code> expects a value surrounded by
quotes, and the terminal will &ldquo;swallow&rdquo; the outer quote and treat their inner
content as a single string.</p>
<h1 id="source-of-the-change">Source of the change<div class="permalink">[<a href="#source-of-the-change">permalink</a>]</div></h1>
<p>The default values for <code>dconf</code> are stored in some XML files in
<code>/usr/share/glib-2.0/schemas</code>. In this case, the value is in
<code>org.gnome.desktop.interface.gschema.xml</code>, which is provided by the
<code>gsettings-desktop-schemas</code>. <a href="https://gitlab.gnome.org/GNOME/gsettings-desktop-schemas/-/blob/48.0/NEWS#L12">The upstream changelog for this project lists the
change in default font under <em>Major changes in 48.beta</em></a>. I&rsquo;m not
running beta; I&rsquo;m running the final release, but it seems that this is one of
those projects where stable releases don&rsquo;t mention all applicable major changes;
end users are expected to read all changelog details for release candidates and
betas too.</p>
]]></description></item><item><title>Design for Google CalDAV support in pimsync</title><link>https://whynothugo.nl/journal/2025/03/04/design-for-google-caldav-support-in-pimsync/</link><pubDate>Tue, 04 Mar 2025 15:29:05 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/03/04/design-for-google-caldav-support-in-pimsync/</guid><description><![CDATA[<p>One of the most popular features present in vdirsyncer and still missing in
pimsync is support for Google&rsquo;s CalDAV service. While CalDAV itself is standard,
Google&rsquo;s implementation is unique in a couple of ways (while still technically
being standards-compliant).</p>
<p>There are two major differences that tools like pimsync and vdirsyncer need to
keep in mind.</p>
<h1 id="calendar-paths">Calendar paths<div class="permalink">[<a href="#calendar-paths">permalink</a>]</div></h1>
<p>The first is not a big deal, and relates to how Google defines the paths for
each calendar. Most servers host all of a users&rsquo;s calendars under a single path,
such as:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>/users/hugo@whynothugo.nl/personal/
</span></span><span style="display:flex;"><span>/users/hugo@whynothugo.nl/work/
</span></span><span style="display:flex;"><span>/users/hugo@whynothugo.nl/travel/
</span></span></code></pre></div><p>Both vdirsyncer and pimsync follow a convention based on this: the last
component in a calendar&rsquo;s path is treated as its name, and is also used as a
name for a local directory which mirrors that calendar.</p>
<p>Google uses something that looks vaguely like so:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>/user/some-user/calendars/aluMp7OOf2/events/
</span></span><span style="display:flex;"><span>/user/some-user/calendars/GCmYjRTK1t/events/
</span></span><span style="display:flex;"><span>/user/some-user/calendars/LZ81HRKLRv/events/
</span></span></code></pre></div><p>Note that each calendar is a collection named &ldquo;events&rdquo; and is itself contained
in a collection with a unique name.</p>
<p>Vdirsyncer implements Google support as a special type of storage, and uses the
second-to-last component as the unique name for a calendar.</p>
<p>For pimsync, I intend to do something similar; I&rsquo;ll implement a behaviour to use
the second-to-last component as a calendar name, but leave this behind an
independent configuration directive. This setting will automatically default to
<code>True</code> if you&rsquo;re syncing with <code>https://www.googleapis.com</code>, but shall also be
usable in other settings where a similar behaviour is required.</p>
<h1 id="oauth-instead-of-basic-auth">OAuth instead of Basic Auth<div class="permalink">[<a href="#oauth-instead-of-basic-auth">permalink</a>]</div></h1>
<p>Google only offers OAuth for authentication to their CalDAV services. The flow
in this authentication mechanism is quite different from Basic Auth (and Digest
Auth), which is what most other providers and implementation use.<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
<p>When using Basic authentication, a user simply provides a username and password,
and pimsync uses that to authenticate itself.</p>
<p>When using Google&rsquo;s flavour of CalDAV, the steps are a lot more:</p>
<ol>
<li>Obtain a <code>client_id</code> and <code>client_secret</code> via Google&rsquo;s Web interface. For
web-based services, the operator of the service provisions these credentials
and users don&rsquo;t need to deal with this, but for software that runs on a local
host, end users need to do this themselves. This only needs to be done once,
but can be quite off-putting and confusing for new users.<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup></li>
<li>Configure pimsync with this <code>client_id</code> and <code>client_secret</code> and initiate the
authentication flow. This opens a web-browser, where the user logs in through
Google&rsquo;s website.</li>
<li>After logging in, the user is redirected to a URL they configured in step 1.
Vdirsyncer runs an HTTP service in a local port to handle this redirection,
but it&rsquo;s always a source of issues when users are configuring a remote host
which doesn&rsquo;t expose arbitrary ports. The only flow usable by desktop
applications was deprecated by Google some years ago without a usable
substitute.</li>
<li>Finally, pimsync can handle the tokens returned by Google to perform
authentication. There are two tokens: one is used to authenticate requests
and expires quite often. The other token is used to renew the first. Both of
these need to be periodically persistent, which is what makes the whole
mechanism stateful.</li>
</ol>
<h1 id="personal-thoughts-on-the-situation">Personal thoughts on the situation<div class="permalink">[<a href="#personal-thoughts-on-the-situation">permalink</a>]</div></h1>
<p>Personally, I&rsquo;d suggest to folks to just stay away from Google and use some
other hosting provider that isn&rsquo;t contiguously campaigning against user&rsquo;s
digital rights and digital autonomy. But the reality is that a lot of folks
simply don&rsquo;t have a choice. The most typical scenario is one&rsquo;s employer using
Google Workspaces to coordinate internally. People who are in such situation
simply need to access calendars on Google&rsquo;s servers and don&rsquo;t have any choice on
their service provider.</p>
<p>And then, if tools like pimsync don&rsquo;t support Google&rsquo;s CalDAV services,
individuals and organisations would have a much harder time migrating away onto
another platform.</p>
<h1 id="proposed-solutions">Proposed solutions<div class="permalink">[<a href="#proposed-solutions">permalink</a>]</div></h1>
<p>I have been pondering for a long time two potential solutions…</p>
<h2 id="an-oauth-proxy">An OAuth proxy<div class="permalink">[<a href="#an-oauth-proxy">permalink</a>]</div></h2>
<p>The first approach is to implement an OAuth proxy. This proxy is configured once
by an operator, and then usable by themselves or by their family and community.
The proxy is configured once with a pair of <code>client_id</code> and <code>client_secret</code>,
negating the need for everyone else to provision their own. When users create
their account on the proxy, they also log in with their Google accounts, and the
access tokens are stored by the proxy, encrypted with their password. Finally,
users simply configure pimsync to talk to this server, which decrypts their
tokens with their password and relays requests to Google&rsquo;s services.</p>
<p>I like the approach in this solution: each of the two components involved do one
thing (i.e.: good separation of concerns), it doesn&rsquo;t further increase
complexity in pimsync, and the proxy is also usable by <em>any</em> CalDAV or CardDAV
application which supports Basic Auth. It doesn&rsquo;t just enable pimsync to use
Google&rsquo;s CalDAV services, but any other CalDAV application, with the application
requiring any custom support.</p>
<p>Alas, for the typical sole user configuring one or two of their own computers,
this doesn&rsquo;t make life a lot easier — it merely makes things harder by having to
configure two separate services and glue then together.</p>
<p>I still think that the proxy is a useful tool to be implemented in future, but
it won&rsquo;t be the primary solution for pimsync. I have a much longer design
specification this idea, so please reach out if you&rsquo;d be interested in
implementing it.</p>
<h2 id="native-oauth-support">Native OAuth support<div class="permalink">[<a href="#native-oauth-support">permalink</a>]</div></h2>
<p>The second potential solution is to implement OAuth support in pimsync itself.
Most feedback I&rsquo;ve received has also hinted that this is the most user-friendly
approach, and this is what I plan to implement in future.</p>
<p>As I <a href="/journal/2024/12/16/pimsync-status-update-2024-12/">mentioned a few months ago</a>, <a href="https://docs.rs/libdav/latest/libdav/"><code>libdav</code></a> itself uses a
middleware approach for extension functionality to its CalDAV and CardDAV
clients. Functionality like implementing authentication and setting custom
headers is already feasible with this approach, so this library doesn&rsquo;t need any
custom support for OAuth. This is great news, because it allows keeping this
library compact and well-focused.</p>
<p>In order to make this feature a reality, the following functionality needs to be
implemented:</p>
<ol>
<li>Support configuring OAuth configuration flows. The OAuth parameters
themselves shall be provided separately, to avoid having to repeat them for
multiple storages (the example below re-uses the same configuration for
CalDAV and CardDAV). The <code>client_id</code> and <code>client_secret</code> parameters can be
specified as an external command with the same syntax as <code>password</code>s. A full
example:</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;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>auth google_foo {
</span></span><span style="display:flex;"><span>    type oauth
</span></span><span style="display:flex;"><span>    auth_url https://accounts.google.com/o/oauth2/v2/auth
</span></span><span style="display:flex;"><span>    client_id 123
</span></span><span style="display:flex;"><span>    client_secret 123
</span></span><span style="display:flex;"><span>    token_file ~/.local/share/pimsync/foo.tokens
</span></span><span style="display:flex;"><span>    scope https://www.googleapis.com/auth/calendar
</span></span><span style="display:flex;"><span>    scope https://www.googleapis.com/auth/carddav
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>storage calendars_foo {
</span></span><span style="display:flex;"><span>    type caldav
</span></span><span style="display:flex;"><span>    url https://apidata.googleusercontent.com/caldav/v2/
</span></span><span style="display:flex;"><span>    auth google_foo
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>storage contacts_foo {
</span></span><span style="display:flex;"><span>    type caldav
</span></span><span style="display:flex;"><span>    url https://www.googleapis.com/.well-known/carddav
</span></span><span style="display:flex;"><span>    auth google_foo
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><ol start="2">
<li>A middleware that implements an OAuth flow on top of the <a href="https://docs.rs/tower/0.5.2/tower/trait.Service.html">tower service
trait</a>. Existing Rust libraries which implement OAuth all seem to
implement too much, including their own internal IO (i.e.: the opposite of
<a href="https://sans-io.readthedocs.io/">sans-io</a>). They are not composible with other libraries, including <code>libdav</code>.
I&rsquo;ll implement a minimal library for OAuth clients, and a lightweight
middleware to fit it into the <code>tower</code> stack.</li>
</ol>
<ol start="3">
<li>
<p>Include the library itself as a dependency of pimsync, and write the final
glue code to use the middleware in cases of a matching configuration.</p>
</li>
<li>
<p>Finally, if the URL matches any of the two URLs above, use the alternative
logic for calendar names that I mentioned above in <a href="#calendar-paths">Calendar Paths</a>.</p>
</li>
</ol>
<p>Note that all OAuth related URLs can be provided in the configuration, so the
implementation is usable with any server which wishes to implement OAuth in
future, and not just the one that exists right now.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>I am no aware of any other existing CalDAV implementation which uses OAuth,
nor have found no evidence of one.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>This entire process would be unnecessary if Google implemented <a href="https://www.rfc-editor.org/rfc/rfc7591">OAuth
Dynamic Client Registration</a>. That would make accessing one&rsquo;s own
data too easy, and that doesn&rsquo;t fit Google&rsquo;s typical modus operandi.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>ab-bday: generate icalendar events for birthdays in vcard</title><link>https://whynothugo.nl/journal/2025/03/03/ab-bday-generate-icalendar-events-for-birthdays-in-vcard/</link><pubDate>Mon, 03 Mar 2025 18:10:12 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/03/03/ab-bday-generate-icalendar-events-for-birthdays-in-vcard/</guid><description><![CDATA[<p>I have published and tagged an initial release of <a href="https://git.sr.ht/~whynothugo/ab-bday">ab-bday</a> (address book
birthday), a small tool which reads a directory full of vCard files, and writes
an iCalendar event for each one.</p>
<p>It&rsquo;s a simple tool, yet I find it immensely useful. Given my usage of existing
standards and conventions, it integrates well with existing tools. It is usable
with <a href="https://pimsync.whynothugo.nl/">pimsync</a> (or <a href="https://vdirsyncer.pimutils.org/en/stable/">vdirsyncer</a>) to synchronise this birthday calendar event
onto a server and onto other devices.</p>
<p>If all your calendar applications on all your devices integrate into your
address book and show birthdays, then this won&rsquo;t be of much use. But if you have
<em>any</em> device which doesn&rsquo;t, this solution works anywhere without any special
support from your calendar.</p>
<p>This initial release implements all the basic features. For future versions I
have a few useful addition in mind:</p>
<ul>
<li><code>-a</code> to create recurring events for anniversaries.</li>
<li><code>-m</code> to monitor the input directory and update calendars immediately as soon
as a contact is modified.</li>
<li>Include the age in each recurring instance of an event.</li>
<li>Localisation. E.g.: Support for event descriptions in other languages.</li>
</ul>
<p>Let me know if this tool is of use to you or if you find any bugs!</p>
]]></description></item><item><title>Status update 2025-02</title><link>https://whynothugo.nl/journal/2025/02/25/status-update-2025-02/</link><pubDate>Tue, 25 Feb 2025 11:53:09 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/02/25/status-update-2025-02/</guid><description><![CDATA[<h1 id="pimsync-owner-of-calendar-metadata">Pimsync &ldquo;owner of calendar&rdquo; metadata<div class="permalink">[<a href="#pimsync-owner-of-calendar-metadata">permalink</a>]</div></h1>
<p>Pimsync synchronises a few metadata attributes for calendars (<code>displayname</code>,
<code>colour</code>, <code>description</code>, <code>order</code>). I had an intent to add <code>owner</code> to this list.
This turns out to be impractical.</p>
<p>A WebDAV server exposes a <code>calendar-user-address-set</code> property, which is the
address (typically an email) of a user. This property would represent the
<code>owner</code> for a calendar. I planned to synchronise this property for every
calendar, so that applications inspecting a calendars on a local directory can
understand the owner of each one. If one of my calendars belongs to
<code>hugo@whynothugo.nl</code>, and another to <code>hugo@example.com</code>, applications would be
able to automatically detect this information and use it for sending out invites
(via iTIP) with the correct <code>Organiser</code> or for sending RSVPs with the correct
<code>Attendant</code>.</p>
<p>I overlooked one important detail. The <code>calendar-user-address-set</code> property is
not defined for each calendar; it&rsquo;s defined for the &ldquo;user principal&rdquo;, a
collection which contains multiple calendars, typically all the calendars
belonging to a single user. Due to this detail there are too many cases where
the results of synchronising it would be unexpected, if not outright invalid.</p>
<p>Example 1: I share my work calendar with a colleague, and they synchronise it
into their server. The &ldquo;owner&rdquo; property is not per-calendar, but at one level
up. Synchronising the owner would change the owner for all their personal
calendars, which would trickle into all sorts of issues. The only solution is to
ignore the owner in this case.</p>
<p>Example 2: I synchronise collections from two different CalDAV sources into a
single local vdir. The two CalDAV sources have a different owner. There is no
way to synchronise the two conflicting values into the local storage.</p>
<p>Finally, pimsync&rsquo;s approach to storages and collections is that <em>collections</em>
(e.g.: calendars or address books) have properties, not the whole storage. The
minimal unit which we synchronise is a collection, and having properties at a
higher level simply doesn&rsquo;t fit the model.</p>
<p>I can&rsquo;t think of an approach for this feature which doesn&rsquo;t produce undesirable
results in surprising ways. So I&rsquo;ve decided to omit this feature, the
alternative would be to surprise users in how their data is overwritten. By the
way, changing the <code>calendar-user-address-set</code> on the server would also impact
how the server processes replies, and I can imagine someone going crazy trying
to understand the source of the bug until the pinpoint it back to pimsync.</p>
<p>Other components of the project do implement related functionality. <code>libdav</code>
provides a function to obtain the current calendar address set, and <code>davcli</code>
exposes this information when inspecting a server. Both of these features shall
remain in place.</p>
<p>Tracking issue: <a href="https://todo.sr.ht/~whynothugo/pimsync/8">https://todo.sr.ht/~whynothugo/pimsync/8</a></p>
<h1 id="pimsync-shell-mechanism">Pimsync &ldquo;shell&rdquo; mechanism<div class="permalink">[<a href="#pimsync-shell-mechanism">permalink</a>]</div></h1>
<p>vdirsyncer allowed specifying a short in-line shell script for retrieving
credentials. I&rsquo;ve implemented this same mechanism in pimsync. It&rsquo;s really just a
shortcut, but it makes lives easier for people, and the implementation cost is
minimal.</p>
<p>You can now specify that credentials are to be retrieved via a command:</p>
<!-- FIXME: gohugo doesn't highlight scfg blocks -->
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span># This is safe to use with any special character.
</span></span><span style="display:flex;"><span>password {
</span></span><span style="display:flex;"><span>    cmd hiq -dFpassword proto=carddavs username=…
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Or via a short sh snippet:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span># Shell rules apply for special characters here, but you can pipe much easier
</span></span><span style="display:flex;"><span>password {
</span></span><span style="display:flex;"><span>    shell pass show communication/migadu.com | head -1
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>In reality, the latter is an alias for the following, which is a bit less
comprehensible for non-hardcore shell users:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>password {
</span></span><span style="display:flex;"><span>    cmd sh -c &#34;pass show communication/migadu.com | head -1&#34;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Tracking issue: <a href="https://todo.sr.ht/~whynothugo/pimsync/136">https://todo.sr.ht/~whynothugo/pimsync/136</a></p>
<h1 id="pimsync-v030">Pimsync v0.3.0<div class="permalink">[<a href="#pimsync-v030">permalink</a>]</div></h1>
<p>I&rsquo;ve tagged a new release with the above features a few other bugfixes. See the
<a href="https://git.sr.ht/~whynothugo/pimsync/tree/6080ea2c3114bdc362473491c31b68e4f31ab5a7/item/CHANGELOG.md#v030">changelog</a> for details.</p>
<h1 id="roadmap">Roadmap<div class="permalink">[<a href="#roadmap">permalink</a>]</div></h1>
<p>I&rsquo;ve tagged <a href="https://todo.sr.ht/~whynothugo/pimsync?search=label%3A%220%3Atodo%22+status%3Aopen">prioritised tasks in the issue tracker as <code>todo</code></a>. This is
mostly for my own visibility, and these fall under the current grant from NLNet.</p>
<p>I had a list of tasks that fell under the scope of the grant, but each time that
I needed to prioritise tasks, I needed to cross-check that list with the issue
tracker, which ended up in a lot of pointless overhear. Tagging all issues is
the obvious solution in hindsight.</p>
]]></description></item><item><title>Gamma correction and blending fonts in linear space</title><link>https://whynothugo.nl/journal/2025/02/24/gamma-correction-and-blending-fonts-in-linear-space/</link><pubDate>Mon, 24 Feb 2025 11:16:08 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/02/24/gamma-correction-and-blending-fonts-in-linear-space/</guid><description><![CDATA[<p>This morning I read about <a href="https://social.treehouse.systems/@dnkl/114057947159191864">foot implementing gamma-correct font
rendering</a>, and while all those words sound familiar, I still didn&rsquo;t know
exactly what “gamma correcting” meant in this context. This drove me into a
reading journey to better understand the topic, and the following is a short
summary of what I&rsquo;ve learnt</p>
<h1 id="facts">Facts<div class="permalink">[<a href="#facts">permalink</a>]</div></h1>
<p>When rendering fonts, especially at small sizes or on high-resolution screens,
the edges of the characters are often anti-aliased to make them look smooth.
Anti-aliasing works by blending the font colour with the background colour at
the edges.</p>
<p>Human vision is non-linear: our eyes are much more sensitive to changes in dark
tones than in lighter tones. E.g.: The difference between 10% and 20% brightness
is much more noticeable to us than the difference between 80% and 90%
brightness.<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
<p>To account for our non-linear vision, RGB values are stored using a non-linear
scale called gamma space. Gamma space allocates a larger range to dark tones,
making images look more natural to our eyes.</p>
<p>We also represent colours in non-linear space due to technical reason related to
ancient <a href="https://en.wikipedia.org/wiki/Cathode-ray_tube">CRTs</a> for which we use
non-linear space, but that&rsquo;s mostly historical baggage.</p>
<h1 id="gamma-space">Gamma space<div class="permalink">[<a href="#gamma-space">permalink</a>]</div></h1>
<p>When storing or displaying an image, pixel values are often raised to a power
(1/2.2) to account for the non-linear response of displays.</p>
<p>To perform accurate calculations on colours we first need to convert them from a
non-linear space into a linear space. To do this, pixel values are raised to the
inverse power (2.2).</p>
<p>This value (2.2) is called gamma. I&rsquo;ll continue using 2.2 below to keep things
simple.</p>
<span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>γ</mi><mo>=</mo><mn>2.2</mn></mrow><annotation encoding="application/x-tex">\gamma = 2.2</annotation></semantics></math></span>
<h1 id="problem">Problem<div class="permalink">[<a href="#problem">permalink</a>]</div></h1>
<p>Colour values are represented in non-linear scale, so if we perform calculations
on them without first converting them into a linear scale, we get inaccurate
results.</p>
<p>This results in incorrect blending of colours, where edges of text might be too
dark, appear less sharp, or be inconsistent across different background.
Contrast might also not be as good as expected.</p>
<h1 id="example">Example<div class="permalink">[<a href="#example">permalink</a>]</div></h1>
<p>Let&rsquo;s say we&rsquo;re blending black and white with 50% coverage. Our colours are
values between 0 and 1. Often these are represented by integers between 0 and
255, but the mathematics are easier to explain with values between 0 and 1.</p>
<h3 id="example-operating-in-non-linear-space">Example operating in non-linear space<div class="permalink">[<a href="#example-operating-in-non-linear-space">permalink</a>]</div></h3>
<p>If we operate with non-linear colour values, we blend by applying the following
operation:</p>
<!-- FIXME: why does \newline not work? -->
<span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>B</mi><mi>l</mi><mi>e</mi><mi>n</mi><mi>d</mi><mi>e</mi><mi>d</mi><mo>=</mo><mi>α</mi><mo>×</mo><mi>W</mi><mi>h</mi><mi>i</mi><mi>t</mi><mi>e</mi><mo>+</mo><mo stretchy="false">(</mo><mn>1</mn><mo>−</mo><mi>α</mi><mo stretchy="false">)</mo><mo>×</mo><mi>B</mi><mi>l</mi><mi>a</mi><mi>c</mi><mi>k</mi></mrow><annotation encoding="application/x-tex">Blended = \alpha \times White + (1 - \alpha) \times Black</annotation></semantics></math></span>
<span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>B</mi><mi>l</mi><mi>e</mi><mi>n</mi><mi>d</mi><mi>e</mi><mi>d</mi><mo>=</mo><mn>0.5</mn><mo>×</mo><mn>0</mn><mo>+</mo><mn>0.5</mn><mo>×</mo><mn>1</mn></mrow><annotation encoding="application/x-tex">Blended = 0.5 \times 0 + 0.5 \times 1</annotation></semantics></math></span>
<span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>B</mi><mi>l</mi><mi>e</mi><mi>n</mi><mi>d</mi><mi>e</mi><mi>d</mi><mo>=</mo><mn>0.5</mn></mrow><annotation encoding="application/x-tex">Blended = 0.5</annotation></semantics></math></span>
<p>Keep in mind that we&rsquo;re using a non-linear scale, so 0.5 is not the &ldquo;halfway&rdquo;
value between 0 and 1.</p>
<h3 id="example-operating-in-linear-space">Example operating in linear space<div class="permalink">[<a href="#example-operating-in-linear-space">permalink</a>]</div></h3>
<p>If we operate by first transforming the colours into linear space, the result is
entirely different. This process is called &ldquo;gamma correction&rdquo;.</p>
<p>First convert the values from non-linear space into linear space. For these
particular values (0 and 1), they are the same, but this is not true for any
other value.</p>
<!-- FIXME: why does \newline not work? -->
<span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>B</mi><mi>l</mi><mi>a</mi><mi>c</mi><msub><mi>k</mi><mtext>linear</mtext></msub><mo>=</mo><msup><mn>0</mn><mn>2.2</mn></msup><mo>=</mo><mn>0</mn></mrow><annotation encoding="application/x-tex">Black_{\text{linear}} = 0^{2.2} = 0</annotation></semantics></math></span>
<span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>W</mi><mi>h</mi><mi>i</mi><mi>t</mi><msub><mi>e</mi><mtext>linear</mtext></msub><mo>=</mo><msup><mn>1</mn><mn>2.2</mn></msup><mo>=</mo><mn>1</mn></mrow><annotation encoding="application/x-tex">White_{\text{linear}} = 1^{2.2} = 1</annotation></semantics></math></span>
<p>Second, perform the blending operation, in linear space:</p>
<span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>B</mi><mi>l</mi><mi>e</mi><mi>n</mi><mi>d</mi><mi>e</mi><msub><mi>d</mi><mtext>linear</mtext></msub><mo>=</mo><mi>α</mi><mo>×</mo><mi>W</mi><mi>h</mi><mi>i</mi><mi>t</mi><msub><mi>e</mi><mtext>linear</mtext></msub><mo>+</mo><mo stretchy="false">(</mo><mn>1</mn><mo>−</mo><mi>α</mi><mo stretchy="false">)</mo><mo>×</mo><mi>B</mi><mi>l</mi><mi>a</mi><mi>c</mi><msub><mi>k</mi><mtext>linear</mtext></msub></mrow><annotation encoding="application/x-tex">Blended_{\text{linear}} = \alpha \times White_{\text{linear}} + (1 - \alpha) \times Black_{\text{linear}}</annotation></semantics></math></span>
<span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>B</mi><mi>l</mi><mi>e</mi><mi>n</mi><mi>d</mi><mi>e</mi><msub><mi>d</mi><mtext>linear</mtext></msub><mo>=</mo><mn>0.5</mn><mo>×</mo><mn>0</mn><mo>+</mo><mn>0.5</mn><mo>×</mo><mn>1</mn></mrow><annotation encoding="application/x-tex">Blended_{\text{linear}} = 0.5 \times 0 + 0.5 \times 1</annotation></semantics></math></span>
<span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>B</mi><mi>l</mi><mi>e</mi><mi>n</mi><mi>d</mi><mi>e</mi><msub><mi>d</mi><mtext>linear</mtext></msub><mo>=</mo><mn>0.5</mn></mrow><annotation encoding="application/x-tex">Blended_{\text{linear}} = 0.5</annotation></semantics></math></span>
<p>Finally, transform the result back into the original non-linear space:</p>
<span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>B</mi><mi>l</mi><mi>e</mi><mi>n</mi><mi>d</mi><mi>e</mi><msub><mi>d</mi><mtext>gamma</mtext></msub><mo>=</mo><msup><mn>0.5</mn><mrow><mn>1</mn><mi mathvariant="normal">/</mi><mn>2.2</mn></mrow></msup><mo>≈</mo><mn>0.73</mn></mrow><annotation encoding="application/x-tex">Blended_{\text{gamma}} = 0.5^{1/2.2} \approx 0.73</annotation></semantics></math></span>
<p>The difference is far from trivial. For a visual reference, the following two
rectangles have their background colour set to 0.5 and 0.73 respectively.</p>
<div style="background-color: rgb(127,127,127); color: black; padding: 0.2em;">
This rectangle's background was calculated in non-linear space.<br>
It's darker than "mid way".
</div>
<div style="background-color: rgb(186,186,186); color: black; padding: 0.2em;">
This rectangle's background was calculated in linear space.<br>
E.g.: is has "gamma correction" applied.<br>
It's "mid way" between black and white.
</div>
<p>The first of the above rectangles is much close to black, and you might even
notice that text on it has less contrast than ideal.</p>
<p>If you want to see some visual examples of blending text with proper gamma
correction, see the sample screenshot in the <a href="https://codeberg.org/dnkl/foot/pulls/1974">PR which implements this feature
for foot</a>. The text is noticeably sharper.</p>
<h1 id="performance">Performance<div class="permalink">[<a href="#performance">permalink</a>]</div></h1>
<p>There&rsquo;s a lot of maths behind applying gamma correction. The above example does
this for a single channel, but real usages have three channels (red, green,
blue).</p>
<p>Fortunately, GPUs are designed for exactly this, and applying gamma correction
using the GPU has an insignificant overhead.</p>
<div class="notice update">
  <header>Update 2025-02-24</header>
  <section>
  Typically the conversion is between 8-bit encoded channels, so it&rsquo;s possible to
implement the translation lookup tables, which is a lot more performant that
doing the actual arithmetic.
  </section>
</div>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>In fact, at night I sometimes adjust the screen brightness from 3% to 5% and
it seems like a big difference. During the day, I adjust the brightness from
80% and 90% and the difference is very little.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>One year of using the web with web fonts disabled</title><link>https://whynothugo.nl/journal/2025/02/10/one-year-of-using-the-web-with-web-fonts-disabled/</link><pubDate>Mon, 10 Feb 2025 15:09:33 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/02/10/one-year-of-using-the-web-with-web-fonts-disabled/</guid><description><![CDATA[<p>Early last year, I disabled web fonts in my browser. Several sites and blogs
which I follow use a custom font that the author thought was pretty but has
quite poor legibility. Some make it hard to tell apart the <code>m</code> and the <code>n</code>.
Others use thin fonts which are hard to focus on visually. Some are just
terrible for readability and merely designed to look good.</p>
<p>Disabling web fonts is a known accessibility feature for users with dyslexia,
low vision, or other cognitive impairments. I also consider my usage of this
toggle an accessibility tweak, despite not belonging to any of the above groups.</p>
<p>My experience was quite positive although not without its caveats. The main
thing that breaks is when sites use web fonts for icons. <strong>Don&rsquo;t use web fonts
for icons</strong>. They are an unnecessary hack and break when enabling this
accessibility feature.</p>
<h1 id="the-good">The good<div class="permalink">[<a href="#the-good">permalink</a>]</div></h1>
<p>As intended, most sites became easier to read, especially those that use exotic
fonts with poor readability.</p>
<p>As an unexpected side effect, pages loaded noticeably faster. The improvement
was immediately obvious and the explanation is simple: instead of having to wait
for an additional font resource to load before rendering, the browser can render
text on websites much sooner. This was a very pleasant side effect.</p>
<h1 id="the-bad">The bad<div class="permalink">[<a href="#the-bad">permalink</a>]</div></h1>
<p>Instead of using SVG or PNG for icons and small images many websites encode
these into fonts, and display them as if they were text. These icons no longer
render when disabling web fonts. Instead, they just render a placeholder for a
character, or sometimes random letters and symbols.</p>
<p>This ugly hack originally existed to support Internet Explorer, which lacked
inline SVG support and had other limitations making, web fonts the easiest
workaround. Despite IE being long dead in 2025, web developers continue using
font icons out of habit. It doesn&rsquo;t help that a lot of online documentation
still promotes this practice.</p>
<p>Many popular website themes and icon libraries still rely on font icons. Some
have migrated, but many haven’t. This process takes time and effort, and
outdated documentation doesn&rsquo;t help here.</p>
<p>I tried to fix some of the more popular tools and themes used for documentation
websites in the open source ecosystem. But the sad reality is that most
developers don&rsquo;t seem to care about accessibility.</p>
<p>A few specific sites break quite badly without web fonts. One of the worst
offenders is Google&rsquo;s documentation websites. Individual icons are replaced with
a whole word or a short sentence, but these overlap with the text around them,
making the whole thing unreadable. I have no idea what kinds of horrible hacks
they&rsquo;re using, but it&rsquo;s honestly amazing how much complexity has been piled onto
simply rendering some text, hyperlinks and a few icons.</p>
<h1 id="conclusion">Conclusion<div class="permalink">[<a href="#conclusion">permalink</a>]</div></h1>
<p>Don&rsquo;t use web fonts for icons. Use the formats that were invented for this
purpose instead. IE, the only reason to use these, is long dead.</p>
<p>When reviewing the accessibility of your website, check how it behaves with web
fonts disabled. Keep this as an item to your accessibility checklist.</p>
<h1 id="related">Related<div class="permalink">[<a href="#related">permalink</a>]</div></h1>
<p>As a reminder, <a href="https://tonsky.me/blog/centering/">font icons seldom align properly with text</a>.</p>
]]></description></item><item><title>Breaking the dependency treadmill</title><link>https://whynothugo.nl/journal/2025/01/24/breaking-the-dependency-treadmill/</link><pubDate>Fri, 24 Jan 2025 14:47:10 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/01/24/breaking-the-dependency-treadmill/</guid><description><![CDATA[<p>Modern dependency practices often feel like an endless treadmill of updates,
churn and bloat driven by huge dependency trees.</p>
<p>I find great value in tools and libraries with minimal dependencies. Simple code
is a sign of good design. Too many layers of abstraction overcomplicate
software, leading to maintenance headaches and security risks. It’s called
over-engineering for a reason.</p>
<p>Armin Ronacher captures this frustration perfectly in <a href="https://lucumr.pocoo.org/2025/1/24/build-it-yourself/">Build It Yourself</a>
and I share his sentiment, especially when it comes to Rust. I highly recommend
giving his article a read. It&rsquo;s a well-written piece on how we can do better.</p>
]]></description></item><item><title>Status update 2025-01</title><link>https://whynothugo.nl/journal/2025/01/22/status-update-2025-01/</link><pubDate>Wed, 22 Jan 2025 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/01/22/status-update-2025-01/</guid><description><![CDATA[<p>I&rsquo;ve recently tagged <a href="https://pimutils.org/blog/2025-01-07-pimsync-v0.2.0/">v0.2.0 of pimsync</a>. With pimsync being in a rather
stable state, I&rsquo;m aspiring to make more frequent releases with small sets of
changes. I&rsquo;ll be publishing release notes in the pimutils.org blog, so packagers
have a lower-volume RSS feed to track releases.</p>
<h1 id="optimised-discovery-process">Optimised discovery process<div class="permalink">[<a href="#optimised-discovery-process">permalink</a>]</div></h1>
<p>When libdav performs DNS-based service discovery for a given <code>base_url</code> it now
first checks whether the input <code>base_url</code> is a valid CalDAV/CardDav context. If
it is, then discovery can be skipped entirely and that input URL is used
instead. This skips unnecessary work, and allows using libdav in situations
where discovery is misconfigured on a server.</p>
<p>Pimsync uses libdav for discovery, so this improvement affects pimsync as well.</p>
<h1 id="pimsync-storage-monitoring">pimsync storage monitoring<div class="permalink">[<a href="#pimsync-storage-monitoring">permalink</a>]</div></h1>
<p>I&rsquo;ve finalised a design for storage monitoring in pimsync. It can now monitor a
storage for changes, and react immediately when they happen. This allows
synchronising changes immediately, rather than on an interval every N minutes.
Currently, monitoring in only implemented for the filesystem storage on Linux
(using inotify). An implementation for BSD using kqueue is pending. CalDAV and
CardDAV have no mechanism to monitor for events, although there is discussion
ongoing to integrate with Web Push. Once this new protocol feature is
stabilised, I&rsquo;ll dive into implementing it for pimsync.</p>
<p>A caveat of how monitoring is integrated is that a full synchronisation is
executed. I.e.: when a single item changes, pimsync still checks all items to
see if they have changes. This adds a bit of extra work (although it&rsquo;s still
minimal CPU use). I intend to iterate upon this further in future, executing a
partial synchronisation (which only sync the item that has changes and doesn&rsquo;t
read or write anything else), but I have other priorities in the immediate
future.</p>
<h1 id="pimsync-faster-start-up">pimsync faster start-up<div class="permalink">[<a href="#pimsync-faster-start-up">permalink</a>]</div></h1>
<p>pimsync now resolves parameters specified via a <code>cmd</code> early during start-up.
This ensures that all prompts for credentials happen quickly, and network IO
happens <em>after</em> these prompts, so you don&rsquo;t have to wait around a second or two
while pimsync starts. I&rsquo;ve also tweaked how locking happens internally:
previously pimsync lock storages so that only one operation happened on each
storage at a time. Now, pimsync only lock individual files, so that we only
avoid concurrent writes to a same file, but multiple operations can run
concurrently on each storage.</p>
<h1 id="various-libdav-improvements">Various libdav improvements<div class="permalink">[<a href="#various-libdav-improvements">permalink</a>]</div></h1>
<p>I&rsquo;ve made substantial changes to handling of escaped characters and reserved
characters in paths to DAV resources. Mishandling of escape sequences is a
common issue for many corner cases in vdirsyncer. I am confident that this set
of changes properly handles all scenarios for libdav and pimsync.</p>
<p>I&rsquo;ve backfilled <a href="https://git.sr.ht/~whynothugo/libdav/tree/main/item/CHANGELOG.md">the changelog</a> with changes in recent versions of libdav as
well, so that consumers can properly track what&rsquo;s changed and how to upgrade.
I&rsquo;ve also made sure that all public interfaces are documented. If anything is
still unclear, please reach out.</p>
<!-- fetching of address set // missing pimsync -->
]]></description></item><item><title>Simple optional dependencies via meta-packages</title><link>https://whynothugo.nl/journal/2025/01/17/simple-optional-dependencies-via-meta-packages/</link><pubDate>Fri, 17 Jan 2025 20:43:23 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/01/17/simple-optional-dependencies-via-meta-packages/</guid><description><![CDATA[<p>I&rsquo;ll present this idea with an example…</p>
<h1 id="context">Context<div class="permalink">[<a href="#context">permalink</a>]</div></h1>
<p><code>todoman</code> optionally depends on <code>py3-repl</code>. If the latter is installed, then
<code>todoman</code> gains a new <code>repl</code> command which starts an interactive console.</p>
<p>One way of indicating that <code>todoman</code> optionally depends on <code>py3-repl</code> is to add
it as an optional dependency. This requires support for optional dependencies in
the package manager and all related tools.</p>
<p>A pitfall of this approach is that there is later no clear indicator that
<code>py3-repl</code> is installed as an optional dependency for <code>todoman</code>. If it is marked
as explicitly installed, then there&rsquo;s no &ldquo;hint&rdquo; for me to later remember that I
installed it as an optional dependency. I&rsquo;ll likely delete it thinking that I
don&rsquo;t use it directly and nothing depends on it. If it is marked as a installed
as a dependency, then it will removed automatically, since nothing explicitly
depends on it.</p>
<p>A package manager might have a different behaviour: when cleaning up, it can
keep around any package that is an optional dependency for another. This would
result in many stale unwanted packages being kept around.</p>
<h1 id="solution">Solution<div class="permalink">[<a href="#solution">permalink</a>]</div></h1>
<p>Create a new meta-package: <code>todoman-repl</code>. This depends on <code>todoman</code> and
<code>py3-repl</code>. If I want <code>todoman</code> with the optional functionality, I simply
install <code>todoman-repl</code>. The package manager won&rsquo;t remove <code>py3-repl</code>, but it also
won&rsquo;t show up as installed explicitly. In fact, the list of desired packages
reflects my exact intent: &ldquo;I want todoman with the repl functionality&rdquo;.</p>
<h1 id="advantages">Advantages<div class="permalink">[<a href="#advantages">permalink</a>]</div></h1>
<p>This does not require any additional complexity or dedicated features in the
package manager or packaging toolchain.</p>
<p>When the package <code>todoman-repl</code> is installed, it clearly reflects the intent of
the user to install <code>todoman</code> with its <code>repl</code> functionality. The <code>repl</code>
dependency is a directly dependency of an explicitly installed package.</p>
]]></description></item><item><title>Why I don't use TikTok</title><link>https://whynothugo.nl/journal/2025/01/16/why-i-dont-use-tiktok/</link><pubDate>Thu, 16 Jan 2025 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/01/16/why-i-dont-use-tiktok/</guid><description><![CDATA[<p>In many ways, I think TikTok is pretty good. It&rsquo;s learnt my tastes, knows my
preferences and how to curate for my tastes. Whenever I have an idle moment, I
can open the app and immediately be presented with some video that I&rsquo;d find
interesting or entertaining. If at a given moment I&rsquo;m not in the mood for a
specific kind of content, I can just dismiss the video and I will be presented
with something else, with the platform somewhat refraining from showing me that
specific kind of content for a while.</p>
<p>And what I&rsquo;ve described is precisely the problem with it. At any idle moment, I
can open the app to pass a single moment and end up being sucked into it for
tens of minutes or even hours. It&rsquo;s very hard to just use it for five to ten
minutes a day. It&rsquo;s designed to be addictive and <em>is</em> addictive.</p>
<p>The only solution for me is to simply not have TikTok installed on my phone.
Like many addictive things in life, the solution is zero consumption, because
consuming a small amount is much harder than abstaining completely.</p>
<p>Public perception around the addictiveness of TikTok is mixed. Some say it&rsquo;s no
addictive at all, while others claim that it&rsquo;s the best thing in the world and
they can quit whenever they what — they just don&rsquo;t feel like quitting.</p>
]]></description></item><item><title>Soju failing with "too many open files"</title><link>https://whynothugo.nl/journal/2025/01/09/soju-failing-with-too-many-open-files/</link><pubDate>Thu, 09 Jan 2025 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2025/01/09/soju-failing-with-too-many-open-files/</guid><description><![CDATA[<p>From the logs (wrapped for readability):</p>
<blockquote>
<p>Jan 9 14:52:37 anchor soju[71321]: 2025/01/09 14:52:37 listener 0.0.0.0:6697:
accept error (retrying in 1s): accept tcp 0.0.0.0:6697: accept4: too many open
files</p>
</blockquote>
<p>Sounds like soju needs more file descriptors that the current limit. I need to
increase the maximum allowed file descriptors.</p>
<h1 id="the-fix">The fix<div class="permalink">[<a href="#the-fix">permalink</a>]</div></h1>
<p>First, give the <code>_soju</code> user a dedicated class:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span><span style="color:#f92672">-_soju:*:895:895::0:0:soju user:/var/soju:/sbin/nologin
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+_soju:*:895:895:soju:0:0:soju user:/var/soju:/sbin/nologin
</span></span></span></code></pre></div><p>Then raise the limit for this class by creating <code>/etc/login.conf.d/soju</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;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>soju:\
</span></span><span style="display:flex;"><span>        :openfiles-max=4096:\
</span></span><span style="display:flex;"><span>        :openfiles-cur=128:\
</span></span><span style="display:flex;"><span>        :tc=default:
</span></span></code></pre></div><p>Finally, rebuild the capability database and restart <code>soju</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;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>cap_mkdb /etc/login.conf
</span></span><span style="display:flex;"><span>rcctl restart soju
</span></span></code></pre></div><h1 id="sources">Sources<div class="permalink">[<a href="#sources">permalink</a>]</div></h1>
<ul>
<li><a href="https://man.openbsd.org/passwd.5">passwd(5)</a></li>
<li><a href="https://man.openbsd.org/login.conf">login.conf</a></li>
</ul>
]]></description></item><item><title>URLs and percent encoding</title><link>https://whynothugo.nl/journal/2024/12/27/urls-and-percent-encoding/</link><pubDate>Fri, 27 Dec 2024 16:06:04 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/12/27/urls-and-percent-encoding/</guid><description><![CDATA[<p>My initial implementation for <a href="https://docs.rs/libdav/latest/libdav/"><code>libdav</code></a>
was to always decode percent-encoded received from a server, and return them to
consumers un-encoded. Likewise, paths passed as arguments should be provided
un-encoded , and <code>libdav</code> would deal with encoding them itself before sending a
request to the server. My mindset was &ldquo;consumers should not need to worry about
percent-encoding URLs, <code>libdav</code> can handle that internally&rdquo;.</p>
<p>In the last couple of days, I&rsquo;ve learnt that my approach was unsound.</p>
<p>I will use the path <code>/path/to/theitem%2Fwithslash.ics</code> as an example. This path
refers to a resource named <code>theitem%2Fwithslash.ics</code> inside the collection
<code>/path/to/</code>. However, decoding this path, it would return
<code>/path/to/theitem/withslash.ics</code>, which points to resource <code>withslash.ics</code> in
the <code>/path/to/theitem/</code> collection. Clearly these are not the same.</p>
<p>URLs describe resources which don&rsquo;t share the name naming limitations as regular
file systems. Resources names may contain any byte sequence, including <code>/</code>
(which needs to be escaped as <code>%2F</code>).</p>
<p>This behaviour is explicitly called out in the
<a href="https://www.rfc-editor.org/rfc/rfc3986#section-2.2">RFC3986, section 2.2</a>:</p>
<blockquote>
<pre><code>reserved    = gen-delims / sub-delims

gen-delims  = &quot;:&quot; / &quot;/&quot; / &quot;?&quot; / &quot;#&quot; / &quot;[&quot; / &quot;]&quot; / &quot;@&quot;

sub-delims  = &quot;!&quot; / &quot;$&quot; / &quot;&amp;&quot; / &quot;'&quot; / &quot;(&quot; / &quot;)&quot;
            / &quot;*&quot; / &quot;+&quot; / &quot;,&quot; / &quot;;&quot; / &quot;=&quot;
</code></pre>
<p>[…] URIs that differ in the replacement of a reserved character with its
corresponding percent-encoded octet are not equivalent. Percent-encoding a
reserved character, or decoding a percent-encoded octet that corresponds to a
reserved character, will change how the URI is interpreted by most
applications. Thus, characters in the reserved set are protected from
normalization and are therefore safe to be used by scheme-specific and
producer-specific algorithms for delimiting data subcomponents within a URI.</p>
</blockquote>
<p>The <a href="https://www.rfc-editor.org/rfc/rfc3986#section-2.3">next section</a> makes a
clarification for unreserved characters:</p>
<blockquote>
<p>URIs that differ in the replacement of an unreserved character with its
corresponding percent-encoded US-ASCII octet are equivalent: they identify the
same resource.</p>
</blockquote>
<p>When implementing client, it&rsquo;s probably safest to treat the path component in
URLs as an opaque string and not to alter it in any way. However, comparing
whether two components are the same requires normalising them, by
percent-decoding non-reserved characters, while maintaining percent-encoded
reserved characters in their original form.</p>
<h1 id="double-encoding-in-webdav">Double-encoding in WebDAV<div class="permalink">[<a href="#double-encoding-in-webdav">permalink</a>]</div></h1>
<p>In WebDAV, resources are listed inside an XML response. In this case, some
characters need to be encoded as XML entities. I.e.: <code>&quot;</code>, <code>'</code>, <code>&lt;</code>, <code>&gt;</code> and <code>&amp;</code>
need to be encoded as <code>&amp;quot;</code>, <code>&quot;&amp;apos;</code>, <code>&amp;lt;</code>, <code>&amp;gt;</code> and <code>&amp;amp;</code>
respectively. These will be decoded when parsing the XML, which happens in a
separate layer, and is invisible to HTTP.</p>
]]></description></item><item><title>Exporting photos from an iPhone</title><link>https://whynothugo.nl/journal/2024/12/22/exporting-photos-from-an-iphone/</link><pubDate>Sun, 22 Dec 2024 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/12/22/exporting-photos-from-an-iphone/</guid><description><![CDATA[<!-- TODO: publish -->
<p>Copying photos out of an iPhone another device is not trivial. The approach
suggested by Apple seems to be to copy your photos onto their servers, but
actually downloading them is a pain. You can either download them one at a time,
or request <a href="https://privacy.apple.com/account">request a takeout</a>, which takes about a week. A ridiculous
approach when you want to move files from one device to another that&rsquo;s 30cm
away.</p>
<p>In my experience, the simplest approach (which is not at all simple) to copying
photos seems to be using <code>usbmuxd</code> with <code>ifuse</code>. These set of tools allow
mounting the phone onto the local file system, and moving the regular photo
files out.</p>
<p>When using <code>usbmuxd</code>, please keep in mind that <a href="https://github.com/libimobiledevice/usbmuxd/issues/224">all users on your target host
have complete access to the iOS device</a>. Even on a single-user system,
this means non-interactive accounts like those used by background system
services have access, including the user <code>nobody</code>.</p>
<p>The latest release of <code>usbmuxd</code> happened in 2020, and doesn&rsquo;t work with recent
devices or recent versions of iOS. You&rsquo;ll need to rebuild from source. All build
dependencies can be installed with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>doas apk add -t .usbmuxd-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    automake <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    autoconf <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    libtool <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    libusb-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    libplist-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    libimobiledevice-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    libimobiledevice-glue-dev
</span></span></code></pre></div><p>Then, clone the source repository and build it as indicated in the instructions:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>git clone https://github.com/libimobiledevice/usbmuxd.git
</span></span><span style="display:flex;"><span>cd usbmuxd
</span></span><span style="display:flex;"><span>./autogen.sh
</span></span><span style="display:flex;"><span>make
</span></span><span style="display:flex;"><span>doas make install
</span></span></code></pre></div><p><code>usbmuxd</code> has some magic mechanism to automatically run when a device is plugged
in, but it is quick hacky and unreliable. Simply run the daemon manually:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>doas usbmuxd --foreground
</span></span></code></pre></div><p>Now plug the iOS device with a USB cable, and mount the iPhone&rsquo;s filesystem:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>mkdir -p ~/mount/iphone
</span></span><span style="display:flex;"><span>ifuse ~/mount/iphone
</span></span></code></pre></div><p>Note that <code>ifuse</code> forks to the background immediately, so while it seems that it
may have exited, it&rsquo;s still running.</p>
<p>Photos and videos will be available under <code>~/mount/iphone/DCIM</code>. They can be
moved around as regular files.</p>
<p><a href="https://github.com/libimobiledevice/ifuse/issues/38">When moving photos out of the phone, the Photos app will still show them as
present</a>. This is because the Photos app keeps its own database of
photos. A workaround for this issue is to delete the Photo app&rsquo;s database. It
will then re-create a new empty database, and show no photos.</p>
<!-- If some photos are left in the phone, they will not be visible on the Photos -->
<!-- app, so I recommend moving all of them at once. -->
<p>To delete the Photo app&rsquo;s database, use:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>cd ~/mount/iphone/PhotoData
</span></span><span style="display:flex;"><span>rm Photos.sqlite Photos.sqlite-shm Photos.sqlite-wal
</span></span></code></pre></div><p>Note that Albums and favourites are generally lost during this process. This
information is also lost when moving the photos out, so I don&rsquo;t bother with
organising photos on-device at all.</p>
<p>Finally, I use <a href="https://paste.sr.ht/~whynothugo/6bcfd4206cca26d9f8f2da6a6638e447b32be1bb">this quick and dirty script</a> to sort photos into
directories by date taken. Do not run this script in a directory with existing
subdirectories; it may overwrite data inside of them.</p>
<p>Note that screenshots don&rsquo;t have date and time metadata, so this also filters
them out rather quickly (I personally don&rsquo;t store screenshots with my &ldquo;real&rdquo;
photos).</p>
]]></description></item><item><title>Pimsync status update 2024-12</title><link>https://whynothugo.nl/journal/2024/12/16/pimsync-status-update-2024-12/</link><pubDate>Mon, 16 Dec 2024 16:39:18 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/12/16/pimsync-status-update-2024-12/</guid><description><![CDATA[<p>When re-using the name <code>vdirsyncer</code>, versioning for this new project was a bit
messy. Now that this new project has its own name, <code>pimsync</code>, versioning can
start from zero. I have published <a href="https://git.sr.ht/~whynothugo/pimsync/refs/v0.1.0">a new tag, v0.1.0</a>, which can be used be
early adopters. I also <a href="https://gitlab.alpinelinux.org/alpine/aports/-/merge_requests/77155">submitted a new package</a> to the Alpine Linux
testing repositories.</p>
<p>Packages from other distributions are welcome to start publishing packages.
Please report any issues, including lack of documentation. The README now
explains requirements, compilation and installation, something obvious that
somehow slipped through the cracks until this point.</p>
<p>I have updated <code>libdav</code> to rely on the <code>hyper</code>/<code>tower</code> middleware stack, which
is currently the standard in the Rust ecosystem. This allows consumers of this
library to alter requests and responses in any way that they need, including
custom authentication, setting a User-Agent header (or any other header),
domain-specific logging, etc. <a href="https://docs.rs/libdav/0.6.1/libdav/">Version 0.6.1 includes further documentation and
examples on this</a>.</p>
<p>Pimsync itself leverages the above feature, and now properly sets a User-Agent
header. This can also be customised in the configuration file. The
<a href="https://pimsync.whynothugo.nl/pimsync.conf.5.html">pimsync.conf(5)</a> man page explains this in detail.</p>
<p>As you might notice from the above link, manual pages are now published in HTML
form, making them convenient to read online. I&rsquo;ve created a small and simple
<a href="https://pimsync.whynothugo.nl/">landing website</a> for pimsync too, mostly as an index of existing
material.</p>
<p>Another small change is that <code>pimsync</code> will now embed the proper version,
regardless of whether it is build from tarballs or a git checkout. I wrote <a href="/journal/2024/12/16/injecting-project-versions-into-builds/">a
separate article with details on this process</a>.</p>
<p>The storage for WebCal calendars is now named <code>webcal</code>, which is less ambiguous
than <code>http</code> (CalDAV also uses HTTP). The documentation and source code was
inconsistent everywhere. The term <code>webcal</code> is now used consistently in all
places.</p>
<h1 id="current-blockers-for-a-v100-release">Current blockers for a v1.0.0 release<div class="permalink">[<a href="#current-blockers-for-a-v100-release">permalink</a>]</div></h1>
<ul>
<li><input checked="" disabled="" type="checkbox"> Publish man pages as HTML online</li>
<li><input checked="" disabled="" type="checkbox"> Publish a landing index.html page</li>
<li><input checked="" disabled="" type="checkbox"> <a href="https://todo.sr.ht/~whynothugo/pimsync/72">Implement protections if collections have been reconfigured</a>.</li>
<li><input checked="" disabled="" type="checkbox"> Flush stale data for collections that are no longer synced.</li>
<li><input disabled="" type="checkbox"> <a href="https://todo.sr.ht/~whynothugo/pimsync/103">Move the <code>interval</code> directive inside individual storage
definitions</a>.</li>
<li><input disabled="" type="checkbox"> <a href="https://todo.sr.ht/~whynothugo/pimsync/85">Implement conflict resolution <code>from a</code> and <code>from b</code></a>.</li>
</ul>
]]></description></item><item><title>Injecting project versions into builds</title><link>https://whynothugo.nl/journal/2024/12/16/injecting-project-versions-into-builds/</link><pubDate>Mon, 16 Dec 2024 16:18:13 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/12/16/injecting-project-versions-into-builds/</guid><description><![CDATA[<p>Applications typically need to embed their version into binaries, for things
like <code>--version</code> to work as expected. The exact version will usually be stored
in version control (e.g.: <code>git</code>).</p>
<p>The recipe presented here ensures that source tarballs include the right version
string (even tarballs auto-generated by code forges), and builds done from a
full git checkout use the output of <code>git-describe</code>.</p>
<p>The examples below work for a Rust project, but the general solution is
applicable to any language.</p>
<h1 id="including-the-version-into-tarballs">Including the version into tarballs<div class="permalink">[<a href="#including-the-version-into-tarballs">permalink</a>]</div></h1>
<p>Most code forges provide tarballs for tags and individual commits. These are
generated using <code>git-archive</code>. Git provides an <code>export-subst</code> feature which
allows us to replace a string with the output of <code>git-describe</code>, effectively
injecting the corresponding version into the tarball.</p>
<p>First of all, we&rsquo;ll create a <code>.gitattributes</code> file which includes a file where
the replacement will be executed, and the <code>export-subst</code> attribute. E.g.:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>/build.rs export-subst
</span></span></code></pre></div><p>This will replace the string <code>$Format:%(describe)$</code> in <code>build.rs</code> with the
corresponding version.</p>
<h1 id="injecting-the-version-during-builds">Injecting the version during builds<div class="permalink">[<a href="#injecting-the-version-during-builds">permalink</a>]</div></h1>
<p>The above merely replaces the string into a file when generating tarballs, but
we still need to consider builds done using a full git checkout of the
repository (which will have the un-replaced string).</p>
<p>When building from a git checkout, we can determine the right version string
using <code>git describe --tags</code>.</p>
<p>The following example uses a replaced version, if it has been replaced, and
otherwise falls back to <code>git describe</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;-webkit-text-size-adjust:none;"><code class="language-rs" data-lang="rs"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> std::process::Command;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">main</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> std::env::var(<span style="color:#e6db74">&#34;PIMSYNC_VERSION&#34;</span>).is_err() {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">let</span> version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;$Format:%(describe)$&#34;</span>; <span style="color:#75715e">// Will be replaced by git-archive.
</span></span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">let</span> version <span style="color:#f92672">=</span> <span style="color:#66d9ef">if</span> version.starts_with(<span style="color:#e6db74">&#39;$&#39;</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">match</span> Command::new(<span style="color:#e6db74">&#34;git&#34;</span>).args([<span style="color:#e6db74">&#34;describe&#34;</span>, <span style="color:#e6db74">&#34;--tags&#34;</span>]).output() {
</span></span><span style="display:flex;"><span>                Ok(o) <span style="color:#66d9ef">if</span> o.status.success() <span style="color:#f92672">=&gt;</span> String::from_utf8_lossy(<span style="color:#f92672">&amp;</span>o.stdout).trim().to_owned(),
</span></span><span style="display:flex;"><span>                Ok(o) <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">panic!</span>(<span style="color:#e6db74">&#34;git-describe exited non-zero: </span><span style="color:#e6db74">{}</span><span style="color:#e6db74">&#34;</span>, o.status),
</span></span><span style="display:flex;"><span>                Err(err) <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">panic!</span>(<span style="color:#e6db74">&#34;failed to execute git-describe: </span><span style="color:#e6db74">{err}</span><span style="color:#e6db74">&#34;</span>),
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>            String::from(version)
</span></span><span style="display:flex;"><span>        };
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">println!</span>(<span style="color:#e6db74">&#34;cargo:rustc-env=PIMSYNC_VERSION=</span><span style="color:#e6db74">{version}</span><span style="color:#e6db74">&#34;</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>As you&rsquo;ll notice, I basically inject the version into an <code>PIMSYNC_VERSION</code>
variable. This is only done if <code>PIMSYNC_VERSION</code> is unset, allowing anyone
compiling to override the version by setting the <code>PIMSYNC_VERSION</code> variable.</p>
<h1 id="using-the-environment-variable">Using the environment variable<div class="permalink">[<a href="#using-the-environment-variable">permalink</a>]</div></h1>
<p>Finally, the application needs to use this variable as a version. This is done
by simply loading the environment variable into a <code>&amp;str</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;-webkit-text-size-adjust:none;"><code class="language-rs" data-lang="rs"><span style="display:flex;"><span><span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">VERSION</span>: <span style="color:#66d9ef">&amp;</span><span style="color:#66d9ef">str</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">env!</span>(<span style="color:#e6db74">&#34;PIMSYNC_VERSION&#34;</span>);
</span></span></code></pre></div><p>The <a href="https://doc.rust-lang.org/std/macro.env.html"><code>env!</code> macro</a> evaluates an
environment variable at compile time. This would fail if the variable is unset,
but the variable is set unconditionally. If it is unset, it is because someone
has been tinkering with the build system and broke it.</p>
<h1 id="caveats">Caveats<div class="permalink">[<a href="#caveats">permalink</a>]</div></h1>
<p>There are none. This relies on code-forge auto-generated tarballs, using
functionality in git itself. You don&rsquo;t need to manually produce tarballs, and
builds done from any commit or tag will reflect their source properly.</p>
]]></description></item><item><title>Vdirsyncer status update 2024-11: renaming to pimsync</title><link>https://whynothugo.nl/journal/2024/11/14/vdirsyncer-status-update-2024-11-renaming-to-pimsync/</link><pubDate>Thu, 14 Nov 2024 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/11/14/vdirsyncer-status-update-2024-11-renaming-to-pimsync/</guid><description><![CDATA[<p>The vdirsyncer rewrite on which I have been working these last months will be
named <strong>pimsync</strong>, not <strong>vdirsyncer v2</strong>.</p>
<p>The idea of a different name originally came to mind due to difficulty
pronouncing the original name, and having to spell it out every time I spoke
about it in person.</p>
<p>But this wasn&rsquo;t the deciding factor for a new name. The main issue with naming
it <code>vdirsyncer</code> is that is makes references everywhere confusing. Documentation,
issues, tutorials and blog posts that refer to vdirsyncer become ambiguous,
since it&rsquo;s not clear which version they&rsquo;re talking about.</p>
<p>With the new edition having a different feature set and different configuration,
it just makes sense to treat it as a different project &ndash; even if a spiritual
successor. The initial release of pimsync will be missing some features in
vdirsyncer, so some folks will also continue to use the previous version for the
time being. Distributions should likewise be able to ship both packages during a
transition period, and end users will be able to co-install both programs until
they&rsquo;ve migrated to the new one.</p>
<p>The documentation for both will remain online. This separate names, it will be
clear which documentation refers to which project.</p>
<p>Despite having a new name, the main goal and general implementation design
remains the same. Thanks @untitaker for writing the original implementation!</p>
<p>The name pimsync comes from the <a href="https://en.wikipedia.org/wiki/Personal_information_manager">PIM</a> acronym, which stands for Personal
information manager. This term describes tools that manage personal information,
including address books and calendars. It is easier to pronounce at loud and to
explain verbally.</p>
<h1 id="documentation">Documentation<div class="permalink">[<a href="#documentation">permalink</a>]</div></h1>
<p>Writing the documentation was a lot more pleasant and fulfilling that I
expected. I admit that I overestimated the effort required for this.</p>
<p>At the moment, the following manual pages are available:</p>
<ul>
<li><a href="https://git.sr.ht/~whynothugo/vdirsyncer-rs/tree/c475ba83439c45d3639fbeb1a9e541d6a8041909/item/pimsync.1.scd">pimsync(1)</a> - general command usage</li>
<li><a href="https://git.sr.ht/~whynothugo/vdirsyncer-rs/tree/c475ba83439c45d3639fbeb1a9e541d6a8041909/item/pimsync.conf.5.scd">pimsync.conf(5)</a> - pimsync configuration file</li>
<li><a href="https://git.sr.ht/~whynothugo/vdirsyncer-rs/tree/c475ba83439c45d3639fbeb1a9e541d6a8041909/item/pimsync-migration.7.scd">pimsync-migration(7)</a> - migration guide from vdirsyncer</li>
</ul>
<p>All reference documentation shall be published as manual pages. These same pages
are also rendered as HTML pages and published into a proper website. For the
moment, the HTML pages are published at <a href="https://mirror.whynothugo.nl/pimsync/main/">a temporary mirror</a>.</p>
<p>Tutorials and how-tos will likely exist in the <a href="https://pimutils.org/">pimutils website</a>, and
contributions are most welcome.</p>
<p>Explanations on why things are designed the way they are will continue being
published in these status updates. There&rsquo;s also a lot of internal documentation
in the library documentations for the different components of pimsync.</p>
<p>The documentation is by no means complete. There&rsquo;s likely plenty of room for
improvement, but it should be a good starting point to understand how to
configure and use pimsync. I expect to continue improving it based on user
feedback and frequent questions. If anything is not clear, that is an issue that
needs to be fixed.</p>
<h1 id="configuration-overhaul">Configuration overhaul<div class="permalink">[<a href="#configuration-overhaul">permalink</a>]</div></h1>
<p>I <a href="/journal/2023/10/05/a-configuration-format-for-vdirsyncer-v2/">discussed configuration formats</a> a bit last year. At the time, I went
with TOML, mostly given that it seemed the one with least difference from the
previous implementation.</p>
<p>While TOML is a good serialisation format, it is not a very good configuration
format for humans to write. To make matters worse, the code that parsed the TOML
configuration was bloated and pointlessly complex. I was happy to drop all of
it.</p>
<p>I don&rsquo;t want to ship a stable release on pimsync with TOML and later regret that
and impose <em>yet another</em> change on end users. I intend to avoid breaking changes
as much as possible after the stable release has been done.</p>
<p>The new configuration format is much simpler and friendlier for humans to
understand and write. The syntax is technically the <a href="https://git.sr.ht/~emersion/scfg">scfg</a> format, but
documentation doesn&rsquo;t expect familiarity or understanding of the underlying
format. Instead, the documentation focuses on explaining how to write the
configuration file itself.</p>
<!-- TODO: scfg doesn't care about string/int -->
<h1 id="unix-socket-support">Unix socket support<div class="permalink">[<a href="#unix-socket-support">permalink</a>]</div></h1>
<p>The implementation for using CalDav and CardDav over a Unix socket has been
completed. See <a href="/journal/2024/08/19/unix-domain-socket-support-for-vdirsyncer/">Unix domain socket support for vdirsyncer</a> for more
background on this.</p>
<h1 id="versioning-discontinuity">Versioning discontinuity<div class="permalink">[<a href="#versioning-discontinuity">permalink</a>]</div></h1>
<p>Previous beta releases were in the order of v2.0.0-beta1, given that this was
the second iteration of vdirsyncer. With the rename, this doesn&rsquo;t make sense:
the next stable release will be v1.0.0 of pimsync.</p>
<p>Given that no distribution has packaged any of the beta versions, this should
not be an issue.</p>
]]></description></item><item><title>Snooze: a simpler cron</title><link>https://whynothugo.nl/journal/2024/11/12/snooze-a-simpler-cron/</link><pubDate>Tue, 12 Nov 2024 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/11/12/snooze-a-simpler-cron/</guid><description><![CDATA[<p><strong>snooze</strong> is a program to run scheduled tasks, and its design matches exactly
what I&rsquo;d been wanting: snooze parses the schedule, sleeps until the desired time
and finally executes the specified command.</p>
<p>Each task is an individual process which can be managed with the usual process
management tools. This means that it is also possible to run an instance of
snooze as an individual service via one&rsquo;s service manager under the usual
service supervision tree.</p>
<p>The <a href="https://manpages.debian.org/bookworm/snooze/snooze.1.en.html">man page</a> describes its usage quite well, and <a href="https://forum.artixlinux.org/index.php/topic,3444.0.html">this post</a> describes its
mode of operation and usage in greater detail. The snooze command is currently
packaged by a few distributions, and <a href="https://github.com/leahneukirchen/snooze">its source</a> is public domain.</p>
<p>A minimal usage example is the following:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>snooze -H9 -M40 echo <span style="color:#e6db74">&#34;it is now 09:40&#34;</span>
</span></span></code></pre></div><p>Note that the snooze command exits after running the command. For recurring
tasks, the service manager should re-start it once it exits, preparing for the
next execution. This design also ensures that there are never any concurrent
executions.</p>
<p>As a bonus use case, snooze can be used easily schedule a one-shot which
inherits the current execution environment.</p>
<h1 id="notes-on-cron">Notes on cron<div class="permalink">[<a href="#notes-on-cron">permalink</a>]</div></h1>
<p>The snooze command&rsquo;s use case overlaps with cron, but the former doesn&rsquo;t depend
on a service that needs to be executed by root. It kind of bothers me that cron
service that needs to run as root and requires privilege escalation for a user
to operate.</p>
<p><code>cronie</code>, <code>dcron</code>, <code>fcron</code> <code>bcron</code> and <code>anacron</code> all have the same design as
cron. It&rsquo;s perfectly fine on a server, but on desktop / interactive systems, I&rsquo;d
prefer something that doesn&rsquo;t require any elevated privileges to enable or
operate if the tasks will run as an unprivileged user.</p>
<p>snooze runs entirely within the process tree of the user, without any additional
privileges in any of the parts involved. It is even suitable for usage inside
containers.</p>
]]></description></item><item><title>Vdirsyncer status update 2024-10: security audit</title><link>https://whynothugo.nl/journal/2024/10/19/vdirsyncer-status-update-2024-10-security-audit/</link><pubDate>Sat, 19 Oct 2024 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/10/19/vdirsyncer-status-update-2024-10-security-audit/</guid><description><![CDATA[<p>The folks from <a href="https://www.radicallyopensecurity.com/">Radically Open Security</a> recently completed their security
audit. This was sponsored by the <a href="https://nlnet.nl/entrust/">NGI0 Entrust</a> Fund via the <a href="https://nlnet.nl/">NLnet
foundation</a>. My deepest appreciation for this opportunity.</p>
<p>The audit uncovered four minor findings, but no huge security issues. I remain
confident in my design and approach. It is good to have a second pair of eyes
reviewing these. The following is a short summary of these findings.</p>
<h1 id="inappropriate-file-mode">Inappropriate file mode<div class="permalink">[<a href="#inappropriate-file-mode">permalink</a>]</div></h1>
<blockquote>
<p>The data in vdir storage is marked executable. Items themselves are
well-escaped, making it difficult to make use of this, however the display
name of a collection is not, so an attacker controlling the display name can
effectively control the contents of an executable file.</p>
</blockquote>
<p>This was a mistake on my part; I used the constant <code>Mode::RWXU</code>, which was not
what I&rsquo;d intended. I replaced with with <code>0o600</code>, which is much easier to read.</p>
<p>In theory, an attacker with access to a remote calendar could have injected a
malicious binary and tricked a local user to double click on the file,
triggering the execution.</p>
<p>The issue by itself is quite minor, but could have been exploited with a
combination of (1) write access to a remote CalDav or CardDav instance and (2)
social engineering.</p>
<p>This issue was fixed in
<a href="https://git.sr.ht/~whynothugo/vdirsyncer-rs/commit/302be5cc7b2c755b91311cc84a89f8f36f8733cd">302be5cc7b2c755b91311cc84a89f8f36f8733cd</a>.</p>
<h1 id="potential-panic-in-error-handling">Potential panic in error handling<div class="permalink">[<a href="#potential-panic-in-error-handling">permalink</a>]</div></h1>
<blockquote>
<p>The application will panic if a storage provider such as a WebDAV server
returns a malformed item.</p>
</blockquote>
<p>Vdirsyncer could be made to panic if a WebDAV server served a completely empty
file. I fixed this issue in the <code>vparser</code> library, and included a unit test to
avoid future regressions.</p>
<p>This issue could have resulted in a remote server performing a denial of service
on vdirsyncer by making it crash continuously. No data corruption or data leak
could have occurred because of this.</p>
<p>This issue was fixed in
<a href="https://git.sr.ht/~whynothugo/vparser/commit/a2a25d881c00f23b5dd60e3c7752c327ca11806a">a2a25d881c00f23b5dd60e3c7752c327ca11806a</a>.</p>
<h1 id="following-symlinks">Following symlinks<div class="permalink">[<a href="#following-symlinks">permalink</a>]</div></h1>
<blockquote>
<p>Symlinks are followed by the vdirsyncer implementation even if they fall
outside the target sync directory.</p>
</blockquote>
<p>Vdirsyncer performs atomic writes by creating a new file and overwriting the
previous one. Because of this, it could never write to a location outside the
vdir, even in the presence of a symlink.</p>
<p>The only scenario where this can potentially be exploited, is where Alice has
write permissions to a vdir, but Bob runs vdirsyncer on it. In such a scenario,
Alice could potentially leak files which are only readable by Bob by uploading
them to a remote storage.</p>
<p>The most likely course of action here will be to treat symlinks as unreadable
files.</p>
<p>This is currently tracked in <a href="https://todo.sr.ht/~whynothugo/vdirsyncer-rs/94">https://todo.sr.ht/~whynothugo/vdirsyncer-rs/94</a></p>
<h1 id="no-data-validation">No data validation<div class="permalink">[<a href="#no-data-validation">permalink</a>]</div></h1>
<blockquote>
<p>An item in a vdir can be replaced with any data, and vdirsyncer will attempt
to submit it to the sync target.</p>
</blockquote>
<p>This is an intentional design choice; if a user has data that is technically
invalid, we still want to permit them to synchronise it in any direction. If we
prohibit synchronising invalid data, users may become incapable of operating on
this data in order to fix the invalidity.</p>
<p>It is also possible that a user is using a calendar client which handles this
&ldquo;technically invalid&rdquo; data just fine, and we don&rsquo;t want to get in the way of
users synchronising this.</p>
<p>While I don&rsquo;t consider this an issue in itself, the fact that it was reported as
a security finding reveals that this behaviour was not properly documented. I
had added a mention on this topic to the documentation so the behaviour is clear
for all users.</p>
<h1 id="other-findings">Other findings<div class="permalink">[<a href="#other-findings">permalink</a>]</div></h1>
<p>Early during the audit some security issues were found in dependencies. These
were unlikely to be exploitable via vdirsyncer, but the dependencies where
promptly updated anyway.</p>
<p>I also added rules to the several of the libraries that fall under this project
to prohibit unsafe code. These are higher level libraries, and there&rsquo;s no need
to escape Rust&rsquo;s typical safety rules. Some dependencies, however, contain
unsafe code. This is inevitable for lower level dependencies.</p>
<p>The auditor and I also had some chats to confirm that my approach on TLS
configuration made sense and that there were no gaps in my reasoning. Their
general feedback mentioned that <em>[&hellip;] the simplicity of the design and choice
of Rust that simultaneously reduces attack surface and averts common memory-
safety bugs.</em></p>
<h1 id="closing-words">Closing words<div class="permalink">[<a href="#closing-words">permalink</a>]</div></h1>
<p>Security is an ongoing process. While no greater issues have been found at this
time, work on vdirsyncer continues, and the security status of the project needs
to be reviewed at a future time.</p>
]]></description></item><item><title>Vdirsyncer status update 2024-09</title><link>https://whynothugo.nl/journal/2024/09/29/vdirsyncer-status-update-2024-09/</link><pubDate>Sun, 29 Sep 2024 10:36:24 -0300</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/09/29/vdirsyncer-status-update-2024-09/</guid><description><![CDATA[<p>I believe that this rewrite of vdirsyncer is stable enough to warrant a stable
release. Initially, I wanted to reach feature parity with the previous
implementation before calling this stable, but I don&rsquo;t think that&rsquo;s the right
mentality: the new version is stable enough for daily use. For use cases where
features are missing, the previous version has to be used until those features
have been ported. For other use cases, I recommend the new implementation
already.</p>
<p>I am considering naming the new project <code>vdirsyncer2</code>, so that both can be
co-installed.</p>
<p>Right now my main blocker for an initial stable release is documentation:
there&rsquo;s <em>some</em> documentation, but not enough for a complete newcomer to figure
out how to fully configure and use vdirsyncer. I can&rsquo;t call this non-beta with
the current state of the documentation.</p>
<p>Work on the documentation is ongoing, so that I can unblock a stable release. I
decided to consolidate documentation into man pages, so that they are easily
accessible offline in the same format as any other utility. Of course, I shall
publish these same pages online too in HTML form.</p>
<p>Security considerations have been moved into these same man pages for better
visibility. On the topic of security considerations, the ongoing security audit
is coming to a close, and I am quite pleased with the results.</p>
<h1 id="splitting-subprojects">Splitting subprojects<div class="permalink">[<a href="#splitting-subprojects">permalink</a>]</div></h1>
<p>I started writing vdirsyncer in Rust in a single repository along with its
dependant libraries. This probably sped up initial development by a marginal
amount, but as the project stabilised, this didn&rsquo;t prove as convenient.</p>
<p>The main problem was when making backwards-incompatible changes to <code>libdav</code>. I
suddenly found that I couldn&rsquo;t commit these changes without breaking vdirsyncer,
implying that I had to implement changes to multiple projects at once (and in a
single commit) even if those changes were not entirely related and had different
scopes.</p>
<p>This was not the first that this happened, and it became clear that as pieces
mature, I want to be able to release dependencies <em>before</em> I deal with upgrading
vdirsyncer for them.</p>
<p>I therefore split <a href="https://git.sr.ht/~whynothugo/libdav"><code>libdav</code></a> and <a href="https://git.sr.ht/~whynothugo/davcli"><code>davcli</code></a> into separate repositories. I
believe this also improves visibility for these projects, since they&rsquo;re now more
than simply &ldquo;a subdirectory in some other project&rdquo;. Both of these are usable in
scenarios that don&rsquo;t involve vdirsyncer at all.</p>
<h1 id="protections">Protections<div class="permalink">[<a href="#protections">permalink</a>]</div></h1>
<p>I implemented <a href="https://todo.sr.ht/~whynothugo/vdirsyncer-rs/70">protections for emptied collections in vdirsyncer</a>. This
means that if you delete a collection entirely, it won&rsquo;t delete the collection
on the other storage by default: it will show a warning and skip it instead.</p>
<p>While I prefer emptied collections to be emptied on the other side for my own
usage, this behaviour is somewhat dangerous and doesn&rsquo;t feel like safe default.
So the protection is enabled by default, and users will have to opt-out of it
with the new <code>on_empty = &quot;sync&quot;</code> configuration directive.</p>
<h1 id="start-up-performance">Start-up performance<div class="permalink">[<a href="#start-up-performance">permalink</a>]</div></h1>
<p>At start-up, vdirsyncer will no longer load pairs and storages that are not
being synchronised (e.g.: if only a single pair was explicitly requested). This
improves usability in two ways:</p>
<ul>
<li>If a disabled pair has some invalid configuration, that won&rsquo;t block users from
synchronising another pair.</li>
<li>Vdirsyncer won&rsquo;t prompt for credentials for storages that are not being
operated upon.</li>
</ul>
<h1 id="other-minor-tweaks">Other minor tweaks<div class="permalink">[<a href="#other-minor-tweaks">permalink</a>]</div></h1>
<p><code>libdav</code>, <code>davcli</code> and <code>vdirsyncer</code> now all properly handle situations where a
CalDav server has a home set with multiple entries. This is not a frequent
situation; most common servers use a single home set. But the edge case had
already come up, so it made sense to properly support it. It&rsquo;s also well covered
by the CalDav specification.</p>
<p>Vdirsyncer now uses internal locks to avoid concurrent access to a same storage.
This makes a difference for scenarios like storage A being synchronised with
storage B and storage B being synchronised with storage C. The locks prevents
both of these operations from running concurrently. Running them concurrently
would often result in errors (although these errors would not result in data
loss).</p>
<p>Several configuration errors now print the full path to the configuration path
and clearer error messages. This helps understand what&rsquo;s wrong when configuring
vdirsyncer.</p>
<p>Username and password are now both optional, so vdirsyncer can be used in
servers which don&rsquo;t expect authentication.</p>
<p>Finally, I tested the project on OpenBSD. The objective was not only to ensure
that vdirsyncer works well on this platform, but to also ensure good
portability. Given the positive results, I don&rsquo;t expect any issues on other BSDs
or Unix-like platforms.</p>
]]></description></item><item><title>Transcribing audio with whisper.cpp</title><link>https://whynothugo.nl/journal/2024/09/22/transcribing-audio-with-whisper.cpp/</link><pubDate>Sun, 22 Sep 2024 11:14:50 +0200</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/09/22/transcribing-audio-with-whisper.cpp/</guid><description><![CDATA[<p>I wanted a quick setup where I can speak to my laptop, and it will record the
audio and then give me a text transcript of what I have said.</p>
<p>My intention is to produce long text by speaking it out loud and then doing some
minor refinement by changing potential typos.</p>
<h1 id="preparation">Preparation<div class="permalink">[<a href="#preparation">permalink</a>]</div></h1>
<p>The first thing that was on the whisper.cpp repository.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>git clone https://github.com/ggerganov/whisper.cpp/
</span></span></code></pre></div><p>Then I built the main example which just takes a wave file as input and produces
text as output.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>make main
</span></span></code></pre></div><p>Next, I had to download the models for recognizing English speech.</p>
<p>The script did not work at first. I had to patch it to use curl instead of wget,
because it uses few specific wget flags which don&rsquo;t work on busybox.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span><span style="color:#f92672">--- a/models/download-ggml-model.sh
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+++ b/models/download-ggml-model.sh
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">@@ -101,11 +101,7 @@ if [ -f &#34;ggml-$model.bin&#34; ]; then
</span></span></span><span style="display:flex;"><span>     exit 0
</span></span><span style="display:flex;"><span> fi
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">-if [ -x &#34;$(command -v wget2)&#34; ]; then
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">-    wget2 --no-config --progress bar -O ggml-&#34;$model&#34;.bin $src/$pfx-&#34;$model&#34;.bin
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">-elif [ -x &#34;$(command -v wget)&#34; ]; then
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">-    wget --no-config --quiet --show-progress -O ggml-&#34;$model&#34;.bin $src/$pfx-&#34;$model&#34;.bin
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">-elif [ -x &#34;$(command -v curl)&#34; ]; then
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+if [ -x &#34;$(command -v curl)&#34; ]; then
</span></span></span><span style="display:flex;"><span>     curl -L --output ggml-&#34;$model&#34;.bin $src/$pfx-&#34;$model&#34;.bin
</span></span><span style="display:flex;"><span> else
</span></span><span style="display:flex;"><span>     printf &#34;Either wget or curl is required to download models.\n&#34;
</span></span></code></pre></div><p>I then downloaded the model files using the patched 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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>sh ./models/download-ggml-model.sh base.en
</span></span></code></pre></div><h1 id="usage">Usage<div class="permalink">[<a href="#usage">permalink</a>]</div></h1>
<p>Next, I needed to record audio snippets with me speaking the actual text. For
this, I used the arecord utility. My first example was rejected because the
audio needs to be recorded at 16 kilohertz. I quickly found the flag to change
the rate at which the audio is recorded.</p>
<p>I used the following command to record showed audio snippets, usually one
paragraph at a time. I would terminate recording by pressing
<kbd>Ctrl</kbd>+<kbd>C</kbd>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>arecord --format<span style="color:#f92672">=</span>cd -r <span style="color:#ae81ff">16000</span> file.wav
</span></span></code></pre></div><p>Finally, I used the following command to transcribe the audio files into text.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>./main -m ./models/ggml-base.en.bin -f file.wav
</span></span></code></pre></div><p>This would usually take about perhaps a second or two. I then copied the text
into this file and simply remove the timestamps because they are not relevant.</p>
<h1 id="results">Results<div class="permalink">[<a href="#results">permalink</a>]</div></h1>
<p>All prose text in this article was dictated by me to my laptop and converted by
whisper with steps mentioned above. Only the URLs and code samples were
copy-pasted from elsewhere.</p>
<p>I only had to fix a few minor typos as described below.</p>
<ul>
<li><code>cycles</code> -&gt; <code>typos</code></li>
<li><code>it uses new specific wget lives</code> -&gt; <code>it uses a few specific wget flags</code></li>
<li><code>busybooks</code> -&gt; <code>busybox</code></li>
<li><code>A record</code> -&gt; <code>arecord</code></li>
<li><code>Ctrl C</code> -&gt; <code>Ctrl+C</code> (I also added <code>&lt;kbd&gt;</code> around this myself)</li>
<li><code>Finally, I use</code> -&gt; <code>Finally, I used</code></li>
</ul>
<p>I also had to apply some editorial fixes which were more related to my own
grammar than the transcription process itself.</p>
<p>I was quite impressed by the fact that wget and curl were actually spelled
correctly.</p>
<p>I found the quality of the transcriptions to be quite reliable and convenient to
use. I will experiment further with using them to take notes. At this stage
there is minor annoyance of having two separate steps for recording and
converting audio. This is simply because I am using an example binary for my
test.</p>
<p>I expect that I should be able to write a small program which uses whisper and
simply pipes input from my microphone straight to whisper so I can have a
continuous stream of text which I would then copy paste perhaps into articles
and maybe refine small typos and mistakes.</p>
<h1 id="future-ideas">Future ideas<div class="permalink">[<a href="#future-ideas">permalink</a>]</div></h1>
<p>I would also like to set up an automated messaging account such that I can send
an instant message with an audio recording to it, and it will relay the
transcript to my email. This would allow me to take spoken notes on my phone,
which I can later process as text on my laptop.</p>
]]></description></item><item><title>Writing an emoji input method, part 1</title><link>https://whynothugo.nl/journal/2024/09/18/writing-an-emoji-input-method-part-1/</link><pubDate>Wed, 18 Sep 2024 20:38:07 +0200</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/09/18/writing-an-emoji-input-method-part-1/</guid><description><![CDATA[<p>Today I started work on emoji-im, an input method to type emoji. The general
ideal is pretty simple, when the input method is active, I type a word, and the
input method pop-up suggests emoji based on the word typed. Pressing space
inserts that emoji.</p>
<p>By implementing this as an input method, it works on any application where
typing regular text is possible.</p>
<p>I knew this was not something that I&rsquo;d be able to complete in a day, but I at
least wanted to get the basic scaffold and general structure set up on this
first day. Spoiler: I think this went quite well.</p>
<p>I decided to use <a href="https://harelang.org/">Hare</a> for this. It&rsquo;s a simple language, quite low level, with
a solid approach to error management. Compilation is fast, and I only need two
dependencies which compile in milliseconds. I used <a href="https://git.sr.ht/~sircmpwn/hare-wayland/">hare-wayland</a> in the past
for <a href="https://whynothugo.nl/journal/2024/07/11/introducing-wlhc-wayland-hot-corners/">wlhc</a>, and found myself quite comfortable developing with it.</p>
<p>I started my project today by generating client code for the Wayland protocols
that I expect to need. I copied some bits from <code>wlhc</code> for this, and came to this
initial <code>Makefile</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;-webkit-text-size-adjust:none;"><code class="language-make" data-lang="make"><span style="display:flex;"><span>DESTDIR<span style="color:#f92672">=</span>
</span></span><span style="display:flex;"><span>PREFIX<span style="color:#f92672">=</span>/usr/local
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">.PHONY</span><span style="color:#f92672">:</span> build
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">build</span><span style="color:#f92672">:</span>
</span></span><span style="display:flex;"><span>	mkdir -p wayland/xdg wayland/wlr wayland/wp
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	hare-wlscanner <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>		&lt; <span style="color:#66d9ef">$(</span>shell pkg-config --variable<span style="color:#f92672">=</span>pkgdatadir wayland-protocols<span style="color:#66d9ef">)</span>/stable/xdg-shell/xdg-shell.xml <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>		&gt; wayland/xdg/shell.ha
</span></span><span style="display:flex;"><span>	hare-wlscanner <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>		&lt; <span style="color:#66d9ef">$(</span>shell pkg-config --variable<span style="color:#f92672">=</span>pkgdatadir wayland-protocols<span style="color:#66d9ef">)</span>/staging/single-pixel-buffer/single-pixel-buffer-v1.xml <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>		&gt; wayland/wp/single_pixel_buffer.ha
</span></span><span style="display:flex;"><span>	hare-wlscanner <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>		&lt; <span style="color:#66d9ef">$(</span>shell pkg-config --variable<span style="color:#f92672">=</span>pkgdatadir wayland-protocols<span style="color:#66d9ef">)</span>/staging/fractional-scale/fractional-scale-v1.xml <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>		&gt; wayland/wp/fractional_scale.ha
</span></span><span style="display:flex;"><span>	hare-wlscanner <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>		&lt; <span style="color:#66d9ef">$(</span>shell pkg-config --variable<span style="color:#f92672">=</span>pkgdatadir wlr-protocols<span style="color:#66d9ef">)</span>/unstable/wlr-layer-shell-unstable-v1.xml <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>		&gt; wayland/wlr/layer_shell.ha
</span></span><span style="display:flex;"><span>	hare-wlscanner <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>		&lt; input-method-unstable-v2.xml <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>		&gt; wayland/zwp/input_method.ha
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	hare build -o emoji-im
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">install</span><span style="color:#f92672">:</span> build
</span></span><span style="display:flex;"><span>	install -m755 emoji-im <span style="color:#66d9ef">$(</span>DESTDIR<span style="color:#66d9ef">)$(</span>PREFIX<span style="color:#66d9ef">)</span>/bin/emoji-im
</span></span></code></pre></div><p>This generates code to unmarshall events and send requests to a wayland server.
The generated files are not intended to be edited manually, and the functions
that they provide are a direct representation of the underlying protocol.</p>
<p>My code is in separate files, but it&rsquo;s all compiled together as one big project.</p>
<p>I find this approach quite convenient. I can actually inspect the generated code
to double check the names and types of functions and their method. The library
itself handles all the wire protocol serialisation and deserialisation.</p>
<p>Coming from Python, I&rsquo;ve used to libraries that generate abstractions to call
some remote method at runtime. In these situations, even though I&rsquo;d be using a
proxy object that wraps some underlying IPC there&rsquo;s no source code for the
client objects; it&rsquo;s all generated at runtime. So double checking types, or
figuring out the little details was not as straightforward as checking the code
for the function I was calling &ndash; there was no code!</p>
<p>In this sense, I find that Rust <code>proc_macros</code> tend to behaves too closely to
Python. They generate code at compile them, and feed it straight to the
compiler. It&rsquo;s not as straightforward to double check details in this code.</p>
<p>I started out writing code to register the Wayland global objects that I&rsquo;d need.
I wrote code to register registered a new input method. Each execution was a
simple:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>make <span style="color:#f92672">&amp;&amp;</span> WAYLAND_DEBUG<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span> ./emoji-im
</span></span></code></pre></div><p>Most of this was some straightforward scaffold that I was pleased to have
finished quickly.</p>
<p>The next step was to intercept keyboard input and determine emoji suggestions
based on that. Because my program would be intercepting all keyboard input, I
wouldn&rsquo;t be able to press <kbd>Ctrl</kbd>+<kbd>c</kbd> to kill it. In order to
ensure that I didn&rsquo;t lock myself out of my own session, I ran:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>watch -n <span style="color:#ae81ff">10</span> pkill emoji-im
</span></span></code></pre></div><p>This would kill the emoji keyboard every ten seconds, so if I ended up in a
state where it was eating up all keystrokes, this would save the day.
Occasionally it gets killed at the wrong time. I could have just ran tests in a
nested compositor.</p>
<p>The wayland protocol for input methods is pretty well designed, and integrating
with it was quite uneventful. Until I had to process keystrokes. Keymaps and Key
events -presses are transmitted using the Xorg. These are quite non-trivial to
parse. Instead of actually parsing the properly, I hard-coded the key sequences
for all letters, <kbd>Space</kbd> and <kbd>Tab</kbd> on my own keyboard.</p>
<p>Some time later I was pointed at <a href="https://git.sr.ht/~stacyharper/hare-xkb/tree">hare-xkb</a>, which I will have to integrate
eventually. I preferred to continue the wave of implementing new bits rather
than sinking time into polishing something that can be left for a final stage.
Adding tiny improvements helps keep motivation up; perfecting inconsequential
details before everything else works does not.</p>
<p>Being able to recognise keystrokes, I started intercepting them and building a
string with the characters. This is literally just concatenating a character to
a string. This is the <code>preedit</code> string, renders in text input areas only as a
placeholder. Depending on this string, I intend to show emoji suggestions. When
<kbd>Space</kbd> is pressed, I intend to insert the selected emoji. At this
early stage, if the input string is <code>smile</code>, I&rsquo;ll insert the <code>🙂</code>. At this
point, I had to leave to deal with some personal stuff.</p>
<p>That&rsquo;s basically it for day 1. This was a pretty fun project, and there&rsquo;s plenty
of work left to do. At this point, I have an emoji input method which, when
active, lets me insert the smile emoji.</p>
]]></description></item><item><title>Dark mode is not about aesthetics</title><link>https://whynothugo.nl/journal/2024/09/10/dark-mode-is-not-about-aesthetics/</link><pubDate>Tue, 10 Sep 2024 15:54:52 +0200</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/09/10/dark-mode-is-not-about-aesthetics/</guid><description><![CDATA[<p>A few years ago I moved to the Netherlands. Offices and houses here have large
windows, and we can enjoy a lot of sunlight during the long summer days. With so
much sunlight shining into my working environment, I need to use my computing
devices on light mode.</p>
<p>Black text on white background is relatively easy to read in a highly lit
environment. Conversely, trying to read white text on dark background is a
futile exercise in a sunny room: I can&rsquo;t see anything with all that glare.</p>
<p>On the other hand, sunset here in the winter is at around 17hs, and most of the
winter is dark and gloomy. In a dimly lit environment with just some warm
lights, looking at a screen with a white background and black text makes my eyes
hurt. It&rsquo;s terrible for my eyes.</p>
<p>Effectively, I need dark mode when my working environment is a dark one.</p>
<p>While dark mode and light mode are often thought of as an aesthetic preference,
I find them to be about usability.</p>
]]></description></item><item><title>Status update 2024-08</title><link>https://whynothugo.nl/journal/2024/08/20/status-update-2024-08/</link><pubDate>Tue, 20 Aug 2024 11:42:39 +0200</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/08/20/status-update-2024-08/</guid><description><![CDATA[<p>Vdirsyncer in its current state has been working for me reliable for quite some
time now, and I&rsquo;m very happy how this has shaped up.</p>
<p>Since the last status update, a lot of polishing has happened, and I&rsquo;ve
completed support for failure scenarios that could lead to data corruption.</p>
<p>Code that writes data into a <code>vdir</code> no uses atomic writes. Data is first written
to a new, temporary, file, and then that file is linked to the appropriate path.
If a catastrophic failure happens mid-way (e.g.: a power failure), then the file
with either remain in its original state, or in its final state, but it will
never end up with half of the data written and half missing.</p>
<p>Right now, the main &ldquo;protections&rdquo; that are missing are to avoid deleting all
items when a collection is fully emptied on one side and disable deletion of
deleted collections. During everyday usage, none of these should be relevant,
but if you ever delete a calendar entirely with the expectation that &ldquo;vdirsyncer
will just download it again&rdquo;, the current result might not be what you expect.
Personally, I often create short-term todo lists which I later delete, so I
collection auto-deletion has been convenient for me.</p>
<p><code>davcli</code> now supports communicating with a server with a username and no
password, or with no authentication at all.</p>
<p>I&rsquo;ve dropped support for &ldquo;item properties&rdquo; in the storage abstractions. The
intent for these was to eventually support synchronising emails. The subtle
differences between emails and calendars has led me to conclude that this is a
bad idea. Mailboxes don&rsquo;t have properties, but emails do. However, calendars
have properties, and events do not. Additionally, emails are immutable, whereas
events are mutable. Having a single sync algorithm that supports both adds
unwanted complexity, and a unsatisfactory implementation for both.</p>
<p>I have also implemented a simple readiness notification mechanism, so that a
service manger knows when vdirsyncer has transitioned from &ldquo;starting up&rdquo; to
&ldquo;running&rdquo;. Once it has reached its running state it should not exit and will
continuously (try to) synchronise periodically.</p>
<p>Finally, I&rsquo;ve tagged a 0.1.0 release of <a href="https://git.sr.ht/~whynothugo/ab-tidy/">ab-tidy</a>, the little helper which
renames vcard files to match the name of the person inside. I have a few other
tiny tool in mind that I&rsquo;d like to implement in the near future.</p>
<h1 id="security-audit">Security audit<div class="permalink">[<a href="#security-audit">permalink</a>]</div></h1>
<p>The grant provided by NLNet and NGI Zero also includes a security audit of
vdirsyncer.</p>
<p>During our initial discussion of scope and expectations, I realised that service
discovery <em>assumes</em> that your local DNS resolver is a validating one. In cases
where the local resolver is not a validating one, vdirsyncer is vulnerable to an
active MITM attack via DNS spoofing. I have since included a clear explanation
of this expectation in a <code>SECURITY.md</code> file which will eventually become part of
the documentation. I have also included a mention of this in <code>libdav</code> API
documentation in and in <code>davcli</code>&rsquo;s readme.</p>
<p>Aside from this, the security audit has found another interesting issue which
has now been addressed: Files created in a <code>vdir</code> were created with the
executable bit set. It&rsquo;s hard to imagine how this could be exploited in
practice, but it&rsquo;s still a valid bug, and has since been fixed.</p>
]]></description></item><item><title>Unix domain socket support for vdirsyncer</title><link>https://whynothugo.nl/journal/2024/08/19/unix-domain-socket-support-for-vdirsyncer/</link><pubDate>Mon, 19 Aug 2024 15:20:48 +0200</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/08/19/unix-domain-socket-support-for-vdirsyncer/</guid><description><![CDATA[<p>I have two seemingly conflicting goals with vdirsyncer:</p>
<ul>
<li>I want to keep the scope and code simple, reach a stable state, and stop
adding new features.</li>
<li>I want to support a variety of use cases, like being able to use client TLS
certificates, custom http transports, etc.</li>
</ul>
<p>After thinking about this for along time, I&rsquo;ve concluded that support for Unix
domain sockets is the best compromise to satisfy both goals.</p>
<h1 id="supported-connection-transports">Supported connection transports<div class="permalink">[<a href="#supported-connection-transports">permalink</a>]</div></h1>
<p>Essentially, vdirsyncer will support two transports to talk to a CalDav server:</p>
<ul>
<li>Using HTTP(s) with the default route, default connection parameters and the
system certificate authorities. This is the typical use case.</li>
<li>Using a Unix domain socket, suitable for any other use case which requires
additional flexibility.</li>
</ul>
<p>The first transport is straightforward, so I&rsquo;ll focus on the second one in the
rest of this article.</p>
<h1 id="potential-usages-of-this-approach">Potential usages of this approach<div class="permalink">[<a href="#potential-usages-of-this-approach">permalink</a>]</div></h1>
<p>By having vdirsyncer communicate with a Unix domain socket, traffic can be
routed wherever necessary. The socket must be set up by an external process.
Here as some examples of what that process could be:</p>
<ul>
<li>A local CalDav server which is only exposed via a socket.</li>
<li>An HTTP proxy that handles client-side TLS certificates.</li>
<li>An HTTP proxy that uses a custom TLS validation mechanism.</li>
<li>A special proxy service that re-writes authentication to something
non-standard.</li>
<li>An SSH client forwarding a port to a remote host.</li>
<li>A proxy that handles TLS with a specific, approved, implementation.</li>
</ul>
<p>The ability to connect to a local socket becomes an escape hatch for infinite
possibilities without having to grow vdirsyncer&rsquo;s codebase by infinite
complexity.</p>
<h1 id="scope">Scope<div class="permalink">[<a href="#scope">permalink</a>]</div></h1>
<p>I believe that this approach keeps a sane scope for vdirsyncer. It doesn&rsquo;t
require implementing all sorts of TLS flags, but allows integrating equivalent
functionality.</p>
<p>I find that this approach aligns well with the Unix philosophy of &ldquo;do one
thing&rdquo;. E.g.: vdirsyncer just does the synchronisation; some external process
handles establishing and encrypting connections. I am not choosing this approach
<em>because</em> it aligns with the Unix philosophy: I&rsquo;m choosing it because it allow
supporting all kind of special scenarios while minimising the amount of code
that vdirsyncer needs to in order to support each scenario.</p>
<p>Personally, I would consider it cleaner design to <em>only</em> support sockets and
require that even HTTP(s) connections be handled by a dedicated program. Such an
approach would likely be impractical for the typical use cases.</p>
<h1 id="alternatives">Alternatives<div class="permalink">[<a href="#alternatives">permalink</a>]</div></h1>
<p>I also considered making vdirsyncer take file descriptors as input on
invocation. Vdirsyncer would use these for network communication. This would
require complex scaffolding to use, and I fear would quickly become too complex
when using a few different storages. That aside, it requires a lot of custom
glue code for almost any use case.</p>
<p>I considered using a <code>connection-cmd</code>, a command that is executed to obtain file
descriptors for a socket to the target server. This provides similar flexibility
as both of the above. This alternative is more unorthodox and doesn&rsquo;t allow
leveraging existing tools as easily (e.g.: you&rsquo;d require special glue code to
communicate to a local server listening on a Unix socket).</p>
<h1 id="impact-on-security">Impact on security<div class="permalink">[<a href="#impact-on-security">permalink</a>]</div></h1>
<p>If you need to use vdirsyncer with a specific (e.g.: audited) TLS
implementation, then you can use that external implementation without vdirsyncer
requiring any changes and without needing to link to it.</p>
<p>This also means that I can drop all custom code for custom TLS configurations.
Less custom security-related code correlates to less chances of a security bugs.</p>
]]></description></item><item><title>Quickly render the Clipboard as a QR code</title><link>https://whynothugo.nl/journal/2024/08/08/quickly-render-the-clipboard-as-a-qr-code/</link><pubDate>Thu, 08 Aug 2024 17:44:29 +0200</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/08/08/quickly-render-the-clipboard-as-a-qr-code/</guid><description><![CDATA[<p>I often want to copy URLs or text or other data from my laptop/desktop on a
phone. Sometimes it&rsquo;s my own phone, sometimes it&rsquo;s someone else&rsquo;s phone, and
sometimes it&rsquo;s a phone that isn&rsquo;t even set up yet.</p>
<p>My solution works as follows:</p>
<ul>
<li>I copy into my CLIPBOARD selection the data that I want to transfer.</li>
<li>From my launcher, I pick an entry to render clipboard as a QR code.</li>
<li>I scan the QR code from the phone.</li>
</ul>
<p>This is convenient and globally accessible. It requires not connectivity between
the devices; only line of sight.</p>
<h2 id="implementation">Implementation<div class="permalink">[<a href="#implementation">permalink</a>]</div></h2>
<p>I created the following <a href="https://specifications.freedesktop.org/desktop-entry-spec/latest/">desktop entry</a> in
<code>~/.local/share/applications/clip2qr.desktop</code> so it shows up in my launcher,
<a href="https://codeberg.org/dnkl/fuzzel/"><code>fuzzel</code></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;-webkit-text-size-adjust:none;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#66d9ef">[Desktop Entry]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Version</span><span style="color:#f92672">=</span><span style="color:#e6db74">1.0</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Name</span><span style="color:#f92672">=</span><span style="color:#e6db74">QR code for CLIPBOARD selection</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">TryExec</span><span style="color:#f92672">=</span><span style="color:#e6db74">qrencode</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Exec</span><span style="color:#f92672">=</span><span style="color:#e6db74">sh -c &#34;wl-paste | qrencode -s 30 -o - | imv -&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Icon</span><span style="color:#f92672">=</span><span style="color:#e6db74">terminal</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Keywords</span><span style="color:#f92672">=</span><span style="color:#e6db74">qr;clipboard;selection</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Type</span><span style="color:#f92672">=</span><span style="color:#e6db74">Application</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Terminal</span><span style="color:#f92672">=</span><span style="color:#e6db74">false</span>
</span></span></code></pre></div><p>This will pipe the clipboard into <code>qrencode</code> using <code>wl-paste</code>, and then render
the image using <code>imv</code>. I can then scan it on the phone.</p>
<p>My launcher does fuzzy matching, so just typing <code>qr</code> shows my entry shows up as
the first result. The whole sequence of keys is <kbd>Super</kbd>+<kbd>d</kbd>
<kbd>q</kbd> <kbd>r</kbd> <kbd>Return</kbd> <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.</p>
<h2 id="dependencies">Dependencies<div class="permalink">[<a href="#dependencies">permalink</a>]</div></h2>
<p>The necessary software can be installed on Alpine Linux with
<code>apk add wl-clipboard libqrencode-tools imv</code>.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><kbd>Super</kbd>+<kbd>d</kbd> is my mapping to open the launcher.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>Typing non-English characters</title><link>https://whynothugo.nl/journal/2024/07/12/typing-non-english-characters/</link><pubDate>Fri, 12 Jul 2024 14:00:58 +0200</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/07/12/typing-non-english-characters/</guid><description><![CDATA[<p>These are different ways of typing non-English characters on computers.</p>
<h2 id="alternative-graph">Alternative Graph<div class="permalink">[<a href="#alternative-graph">permalink</a>]</div></h2>
<p>The <kbd>AltGr</kbd> (Alternative Graph) key is typically located on the right
of the space bar. Some keyboard don&rsquo;t have an <kbd>AltGr</kbd> key but have a
second Alt key instead. In this case, the right Alt key can be configured to
work as <kbd>AltGr</kbd>.</p>
<p>The <kbd>AltGr</kbd> key provide quick access a few symbols common in Western
European languages. These are some examples that work on my current
configuration:</p>
<ul>
<li><kbd>AltGr</kbd> + <kbd>a</kbd> = <code>á</code></li>
<li><kbd>AltGr</kbd> + <kbd>s</kbd> = <code>ß</code></li>
<li><kbd>AltGr</kbd> + <kbd>e</kbd> = <code>é</code></li>
<li><kbd>AltGr</kbd> + <kbd>n</kbd> = <code>ñ</code></li>
<li><kbd>AltGr</kbd> + <kbd>p</kbd> = <code>ö</code></li>
<li><kbd>AltGr</kbd> + <kbd>k</kbd> = <code>œ</code></li>
<li><kbd>AltGr</kbd> + <kbd>5</kbd> = <code>€</code></li>
<li><kbd>AltGr</kbd> + <kbd>Shift</kbd> + <kbd>a</kbd> = <code>Á</code></li>
<li><kbd>AltGr</kbd> + <kbd>Shift</kbd> + <kbd>s</kbd> = <code>§</code></li>
</ul>
<p>It is not possible to type the <code>°</code> symbol nor <code>ǔ</code> with <kbd>AltGr</kbd> on an
English keyboard.</p>
<h2 id="dead-letters">Dead letters<div class="permalink">[<a href="#dead-letters">permalink</a>]</div></h2>
<p><a href="https://en.wikipedia.org/wiki/Dead_key">Dead letters</a> are those that don&rsquo;t immediately insert a character when pressed.
The naming comes from the days of typewriters.</p>
<p>Any keyboard (typically a &ldquo;US English&rdquo; one) can be configured to be used with
the &ldquo;US English International&rdquo; variant, which enables some dead keys.</p>
<p>Dead keys are used in sequences: they change the result of the following key.
The <code>,</code> notation in the examples below means &ldquo;one key after another&rdquo;:</p>
<ul>
<li><kbd>&rsquo;</kbd> = Dead key (doesn&rsquo;t type anything)</li>
<li><kbd>&rsquo;</kbd>, <kbd>Space</kbd> = <code>'</code></li>
<li><kbd>&rsquo;</kbd>, <kbd>a</kbd> = <code>à</code></li>
<li><kbd>&rsquo;</kbd>, <kbd>p</kbd> = <code>ḱ</code></li>
<li><kbd>&rsquo;</kbd>, <kbd>p</kbd> = <code>ṕ</code></li>
<li><kbd>&quot;</kbd> = Dead key (doesn&rsquo;t type anything)</li>
<li><kbd>&quot;</kbd>, <kbd>Space</kbd> = <code>&quot;</code></li>
<li><kbd>&quot;</kbd>, <kbd>u</kbd> = <code>ü</code></li>
<li><kbd>&quot;</kbd>, <kbd>Shift</kbd> + <kbd>u</kbd> = <code>Ü</code></li>
<li><kbd>~</kbd> = Dead key (doesn&rsquo;t type anything)</li>
<li><kbd>~</kbd>, <kbd>Space</kbd> = <code>~</code></li>
<li><kbd>~</kbd>, <kbd>n</kbd> = <code>ñ</code></li>
<li><kbd>~</kbd>, <kbd>y</kbd> = <code>ỹ</code></li>
</ul>
<p>This approach makes it easy to write Latin characters, but requires an extra
keystroke to type quotes and double quotes. For a Spanish speaker this can be a
reasonable compromise, especially if when replacing the keyboard is not
feasible.</p>
<p><code>AltGr</code> is also available in this layout. This provides access to some extra
characters (like <code>ß</code>), but also provides redundant access to others:</p>
<ul>
<li><kbd>AltGr</kbd> + <kbd>a</kbd> = <code>á</code></li>
<li><kbd>AltGr</kbd> + <kbd>s</kbd> = <code>ß</code></li>
</ul>
<p>Dead keys are usually easier to reach than <code>AltGr</code>, which make them easier to
use when touch-typing.</p>
<h2 id="compose">Compose<div class="permalink">[<a href="#compose">permalink</a>]</div></h2>
<p>The <kbd>⎄</kbd> (Compose) key is extremely rare in modern keyboards. Windows
does not support the Compose key<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>, so most keyboard manufacturers don&rsquo;t
include it.</p>
<p>Those who use the Compose key usually configure some other key to function as
Compose, typical candidates being:</p>
<ul>
<li><kbd>CapsLock</kbd></li>
<li><kbd>Menu</kbd>: Often seen on left side of the right <kbd>Control</kbd> key.</li>
<li><kbd>AltGr</kbd>: Compose provide access to a superset of what
<kbd>AltGr</kbd> can type.</li>
<li><kbd>Esc</kbd>: This only really makes sense when some other key is mapped to
<kbd>Esc</kbd>.</li>
<li><kbd>ScrollLock</kbd>: Usage of this is extremely rare nowadays.<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup></li>
</ul>
<p>Pressing the compose key signals to the computer that the following keystrokes
are to insert alternate characters. The keys are pressed one after the other,
and not all at once.</p>
<ul>
<li><kbd>⎄</kbd>, <kbd>&rsquo;</kbd>, <kbd>a</kbd> = <code>á</code></li>
<li><kbd>⎄</kbd>, <kbd>s</kbd>, <kbd>s</kbd> = <code>ß</code> (this is usually thought of as
a &ldquo;double s&rdquo;)</li>
<li><kbd>⎄</kbd>, <kbd>o</kbd>, <kbd>o</kbd> = <code>°</code></li>
<li><kbd>⎄</kbd>, <kbd>+</kbd>, <kbd>-</kbd> = <code>±</code></li>
<li><kbd>⎄</kbd>, <kbd>v</kbd>, <kbd>c</kbd> = <code>č</code></li>
<li><kbd>⎄</kbd>, <kbd>_</kbd>, <kbd>u</kbd> = <code>ū</code></li>
<li><kbd>⎄</kbd>, <kbd>&rsquo;</kbd>, <kbd>u</kbd> = <code>ú</code></li>
<li><kbd>⎄</kbd>, <kbd>`</kbd>, <kbd>u</kbd> = <code>ù</code></li>
<li><kbd>⎄</kbd>, <kbd>c</kbd>, <kbd>u</kbd> = <code>ǔ</code> (<a href="https://en.wikipedia.org/wiki/Caron">u with charon</a>)</li>
<li><kbd>⎄</kbd>, <kbd>u</kbd>, <kbd>u</kbd> = <code>ŭ</code> (<a href="https://en.wikipedia.org/wiki/%C5%AC">u with breve</a>)</li>
<li><kbd>⎄</kbd>, <kbd>~</kbd>, <kbd>n</kbd> = <code>ñ</code></li>
<li><kbd>⎄</kbd>, <kbd>&quot;</kbd>, <kbd>_</kbd>, <kbd>u</kbd> = <code>ṻ</code></li>
</ul>
<p>On most Unix-like systems, these mappings are defined in
<code>/usr/share/X11/locale/en_US.UTF-8/Compose</code>.</p>
<p>The compose key requires learning some sequences and mnemonics, but sequences
are consistent. The example given above should be enough to infer how to type
<code>ǐ</code> or <code>É</code>.</p>
<p>The compose key allows provides access to all letters from European languages,
Chinese tonal marks (for pinyin) and many scientific and mathematical symbols.</p>
<h2 id="eastern-asian-languages-cjk">Eastern Asian languages (CJK)<div class="permalink">[<a href="#eastern-asian-languages-cjk">permalink</a>]</div></h2>
<p>Typing Chinese, Japanese or Korean requires using an <strong>Input Method</strong>. An input
method is a program that assists in converting keystrokes into the desired
characters. Usually, this includes a pop-up window to select the right
characters.</p>
<p>For example, to type <code>雨果</code> on an Keyboard, I need to know the <a href="https://en.wikipedia.org/wiki/Pinyin">pinyin</a>
representation of these characters. The pinyin for <code>雨果</code> is <code>yǔ gǔo</code>. With a
Chinese input method active, I type <code>yuguo</code> and a little pop-up window shows me
suggestions that match 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;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><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></span><span style="display:flex;"><span>[...]
</span></span></code></pre></div><p>I can cycle through these with <kbd>Tab</kbd> and confirm my selection with
<kbd>Space</kbd>.</p>
<p>I can switch back to English input by pressing <kbd>Ctrl</kbd> +
<kbd>Space</kbd>.</p>
<p>The general idea is similar to code completion in code editors.</p>
<h2 id="unicode-character-points">Unicode character points<div class="permalink">[<a href="#unicode-character-points">permalink</a>]</div></h2>
<p>The above approaches all require reconfiguring the keyboard layout (or running
an input method program).</p>
<p>It is also possible to type any character using Unicode code points. On
Unix-like operating systems, this is typically done with <kbd>Ctrl</kbd> +
<kbd>Shift</kbd> + <kbd>u</kbd>, followed by the hexadecimal Unicode code point.
For example:</p>
<ul>
<li><kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>u</kbd>, <kbd>e</kbd>, <kbd>1</kbd>
= <code>á</code></li>
<li><kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>u</kbd>, <kbd>1</kbd>, <kbd>f</kbd>,
<kbd>4</kbd>, <kbd>3</kbd>, <kbd>c</kbd> = <code>🐼</code> (panda emoji)</li>
<li><kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>u</kbd>, <kbd>6</kbd>, <kbd>7</kbd>,
<kbd>9</kbd>, <kbd>c</kbd> = <code>果</code></li>
</ul>
<p>Clearly this is far less convenient and requires memorising or looking up
Unicode character points.</p>
<h2 id="german-norwegian-and-other-european-keyboards">German, Norwegian and other European keyboards<div class="permalink">[<a href="#german-norwegian-and-other-european-keyboards">permalink</a>]</div></h2>
<p>Some country-specific keyboards include extra keys with the extra letters
commonly used in the local language. The are convenient for bilingual usage
(e.g.: English and German), but are limited when needing to type multiple
languages (e.g.: English, German and Spanish).</p>
<h2 id="arabic-cyrillic">Arabic, Cyrillic<div class="permalink">[<a href="#arabic-cyrillic">permalink</a>]</div></h2>
<p>Any keyboard can be configured to have an Arabic or Cyrillic layout. Each key
will insert a single letter. Typing English requires switching to an English
layout, so a global key combination is usually used to switch back and forth.</p>
<h2 id="emoji">Emoji<div class="permalink">[<a href="#emoji">permalink</a>]</div></h2>
<p>I intend to write an emoji-picker input method in future. The intended usage is:</p>
<ul>
<li>Press some key combination to enable it (e.g.: <kbd>⌘</kbd> + <kbd>,</kbd>).</li>
<li>Type <code>sparkles</code>.</li>
<li>Input method shows a pop-up with suggestions, in this case: <code>✨</code>.</li>
<li>Press the same key combination or <kbd>Ctrl</kbd> + <kbd>Space</kbd> to switch
back.</li>
</ul>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><a href="https://github.com/samhocevar/wincompose">WinCompose</a> implements the Compose key for Windows.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p><kbd>ScrollLock</kbd> changes the behaviour of the arrow keys. Usually the
arrow keys move the cursor around, but when Scroll Lock is enabled, the
arrow keys move on-screen content instead (e.g.: a document, spreadsheet,
etc) without moving the cursor. With the popularisation of mice, usage of
this key has become extremely niche.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>Introducing wlhc: wayland hot corners</title><link>https://whynothugo.nl/journal/2024/07/11/introducing-wlhc-wayland-hot-corners/</link><pubDate>Thu, 11 Jul 2024 13:19:00 +0200</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/07/11/introducing-wlhc-wayland-hot-corners/</guid><description><![CDATA[<p>In theory I&rsquo;ve been resting in bed with COVID these last days, but it&rsquo;s hard for
me to sit my entirely idle, so I wrote a invested some of this time in
familiarising myself with <code>hare-wayland</code> and <code>hare-ev</code>.</p>
<p><code>wlhc</code> is a small program to enable <a href="https://en.wikipedia.org/wiki/Screen_hotspot">&ldquo;hot corners&rdquo;</a> on wayland desktops. It
takes a corner and a command as argument. When the mouse (or touchpad) pointer
hits that corner of the screen, <code>wlhc</code> executes the provided command.</p>
<p>The implementation uses the <code>wlr-layer-shell</code> protocol to detect when the mouse
hits a corner. This is a bit of an abuse of this protocol (i.e.: it&rsquo;s not really
designed for this), but it works.</p>
<p>The whole project is available at <a href="https://git.sr.ht/~whynothugo/wlhc">https://git.sr.ht/~whynothugo/wlhc</a> and
released under the <a href="https://git.sr.ht/~whynothugo/wlhc/tree/main/item/LICENCE.md">ISC licence</a>. The README contains all the documentation.</p>
<p>Right now there&rsquo;s only one annoyance: the <code>Makefile</code> hard-codes paths to the
wayland protocol files. I think most distributions ship these in the same
location, but the build script may require tinkering on distributions with
unusual filesystem layouts.</p>
<p>Working on a small project like this in Hare has been a pleasure. Hare doesn&rsquo;t
add any layers over the underlying abstractions, so it&rsquo;s always quite
straightforward to understand what&rsquo;s going on. The dopamine hits of completing
little milestones while advancing on this project have been invaluable in my
current state of health.</p>
<p>The main downside of Hare right now is that the ecosystem is still immature, and
I had to apply patches (now merged upstream) to both dependencies in order for
them to build with the latest compiler. I&rsquo;ll wait for these to trickle down into
stable releases before tagging a release of <code>wlhc</code> itself.</p>
]]></description></item><item><title>Editing filenames and directories in Vim</title><link>https://whynothugo.nl/journal/2024/07/02/editing-filenames-and-directories-in-vim/</link><pubDate>Tue, 02 Jul 2024 11:49:51 +0200</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/07/02/editing-filenames-and-directories-in-vim/</guid><description><![CDATA[<p>I have a list with ideas of &ldquo;useful tools that I want to write someday&rdquo; that
would likely take hundreds of years to complete. Finding out that somebody has
already written one the tools in this list pleases me immensely, not only
because I can use the tool in this lifetime, but actually right now!</p>
<p>Today I came across <code>vidir</code>, from the <a href="https://joeyh.name/code/moreutils/">moreutils</a> package. It lets me edit a
filenames and directories in Vim.</p>
<p>The idea is pretty simple; when I want to edit the filenames inside a directory,
I run <code>vidir path/to/directory/</code>. This opens my text editor<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> with a
temporary file where each line contains a number and a filename. I can edit
filenames, exit Vim, and <code>vidir</code> will rename these files to match my changes.
Deleting lines deletes the corresponding files.</p>
<p>Here&rsquo;s an example (when using <code>vidir photos/Unsorted/</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;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>0001	photos/20120718_002.jpg
</span></span><span style="display:flex;"><span>0002	photos/IMG_0021.JPEG
</span></span><span style="display:flex;"><span>0003	photos/IMG_0166.JPEG
</span></span><span style="display:flex;"><span>0004	photos/IMG_0191.JPEG
</span></span><span style="display:flex;"><span>0005	photos/IMG_1168.JPEG
</span></span><span style="display:flex;"><span>0006	photos/IMG_1170.JPEG
</span></span><span style="display:flex;"><span>0007	photos/IMG_1465.JPEG
</span></span><span style="display:flex;"><span>0008	photos/IMG_1796.JPEG
</span></span><span style="display:flex;"><span>0009	photos/IMG_1802.JPEG
</span></span><span style="display:flex;"><span>0010	photos/IMG_1902.JPEG
</span></span><span style="display:flex;"><span>0011	photos/IMG_2032.JPG
</span></span><span style="display:flex;"><span>[...]
</span></span></code></pre></div><p>I can quickly replace all instances of <code>\.JPEG$</code> and <code>\.jpg$</code> with <code>.jpeg</code> using
Vim&rsquo;s replacement command<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>. Once I exit Vim, <code>vidir</code> will rename all
these files at once. If I give multiple files the same name, one of them will
include a trailing <code>~</code>, the next one a trailing <code>~1</code>, and so forth. This
prevents unintentionally overwriting files.</p>
<p>The numbers at the beginning of each line are used internally by <code>vidir</code> to
match which line corresponds to which original file.</p>
<p>My own idea was pretty similar, but for some reason I had planned on using inode
numbers instead of sequential numbers. I now see that it the design could be
much simpler.</p>
<div class="notice update">
  <header>Update 2024-07-02</header>
  <section>
  <p><code>vidir</code> can also handle files in multiple directories or an entire tree of
files. 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;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>fd -t d  | xargs vidir  # all files in current directory tree
</span></span><span style="display:flex;"><span>fd &#39;py$&#39; | xargs vidir  # all python files in the current directory tree
</span></span></code></pre></div>
  </section>
</div>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><code>neovim</code> in my case, but this can be controlled by setting the <code>$EDITOR</code>
environment variable.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>This isn&rsquo;t a great example: this task can easily be done with a script. I
wish I could remember more interesting use-cases for which I&rsquo;ve needed this
tool, but my notes on this don&rsquo;t elaborate further.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>Status update 2024-06</title><link>https://whynothugo.nl/journal/2024/06/30/status-update-2024-06/</link><pubDate>Sun, 30 Jun 2024 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/06/30/status-update-2024-06/</guid><description><![CDATA[<h2 id="renaming-items">Renaming items<div class="permalink">[<a href="#renaming-items">permalink</a>]</div></h2>
<p>Last month <a href="/journal/2024/05/28/status-update-may-2024/#ab-tidy">I mentioned a bug when items are renamed in a collection</a>.
After renaming an item, vdirsyncer would check the entire content on both sides
during every single execution. I have now fixed this.</p>
<p>I have now renamed all my vcard files to match the name of the person they
describe. This lets me use a terminal or file manager to easily visualise files.
In the future, I&rsquo;d like to write on a GUI vcard viewer, so I can just double
click on vcard files when using a file manager.</p>
<p>I still haven&rsquo;t released <code>ab-tidy</code> since it relies on yet-unreleased features of
Hare itself. I will tag a release once upstream ships a new release.</p>
<h2 id="properties">Properties<div class="permalink">[<a href="#properties">permalink</a>]</div></h2>
<p>I have implemented synchronisation of properties. For calendars, these are
DisplayName, Description, Colour and Order. The latter two are not formally
standardised, but are widely supported de-facto standards.</p>
<p>For calendar stored in a vdir, these are supported by tools like khal and
todoman. For calendars in CalDav, these properties are supported by a great deal
of software, including Nextcloud, <a href="https://www.davx5.com/">DAVx5</a>, iOS&rsquo;s calendars and reminders app.</p>
<p>Synchronising properties is subject to race conditions. For example, when
synchronising, vdirsyncer will read the colour on both storages, and plan to
copy the value from one storage onto the other. If another process changed the
colour of the target storage in the meantime, that value would be lost. This is
really unlikely to happen in practice, and the data loss is minimal (in this
example, only the newly applied colour is lost).</p>
<p>This issue is due to limitations of the CalDav protocol, so this is unlikely to
change in future. I will simply document it well and move on.</p>
<h2 id="reading-etags">Reading Etags<div class="permalink">[<a href="#reading-etags">permalink</a>]</div></h2>
<p>Previously the <code>Storage</code> abstraction didn&rsquo;t always return an <code>Etag</code> (because
CalDav servers sometimes don&rsquo;t return an Etag). I have changed this logic to
always return an Etag, or an error if a race condition occurred reading it.</p>
<p>This has simplified logic during synchronisation and was required in order to
handle renamed items.</p>
<h2 id="focus">Focus<div class="permalink">[<a href="#focus">permalink</a>]</div></h2>
<p>I&rsquo;ve been focusing a lot on actually completing development milestones rather
than attempting to polish everything into a perfect state (nothing is really
perfect anyway). This has worked pretty well so far, and having a working
version quickly allows <em>some</em> level of polish. As long as no bugs are
introduced, I&rsquo;m fine with keeping this &ldquo;good enough&rdquo; until there is an actual
need to improve them.</p>
<p>Memory usage is a good example; I&rsquo;ve been focusing a lot on not using too much
memory but vdirsyncer uses about 16MiB of ram when synchronising my collections
3500 items. This <em>could</em> be better, but it&rsquo;s a bit of an unnecessary improvement
at this stage.</p>
<h2 id="properties-for-items">Properties for items<div class="permalink">[<a href="#properties-for-items">permalink</a>]</div></h2>
<p>I&rsquo;ve kept properties for items scope for quite some time now, but I am concerned
this might have been a mistake. Calendars and contacts don&rsquo;t have properties for
items, they only have properties for collections. This feature was only in scope
for potentially synchronising emails in future (where &ldquo;properties&rdquo; would
basically be &ldquo;flags&rdquo;).</p>
<p>This has added some complexity that I&rsquo;m not certain will ever pay off.
Synchronisation of properties currently only works for collections, since
dealing with the added complexity of item-properties would have substantially
delayed properties for calendars and address books.</p>
<h2 id="documentation">Documentation<div class="permalink">[<a href="#documentation">permalink</a>]</div></h2>
<p>I have split the user documentation, developer documentation and contributor
documentation. This provides clearer starting points for each mode of usage.</p>
<p>The user documentation still needs more work, especially configuring and setting
up from scratch.</p>
<h2 id="vdir-safety-checks">Vdir safety checks<div class="permalink">[<a href="#vdir-safety-checks">permalink</a>]</div></h2>
<p>The vdir (filesystem) storage had a lot of pending <code>FIXME</code> comments regarding
security. Most of them were to prevent malicious input from writing elsewhere in
the filesystem.</p>
<p>I have since cleared all of those and implemented the appropriate checks.</p>
<hr>
<p>That&rsquo;s it for this month. Cheers!</p>
]]></description></item><item><title>IRC is great for public spaces</title><link>https://whynothugo.nl/journal/2024/06/25/irc-is-great-for-public-spaces/</link><pubDate>Tue, 25 Jun 2024 12:09:14 +0200</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/06/25/irc-is-great-for-public-spaces/</guid><description><![CDATA[<p>IRC is great for public rooms or public meeting points.</p>
<p>I like to imagine IRC servers as virtual co-working spaces. The kind of
co-working space with lots of meeting rooms. Each channel is a one of these
meeting room, and anyone can walk in at any time. If you were there first in a
room, you can ask others to leave or kick them out. If you reserve a room (e.g.:
register a channel), you can have the staff kick out unwanted guests. If
somebody misbehaves, the moderators or administrator will throw them out of the
building entirely.</p>
<p>IRC isn&rsquo;t end to end encrypted, but it&rsquo;s encrypted at flight. This is in the
same vein that meeting rooms are private, but you trust that the owner hasn&rsquo;t
installed hidden microphones inside the walls. If you need tighter security, you
want your own building (e.g.: your own IRC server).</p>
<p>You can also be sure that only people in the room can hear your conversations.
If someone walks in, they can&rsquo;t suddenly know what was spoken before they walked
in. If someone is keeping logs, they might leak what you said, just like someone
having a recorder in the coat pocket can record anything that happens in the
room.</p>
<p>For a public or semi-public meeting point of an open source project, or open
community, all this is exactly what&rsquo;s needed; no more, no less. The protocol is
well-established, lightweight and standardised. And there exist plenty of client
implementations for a huge variety of devices and platforms.</p>
]]></description></item><item><title>Notes on saving coredumps</title><link>https://whynothugo.nl/journal/2024/06/24/notes-on-saving-coredumps/</link><pubDate>Mon, 24 Jun 2024 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/06/24/notes-on-saving-coredumps/</guid><description><![CDATA[<p>When programs crash in certain specific ways, the kernel can generate a core
dump file. A core dump is a file containing an image of the process&rsquo;s memory at
the moment when it was terminated.</p>
<p>A core dump can later be used with tools such as gdb to generate a backtrace of
the program and debug the root cause of the crash.</p>
<h1 id="relevant-runtime-parameters">Relevant runtime parameters<div class="permalink">[<a href="#relevant-runtime-parameters">permalink</a>]</div></h1>
<p>The <a href="https://www.man7.org/linux/man-pages/man3/getrlimit.3p.html"><code>RLIMIT_CORE</code></a> resource limit defines a maximum size for core dump. Like
most resource limits, it has a soft limit (which can be adjusted by an
unprivileged user) and a hard limit (the maximum value to which an unprivileged
user can adjust the soft limit). These can be queried via <code>ulimit -Hc</code> and
<code>ulimit -c</code> respectively. <code>ulimit</code> is a shell built-in, consult your shell&rsquo;s
documentation for more details<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>. The default value is zero, which means
that no core dumps are generated.</p>
<p>The <a href="https://www.kernel.org/doc/html/latest/admin-guide/sysctl/kernel.html#core-pattern"><code>kernel.core_pattern</code></a> kernel attribute defines where core dumps will be
stored. If the first character of this pattern is <code>|</code>, then the rest of the
attribute is interpreted as the path to an program which will be executed with
the core dump piped into its stdin. The default value is <code>core</code>, so core dumps
would be generated into a file named <code>core</code> in the process&rsquo;s working directory.</p>
<p>If the value of <code>kernel.core_pattern</code> does not start with a pipe, the core dump
is only saved if the terminated process had permission to write to the resulting
path. However, if the pattern does start with a pipe, the new process started by
the kernel runs with elevated privileges (e.g.: as <code>root</code>).</p>
<h1 id="saving-core-dumps">Saving core dumps<div class="permalink">[<a href="#saving-core-dumps">permalink</a>]</div></h1>
<p>I take the simplest approach, saving all core dumps into a common directory. I
want any user to be able to write dumps, but only members of the <code>wheel</code> group
can read dumps, and only the ones that they own.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>mkdir /var/coredump/
</span></span><span style="display:flex;"><span>chown root:wheel /var/coredump
</span></span><span style="display:flex;"><span>chmod <span style="color:#ae81ff">1775</span> /var/coredump
</span></span></code></pre></div><p>The <code>1</code> in the above permissions is the sticky bit: only the owner of a file
within the directory can delete or rename it. This setup allows any local user
to fill the disk completely by forking and crashing continuously.</p>
<p>Next, I&rsquo;ll set the <code>kernel.core_pattern</code> attribute (this can be made permanent
via a configuration file in <code>/etc/sysctl.d</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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>sysctl -w kernel.core_pattern<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;/var/coredump/%t_%P_%u_%s_%f&#39;</span>
</span></span></code></pre></div><p>From the above pattern:</p>
<ul>
<li><code>%t</code>: UNIX time of dump</li>
<li><code>%P</code>: Global PID</li>
<li><code>%u</code>: Global UID</li>
<li><code>%s</code>: Signal number</li>
<li><code>%f</code>: Executable filename</li>
</ul>
<p>I initially tried <code>%E</code> (executable path) instead of <code>%f</code> first. It saves the
full path to the executable replacing <code>/</code> with <code>!</code>. I didn&rsquo;t find the full path
of any use, so opted for the simpler variation.</p>
<h1 id="testing-the-setup">Testing the setup<div class="permalink">[<a href="#testing-the-setup">permalink</a>]</div></h1>
<p>First, I&rsquo;ll create a quick helper that segfaults right away. C makes this easier
than any other language!</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-c" data-lang="c"><span style="display:flex;"><span><span style="color:#66d9ef">int</span> <span style="color:#a6e22e">main</span>(<span style="color:#66d9ef">int</span> argc, <span style="color:#66d9ef">char</span> <span style="color:#f92672">*</span>argv[]) {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">int</span> <span style="color:#f92672">*</span>i <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span>	<span style="color:#f92672">*</span>i <span style="color:#f92672">=</span> <span style="color:#ae81ff">42</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>I&rsquo;ll then raise the core size resource limit:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>ulimit -c <span style="color:#ae81ff">256</span>
</span></span></code></pre></div><p>And then run my test program that segfaults:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>cc segfault.c -o segfault
</span></span><span style="display:flex;"><span>./segfault
</span></span><span style="display:flex;"><span>doas ./segfault
</span></span></code></pre></div><p>I now see both core dumps in <code>/var/coredump</code><sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>.rw------- 201k hugo hugo 2024-06-24 13:07 1719227265_11806_1000_11_segfault
</span></span><span style="display:flex;"><span>.rw------- 201k root root 2024-06-24 13:07 1719227268_11866_0_11_segfault
</span></span></code></pre></div><ul>
<li><code>1719227265</code> is the timestamp.</li>
<li><code>11806</code> is the PID.</li>
<li><code>1000</code> is the UID.</li>
<li><code>11</code> is the signal number</li>
<li><code>segfault</code> is the name of my test program.</li>
</ul>
<p>I also confirm that permissions are as expected:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-console" data-lang="console"><span style="display:flex;"><span>&gt; file 1719227265_11806_1000_11_segfault 1719227268_11866_0_11_segfault
</span></span><span style="display:flex;"><span>1719227265_11806_1000_11_segfault: ELF 64-bit LSB core file, x86-64, version 1 (SYSV), SVR4-style, from &#39;./segfault&#39;
</span></span><span style="display:flex;"><span>1719227268_11866_0_11_segfault:    regular file, no read permission
</span></span></code></pre></div><p>I can also debug this dump by pointing <code>gdb</code> to the original binary and the
coredump itself:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>gdb ./segfault /var/coredump/1719227677_18833_1000_11_segfault
</span></span></code></pre></div><h1 id="portability">Portability<div class="permalink">[<a href="#portability">permalink</a>]</div></h1>
<ul>
<li><code>RLIMIT_CORE</code> is standard and exists in the BSDs as well.</li>
<li><code>kernel.core_pattern</code> seems to be Linux-specific.</li>
</ul>
<h1 id="piping-to-a-dedicated-handler">Piping to a dedicated handler<div class="permalink">[<a href="#piping-to-a-dedicated-handler">permalink</a>]</div></h1>
<p>A more sophisticated setup could pipe to a dedicated handler. This handler could
extract additional process information, drop privileges, and save the dump along
with this extra information into a location where it has exclusive write
permissions.</p>
<p>For saving the metadata, a <code>.info</code> file seems like the obvious choice, but one
could also use <a href="https://www.man7.org/linux/man-pages/man7/xattr.7.html">extended file attributes</a><sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>. Extended attributes
would ensure that if a coredump is deleted, no orphan metadata is left behind.</p>
<p>For my use-case, I don&rsquo;t really need any more information. Even the filename
patterns described above already have more data than I need.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>POSIX specifies that this value is defined in 512byte blocks, but behaviour
diverges between popular shells.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>You might have noticed that I set a limit of 128KB, but the dumps are 201KB.
This seems to be a bug in <code>zsh</code>.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>I&rsquo;ve been reading a lot on Haiku&rsquo;s design lately. It uses extended
attributes in several interesting ways. Storing metadata for non-portable
files like this seems like a good usage of extended attributes, although
also not strictly necessary.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>SSH as a sudo replacement</title><link>https://whynothugo.nl/journal/2024/06/13/ssh-as-a-sudo-replacement/</link><pubDate>Thu, 13 Jun 2024 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/06/13/ssh-as-a-sudo-replacement/</guid><description><![CDATA[<p>A major caveat in tools like <code>sudo</code> and <a href="https://man.openbsd.org/doas"><code>doas</code></a> for that matter is that they
rely on <a href="https://en.wikipedia.org/wiki/Setuid"><code>setuid</code></a> binaries and privilege escalation in order to run commands as
root.</p>
<p>The design is not ideal, and also drags in a few limitations:</p>
<ol>
<li>The whole user session needs to retain capabilities to perform privilege
escalation.</li>
<li>They don&rsquo;t work when running an entire user session in a restricted user
namespace.</li>
<li><code>setuid</code> binaries introduce limitations on how the whole system is secured.</li>
</ol>
<p>An interesting alternative with a is <a href="https://skarnet.org/software/s6/s6-sudod.html"><code>s6-sudod</code></a>, which splits the program into
two parts: a privileged server and an unprivileged client.</p>
<p>This is a summary of an experiment from a few weeks ago where I experimented
with using <code>ssh</code> locally to perform the same role as <code>sudo</code>, without exposing
this <code>sshd</code> instance to the network.</p>
<h1 id="goals">Goals<div class="permalink">[<a href="#goals">permalink</a>]</div></h1>
<ul>
<li>Enable authorised users (and only authorised users) to run commands as root.</li>
<li>Don&rsquo;t use privilege escalation.</li>
</ul>
<h1 id="implementation">Implementation<div class="permalink">[<a href="#implementation">permalink</a>]</div></h1>
<p>First, I configured a dedicated SSH key that will be authorised for
authentication as root. This key is not in the regular <code>authorized_keys</code> file,
but in a separate file which will only be used for this purpose:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>mkdir /root/.ssh/
</span></span><span style="display:flex;"><span>echo ssh-ed25519 AAAAC3Nza... &gt; /root/.ssh/local_keys
</span></span></code></pre></div><p>I then ran an <code>sshd</code> server instance bound to a unix domain socket. Permissions
are tightened so unauthorised users cannot even access the socket. This instance
overrides the <code>PermitRootLogin</code> option to enable logging in as root<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>
and uses the newly created <code>/root/.ssh/local_keys</code> as a source for authorised
keys:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>mkdir /run/sshd/
</span></span><span style="display:flex;"><span>chown root:wheel /run/sshd/
</span></span><span style="display:flex;"><span>chmod <span style="color:#ae81ff">750</span> /run/sshd/
</span></span><span style="display:flex;"><span>s6-ipcserver /run/sshd/sshd.sock sshd -ie -o AuthorizedKeysFile<span style="color:#f92672">=</span>/root/.ssh/local_keys -o PermitRootLogin<span style="color:#f92672">=</span>yes
</span></span></code></pre></div><p>The root account was locked to disallow logging in via any mechanism. This was
done by prefixing the password&rsquo;s hash with <code>!</code> (so no password&rsquo;s hash can ever
match this value). <code>sshd</code> interprets this special prefix as the account being
locked and won&rsquo;t allow logging in as root.</p>
<p>I changed the root password in <code>/etc/passwd</code> and replaced the <code>!</code> with an <code>*</code>.
<code>sshd</code> won&rsquo;t give new value any special interpretation, and will allow logging
in as root. The value <code>*</code> will never match the hash of any password either, so
logging in via password remains effectively disabled.<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup></p>
<p>I then needed to connect to the local <code>sshd</code> instance. While <code>sshd</code> has a <code>-i</code>
flag which allows passing an existing socket to it, <code>ssh</code> has no equivalent
flag. The <code>ProxyCommand</code> option can be (ab)used for 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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>ssh -o ProxyCommand<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;socat STDIO UNIX-CONNECT:/run/sshd/sshd.sock&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -i .ssh/root-key.pub <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -t <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    root@root <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;cd </span><span style="color:#66d9ef">$(</span>pwd<span style="color:#66d9ef">)</span><span style="color:#e6db74">; &#39;</span>$SHELL<span style="color:#e6db74">&#39; --login&#34;</span>
</span></span></code></pre></div><p>I&rsquo;m using a hardware-bound SSH key in this case, which means that I need to tap
the physical device to authorise this connection. I could also use an ssh-agent
that requires explicit approval before disclosing keys (e.g.: <code>hissh-agent</code>).</p>
<p>A little caveat here is that <code>socat</code> will read all input from <code>ssh</code>, and then
write it into the socket, effectively duplicating the overhead of the
connection. I read the relevant manual pages a few more times, and couldn&rsquo;t find
a solution. I came across <a href="https://man.openbsd.org/ssh_config.5#ProxyUseFdpass"><code>ProxyUseFdpass</code></a>, but wasn&rsquo;t entirely sure how to
make it work.</p>
<p>After some more research online (and some major frustration), I found <a href="https://www.gabriel.urdhr.fr/2016/08/07/openssh-proxyusefdpass/">a clear
usage example from 2016</a>. It turnes out that <code>ProxyUseFdpass</code> was quite
straightforward and allows me to specify a command that sends the socket file
descriptor (via <code>stdout</code>) to <code>ssh</code>, and <code>ssh</code> then connects over this socket.</p>
<p>I saved the following script into <code>/home/hugo/tmp/passfd.py</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;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env python3</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># From: https://www.gabriel.urdhr.fr/2016/08/07/openssh-proxyusefdpass/</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> sys
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> socket
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> array
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Create the file descriptor:</span>
</span></span><span style="display:flex;"><span>s <span style="color:#f92672">=</span> socket<span style="color:#f92672">.</span>socket(socket<span style="color:#f92672">.</span>AF_UNIX, socket<span style="color:#f92672">.</span>SOCK_STREAM)
</span></span><span style="display:flex;"><span>s<span style="color:#f92672">.</span>connect(<span style="color:#e6db74">&#34;/run/sshd/sshd.sock&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Pass the file descriptor:</span>
</span></span><span style="display:flex;"><span>fds <span style="color:#f92672">=</span> array<span style="color:#f92672">.</span>array(<span style="color:#e6db74">&#34;i&#34;</span>, [s<span style="color:#f92672">.</span>fileno()])
</span></span><span style="display:flex;"><span>ancdata <span style="color:#f92672">=</span> [(socket<span style="color:#f92672">.</span>SOL_SOCKET, socket<span style="color:#f92672">.</span>SCM_RIGHTS, fds)]
</span></span><span style="display:flex;"><span>socket<span style="color:#f92672">.</span>socket(fileno <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>)<span style="color:#f92672">.</span>sendmsg([<span style="color:#e6db74">b</span><span style="color:#e6db74">&#39;</span><span style="color:#ae81ff">\0</span><span style="color:#e6db74">&#39;</span>], ancdata)
</span></span></code></pre></div><p>And the command to connect to ssh now becomes:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>ssh -o ProxyCommand<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;/home/hugo/tmp/passfd.py&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -i .ssh/root-key.pub <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -o ProxyUseFdpass<span style="color:#f92672">=</span>yes <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -t <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    root@root <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;cd </span><span style="color:#66d9ef">$(</span>pwd<span style="color:#66d9ef">)</span><span style="color:#e6db74">; &#39;</span>$SHELL<span style="color:#e6db74">&#39; --login&#34;</span>
</span></span></code></pre></div><p>The linked article mentioned using <code>nc</code> (for a somewhat different use case).
Initially, it would seem that <code>nc -FU /run/sshd/sshd.sock</code> would work, but the
manual page actually specifies that this is not 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;-webkit-text-size-adjust:none;"><code class="language-man" data-lang="man"><span style="display:flex;"><span>-F		Pass the first connected socket using sendmsg(2) to stdout and exit. This
</span></span><span style="display:flex;"><span>		is useful in conjunction with -X to have nc perform connection setup
</span></span><span style="display:flex;"><span>		with a proxy but then leave the rest of the connection to another
</span></span><span style="display:flex;"><span>		program (e.g. ssh(1) using the ssh_config(5) ProxyUseFdpass option).
</span></span><span style="display:flex;"><span>		Cannot be used with -c or -U.
</span></span></code></pre></div><h1 id="conclusion">Conclusion<div class="permalink">[<a href="#conclusion">permalink</a>]</div></h1>
<p>This technique works. It relies mainly on OpenSSH for all the sensitive security
details. Not only does OpenSSH have a great track record, but it also enables
various forms of authentication including using a hardware-based SSH key.</p>
<p>Configuring this on a new host has no complex steps, and the above <code>ipcserver</code>
command can just be executed via the system&rsquo;s service manager.</p>
<p>The above <code>passfd.py</code> script is a quick hack to move the experiment forward; for
daily usage it would be best to write a tiny executable that does the same thing
and put it into <code>/usr/local/bin</code>. The whole <code>ssh</code> command could also be placed
in a tiny wrapper.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>I didn&rsquo;t edit <code>/etc/ssh/sshd_config</code> because I don&rsquo;t want to enable logging
in as root over the network-bound sshd instance. It still contains the usual
<code>PermitRootLogin no</code>.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>I have <code>PasswordAuthentication no</code> in my <code>sshd_config</code> anyway; it is always
a good idea to disable password-based authentication for <code>sshd</code>.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>Status update, May 2024</title><link>https://whynothugo.nl/journal/2024/05/28/status-update-may-2024/</link><pubDate>Tue, 28 May 2024 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/05/28/status-update-may-2024/</guid><description><![CDATA[<p>This has been a slow month. I took a short vacation the last days of April, and
as soon as I got back had some health issues which got in the way of me doing
anything productive or fun.</p>
<h1 id="vdirsyncer-alpha-release">vdirsyncer alpha release<div class="permalink">[<a href="#vdirsyncer-alpha-release">permalink</a>]</div></h1>
<p>I have finally <a href="https://pimutils.org/blog/2024-05-22-vdirsyncer-2.0.0-alpha0/">tagged an alpha release of vdirsyncer 2</a>. This version is
stable enough that I&rsquo;ve been using it for over a month now no issues. I
generally keep <code>vdirsyncer daemon</code> running in background my calendars &amp; contacts
simply synchronise across devices continuously.</p>
<p>While some features are missing, none of the ones that I use are missing, so it
might be suitable for you as well. I&rsquo;d appreciate feedback from testers, but
please do make a full backups of your data just in case!</p>
<p>In hindsight, the code quality is much better than what I&rsquo;d usually call alpha
or beta quality, but I tagged it as alpha as an indicator that user-facing
aspects of program may well change before the final release (in fact, some
command line arguments have already changed since then).</p>
<h2 id="caveats-on-daemon-mode">Caveats on daemon mode<div class="permalink">[<a href="#caveats-on-daemon-mode">permalink</a>]</div></h2>
<p>For now, the daemon mode simply synchronises at a given interval. I intend to
work on implementing storage monitoring in future, so as to trigger a
synchronisation only when a storage indicates that an item has changed. The
&ldquo;fixed interval&rdquo; approach is only a temporarily solution until storage
monitoring is fully ironed out.</p>
<h1 id="ab-tidy">ab-tidy<div class="permalink">[<a href="#ab-tidy">permalink</a>]</div></h1>
<p><code>ab-tidy</code> is a tiny tool to tidy up an address book. It operates on a directory
where each contact is an individual vcard file (e.g.: a &ldquo;vdir&rdquo;) and renames each
file to match the name of the contact inside of it.</p>
<p>After running it, the directory is easy to browse with a file manager, terminal,
or even <code>ls</code>.</p>
<p>I wrote this in Hare in one afternoon, and I have to say that writing Hare has
been a pleasant (and fun) experience so far. The standard library provides
abstractions right on top of the primitives that the operating system offers.
The standard library is also readable and approachable code, even for a Hare
newbie like myself. The layer at which the it provides abstractions are not too
far from C itself, but the error handling is lovely (it uses tagged unions much
like Rust, Zig and other recent languages).</p>
<p>This tool also helped me find <a href="https://todo.sr.ht/~whynothugo/vdirsyncer-rs/52">another bug in vdirsyncer: when a file is
renamed locally, the status database isn&rsquo;t updated properly</a>. As a result,
the file get read from both storages each time a synchronisation occurs. The
end result is the exact same, but it adds unnecessary network traffic on each
run.</p>
<h1 id="darkman-v200">darkman v2.0.0<div class="permalink">[<a href="#darkman-v200">permalink</a>]</div></h1>
<p>Another release that has been delayed more than necessary.</p>
<p>The main breaking change is that geoclue is no longer used by default. Geoclue
automatically communicates external services to determine the current location
by default, so I want users to explicitly opt into it to ensure that here is
clear consent of what is going on. Geoclue also requires some manual
configuration to work properly on distributions, so for many use cases, just
putting latitude and longitude manually into the configuration file is good
enough.</p>
<p>See the <a href="https://gitlab.com/WhyNotHugo/darkman/-/blob/v2.0.0/CHANGELOG.md#200">changelog</a> for a full list of all changes.</p>
]]></description></item><item><title>Growing my root partition to the left</title><link>https://whynothugo.nl/journal/2024/05/27/growing-my-root-partition-to-the-left/</link><pubDate>Mon, 27 May 2024 18:31:17 +0200</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/05/27/growing-my-root-partition-to-the-left/</guid><description><![CDATA[<p>When I initially <a href="/journal/2023/11/19/setting-up-an-alpine-linux-workstation/#configuring-automatic-login">set up my laptop</a>, I created a 16GB swap partition on
it. The large size is so that I can suspend to disk (aka: hibernate) onto it.</p>
<p>Resuming from hibernation from this partition doesn&rsquo;t entirely work. <a href="https://gitlab.alpinelinux.org/alpine/mkinitfs/-/issues/36">Alpine&rsquo;s
initfs can&rsquo;t re-use the same passphrase to unlock more than one
partition</a>. Well, it can, but I&rsquo;d be prompted to type the same twice
password every time the system boots. LVM was also an option, but that seems
like one layer of complexity too many for such a simple setup.</p>
<p>I eventually set up hibernation to use a swapfile inside the main partition. But
the original swap partition is still there, idle and unused.</p>
<p>Having run out of space yesterday trying to compile the <code>main/rust</code> package, I
decided that it&rsquo;s time to recover this idle space<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.</p>
<h1 id="overview">Overview<div class="permalink">[<a href="#overview">permalink</a>]</div></h1>
<p>My disk layout is as follows:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-console" data-lang="console"><span style="display:flex;"><span># fdisk -l
</span></span><span style="display:flex;"><span>Found valid GPT with protective MBR; using GPT
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span>Disk /dev/nvme0n1: 500118192 sectors, 2534M
</span></span><span style="display:flex;"><span>Logical sector size: 512
</span></span><span style="display:flex;"><span>Disk identifier (GUID): XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
</span></span><span style="display:flex;"><span>Partition table holds up to 128 entries
</span></span><span style="display:flex;"><span>First usable sector is 2048, last usable sector is 500118158
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span>Number  Start (sector)    End (sector)  Size Name
</span></span><span style="display:flex;"><span>     1            2048         1230847  600M
</span></span><span style="display:flex;"><span>     2         1230848        34785279 16.0G
</span></span><span style="display:flex;"><span>     3        34785280       500117503  221G
</span></span></code></pre></div><p>I put the swap first, because that saved me doing the math (a single
subtraction) required to put it at the end. Removing the swap partition is easy,
but growing the main system partition to the left is a bit more work.</p>
<h1 id="deleting-the-swap-partition">Deleting the swap partition<div class="permalink">[<a href="#deleting-the-swap-partition">permalink</a>]</div></h1>
<p>I delete the swap partition with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>sfdisk --delete /dev/nvme0n1 <span style="color:#ae81ff">2</span>
</span></span></code></pre></div><p>It is effectively gone:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-console" data-lang="console"><span style="display:flex;"><span># fdisk -l /dev/nvme0n1
</span></span><span style="display:flex;"><span>Found valid GPT with protective MBR; using GPT
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span>Disk /dev/nvme0n1: 500118192 sectors, 2534M
</span></span><span style="display:flex;"><span>Logical sector size: 512
</span></span><span style="display:flex;"><span>Disk identifier (GUID): XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
</span></span><span style="display:flex;"><span>Partition table holds up to 128 entries
</span></span><span style="display:flex;"><span>First usable sector is 2048, last usable sector is 500118158
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span>Number  Start (sector)    End (sector)  Size Name
</span></span><span style="display:flex;"><span>     1            2048         1230847  600M
</span></span><span style="display:flex;"><span>     3        34785280       500117503  221G
</span></span></code></pre></div><p>It low-key bothers me that now partitions are <code>1</code> and <code>3</code> with no <code>2</code>. Hopefully
I can fix this later.</p>
<h1 id="backup">Backup<div class="permalink">[<a href="#backup">permalink</a>]</div></h1>
<p>This whole operation can potentially destroy all data on my disk. Skipping a
backup would tempt the fates to actually make this whole operation fail.</p>
<p>I make a backup of the whole disk by just dumping an image of it into an
external drive:</p>
<ol>
<li>Reboot.</li>
<li>Choose to boot from an Alpine bootable USB drive.</li>
<li>Log in as root.</li>
<li>Plug in USB drive.</li>
<li><code>modprobe ext4 &amp;&amp; mount /dev/sdb1 /mnt</code></li>
<li><code>dd if=/dev/nvme0n1 of=/mnt/dump_2024-05-27.img bs=512</code></li>
</ol>
<h1 id="moving-the-partition-to-the-left">Moving the partition to the left<div class="permalink">[<a href="#moving-the-partition-to-the-left">permalink</a>]</div></h1>
<p>This isn&rsquo;t as trivial because the partition that I&rsquo;m moving is my main partition
and it needs to be unmounted before an operation like this. I&rsquo;ll use a <a href="https://gparted.org/">gparted</a>
bootable USB drive for this.</p>
<p>After downloading gparted, I flash it onto a USB drive using:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>dd <span style="color:#66d9ef">if</span><span style="color:#f92672">=</span>gparted-live-1.6.0-3-amd64.iso of<span style="color:#f92672">=</span>/dev/sda bs<span style="color:#f92672">=</span><span style="color:#ae81ff">512</span>
</span></span></code></pre></div><p>I reboot the laptop again and boot into this USB drive. The touchpad seems to
work for moving around the cursor, but not for clicking. The easiest solution is
to just grab a wired USB mouse, which works right away.</p>
<p>I right click on the main partition, and choose &ldquo;Resize/Move&rdquo;. I type <code>999999</code>
on the &ldquo;New size&rdquo; field and it automatically sets it to occupy all available
space.</p>
<p>I expected this operation to take a long time since every single byte in this
partition needs to be moved leftwards. It took 44 minutes.</p>
<p>Once gparted has done its job, I proceed to grow the filesystem inside the
partition. First, I unlock the encrypted partition with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>cryptsetup luksOpen /dev/nvme0n1p3 root
</span></span></code></pre></div><p>The <code>btrfs</code> tool operates on a mounted filesystem, so I mount my filesystem and
then resize 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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>mount -t btrfs /dev/mapper/root /mnt
</span></span><span style="display:flex;"><span>btrfs filesystem resize max /mnt/
</span></span></code></pre></div><p>It is instantaneous. Nice.</p>
<p>I can not unmount the filesystem:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>umount /mnt
</span></span></code></pre></div><h1 id="fixing-the-partition-number">Fixing the partition number<div class="permalink">[<a href="#fixing-the-partition-number">permalink</a>]</div></h1>
<p>My disk now has partitions <code>1</code> and <code>3</code>. I want to rename the latter to <code>2</code>.</p>
<p>This is one by deleting the partition from the partition table and creating a
new one with the exact same size. This operation doesn&rsquo;t touch the actual
partition itself at all, only its definition in the partition table.</p>
<div class="notice update">
  <header>Update </header>
  <section>
  <a href="https://jacksonchen666.com/">Jackson</a> points out that <code>sfdisk</code> has a <code>--reorder</code> flag which
would have made this operation a lot less error prone. This whole section could
have been <code>sfdisk --reorder /dev/nvme0n1</code>.
  </section>
</div>
<p>First, I print the current details for the partition, since I&rsquo;ll need to
replicate those:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-console" data-lang="console"><span style="display:flex;"><span># fdisk -l
</span></span><span style="display:flex;"><span>Found valid GPT with protective MBR; using GPT
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span>Disk /dev/nvme0n1: 500118192 sectors, 2534M
</span></span><span style="display:flex;"><span>Logical sector size: 512
</span></span><span style="display:flex;"><span>Disk identifier (GUID): XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
</span></span><span style="display:flex;"><span>Partition table holds up to 128 entries
</span></span><span style="display:flex;"><span>First usable sector is 2048, last usable sector is 500118158
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span>Number  Start (sector)    End (sector)  Size Name
</span></span><span style="display:flex;"><span>     1            2048         1230847  600M
</span></span><span style="display:flex;"><span>     3         1230848       500117503  237G
</span></span></code></pre></div><p>I then delete and recreate the partition as number 2:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>sfdisk --delete /dev/nvme0n1 <span style="color:#ae81ff">3</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#39;1230848&#39;</span> | sfdisk --append /dev/nvme0n1
</span></span></code></pre></div><p>I only specify the start sector for the new partition, and the default uses all
available space (which matches the previously existing one). Because only
partition <code>1</code> exist, the new partition is assigned number <code>2</code>. The final result
look as expected:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-console" data-lang="console"><span style="display:flex;"><span># fdisk -l
</span></span><span style="display:flex;"><span>Found valid GPT with protective MBR; using GPT
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span>Disk /dev/nvme0n1: 500118192 sectors, 2534M
</span></span><span style="display:flex;"><span>Logical sector size: 512
</span></span><span style="display:flex;"><span>Disk identifier (GUID): XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
</span></span><span style="display:flex;"><span>Partition table holds up to 128 entries
</span></span><span style="display:flex;"><span>First usable sector is 2048, last usable sector is 500118158
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span>Number  Start (sector)    End (sector)  Size Name
</span></span><span style="display:flex;"><span>     1            2048         1230847  600M
</span></span><span style="display:flex;"><span>     2         1230848       500117503  237G
</span></span></code></pre></div><p>I reboot the system, provide my passphrase and&hellip; it works! I have effectively
reclaimed those 16GiB.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>I also reclaimed 24GiB by running <code>apk cache clean</code>, which deleted from my
local cache packages that no longer exist upstream.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>Darkman portal configuration</title><link>https://whynothugo.nl/journal/2024/04/09/darkman-portal-configuration/</link><pubDate>Tue, 09 Apr 2024 10:33:42 +0200</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/04/09/darkman-portal-configuration/</guid><description><![CDATA[<p>Since early 2022, <a href="https://darkman.whynothugo.nl/">darkman</a> supports exposing the dark mode / light mode
preference via the appropriate <code>xdg-desktop-portal</code> API. While this works quite
well, it&rsquo;s been a constant source of questions, since setting it up and actually
understanding how it works is far from trivial.</p>
<p>The <code>xdg-desktop-portal</code> is a kitchen-sink service<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>, which
implements dozens of unrelated functionalities mashed together. Some of these
features are implemented directly in the portal, while others are delegated to
<em>portal implementations</em>.</p>
<p>One of the features implemented by the <code>xdg-desktop-portal</code> is the <a href="https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Settings.html">settings
interface</a>. This interface exposes settings to local user applications. Amongst
a few other values, it exposes a <code>color-scheme</code> preference, which can be either
dark mode, light mode, or &ldquo;no preference&rdquo;.</p>
<p>However, the <code>xdg-desktop-portal</code> itself isn&rsquo;t the source of this information;
it queries <em>portal implementations</em> for the correct value. Which portal
implementation it uses requires careful configuration, and this is the main
source of confusion.</p>
<p>There are several ways to configure which portal implementation is used.</p>
<p>Configuration files can exist in system-wide locations, or user-specific
locations. I&rsquo;ll only cover user-specific locations here; if you need to
configure something system-wide for multiple users, consult <code>man 5 portals.conf</code>
for additional details.</p>
<p>Depending on the filename given to the configuration file, it will apply only to
specific desktop environments (e.g.: <code>kde-portals.conf</code> would only apply on KDE)
or apply to any environment (e.g.: <code>portals.conf</code> will be used by default on any
desktop environment unless a desktop-specific configuration is found)</p>
<p>I prefer using <code>portals.conf</code>, since my user account always runs sway and I
don&rsquo;t need per-DE functionality for it. If you use your same user account with
different compositors or DEs, you&rsquo;ll need to take the other approach.</p>
<h2 id="a-single-configuration-file">A single configuration file<div class="permalink">[<a href="#a-single-configuration-file">permalink</a>]</div></h2>
<p>The most straightforward location for a user configuration is
<code>~/.config/xdg-desktop-portal/portals.conf</code>, which overrides other directories.</p>
<p>To use <code>darkman</code> as a source for settings (which results in its dark / light
mode value being exposed), the following configuration would suffice:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-dosini" data-lang="dosini"><span style="display:flex;"><span><span style="color:#66d9ef">[preferred]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">org.freedesktop.impl.portal.Settings</span><span style="color:#f92672">=</span><span style="color:#e6db74">darkman</span>
</span></span></code></pre></div><p>In my case, I also configure the <code>xdg-desktop-portal</code> to use <code>xdp-wlr</code> for
screen casting (this in turn is used by Firefox). My real configuration file
currently looks 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;-webkit-text-size-adjust:none;"><code class="language-dosini" data-lang="dosini"><span style="display:flex;"><span><span style="color:#66d9ef">[preferred]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">org.freedesktop.impl.portal.ScreenCast</span><span style="color:#f92672">=</span><span style="color:#e6db74">wlr</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">org.freedesktop.impl.portal.Settings</span><span style="color:#f92672">=</span><span style="color:#e6db74">darkman</span>
</span></span></code></pre></div><p>If you are using a distribution with a pre-configured desktop environment, when
you create a configuration file it will completely override the system-wide
configuration. You may need to copy-paste any lines from the system-wide
configuration file into your own to retain existing integrations.</p>
<h2 id="per-de-configuration-files">Per-DE configuration files<div class="permalink">[<a href="#per-de-configuration-files">permalink</a>]</div></h2>
<p>I mentioned before that it is possible to create per-DE configuration files.</p>
<p>For example, if you need to support both sway and KDE, you&rsquo;ll need two
configuration files<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>:</p>
<ul>
<li><code>~/.config/xdg-desktop-portal/sway-portals.conf</code></li>
<li><code>~/.config/xdg-desktop-portal/kde-portals.conf</code></li>
</ul>
<p>Creating the files alone isn&rsquo;t enough: the <code>xdg-desktop-portal</code> needs to know
which desktop you are currently running. This is configured via the
<code>XDG_CURRENT_DESKTOP</code> environment variable, which must be set for the
<code>xdg-desktop-portal</code> process itself. If you are using a service manager, you
need to inject this variable into the environment that it creates for services.
If you are starting the service manually, you can just export it with the usual
mechanisms.</p>
<p>For the above two example, this variable should be defined as
<code>XDG_CURRENT_DESKTOP=sway</code> or <code>XDG_CURRENT_DESKTOP=kde</code> respectively.</p>
<h2 id="darkman-and-xdg_current_desktop">Darkman and XDG_CURRENT_DESKTOP<div class="permalink">[<a href="#darkman-and-xdg_current_desktop">permalink</a>]</div></h2>
<p>Darkman doesn&rsquo;t care about the value of <code>XDG_CURRENT_DESKTOP</code>. It will expose
its portal implementation bus in <code>org.freedesktop.impl.portal.desktop.darkman</code>.
When the <code>xdg-desktop-portal</code> is configured with
<code>org.freedesktop.impl.portal.Settings=darkman</code>, it will query darkman&rsquo;s bus for
settings.</p>
<p>Darkman will only respond to queries for the <code>color-scheme</code> preference.</p>
<h2 id="start-up-order">Start-up order<div class="permalink">[<a href="#start-up-order">permalink</a>]</div></h2>
<p>The <code>XDG_CURRENT_DESKTOP</code> variable needs to be defined in the execution
environment of <code>xdg-desktop-portal</code>. If it starts during early session
initialisation, ensure that the ordering is as expected.</p>
<p>The <code>xdg-desktop-portal</code> will try to communicate with configured <em>portal
implementations</em> (e.g.: darkman) as soon as it starts.</p>
<p>When <code>dbus-daemon</code> is configured to auto-start services (this is the default),
then it will start darkman immediately.</p>
<p>If you are using a service manager to start these service, you should consider
darkman a dependency of <code>xdg-desktop-portal</code>. In this case, darkman must start
<em>and be ready</em> before the <code>xdg-desktop-portal</code> starts.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>I find the general design of the xdg-desktop-portal quite poor. It&rsquo;s the far
opposite of &ldquo;do one thing and do it well&rdquo;; it does dozens of things in
highly opinionated ways, and is quite coupled with several other systems.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Keep in mind that other locations are searched if these do not exist. Again,
see <code>man 5 portals.conf</code> for full details.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>Vdirsyncer status update, March 2024</title><link>https://whynothugo.nl/journal/2024/03/29/vdirsyncer-status-update-march-2024/</link><pubDate>Fri, 29 Mar 2024 15:11:00 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/03/29/vdirsyncer-status-update-march-2024/</guid><description><![CDATA[<h2 id="sync-status">Sync status<div class="permalink">[<a href="#sync-status">permalink</a>]</div></h2>
<p>As I mentioned before, when synchronising two storages, the sync algorithm keeps
around a local &ldquo;status&rdquo; with some basic metadata. Future executions use this
metadata to understand which side has changed and which side needs updating.</p>
<p>I had to re-write most of my previous status implementation due to the issues
that I mentioned in my <a href="/journal/2024/01/27/vdirsyncer-rewriting-status-management/">previous status update</a>. The new implementation keeps
track of the metadata for the latest <em>synchronised</em> version, instead of the
latest <em>seen</em> version. I&rsquo;ve also made sure that all operations are atomic and
race-free. Atomicity ensures that if the process is ever interrupted, the status
never ends up in a partially updated state. Race-free queries ensure that if
another process applied some other operation concurrently, the current one will
bail rather than overwrite conflicting information.</p>
<h2 id="sync-errors">Sync errors<div class="permalink">[<a href="#sync-errors">permalink</a>]</div></h2>
<p>I also split synchronisation errors into two groups: non-fatal and fatal.</p>
<p>Non-fatal errors imply that the whole operation can continue, leaving just the
failing item out of sync. Some examples of non-fatal errors are transient
network error, or a server rejecting a calendar element for some server-specific
reason.</p>
<p>Fatal errors imply that the entire operation must abort. Fatal errors occur
mainly when writing to the status file fails for some unexpected reason.</p>
<h2 id="error-reporting-during-sync">Error reporting during sync<div class="permalink">[<a href="#error-reporting-during-sync">permalink</a>]</div></h2>
<p>I need to provide a mechanism to report non-fatal errors to the end user. My
current scope includes a command line tool, but an important goal of the sync
library is that it has to provide enough functionality to build a proper GUI as
well.</p>
<p>Initially, I tried accumulating non-fatal errors and later returning them
together. This became rather entangled as fatal errors can interrupt the flow in
various code paths, and the non-fatal errors need taking into consideration in
each place. It also means that output would not be available until after the
entire process completes.</p>
<p>I settled on a <code>on_error(SyncError)</code> parameter for the <code>Plan::execute</code> method.
This parameter is a function that gets called each time an error happens. The
error is not a plain string, but actually a proper error type with all the
details necessary to explain to the user what went wrong.</p>
<p>Vdirsyncer itself passes an <code>on_error</code> function that merely logs the errors.</p>
<h2 id="collection-auto-creation">Collection auto-creation<div class="permalink">[<a href="#collection-auto-creation">permalink</a>]</div></h2>
<p>I sometimes create a new todo list on another device (or on my phone), and I
want these to replicate automatically without reconfiguring vdirsyncer.</p>
<p>When synchronising two storages, my new implementation will replicate new
collections (e.g.: calendars, address books) to the other side too. It will also
delete from one side a collection that was deleted on the other.</p>
<p>I later intend to make this feature optional; I have no doubt that many users
would prefer no auto-creating or auto-deletion. Disabling this feature would
result in the same behaviour as the previous vdirsyncer.</p>
<p>I also intend to add a feature to prevent emptying a collection if it was
emptied on the other side. Again, this is a safety measure, and also exists in
the previous vdirsyncer.</p>
<h2 id="conflict-resolution">Conflict resolution<div class="permalink">[<a href="#conflict-resolution">permalink</a>]</div></h2>
<p>I thought long and hard about conflict resolution, and tried a couple of
prototypes. Most dedicated APIs end up having too much complexity and various
limitations.</p>
<p>After enough thought, I settled on a simpler approach.</p>
<p>The module that handles synchronisation, <code>vstorage::sync</code> creates a plan. This
plan has all the individual actions that need to happen to synchronise the
storages. It also includes details about the items that are in conflict
(although no explicit action is actually taken for these).</p>
<p>The details for in-conflict items are enough to <em>manually</em> resolve the conflict.
E.g.: they include the path to the item on both sides as well as their UID and
Etag.</p>
<p>Instead of using a dedicated API for &ldquo;conflict resolution&rdquo; provided by this
library, consumers can read details on conflicting items from the plan, and
resolve the conflict externally by updating one or both storages with the
desired final version of the item. Most of the work involved here was ensuring
that enough information is made available for consumers to resolve conflicts in
a race-free way (e.g.: by exposing the current <code>Etag</code>s).</p>
<p>Vdirsyncer is built on top of this library, so it will follow these steps when
resolving conflicts:</p>
<ul>
<li>Prepare a plan by using <code>vstorage::sync</code>.</li>
<li>Inspect all items that are in conflict.</li>
<li>If the conflict resolution strategy is <code>keep a</code> or <code>keep b</code>, vdirsyncer
replace the &ldquo;conflict&rdquo; actions with <code>CopyToB</code> or <code>CopyToA</code> respectively.</li>
<li>If the conflict resolution is a custom command, vdirsyncer will download both
sides of the conflicting item and call the custom command. If the command
exits with a success exit code, vdirsyncer will update both storages with the
returned content (by using the <code>Storage::update_item</code> method.
<ul>
<li>In the latter case, during the next synchronisation the vdirsyncer will
identify that the item is in sync again, resolving the conflict.</li>
</ul>
</li>
</ul>
<p>Providing enough information to resolve conflicts manually puts little burden in
implementing conflict resolution while maximising flexibility.</p>
<p>This also means that conflict resolution can continue in the foreground
(potentially, with user interaction) while continuing synchronisation in the
background.</p>
<p>In the coming weeks, I will continue working on the configuration aspect of
conflict resolution (e.g.: reading the configured command and executing it).
Unless any new issues pop up, I intend to publishing an alpha release after
this.</p>
<h2 id="current-state">Current state<div class="permalink">[<a href="#current-state">permalink</a>]</div></h2>
<p>I have used this version of vdirsyncer myself for these last several months, and
feel quite pleased with two large improvements over the previous version:</p>
<ul>
<li>If any single item fails for any reason, all others continue to synchronise.</li>
<li>I can run it in the background and it keeps things in sync without need for
restarts or prompts for credentials.</li>
</ul>
<p>On the downside, the lack of conflict resolution soon became an issue.</p>
]]></description></item><item><title>Notes on s6</title><link>https://whynothugo.nl/journal/2024/03/25/notes-on-s6/</link><pubDate>Mon, 25 Mar 2024 10:26:41 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/03/25/notes-on-s6/</guid><description><![CDATA[<p><strong>s6</strong> is a collection of programs that can be used for service supervision.
<strong>s6-rc</strong> is a service manager built on top of s6. Both are composed of multiple
simple tools that can be used independently or with the rest of the suite.</p>
<p>s6-rc can be used to manage system service, user-session service, or an ad-hoc
collection of services. s6 is equally flexible and can be used for any sort of
service.</p>
<p>The <a href="https://www.skarnet.org/software/s6/index.html">official documentation</a> does a good job of explaining every component
in great details. This is a high level overview.</p>
<h2 id="s6">s6<div class="permalink">[<a href="#s6">permalink</a>]</div></h2>
<p><code>s6</code> is a small suite of programs to supervise services (or really any other
type of process). Understanding this suite first helps understand <code>s6-rc</code> later
on.</p>
<h3 id="s6-log">s6-log<div class="permalink">[<a href="#s6-log">permalink</a>]</div></h3>
<p><a href="https://www.skarnet.org/software/s6/s6-log.html"><code>s6-log</code></a> reads input from stdin and saves logs, handling storage and rotation.</p>
<p>Log rotation needs to be handled by the same process that writes logs; any
separation between processes eventually leads to race conditions, requiring
inter-process locks or other similar issues.</p>
<p>Generally, another service is executed piping its output to <code>s6-log</code>. Service
processes themselves don&rsquo;t ever need access to the log files.</p>
<p>Like most tools in this list, it can be used completely independently of the s6
suite. This implies, amongst other things, that it is easy to test <code>s6-log</code> by
itself to get a better feeling of how it works.</p>
<p>The easiest usage of <code>s6-log</code> is to pipe output from a process to 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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>offlineimap 2&gt;&amp;<span style="color:#ae81ff">1</span> | s6-log T /path/to/logs/offlineimap/
</span></span></code></pre></div><p><code>s6-log</code> takes an output <em>directory</em>, given that it will handles log rotation
inside that of it. It also keeps a lock to avoid concurrent instances writing to
the same directory.</p>
<p><code>s6-log</code> focuses on ensuring that all logs are saved and no messages are lost.
This causes frequent writes to disk and may not be ideal when the primary
storage is an SD card or some other device that wears out quickly with frequent
writes. Consider using <a href="https://sr.ht/~martijnbraam/logbookd/"><code>logbookd</code></a> for scenario which require reduced writes
and losing logs in an acceptable compromise.</p>
<h3 id="s6-supervise">s6-supervise<div class="permalink">[<a href="#s6-supervise">permalink</a>]</div></h3>
<p><a href="https://www.skarnet.org/software/s6/s6-supervise.html"><code>s6-supervise</code></a> is the <strong>service supervisor</strong> program. It runs as direct parent
of a service process. It handles restarting the service when it dies,
terminating it (when explicitly requested), and notifies registered processed
when it is up or down.</p>
<p>The definition of a single service is represented by a directory called a
<em><a href="https://www.skarnet.org/software/s6/servicedir.html">service directory</a></em>. For advanced configurations, it is possible to
programmatically generate these directories. If you&rsquo;re curious what these look
like, I have <a href="https://git.sr.ht/~whynothugo/dotfiles/tree/6d6e12da68d9ecd44fef26d8f3a0c5b5c4544569/item/home/.config/s6">a collection of service directories</a> that I use for my
desktop sessions.</p>
<p>A running <code>s6-supervise</code> instance can be manually controlled via <a href="https://www.skarnet.org/software/s6/s6-svc.html"><code>s6-svc</code></a>.</p>
<p>Much like <code>s6-log</code>, <code>s6-supervise</code> (together with <code>s6-svc</code>) can be used
independently of the rest of the s6 suite. It is perfectly feasible to write or
adapt some other service manager and use <code>s6-supervise</code> for service supervision
without using any other pieces of the <code>s6</code>.</p>
<h3 id="s6-svscan">s6-svscan<div class="permalink">[<a href="#s6-svscan">permalink</a>]</div></h3>
<p><a href="https://www.skarnet.org/software/s6/s6-svscan.html"><code>s6-svscan</code></a> reads a <em><a href="https://www.skarnet.org/software/s6/scandir.html">scan directory</a></em>; a directory with multiple <em>service
directories</em>. It then runs an instance of <code>s6-supervise</code> for each one.
<code>s6-svscan</code> is often the root of the service process tree.</p>
<p><code>s6-svscan</code> can be manually controlled via <a href="https://www.skarnet.org/software/s6/s6-svscanctl.html"><code>s6-svscanctl</code></a>.</p>
<h2 id="s6-rc">s6-rc<div class="permalink">[<a href="#s6-rc">permalink</a>]</div></h2>
<p><a href="https://www.skarnet.org/software/s6-rc/"><strong>s6-rc</strong></a> is a <strong>service manager</strong>. It is a collection of programas
that builds on top of <strong>s6</strong> and implements dependency management, bundles
(these are similar to OpenRC runlevels) and one-shot services.</p>
<p>While <code>s6-rc</code> sounds like the obvious choice to use on top of <code>s6</code>, it is
perfectly feasible to use a separate service manager which uses <code>s6</code> for service
supervision under the hood. An example of this is <a href="https://jjacky.com/anopa/">anopa</a>.</p>
<p><code>s6-rc</code> doesn&rsquo;t actually run the services itself; it delegates this to
<code>s6-svscan</code> under the hood. <code>s6-rc</code> merely mutates the <em>scan directory</em>, and
makes <code>s6-svccan</code> reload, start, or stop services as needed. It also uses its
own &ldquo;live&rdquo; directory for its runtime data and state.</p>
<p><a href="https://www.skarnet.org/software/s6-rc/s6-rc-init.html"><code>s6-rc-init</code></a> initialises the <em>scan directory</em> with all services disabled by
default. Further state changes (including starting up an initial set of
services) can be triggered via the <code>s6-rc</code> command.</p>
<p><code>s6-rc</code> keeps its definitions in a compiled database, which must first be
generated using <code>s6-rc-compile</code>. The compiled database is then re-usable, so it
is possible to keep versioned copies, as well as re-use the last-compiled
database after system reboots.</p>
<p>While compiling a database initially sounded like a tedious step at first, the
compilation step helps ensure that the directory used by <code>s6-rc</code> contains valid
data before it is used. For example, when I accidentally added a non-existent
service as a dependency for another, compilation failed. This ensures that the
source directory can&rsquo;t have completely bogus data (which could yield terrible
results after a reboot). It also prevents re-doing all the parsing work on each
start-up: it is done once by an individual tool and the pre-digested data is
loaded in subsequent runs.</p>
<p>I find this last trait an unusual approach, but it is a solid design choice.</p>
<p><strong>Caveat</strong>: Note that the <code>$SERVICENAME/log</code> pattern supported by <code>s6-svscan</code> is
not supported by <code>s6-rc</code>. Loggers must be declared via <code>provider_for</code> and
<code>consumer_for</code>.</p>
<h2 id="process-tree">Process tree<div class="permalink">[<a href="#process-tree">permalink</a>]</div></h2>
<p><code>s6-svscan</code> is the parent of all <code>s6-supervise</code> processes, and <code>s6-supervise</code>
process is the parent of a different service. This results in a process tree
that is intuitively quite intuitive to understand (<code>ps auxf</code> will even render
this in a way that can be visually comprehended).</p>
<h2 id="readiness-notification">Readiness notification<div class="permalink">[<a href="#readiness-notification">permalink</a>]</div></h2>
<p>Services supervised by <code>s6-supervise</code> can implement <a href="https://skarnet.org/software/s6/notifywhenup.html">service startup
notifications</a>, often referred to as <em>readiness notification</em>.
This allows services to notify their supervisor when they are <em>ready</em>. While the
exact definition of <em>ready</em> varies for each service, this usually indicates that
the service is ready to accept client connections. For example, a service that
sets up a unix domain socket on which it listens to connections will only send a
readiness notification after the socket is ready to receive client connections.</p>
<p>Readiness notification allows ordering dependant service and having each one
start only when their dependency is ready. For example, <code>darkman</code> will connect
to <code>dbus</code> as soon as it starts, so <code>darkman</code> needs to be started after <code>dbus</code> is
<em>ready</em>. Starting <code>darkman</code> after dbus has merely <em>started</em> will inevitably lead
to race conditions where <code>darkman</code> tries to connect to <code>dbus</code> before the latter
has set up its socket.</p>
<h2 id="usage-via-wrappers">Usage via wrappers<div class="permalink">[<a href="#usage-via-wrappers">permalink</a>]</div></h2>
<p>Personally, I find that using the s6 utilities directly tends be too low level.
Most of my usage so far has been using own wrapper scripts. I expect that other
high-level tools will show up over time, and the components mentioned above will
likely not be used directly by most users.</p>
<h2 id="see-also">See also<div class="permalink">[<a href="#see-also">permalink</a>]</div></h2>
<ul>
<li>As mentioned above: the official documentation is well detailed, albeit a bit
terse and it takes a while to figure out how to navigate it properly.</li>
<li>The same documentation is available (from a third party) as <a href="https://sr.ht/~flexibeast/s6-man-pages/">man pages</a>. These
are also available in the alpine repositories as <code>s6-man-pages</code> and
<code>s6-rc-man-pages</code>. I do wish that upstream would focus on man pages first, and
then use those as a base for the HTML pages.</li>
<li>The gentoo wiki has <a href="https://wiki.gentoo.org/wiki/S6">an s6 article</a> with further information.</li>
</ul>
<p>There are numerous additional utilities in the s6 and s6-rc suites. It&rsquo;s worth
getting to know the basics of most of them.</p>
]]></description></item><item><title>My 'lock-and-sleep' script</title><link>https://whynothugo.nl/journal/2024/02/26/my-lock-and-sleep-script/</link><pubDate>Mon, 26 Feb 2024 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/02/26/my-lock-and-sleep-script/</guid><description><![CDATA[<p>My <code>lock-and-sleep</code> locks the system and puts in back to sleep after 10 seconds
of idle as long as it remains locked.</p>
<p>Locking the system before going to sleep ensures that the system never shows an
unlocked desktop when waking up from sleep. If the screen locker fails to start
for some reason, this fact becomes clear sooner rather than later.</p>
<p>Going back to sleep automatically prevents the system from remaining active if
woken up by accident. For example, if someone (some person, or some cat) presses
any key during the night the system wakes up. I don&rsquo;t want it to remain awake
indefinitely in such situations.</p>
<p>When the system wakes up, it won&rsquo;t always reset the idle timer. If I press the
space-bar when the system is asleep, this key-press event is &ldquo;swallowed&rdquo; by the
firmware (which wakes up the systems) and never delivered to the operating
system or the compositor. The idle timer continues running since it originally
started, and the compositor never send an event indicating that the system has
resumed from idle.</p>
<h2 id="the-script">The script<div class="permalink">[<a href="#the-script">permalink</a>]</div></h2>
<p>This script also exists <a href="https://git.sr.ht/~whynothugo/dotfiles/tree/main/item/home/.local/bin/lock-and-sleep">in my dotfiles repository</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;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env python3</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Requires swaylock &gt; 1.7.2.</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> os
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> sys
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> subprocess
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>(r, w) <span style="color:#f92672">=</span> os<span style="color:#f92672">.</span>pipe()
</span></span><span style="display:flex;"><span>os<span style="color:#f92672">.</span>set_inheritable(r, <span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>swaylock <span style="color:#f92672">=</span> subprocess<span style="color:#f92672">.</span>Popen([<span style="color:#e6db74">&#34;swaylock&#34;</span>, <span style="color:#e6db74">&#34;--ready-fd&#34;</span>, str(w)], pass_fds<span style="color:#f92672">=</span>(w,))
</span></span><span style="display:flex;"><span>os<span style="color:#f92672">.</span>close(w)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Wait for swaylock to be ready.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">with</span> os<span style="color:#f92672">.</span>fdopen(r, <span style="color:#e6db74">&#34;rb&#34;</span>) <span style="color:#66d9ef">as</span> ready_pipe:
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">while</span> <span style="color:#66d9ef">True</span>:
</span></span><span style="display:flex;"><span>        pipe_data <span style="color:#f92672">=</span> ready_pipe<span style="color:#f92672">.</span>read(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> pipe_data <span style="color:#f92672">==</span> <span style="color:#e6db74">b</span><span style="color:#e6db74">&#34;&#34;</span>:
</span></span><span style="display:flex;"><span>            print(<span style="color:#e6db74">&#34;fatal: swaylock closed fd before locking&#34;</span>)
</span></span><span style="display:flex;"><span>            sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> pipe_data <span style="color:#f92672">==</span> <span style="color:#e6db74">b</span><span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">break</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">while</span> <span style="color:#66d9ef">True</span>:
</span></span><span style="display:flex;"><span>    subprocess<span style="color:#f92672">.</span>call([<span style="color:#e6db74">&#34;powerctl&#34;</span>, <span style="color:#e6db74">&#34;mem&#34;</span>])  <span style="color:#75715e"># blocks until wakeup from sleep.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    wayidle <span style="color:#f92672">=</span> subprocess<span style="color:#f92672">.</span>Popen([<span style="color:#e6db74">&#34;wayidle&#34;</span>, <span style="color:#e6db74">&#34;--timeout&#34;</span>, <span style="color:#e6db74">&#34;10&#34;</span>])
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    (pid, returncode) <span style="color:#f92672">=</span> os<span style="color:#f92672">.</span>wait()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> pid <span style="color:#f92672">==</span> swaylock<span style="color:#f92672">.</span>pid:
</span></span><span style="display:flex;"><span>        wayidle<span style="color:#f92672">.</span>kill()
</span></span><span style="display:flex;"><span>        sys<span style="color:#f92672">.</span>exit(returncode)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> pid <span style="color:#f92672">==</span> wayidle<span style="color:#f92672">.</span>pid <span style="color:#f92672">and</span> returncode <span style="color:#f92672">!=</span> <span style="color:#ae81ff">0</span>:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;fatal: wayidle exited non-zero&#34;</span>)
</span></span><span style="display:flex;"><span>        sys<span style="color:#f92672">.</span>exit(returncode)  <span style="color:#75715e"># swaylock will continue running</span>
</span></span></code></pre></div><p>Let&rsquo;s go over it bit by bit.</p>
<h2 id="swaylock--172"><code>swaylock</code> &gt; 1.7.2<div class="permalink">[<a href="#swaylock--172">permalink</a>]</div></h2>
<p>This script requires <code>swaylock &gt; 1.7.2</code>. It relies on <a href="https://github.com/swaywm/swaylock/pull/281">swaylock&rsquo;s readiness
notification</a>, which indicates once <code>swaylock</code> has finished locking
the screen.</p>
<p>With previous releases, there exists no trivial way to do this <em>and</em> <a href="https://docs.python.org/3/library/os.html#os.wait"><code>wait</code></a>
for <code>swaylock</code> to exit (more on this below).</p>
<h2 id="running-swaylock">Running <code>swaylock</code><div class="permalink">[<a href="#running-swaylock">permalink</a>]</div></h2>
<p>First comes the initial block which runs <code>swaylock</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;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>(r, w) <span style="color:#f92672">=</span> os<span style="color:#f92672">.</span>pipe()
</span></span><span style="display:flex;"><span>os<span style="color:#f92672">.</span>set_inheritable(r, <span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>swaylock <span style="color:#f92672">=</span> subprocess<span style="color:#f92672">.</span>Popen([<span style="color:#e6db74">&#34;swaylock&#34;</span>, <span style="color:#e6db74">&#34;--ready-fd&#34;</span>, str(w)], pass_fds<span style="color:#f92672">=</span>(w,))
</span></span><span style="display:flex;"><span>os<span style="color:#f92672">.</span>close(w)
</span></span></code></pre></div><p><code>os.pipe()</code> creates a pipe, a pair of file descriptors, <code>w</code> and <code>r</code>. Any data
written to <code>w</code> gets read via <code>r</code>. After closing all the copies of <code>w</code>, <code>r</code> will
yield an <code>EOF</code> (end of file).</p>
<p>The child process (<code>swaylock</code>) inherits <em>a copy</em> of <code>w</code>, and the Python script
keeps the original copy. If the Python script keeps its the copy of the <code>w</code>
descriptor open, then the <code>r</code> one will never reach its end. The script
explicitly closes it with the <code>os.close(w)</code> call.</p>
<p>When <code>swaylock</code> writes to on one of these file descriptors (<code>w</code>), this Python
script will read from the other one (<code>r</code>).</p>
<p><code>swaylock</code> will then start up, and as soon as it locks the system, it will write
<code>\n</code> into <code>w</code> and then close it.</p>
<h2 id="waiting-for-the-system-to-lock">Waiting for the system to lock<div class="permalink">[<a href="#waiting-for-the-system-to-lock">permalink</a>]</div></h2>
<p>The next section now becomes relevant:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">with</span> os<span style="color:#f92672">.</span>fdopen(r, <span style="color:#e6db74">&#34;rb&#34;</span>) <span style="color:#66d9ef">as</span> ready_pipe:
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">while</span> <span style="color:#66d9ef">True</span>:
</span></span><span style="display:flex;"><span>        pipe_data <span style="color:#f92672">=</span> ready_pipe<span style="color:#f92672">.</span>read(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> pipe_data <span style="color:#f92672">==</span> <span style="color:#e6db74">b</span><span style="color:#e6db74">&#34;&#34;</span>:
</span></span><span style="display:flex;"><span>            print(<span style="color:#e6db74">&#34;fatal: swaylock closed fd before locking&#34;</span>)
</span></span><span style="display:flex;"><span>            sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> pipe_data <span style="color:#f92672">==</span> <span style="color:#e6db74">b</span><span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">break</span>
</span></span></code></pre></div><p>This reads from <code>r</code> in an infinite loop (<code>while True</code>). If <code>swaylock</code> writes a
<code>\n</code>, this indicates that locking the screen has finished. The loop breaks and
the program continues executing in the next section.</p>
<p>If <code>swaylock</code> does not print a <code>\n</code>, this indicates that it has exited before
locking the screen. If this happens, the script exits with an error. I avoid
putting the system to sleep in this case, since that would hide the fact that
something went wrong with the screen locker.</p>
<h2 id="the-main-loop">The main loop<div class="permalink">[<a href="#the-main-loop">permalink</a>]</div></h2>
<p>Lastly, the &ldquo;main loop&rdquo;. Again, an infinite loop:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">while</span> <span style="color:#66d9ef">True</span>:
</span></span><span style="display:flex;"><span>    subprocess<span style="color:#f92672">.</span>call([<span style="color:#e6db74">&#34;powerctl&#34;</span>, <span style="color:#e6db74">&#34;mem&#34;</span>])  <span style="color:#75715e"># blocks until wakeup from sleep.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    wayidle <span style="color:#f92672">=</span> subprocess<span style="color:#f92672">.</span>Popen([<span style="color:#e6db74">&#34;wayidle&#34;</span>, <span style="color:#e6db74">&#34;--timeout&#34;</span>, <span style="color:#e6db74">&#34;10&#34;</span>])
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    (pid, returncode) <span style="color:#f92672">=</span> os<span style="color:#f92672">.</span>wait()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> pid <span style="color:#f92672">==</span> swaylock<span style="color:#f92672">.</span>pid:
</span></span><span style="display:flex;"><span>        wayidle<span style="color:#f92672">.</span>kill()
</span></span><span style="display:flex;"><span>        sys<span style="color:#f92672">.</span>exit(returncode)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> pid <span style="color:#f92672">==</span> wayidle<span style="color:#f92672">.</span>pid <span style="color:#f92672">and</span> returncode <span style="color:#f92672">!=</span> <span style="color:#ae81ff">0</span>:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;fatal: wayidle exited non-zero&#34;</span>)
</span></span><span style="display:flex;"><span>        sys<span style="color:#f92672">.</span>exit(returncode)  <span style="color:#75715e"># swaylock will continue running</span>
</span></span></code></pre></div><p>This starts off by calling <code>powerctl mem</code>, which puts the system to sleep. This
command exits after the system has resumed from sleep. This guarantees that the
next line of this script executes after the system has woken up again.</p>
<p>At this point, <a href="https://git.sr.ht/~whynothugo/wayidle"><code>wayidle</code></a> runs. <code>wayidle</code> sets an idle timer (for 10 seconds in
this case) and exits once the timer expires. I tried using <code>swayidle</code> here, but
it didn&rsquo;t quite fit, because <code>swayidle</code> doesn&rsquo;t know when the system wakes up
again, so it won&rsquo;t run another timer and the system will remain awake.</p>
<p>The script will then wait for one of its children (<code>wayidle</code> or <code>swaylock</code>) to
exit.</p>
<p>If <code>swaylock</code> exits first, then it has unlocked the system (or crashed). In this
case <code>wayidle</code> gets explicitly <code>kill</code>ed, and the script exits (with the status
code of <code>swaylock</code>, to reflect any errors).</p>
<p>If <code>wayidle</code> exits first and without any errors, this means that the system
reached the idle timeout. The loop starts over, executing <code>powerctl mem</code> and
putting the system back to sleep.</p>
<p>If <code>wayidle</code> exits first but with an error, then the script exits with the same
error. This shouldn&rsquo;t happen, and implies an unexpected error with the idle
timeout. The systems stays awake; if it went to sleep again, then it could end
up in an irrecoverable sleep-loop.</p>
<h2 id="closing-notes">Closing notes<div class="permalink">[<a href="#closing-notes">permalink</a>]</div></h2>
<p>I configured <code>sway</code> to run this script whenever I tap the power button on my
computer:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>bindsym --release XF86PowerOff exec lock-and-sleep
</span></span></code></pre></div><p>I don&rsquo;t think I can make this script any simpler, and I welcome any feedback on
it.</p>
<p>Feel free to re-use any bits or ideas to your liking.</p>
]]></description></item><item><title>vdirsyncer: rewriting status management</title><link>https://whynothugo.nl/journal/2024/01/27/vdirsyncer-rewriting-status-management/</link><pubDate>Sat, 27 Jan 2024 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/01/27/vdirsyncer-rewriting-status-management/</guid><description><![CDATA[<h2 id="current-state">Current state<div class="permalink">[<a href="#current-state">permalink</a>]</div></h2>
<p>Vdirsyncer keeps a &ldquo;status&rdquo; file locally. The status file contains metadata
from items on both storages the last time they were synchronised. My current
implementation runs the synchronisation process, and then returns a new status,
which should be saved to disk until next time.</p>
<p>I decided on this API since it made the whole codebase easier to reuse in
different scenarios, including situation where the local system has no
persistent storage (and the status could be saved onto a storage server, or
wherever feasible).</p>
<p>This idea turned out to be problematic.</p>
<h2 id="handling-interruptions">Handling interruptions<div class="permalink">[<a href="#handling-interruptions">permalink</a>]</div></h2>
<p>If synchronisation suffers a fatal interruption (e.g.: power failure, <code>SIGKILL</code>,
etc), none of the updated status is saved, which <em>could</em> result in problems
during the next synchronisation. Specifically, if content changes on either side
before another synchronisation, it would result in conflict. Deleted content
might be mistakenly restored on the other side. While annoying, I don&rsquo;t think it
can ever result in data being deleted.</p>
<p>I initially  decided to leave this in its current state for the <code>alpha0</code>
release, and to address it before a stable beta release.</p>
<p>Regrettably, this wasn&rsquo;t the only issue with the status management.</p>
<h2 id="recovery-after-one-time-failures">Recovery after one-time failures<div class="permalink">[<a href="#recovery-after-one-time-failures">permalink</a>]</div></h2>
<p>Due to a transient network issue, a single synchronisation failed mid-way (it
had plenty of operations pending). Upon retrying, there were no operations to
run. Something was wrong.</p>
<p>The problem turned out to be that I was updating the status file with the new
metadata even if synchronisation failed. So the next time that synchronisation
runs, it looks like the file hasn&rsquo;t changed, and there is nothing to do. In
reality, it hasn&rsquo;t changed since the last <em>failed</em> synchronisation, but it has
changed since the last <em>successful</em> one.</p>
<p>Unlike the problem above, I feel that this one is problematic enough to delay
an <code>alpha0</code> release.</p>
<p>To address I am considering using the <em>previous</em> state as input when
synchronising, and only updating status data for items which are successfully
synchronised. However, during the first run, there is no previous state, and
for items that are identical on both sides, no action needs to be taken.
Currently, the status gets updated based on the actions that are executed, so
&ldquo;no action&rdquo; means that items never end up in the status database.</p>
<p>To work around this last issue, a new action is needed, a no-op in terms of
synchronisation that merely saves the item into the status file. This would
only really be relevant during the first-time synchronisation, or if some other
tool synchronised both storages.</p>
]]></description></item><item><title>Nine months of Xendmail</title><link>https://whynothugo.nl/journal/2024/01/16/nine-months-of-xendmail/</link><pubDate>Tue, 16 Jan 2024 11:27:11 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/01/16/nine-months-of-xendmail/</guid><description><![CDATA[<p>Last year I wrote <a href="https://git.sr.ht/~whynothugo/xendmail">xendmail</a>, a tool based on the proposal mentioned in my
<a href="https://whynothugo.nl/journal/2023/03/30/thoughts-on-sendmail-in-2023/">thoughts on sendmail in 2023</a> article. Xendmail exposes the same
interface as <a href="https://man.openbsd.org/sendmail.8"><code>sendmail</code></a><sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>, it takes an email as input,
reads credentials from the user&rsquo;s secret store, and dispatches the email via an
the appropriate SMTP server. Xendmail implements the 5% of <code>sendmail</code> that I
need, plus 5% extra which no other implementation offers.</p>
<p>The approach has somewhat worked, but has some issues. The experiment has been
a success: it has resulted in useful findings. Xendmail itself, however, is
likely not the way forward.</p>
<p>To summarise, my goals with Xendmail were:</p>
<ul>
<li>Provide a sendmail-compatible interface for any application to send messages.
<ul>
<li>This cancels the need to configure email credentials repeatedly in every
other application.</li>
</ul>
</li>
<li>SMTP credentials are not exposed or resting in plain-text.</li>
<li>No emails are accidentally dispatched without being aware of it.</li>
<li>Enable non-root users to configure the setup, so it is feasible on shared
hosts.</li>
</ul>
<p>These goal were well met, but additional requirements should have been in scope
too&hellip;</p>
<h2 id="problem-1-not-offline-mode-or-outbox">Problem 1: Not offline mode or outbox<div class="permalink">[<a href="#problem-1-not-offline-mode-or-outbox">permalink</a>]</div></h2>
<p>Usually I write an email message (or prepare a git patch) and then hand it over
to xendmail, which sends it out immediately. Whenever there is no internet
connection available, xendmail outright fails, and I have to figure out what to
do with the message myself. I usually save it into drafts and need to remember
to manually dispatch it later.</p>
<p>The solution for this issue would be to save messages into a local outbox, and
have it dispatched later when I am online again. This is pretty much what
old-school monolithic email clients did.</p>
<h2 id="problem-2-poor-integration-with-automated-emails">Problem 2: Poor integration with automated emails<div class="permalink">[<a href="#problem-2-poor-integration-with-automated-emails">permalink</a>]</div></h2>
<p>Not all emails are prepared manually: Tools like <code>rss-email</code><sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>, prepare
emails automatically and attempt to dispatch them.</p>
<p>Xendmail tries to use the secret store to read the SMTP credentials, which
results in the secret store prompting for user consent before disclosing
credentials. This results in random interruptions during the day.</p>
<p>I have considered an approach where xendmail has some form of privileged access
to the secret store and can read serets without my approval, but have decided
against the idea. Aside from a lot of complexity issues, any misconfigured or
misbehaving client can start sending out unwanted emails without indication.</p>
<p>It is clear that xendmail, in its current form, is a bad fit for this type of
automated tools.</p>
<h2 id="using-something-like-opensmtpd">Using something like <code>opensmtpd</code><div class="permalink">[<a href="#using-something-like-opensmtpd">permalink</a>]</div></h2>
<p>The idea of putting emails into an queue and dispatching them later when
network is available again is nothing new. Classic SMTP servers to exactly
this.</p>
<p>There are mainly two obstacles with locally running a classic SMTP server like
<code>opensmtpd</code> or alike:</p>
<ol>
<li>
<p>They need to be configured and executed by the root user. The whole point of
these experiments is to find a solution where each user can configure their own
email on a system with no root access (e.g.: on a shared machine).</p>
</li>
<li>
<p>Credential management is still an issue. There are two possible approaches:</p>
<ul>
<li>If the locally running SMTP server requires a password, then automated
tools cannot queue messages without unexpected interruptions.</li>
<li>If credentials are stored in plain-text form or don&rsquo;t require user
approval before access, then misbehaving processes can send out messages
without limitations (and without any awareness of it).</li>
</ul>
</li>
</ol>
<p>The first of these items (requiring root for configuration and operation) is
enough to make this approach non-viable for the given goals.</p>
<h2 id="future-endeavours">Future endeavours<div class="permalink">[<a href="#future-endeavours">permalink</a>]</div></h2>
<p>Based on these learnings, I&rsquo;m still thinking of the next approach to try. For
the moment, key observations are:</p>
<ul>
<li>Try sending an email, and save it into an outbox maildir if sending fails.</li>
<li>A status bar indicator with the amount of messages in queue.</li>
<li>A simple UI to review the outbox and send all messages.</li>
</ul>
<p>I will continue using Xendmail for the time being, but my intent is to
eventually deprecate it in favour of one or more tools that can fit this
description.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Plenty of applications and libraries support sending emails via
sendmail, so using its interface enables easy integration with all those
tools.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p><code>rss-email</code> reads remote RSS feeds and generates an email messages for
each new entry.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>Setting up an IRC bouncer (soju) on OpenBSD</title><link>https://whynothugo.nl/journal/2024/01/12/setting-up-an-irc-bouncer-soju-on-openbsd/</link><pubDate>Fri, 12 Jan 2024 16:13:26 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/01/12/setting-up-an-irc-bouncer-soju-on-openbsd/</guid><description><![CDATA[<p>Given the <a href="https://outage.sr.ht/">outage at sourcehut</a> right now<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>, I need an alternative
bouncer to use IRC without leaving a client running 24/7. Running my own seems
like a simple enough choice.</p>
<p>I opted to run <a href="https://git.sr.ht/~emersion/soju"><strong>soju</strong></a> (<a href="https://github.com/emersion/soju/">mirror</a>) on my personal server running
OpenBSD. soju is what powers chat.sr.ht. It is well tested, I know it fits my
needs, and has support for connecting to multiple networks. Its bouncer/network
support integrates nicely with <code>senpai</code>, <a href="/journal/2023/07/05/senpai-a-modern-irc-terminal-client/">a modern irc terminal
client</a>.</p>
<p>The OpenBSD port for soju seems rather new, and there doesn&rsquo;t seem to be a
binary package for it in the latest stable release. I opted to upgrade from
<a href="https://www.openbsd.org/faq/faq5.html#Flavors">-stable to -current</a>, which was really as simple as running
<code>sysupgrade -s</code>, rebooting, and finally running <code>pkg_add -U</code>.</p>
<p>Installing soju is as simple as running <code>pkg_add soju</code>.</p>
<p>I also needed to create a DNS entry for <code>irc.whynothugo.nl</code>, and point that to
the same server.</p>
<p>Since soju uses TLS, I need certificates for it. I added the new domain to
<code>/etc/acme-client.conf</code> and then executed <code>acme-client irc.whynothugo.nl</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;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>domain irc.whynothugo.nl {
</span></span><span style="display:flex;"><span>        domain key &#34;/etc/ssl/private/irc.whynothugo.nl.key&#34;
</span></span><span style="display:flex;"><span>        domain full chain certificate &#34;/etc/ssl/irc.whynothugo.nl.fullchain.pem&#34;
</span></span><span style="display:flex;"><span>        sign with letsencrypt
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>To avoid <code>acme-client</code> from messing up permissions for the certificates, I&rsquo;ll
copy them to another location on renewal. My renewal script is then:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/sh
</span></span></span><span style="display:flex;"><span>set -e
</span></span><span style="display:flex;"><span>acme-client irc.whynothugo.nl
</span></span><span style="display:flex;"><span>install -Dm <span style="color:#ae81ff">644</span> -o _soju /etc/ssl/irc.whynothugo.nl.fullchain.pem /etc/soju/
</span></span><span style="display:flex;"><span>install -Dm <span style="color:#ae81ff">600</span> -o _soju /etc/ssl/private/irc.whynothugo.nl.key /etc/soju/
</span></span><span style="display:flex;"><span>rcctl reload soju
</span></span></code></pre></div><p>And the configuration file at <code>/etc/soju/config</code> reads:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>db sqlite3 /var/soju/main.db
</span></span><span style="display:flex;"><span>message-store fs /var/soju/logs/
</span></span><span style="display:flex;"><span>listen ircs://
</span></span><span style="display:flex;"><span>tls /etc/soju/irc.whynothugo.nl.fullchain.pem /etc/soju/irc.whynothugo.nl.key
</span></span></code></pre></div><p>As a on-off, I need to manually copy the certificates into the right location,
and manually start and enable soju:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span><span style="color:#75715e"># These two lines copied from the above script:</span>
</span></span><span style="display:flex;"><span>install -Dm <span style="color:#ae81ff">644</span> -o _soju /etc/ssl/irc.whynothugo.nl.fullchain.pem /etc/soju/
</span></span><span style="display:flex;"><span>install -Dm <span style="color:#ae81ff">600</span> -o _soju /etc/ssl/private/irc.whynothugo.nl.key /etc/soju/
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>rcctl start soju
</span></span><span style="display:flex;"><span>rcctl enable soju
</span></span></code></pre></div><p>With soju running, I reconfigured senpai locally and connected to it. In order
to connect the bouncer to a new network, I used:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>/msg BouncerServ network create -addr irc.libera.chat
</span></span></code></pre></div><p>It works!</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>My warmest sympathies to the team working hard to restore services during
this attack.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>Extended usages of the primary selection</title><link>https://whynothugo.nl/journal/2024/01/06/extended-usages-of-the-primary-selection/</link><pubDate>Sat, 06 Jan 2024 00:50:20 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2024/01/06/extended-usages-of-the-primary-selection/</guid><description><![CDATA[<p>When some text is selected, it becomes the <strong>primary selection</strong>. Other
applications can then access this primary selection. The most common usage is
to paste it by middle clicking elsewhere, but that&rsquo;s really the full extent of
the interactions that are currently available<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.</p>
<p>There is a lot of unexplored potential in this feature. For example, it is
perfectly feasible to have some desktop-wide special menu with actions that
apply on it. This menu could be opened with
<kbd>Ctrl</kbd>+<kbd>RightClick</kbd>, or maybe
<kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>M</kbd> and could have entries such as:</p>
<ul>
<li>Show selection in dictionary.</li>
<li>Read selected text out load.</li>
<li>Read dictionary definition of selected word out loud.</li>
<li>Translate selection into another language.</li>
<li>Save selection into snippets / notes.</li>
<li><strong>Added 2024-01-16</strong>: Render a QR code with the current selection.</li>
</ul>
<p>Implementing such features is pretty feasible and doesn&rsquo;t require any new
architectural changes or modifications to existing compositors. An early
prototype could map a global hotkey directly to an action, for example
<kbd>Super</kbd>+<kbd>D</kbd> to open a given word in a dictionary, or
<kbd>Super</kbd>+<kbd>R</kbd> to read the selection out load with an existing
text-to-speech engine.</p>
<p>This idea has been floating in my head for a long time, but I haven&rsquo;t had the
opportunity to experiment with it. Specifically, I am not familiar with any
desktop dictionary or desktop translation application which I can be executed
programmatically. E.g.: it should be possible for a script to execute the
application and provide some input text (the current primary selection)
programmatically.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>This applies to most Unix-like desktops, including Linux or BSD with
either Xorg and Wayland. I have no reference of other platforms such as
Windows or macOS.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>vdirsyncer: preparing for alpha version</title><link>https://whynothugo.nl/journal/2023/12/21/vdirsyncer-preparing-for-alpha-version/</link><pubDate>Thu, 21 Dec 2023 18:30:23 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/12/21/vdirsyncer-preparing-for-alpha-version/</guid><description><![CDATA[<p>I&rsquo;ve been synchronising my calendars with the still-experimental rewrite of
vdirsyncer lately. <code>vdirsyncer --sync --daemon</code> has been running for about
three days now. The <code>--daemon</code> flag makes it run continuously, synchronising
changes every five minutes.</p>
<p>Eventually I want <code>--daemon</code> to monitor for changes and immediately synchronise
when changes occur. Until monitoring of storages is actually implemented, it
simply syncs every five minutes.</p>
<p>This solves a long standing issue in vdirsyncer: there&rsquo;s no way to continuously
sync in background. Not unless credentials are available in plain-text form or
available without user intervention in a similar way.</p>
<p>The results so far have been quite satisfactory. I&rsquo;ve created multiple events
on other devices, and they&rsquo;ve shown up soon on my desktop. I even have a
misconfigured storage that fails every time. Vdirsyncer keeps synchronising
everything else just fine.</p>
<h2 id="an-alpha-release">An alpha release<div class="permalink">[<a href="#an-alpha-release">permalink</a>]</div></h2>
<p>I still have heavy backups of my calendars and collections. And some more
refinement is still required before I can publish an alpha release for others
to use. But I&rsquo;m still happy to have reached this point.</p>
<p>I&rsquo;ll be winding down for the remainder of this year, but my general roadmap
right now is:</p>
<ul>
<li>Draft a migration guide, mentioning all the little things that change between
the previous implementation and this one.</li>
<li>Finish writing clear instructions on how the configuration file needs to be
upgraded (I&rsquo;ve tried to keep these changes to a minimum).</li>
<li>Write a lot more unit tests for some storages.</li>
<li>Handle a failures to write the state to disk (this is a fatal error anyway).</li>
<li>Ensure that all file-system writes are atomic.</li>
<li>Publish an alpha release for others to test.</li>
</ul>
<p>The initial alpha release will be in a state where it may lose or corrupt your
data. I hope it doesn&rsquo;t, but the word alpha means exactly this: it&rsquo;s not a
final version.</p>
<h2 id="vparser">vparser<div class="permalink">[<a href="#vparser">permalink</a>]</div></h2>
<p>I have also published v1.0.0 of <a href="https://git.sr.ht/~whynothugo/vparser"><code>vparser</code></a>, the low level parser for icalendar
and vcard. I have a few other project for which I&rsquo;ll use this in future too,
including normalising calendar entries and merging contacts.</p>
]]></description></item><item><title>Specification and development status for valarmd</title><link>https://whynothugo.nl/journal/2023/11/30/specification-and-development-status-for-valarmd/</link><pubDate>Thu, 30 Nov 2023 11:02:38 +0800</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/11/30/specification-and-development-status-for-valarmd/</guid><description><![CDATA[<p>I have made a few mentions to <code>valarmd</code>, a project of mine which is paused for
the moment. It is a daemon used to show desktop notifications for alarms in
calendar entries. This is its specification. I hope to continue working on it
after completing my current work on <code>vdirsyncer</code>.</p>
<h2 id="specification-for-valarmd">Specification for <code>valarmd</code><div class="permalink">[<a href="#specification-for-valarmd">permalink</a>]</div></h2>
<p>The basic usage is:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>valarmd --socket /path/to/socket --vdir /path/to/vdir
</span></span></code></pre></div><p><code>valarmd</code> shall read icalendar components (both events and todos) from a local
file-system directory, extracting their respective dates, alarms and summaries.
It will also monitor the directory for any changes to these files and load any
changes.</p>
<p>At the time of each alarm, <code>valarmd</code> will write a single event into its socket.
An event is a JSON object, encoded into a single line:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{<span style="color:#f92672">&#34;summary&#34;</span>: <span style="color:#e6db74">&#34;Meeting with Alice&#34;</span>, <span style="color:#f92672">&#34;alarm&#34;</span>: <span style="color:#ae81ff">1700811229</span>, <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;event&#34;</span>, <span style="color:#f92672">&#34;start&#34;</span>: <span style="color:#ae81ff">1700813029</span>}
</span></span></code></pre></div><p>Programs that read events from this socket can perform all kind of actions:</p>
<ul>
<li>Show a notification via a notification daemon.</li>
<li>Play an audible notification</li>
<li>Read the event summary via text-to-speech.</li>
<li>Send an email, print job or fax with the details of the alarm.</li>
<li>Emit a D-Bus signal.</li>
</ul>
<p>Clearly, anything can be done with the information. <em>How</em> these alarms are
presented is up to end users.</p>
<h2 id="development-status">Development status<div class="permalink">[<a href="#development-status">permalink</a>]</div></h2>
<p>The current work in progress is written in Rust. Parsing icalendar timezones
and recurrence rules is a bit not trivial, and existing Rust libraries have
some limitations that need to be addressed:</p>
<ul>
<li><code>chrono</code>: It is not possible to create a <code>Vec&lt;DateTime&gt;</code> where datetimes have
a different timezones. <a href="https://github.com/chronotope/chrono/issues/822">There is an issue for this upstream</a>. This is
required to properly handle all form of recurring events.</li>
<li><code>rrule</code>: Doesn&rsquo;t support using custom timezone implementations. A custom
timezone implementation is needed to represent data read from icalendar
components. I <a href="https://github.com/fmeringdal/rust-rrule/pull/85">attempted to address the issue</a> but it&rsquo;s more
complicated that it seems.</li>
</ul>
<p>The current (incomplete and broken) implementation is in <a href="https://git.sr.ht/~whynothugo/valarmd">the <code>valarmd</code>
repository</a>. There is also a <a href="https://lists.sr.ht/~whynothugo/public-inbox/%3C20221216132356.mhb75o777q3udmjw%40navi%3E">short thread</a> covering its status in
further detail.</p>
<p>Given limitations of existing libraries (and being focused elsewhere), this
project remains paused. A few workarounds have been mentioned, but they only
work for <em>some</em> events in my personal calendar. <strong>Missing some alarms is not a
reasonable compromise.</strong></p>
<h2 id="considering-for-golang">Considering for golang<div class="permalink">[<a href="#considering-for-golang">permalink</a>]</div></h2>
<p>I had considered golang before using Rust, but its timezone implementation is
annoying to work with. Golang&rsquo;s timezone implementation is represented by the
<a href="https://godocs.io/time#Location"><code>Location</code></a> type. Creating a new <code>Location</code> instance cannot be done directly;
it can only be done by providing <em>IANA Time Zone database-formatted data</em> as
input.</p>
<p>So in order to create a <code>Location</code> instance from an icalendar <code>VTIMEZONE</code>, it
needs to be converted into IANA Time Zone database-format first, <em>and then</em>
into <code>Location</code>.</p>
<p>This is an awful API (which <a href="https://github.com/golang/go/issues/49951">won&rsquo;t be fixed</a>), and only a single
example. In general, I don&rsquo;t consider golang&rsquo;s datetime and timezone interfaces
flexible enough for tools that need to interoperate with icalendar data.</p>
<h2 id="see-also">See also<div class="permalink">[<a href="#see-also">permalink</a>]</div></h2>
<ul>
<li><a href="https://github.com/pimutils/khal/issues/148">An issue from 2015 requesting exactly this</a>.</li>
</ul>
]]></description></item><item><title>vdirsyncer status update, November 2023</title><link>https://whynothugo.nl/journal/2023/11/27/vdirsyncer-status-update-november-2023/</link><pubDate>Mon, 27 Nov 2023 19:51:45 +0800</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/11/27/vdirsyncer-status-update-november-2023/</guid><description><![CDATA[<p>Designing the low level icalendar parser took longer than it should have taken.
To be sincere, part of the problem was my trying to be too ambitious in its
scope and growing beyond the strictly necessary requirements.</p>
<h2 id="requirements">Requirements<div class="permalink">[<a href="#requirements">permalink</a>]</div></h2>
<p>The main goal of this parser (now called <code>vparser</code>) is to parse only the basic
structure of icalendar and vcard files. The main requirements for vdirsyncer&rsquo;s
use case are:</p>
<ul>
<li>Extracting the <code>UID</code> from an entry (calendar component or vcard).</li>
<li>Normalise a component:
<ul>
<li>Sort its properties into a deterministic order.</li>
<li>Recognise and ignore specific properties (e.g.: <code>PRODID</code>).</li>
</ul>
</li>
<li>Avoid copying data in memory wherever possible. This will run once for every
single item
, so needs to be well-performant.</li>
<li>Handle potentially invalid components. The general structure needs to be
reasonably valid:
<ul>
<li>The syntax of content lines must be correct.</li>
<li>The syntax of individual parameter types or values may be invalid.</li>
</ul>
</li>
</ul>
<h2 id="focusing-on-a-this-use-case">Focusing on a this use case<div class="permalink">[<a href="#focusing-on-a-this-use-case">permalink</a>]</div></h2>
<p>I consider code re-usability important, and therefore wanted this parser to be
reusable for other applications that need to parse icalendar data.</p>
<p>My original intent was for this implementation to be usable in other sorts of
applications, including something like a calendar GUI application. However,
such an application would have somewhat conflicting requirements, and trying to
design a parser that would fit such a use case resulted in a design that was
not ideal for vdirsyncer.</p>
<p>Designing a lower level library optimised for all usages turned out to be
unrealistic. The design on which I have finally settled  is optimal for
vdirsyncer, and quite suitable for other low-level tools, for example:</p>
<ul>
<li><code>icalendar-funnel</code>: a tool that shall read two distinct calendars and copy
their items into a third (this is a special kind of one-way sync).</li>
<li><code>vcard-merge</code>: a tool that shall merge two vcard entries into a single one.</li>
</ul>
<p>However, its performance may not be optimal for something like a calendar UI.</p>
<h2 id="quick-intro-to-content-lines">Quick intro to content lines<div class="permalink">[<a href="#quick-intro-to-content-lines">permalink</a>]</div></h2>
<p>As a quick reference, content lines look something 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;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>DTSTART;TZID=America/New_York:19970902T090000
</span></span></code></pre></div><ul>
<li>The <code>name</code> is <code>DTSTART</code>.</li>
<li>The only parameter here is: <code>TZID=America/New_York</code>.
<ul>
<li>The parameter&rsquo;s name is <code>TZID</code>.</li>
<li>The parameter&rsquo;s value is <code>America/New_York</code></li>
</ul>
</li>
<li>The value is <code>19970902T090000</code></li>
</ul>
<p>Note that a single content line may have zero, one or more parameters.</p>
<h2 id="designs-considered">Designs considered<div class="permalink">[<a href="#designs-considered">permalink</a>]</div></h2>
<p>I already had quite a few designs in mind for my parser, and quite a few notes
spread across different places. I&rsquo;ve kept around and present here only a few of
these (most others were really regurgitations of the same ideas presented
below).</p>
<p>The following all share a same general approach: export a trait<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> which
consumers must implement. The parser will parse the entire input data. As it
encounters elements on the input being parsed, it will call the appropriate
functions on the trait handler implementation.</p>
<p>The general idea is inspired on Java&rsquo;s SAX parsers. Generally, these handlers
would implement a <a href="https://en.wikipedia.org/wiki/State_pattern">state pattern</a>.</p>
<h3 id="version-1">Version 1<div class="permalink">[<a href="#version-1">permalink</a>]</div></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">trait</span> ParseHandler {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// TODO: maybe use an enum with variants KnownName and XName?
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">name</span>(<span style="color:#f92672">&amp;</span>self, name: <span style="color:#a6e22e">Cow</span><span style="color:#f92672">&lt;</span><span style="color:#66d9ef">str</span><span style="color:#f92672">&gt;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">param</span>(<span style="color:#f92672">&amp;</span>self, name: <span style="color:#a6e22e">Cow</span><span style="color:#f92672">&lt;</span><span style="color:#66d9ef">str</span><span style="color:#f92672">&gt;</span>, value: <span style="color:#66d9ef">&amp;</span>[Cow<span style="color:#f92672">&lt;</span><span style="color:#66d9ef">str</span><span style="color:#f92672">&gt;</span>]);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">value</span>(<span style="color:#f92672">&amp;</span>self, value: <span style="color:#a6e22e">Cow</span><span style="color:#f92672">&lt;&amp;</span><span style="color:#66d9ef">str</span><span style="color:#f92672">&gt;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">end_of_file</span>(<span style="color:#f92672">&amp;</span>self);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">/// Called when invalid data is found.
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">///
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">/// Includes all input text until the next `\r\n`.
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">invalid</span>(<span style="color:#f92672">&amp;</span>self, value: <span style="color:#a6e22e">Cow</span><span style="color:#f92672">&lt;&amp;</span><span style="color:#66d9ef">str</span><span style="color:#f92672">&gt;</span>);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>A consumer of the parser would implement this trait and handle events as
function calls. For the following example line:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>BEGIN:VTODO
</span></span></code></pre></div><p>The following sequence would be invoked (this won&rsquo;t strictly compile, and is
simplified for readability):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span>handler.name(<span style="color:#e6db74">&#34;BEGIN&#34;</span>);
</span></span><span style="display:flex;"><span>handler.value(<span style="color:#e6db74">&#34;VTODO&#34;</span>);
</span></span></code></pre></div><p>Pros:</p>
<ul>
<li>Skipping a content lines is as simple as ignoring all events until a
corresponding <code>handle.name(&quot;END&quot;)</code> is received.</li>
<li>Using <code>Cow</code> allows passing a reference most of the time, but owned data in
only when a content line is folded.</li>
</ul>
<!-- - Potentially, if `params()` does nothing, the compiler might optimise it away. -->
<!--   However, I'm not a fan of relying on the compiler to fix smells in my code. -->
<p>Cons:</p>
<ul>
<li>Using <code>Cow</code> for <code>params</code> implies creating copies of parameters even in
situations which don&rsquo;t care about them (which is every single scenario in the
case of <code>vdirsyncer</code>).</li>
<li>Likewise, the array of parameters always needs to be allocated, even if
unused.
<ul>
<li>As I review this, I realise that re-using the same <code>Vec</code> for each call is
feasible, although I suspect that lifetimes would be quite unpleasant to
deal with for consumers that make use of parameters.</li>
</ul>
</li>
<li>For usages that need to further interpret the calendar component data, this
API is quite tedious to use.</li>
<li>Requires additional logic to reconstruct content lines; which means
<code>vdirsyncer</code> would be decomposing them and the re-composing them again,
adding pointless complexity.</li>
</ul>
<p>I didn&rsquo;t consider this <strong>terrible</strong>, but it&rsquo;s definitely not good either.</p>
<h3 id="version-2">Version 2<div class="permalink">[<a href="#version-2">permalink</a>]</div></h3>
<p>I tried using an enum as a type handed over to the handler, and a single event
per line:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#75715e">#[non_exhaustive]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">enum</span> <span style="color:#a6e22e">ContentLine</span><span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;input</span><span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>    Begin {
</span></span><span style="display:flex;"><span>        params: Vec<span style="color:#f92672">&lt;</span>Cow<span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;input</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">&#39;input</span> <span style="color:#66d9ef">str</span><span style="color:#f92672">&gt;&gt;</span>,
</span></span><span style="display:flex;"><span>        value: <span style="color:#a6e22e">Cow</span><span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;input</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">&#39;input</span> <span style="color:#66d9ef">str</span><span style="color:#f92672">&gt;</span>,
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    End {
</span></span><span style="display:flex;"><span>        params: Vec<span style="color:#f92672">&lt;</span>Cow<span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;input</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">&#39;input</span> <span style="color:#66d9ef">str</span><span style="color:#f92672">&gt;&gt;</span>,
</span></span><span style="display:flex;"><span>        value: <span style="color:#a6e22e">Cow</span><span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;input</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">&#39;input</span> <span style="color:#66d9ef">str</span><span style="color:#f92672">&gt;</span>,
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// ... TODO: maybe variants for each standard name too?
</span></span></span><span style="display:flex;"><span>    Other {
</span></span><span style="display:flex;"><span>        name: <span style="color:#a6e22e">Cow</span><span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;input</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">&#39;input</span> <span style="color:#66d9ef">str</span><span style="color:#f92672">&gt;</span>,
</span></span><span style="display:flex;"><span>        params: Vec<span style="color:#f92672">&lt;</span>Cow<span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;input</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">&#39;input</span> <span style="color:#66d9ef">str</span><span style="color:#f92672">&gt;&gt;</span>,
</span></span><span style="display:flex;"><span>        value: <span style="color:#a6e22e">Cow</span><span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;input</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">&#39;input</span> <span style="color:#66d9ef">str</span><span style="color:#f92672">&gt;</span>,
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    Invalid(Cow<span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;input</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">&#39;input</span> <span style="color:#66d9ef">str</span><span style="color:#f92672">&gt;</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">trait</span> ParseHandler {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">content_line</span>(<span style="color:#f92672">&amp;</span>self, line: <span style="color:#a6e22e">ContentLine</span>);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Compared to the previous approach, usage is (mildly) easier for consumers that
want to actually interpret the calendar component&rsquo;s data, but doesn&rsquo;t provide
great improvements for the <code>vdirsyncer</code> use case.</p>
<p>Pros:</p>
<ul>
<li>Skipping content lines as simple as the previous approach.</li>
<li>Each line is a single event, which is simpler to handle for consumers that
need to interpret the calendar component data.</li>
</ul>
<p>Cons:</p>
<ul>
<li>Still parses parameters for lines that we don&rsquo;t care, and allocates lots of
<code>Vec</code>s that may never be used.
<ul>
<li>Again, maybe using the same <code>Vec</code> that merely gets repopulated for each
line was feasible. The above type would have a <code>&amp;[Cow&lt;str&gt;]</code> instead of
<code>Vec&lt;Cow&lt;str&gt;&gt;</code>. However, this sounds like lifetimes could be even more
complicated, given that the array has a shorter lifetime than the
references (I&rsquo;m not even 100% sure that this is allowed).</li>
</ul>
</li>
<li>Requires different enum types for vcard and icalendar. Keeping all the
variants updated would be quite tedious, especially given that most won&rsquo;t
even be used in <code>vdirsyncer</code>&rsquo;s use case.</li>
<li>Also requires additional logic to reconstruct content lines as the previous
approach.
<ul>
<li>On the other hand, given that this approach uses one event per line, it is
possible to simply provide a reference to the original line.</li>
</ul>
</li>
</ul>
<h3 id="version-3">Version 3<div class="permalink">[<a href="#version-3">permalink</a>]</div></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">ContentLine</span><span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;input</span><span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>    data: <span style="color:#a6e22e">Cow</span><span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;input</span>, <span style="color:#66d9ef">str</span><span style="color:#f92672">&gt;</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">impl</span><span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;input</span><span style="color:#f92672">&gt;</span> ContentLine<span style="color:#f92672">&lt;</span>&#39;_<span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">name</span>(<span style="color:#f92672">&amp;</span>self) -&gt; <span style="color:#a6e22e">Cow</span><span style="color:#f92672">&lt;</span><span style="color:#66d9ef">str</span><span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// ...
</span></span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">params</span>(<span style="color:#f92672">&amp;</span>self) -&gt; <span style="color:#a6e22e">Cow</span><span style="color:#f92672">&lt;</span><span style="color:#66d9ef">str</span><span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// ...
</span></span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">value</span>(<span style="color:#f92672">&amp;</span>self) -&gt; <span style="color:#a6e22e">Cow</span><span style="color:#f92672">&lt;</span><span style="color:#66d9ef">str</span><span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// ...
</span></span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">raw</span>(<span style="color:#f92672">&amp;</span>self) -&gt; <span style="color:#a6e22e">Cow</span><span style="color:#f92672">&lt;</span><span style="color:#66d9ef">str</span><span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// ...
</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">trait</span> ParseHandler {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">content_line</span>(<span style="color:#f92672">&amp;</span>self, line: <span style="color:#a6e22e">ContentLine</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">invalid_line</span>(<span style="color:#f92672">&amp;</span>self, line: <span style="color:#a6e22e">Cow</span><span style="color:#f92672">&lt;&amp;</span><span style="color:#66d9ef">str</span><span style="color:#f92672">&gt;</span>);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This approach does away with the enum type used above, but keeps the idea of a
single event per content line.</p>
<p>Pros:</p>
<ul>
<li>Skipping content lines still simple as the previous approach.</li>
<li>Doesn&rsquo;t create <code>Vec</code>s with pointers to parameters that are never used.</li>
<li>Requires no extra complexity to access the original content line <em>unfolded</em>.</li>
</ul>
<p>Cons:</p>
<ul>
<li>Requires parsing a lot of portions twice; once when extracting the content
line, and again when accessing the name, value or parameters.</li>
<li>Using <code>Cow</code> to hold data in <code>ContentLine</code> means that we copy around every
line when unfolding, even if the unfolded portions are never used.</li>
<li>Requires extra complexity to re-create the original content line in its
folded form.</li>
</ul>
<p>Overall, this is a minor improvement over previous approaches. Mostly the API
provides a cleaner API (for all use cases).</p>
<h3 id="version-4">Version 4<div class="permalink">[<a href="#version-4">permalink</a>]</div></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">enum</span> <span style="color:#a6e22e">Name</span><span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;input</span><span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>    Begin,
</span></span><span style="display:flex;"><span>    End,
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// ... TODO: variants for each standard name
</span></span></span><span style="display:flex;"><span>    Other(Cow<span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;input</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">&#39;input</span> <span style="color:#66d9ef">str</span><span style="color:#f92672">&gt;</span>),
</span></span><span style="display:flex;"><span>    Invalid(Cow<span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;input</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">&#39;input</span> <span style="color:#66d9ef">str</span><span style="color:#f92672">&gt;</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">pub</span> <span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">ContentLine</span><span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;input</span><span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>    name: <span style="color:#a6e22e">Name</span><span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;input</span><span style="color:#f92672">&gt;</span>,
</span></span><span style="display:flex;"><span>    data: <span style="color:#66d9ef">&amp;</span><span style="color:#a6e22e">&#39;input</span> <span style="color:#66d9ef">str</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">// Implements impl Iterator&lt;Item = &amp;str&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">Params</span> {}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">impl</span><span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;input</span><span style="color:#f92672">&gt;</span> ContentLine<span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;input</span><span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">name</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">&#39;input</span> self) -&gt; <span style="color:#66d9ef">&amp;</span><span style="color:#a6e22e">&#39;input</span> <span style="color:#a6e22e">Name</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&amp;</span>self.name
</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">pub</span> <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">params</span>(<span style="color:#f92672">&amp;</span>self) -&gt; <span style="color:#a6e22e">Params</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// ...
</span></span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">value</span>(<span style="color:#f92672">&amp;</span>self) -&gt; <span style="color:#a6e22e">Cow</span><span style="color:#f92672">&lt;</span><span style="color:#66d9ef">str</span><span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// ...
</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">trait</span> ParseHandler {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">content_line</span>(<span style="color:#f92672">&amp;</span>self, line: <span style="color:#a6e22e">ContentLine</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">invalid_line</span>(<span style="color:#f92672">&amp;</span>self, line: <span style="color:#a6e22e">Cow</span><span style="color:#f92672">&lt;&amp;</span><span style="color:#66d9ef">str</span><span style="color:#f92672">&gt;</span>);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This API is more convenient for situations which want to interpret the data
inside calendar components. It also avoids parsing the name portion of each
line twice.</p>
<p>Pros:</p>
<ul>
<li>Skipping a content lines is as simple as ignoring all events until a matching
<code>Name::End</code> is found.</li>
<li>Does not unfold and copy data that won&rsquo;t be used.</li>
<li>Accessing parameters has a much more ergonomic interface.</li>
<li>The cost of accessing parameters is only paid when they are actually
required.</li>
<li>No <code>Cow</code> for data that won&rsquo;t be used.</li>
</ul>
<p>Cons:</p>
<ul>
<li>Still requires a second pass to access parameters.</li>
<li>The enum type for the name might be tedious to maintain, and their value is
questionable.
<ul>
<li>On the other hand: it is perfectly feasible to replace it with a <code>&amp;'str</code>
instead; another version of the handler that is omitted here did exactly
that. We&rsquo;ll see more of this idea below.</li>
</ul>
</li>
</ul>
<h2 id="taking-a-step-back">Taking a step back<div class="permalink">[<a href="#taking-a-step-back">permalink</a>]</div></h2>
<p>At this point, a few patterns where clear:</p>
<ul>
<li>Whenever I avoided unfolding and re-allocating data that vdirsyncer doesn&rsquo;t
care about (mostly parameters), interpreting that data requires parsing that
same portion of data twice.
<ul>
<li>Implying that I should prioritise the vdirsyncer use case, rather than try
to make this too generic.</li>
</ul>
</li>
<li>Using a <code>Cow&lt;str&gt;</code> as a field in the struct type means unfolding and copying
the entire line, including portions that would never be used.</li>
<li>Using enums for the line&rsquo;s name doesn&rsquo;t seem to provide a worthwhile
advantage, and always brings in maintenance overhead.</li>
</ul>
<h2 id="current-version">Current Version<div class="permalink">[<a href="#current-version">permalink</a>]</div></h2>
<p>After the above experiments, I set out to design something that would be a
perfect fit for vdirsyncer and other low level tools. Higher level tools will
not have the same level of consideration.</p>
<p>The resulting data type merely includes references (pointers) to the original
data:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#e6db74">/// A valid content line.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">///
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">/// May be folded via a CRLF immediately followed by a single linear
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">/// white-space character (i.e., SPACE or HTAB).
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">ContentLine</span><span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;input</span><span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>    raw: <span style="color:#66d9ef">&amp;</span><span style="color:#a6e22e">&#39;input</span> <span style="color:#66d9ef">str</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Everything before the first colon or semicolon (whichever comes first).
</span></span></span><span style="display:flex;"><span>    name: <span style="color:#66d9ef">&amp;</span><span style="color:#a6e22e">&#39;input</span> <span style="color:#66d9ef">str</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Everything after the unquoted semicolon and before the first unquoted colon.
</span></span></span><span style="display:flex;"><span>    params: <span style="color:#66d9ef">&amp;</span><span style="color:#a6e22e">&#39;input</span> <span style="color:#66d9ef">str</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Everything after the first unquoted colon.
</span></span></span><span style="display:flex;"><span>    value: <span style="color:#66d9ef">&amp;</span><span style="color:#a6e22e">&#39;input</span> <span style="color:#66d9ef">str</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">trait</span> ParseHandler {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">content_line</span>(<span style="color:#f92672">&amp;</span>self, line: <span style="color:#a6e22e">ContentLine</span>);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Pros:</p>
<ul>
<li>Skipping a component is still as simple as ignoring all lines until a
matching <code>END:XXX</code> event.</li>
<li>Parses everything that vdirsyncer cares about in one pass.</li>
<li>No eager unfolding and copying of data that won&rsquo;t be used.</li>
<li>There is no need to reconstruct the original line; a reference to it is
passed.</li>
</ul>
<p>Cons (sort of):</p>
<ul>
<li>Accessing individual parameters still needs a second pass.
<ul>
<li>But vdirsyncer doesn&rsquo;t actually access those at all.</li>
</ul>
</li>
<li>For usages that need to further interpret the calendar component data, this
still requires a second pass to access individual parameters.
<ul>
<li>Again, vdirsyncer doesn&rsquo;t need this.</li>
</ul>
</li>
</ul>
<p>All the disadvantages here affect other type of tools, but not vdirsyncer
specifically.</p>
<h3 id="accessors">Accessors<div class="permalink">[<a href="#accessors">permalink</a>]</div></h3>
<p>The data in the structure above points to the original input data, which may
contain folded lines. Accessing it is implemented via methods that unfold
content on-demand:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">impl</span><span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;input</span><span style="color:#f92672">&gt;</span> ContentLine<span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;input</span><span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">raw</span>(<span style="color:#f92672">&amp;</span>self) -&gt; <span style="color:#66d9ef">&amp;</span><span style="color:#a6e22e">&#39;input</span> <span style="color:#66d9ef">str</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// ...
</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">fn</span> <span style="color:#a6e22e">name</span>(<span style="color:#f92672">&amp;</span>self) -&gt; <span style="color:#a6e22e">Cow</span><span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;input</span>, <span style="color:#66d9ef">str</span><span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// ...
</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">fn</span> <span style="color:#a6e22e">params</span>(<span style="color:#f92672">&amp;</span>self) -&gt; <span style="color:#a6e22e">Cow</span><span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;input</span>, <span style="color:#66d9ef">str</span><span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// ...
</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">fn</span> <span style="color:#a6e22e">value</span>(<span style="color:#f92672">&amp;</span>self) -&gt; <span style="color:#a6e22e">Cow</span><span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;input</span>, <span style="color:#66d9ef">str</span><span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// ...
</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">fn</span> <span style="color:#a6e22e">re_folded</span>(<span style="color:#f92672">&amp;</span>self) -&gt; <span style="color:#a6e22e">Cow</span><span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;input</span>, <span style="color:#66d9ef">str</span><span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// ...
</span></span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Some notes on this:</p>
<ul>
<li>vdirsyncer doesn&rsquo;t even read parameters, so that won&rsquo;t even be used.</li>
<li>Names are seldom folded (due to being at the beginning of the line), so
<code>name()</code> will usually return a <code>Cow::Borrowed</code> (e.g.: a reference).
<ul>
<li>Names are accessed for <em>every</em> line, so maybe I&rsquo;ll escape them prematurely
and avoid a second pass for them too.</li>
</ul>
</li>
<li>Accessing values is only done for a small minority of lines, and only those
will be unfolded.</li>
<li>The <code>re_folded</code> method can be used to normalise how lines are folded. I
won&rsquo;t be using this initially and want to evaluate whether it is ever of any
real impact in real-life conflict resolution.</li>
<li>Explicit lifetimes here allow keeping refernces beyond the lifetime of the
parser itself.</li>
</ul>
<h2 id="inversion-of-control">Inversion of control<div class="permalink">[<a href="#inversion-of-control">permalink</a>]</div></h2>
<p>An issue with this approach is that parsing can&rsquo;t be stopped immediately.
There&rsquo;s no way to stop the parser immediately and ignore the rest of a file.</p>
<p>I addressed this by simply using a <code>Parser</code> type which can be used to iterates
over lines:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">Parser</span><span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;data</span><span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>    data: <span style="color:#66d9ef">&amp;</span><span style="color:#a6e22e">&#39;data</span> <span style="color:#66d9ef">str</span>,
</span></span><span style="display:flex;"><span>    index: <span style="color:#66d9ef">usize</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">impl</span><span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;data</span><span style="color:#f92672">&gt;</span> Parser<span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;data</span><span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">new</span>(data: <span style="color:#66d9ef">&amp;</span><span style="color:#a6e22e">&#39;data</span> <span style="color:#66d9ef">str</span>) -&gt; <span style="color:#a6e22e">Parser</span><span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;data</span><span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// ...
</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">impl</span><span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;data</span><span style="color:#f92672">&gt;</span> Iterator <span style="color:#66d9ef">for</span> Parser<span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;data</span><span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Item</span> <span style="color:#f92672">=</span> ContentLine<span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;data</span><span style="color:#f92672">&gt;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">next</span>(<span style="color:#f92672">&amp;</span><span style="color:#66d9ef">mut</span> self) -&gt; Option<span style="color:#f92672">&lt;</span>Self::Item<span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// ...
</span></span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This is also a more idiomatic Rust API, which actually turned out to be more
pleasant to use.</p>
<h2 id="implementing-the-parser">Implementing the parser<div class="permalink">[<a href="#implementing-the-parser">permalink</a>]</div></h2>
<p>Implementing the parser is relatively straightforward. Given that I want to
handle invalid input gracefully, I only really need to concearn myself with a
subset of the <a href="https://www.rfc-editor.org/rfc/rfc5545#section-3.1">rules for icalendar content lines</a>.</p>
<p>In general terms, I&rsquo;ll parse each line until finding one of the following:</p>
<ul>
<li>A semicolon: indicates that the name ends here and parameters.</li>
<li>A colon: indicates that the name/parameters ends and the value begins.</li>
<li>A <code>\r\n</code>:
<ul>
<li>When followed by whitespace indicates a continuation line.</li>
<li>Otherwise, it indicates the end of this content line.</li>
</ul>
</li>
</ul>
<p><em>(these are the same rules as those in the comments in the sample code above)</em></p>
<p>In my head, this looked like a decision tree, and this is pretty much what <a href="https://git.sr.ht/~whynothugo/vparser/tree/9ecc24430c1ee5a802d37545ac62b6a2e7081763/item/src/lib.rs#L115-245">the
implementation</a> looks like.</p>
<p>The parsing code is one huge function. I usually consider functions this long
an indicator of poor code quality, but I find this to be an exception. The
code&rsquo;s indentation actually reflects depth in this decision tree that I
mentioned. And the lack of fancy macros or &ldquo;smart&rdquo; helpers makes it
straightforward to read and follow.</p>
<p>When writing this, I initially represented the main branches with a <code>todo!()</code>
for each one. I and then filled in the missing parts, usually by writing tests
that would reach this unreachable code and refining until those passed.</p>
<p>I also looked into <a href="https://bheisler.github.io/criterion.rs/book/index.html">criterion</a> for performance benchmarks (I only used it for
one specific improvement so far). It turns out to be quite simple to use. I
intend to use this for a few pending improvements after the first working
releases.<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>.</p>
<h2 id="the-rest-of-this-month">The rest of this month<div class="permalink">[<a href="#the-rest-of-this-month">permalink</a>]</div></h2>
<p>It&rsquo;s been a few weeks since the last vdirsyncer status update. The design work
mentioned above took only a couple of days. I&rsquo;m still not very happy with the
time consumed on it. A less polished approach could have sufficed for an
initial implementation, and this won&rsquo;t be as flexible as I hoped it would be.</p>
<p>The implementation itself didn&rsquo;t take long. I already had a pretty clear mental
picture on what it would look like, so it was mostly a day or two of writing
tests, writing code, and iterating on that.</p>
<p>I&rsquo;m now finalising integrating this into the vdirsyncer codebase and dealing
with (what I hope are) the final bits of the basics in conflict resolution.</p>
<p>All this aside, I&rsquo;ve been enjoying some time off recently, and also a lot of
time working around more networking issues than I had anticipated.</p>
<p>Until next month!</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>If you are not familiar with Rust, think of a think of a Java
interface or Python abstract class.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>It is quite tempting to start diving into performance tweaks
and improvements. But that is honestly terrible at this stage because it
just postpones an actual funcional release.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>Measuring test coverage in Rust</title><link>https://whynothugo.nl/journal/2023/11/27/measuring-test-coverage-in-rust/</link><pubDate>Mon, 27 Nov 2023 13:10:56 +0800</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/11/27/measuring-test-coverage-in-rust/</guid><description><![CDATA[<p>I want to measure test coverage for the <a href="/journal/2023/11/27/vdirsyncer-status-update-november-2023/"><code>vparser</code> library</a>, and this
is my first time measuring coverage with Rust. Some notes for future reference.</p>
<p>First, run the tests with instrumentation enabled:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-console" data-lang="console"><span style="display:flex;"><span>&gt; RUSTFLAGS<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;-C instrument-coverage&#34;</span> cargo test -p vparser
</span></span><span style="display:flex;"><span>   Compiling vparser v0.1.0 (/home/hugo/src/git.sr.ht/~whynothugo/vdirsyncer-rs/vparser)
</span></span><span style="display:flex;"><span>    Finished test [unoptimized + debuginfo] target(s) in 0.33s
</span></span><span style="display:flex;"><span>     Running unittests src/lib.rs (target/debug/deps/vparser-5204a1fc06fb7f74)
</span></span><span style="display:flex;"><span>...
</span></span></code></pre></div><p>In some cases, you might need to merge multiple <code>profraw</code> files into a single
one. This was not required in this particular case, but should be kept in mind
if multiple files were generated.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>llvm-profdata merge -sparse vparser/default_*.profraw -o vparser.profdata
</span></span></code></pre></div><p>Finally, show coverage with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>llvm-cov show --instr-profile vparser.profdata --object target/debug/deps/vparser-5204a1fc06fb7f74 --show-line-counts-or-regions
</span></span></code></pre></div><p>Note that the path <code>target/debug/deps/vparser-5204a1fc06fb7f74</code> should match
the one printed by <code>cargo test</code> above.</p>
<p>Both <code>llvm-profdata</code> and <code>llvm-cov</code> are provided by the <code>llvm</code> package.</p>
<p>It would be nice to find a plugin that reflects these results inside neovim (to
quickly spot lines that are not covered).</p>
]]></description></item><item><title>Setting a battery charge threshold</title><link>https://whynothugo.nl/journal/2023/11/24/setting-a-battery-charge-threshold/</link><pubDate>Fri, 24 Nov 2023 12:46:47 +0800</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/11/24/setting-a-battery-charge-threshold/</guid><description><![CDATA[<p>Modern batteries degrade faster if continuously charged to 100%. Some vendors
provide software implementations to avoid continuously feeding the battery when
full to avoid overloading it. Usually this is tricky, since they have to invent
some kind of mechanism to determine whether the user wants the battery fully
charged or not.</p>
<p>In the case of my laptop, the grand majority of my usages happens while plugged
into a power source. Continuously charging the battery in this state merely
serves to reduce its battery life and keep it warm, neither of which is
desirable.</p>
<p>I decided to set the maximum charge to 80% to avoid this. Sadly, I don&rsquo;t have a
second identical laptop to use as control group, so I&rsquo;ll never know if this has
the impact that I expect. The theory indicates that it should.</p>
<h2 id="setting-the-threshold">Setting the threshold<div class="permalink">[<a href="#setting-the-threshold">permalink</a>]</div></h2>
<p>The Linux battery driver exposes a value to configure the charge threshold.
Changing it is as simple as writing a number into the <code>charge_stop_threshold</code>
file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>echo <span style="color:#ae81ff">80</span> &gt; /sys/class/power_supply/BAT0/charge_stop_threshold
</span></span></code></pre></div><p>The above won&rsquo;t work unless it is executed as <code>root</code>. The following approach
works as an unprivileged user:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>echo <span style="color:#ae81ff">80</span> | doas tee /sys/class/power_supply/BAT0/charge_stop_threshold
</span></span></code></pre></div><p>However, when the system reboots, the driver is loaded in its default state,
and this setting is not preserved. It needs to be specified at each start-up.</p>
<h2 id="running-a-command-after-each-start-up">Running a command after each start-up<div class="permalink">[<a href="#running-a-command-after-each-start-up">permalink</a>]</div></h2>
<p>I could write a system service that does this, but it seems like an overkill
for a one-liner.</p>
<p>Alpine currently uses OpenRC, which includes a very simple mechanism to run
custom scripts during start-up. Basically, they just need to be placed in the
<code>/etc/local.d/</code> directory, with the extension <code>.start</code>.</p>
<p>The main caveat to keep in mind is that these scripts are in a blocking manner;
the next service won&rsquo;t start until they&rsquo;re done. Fortunately, this specific
script is near-instantaneous, so this isn&rsquo;t a problem.</p>
<p>For a more in-depth explanation on this feature, see <a href="https://wiki.gentoo.org/wiki//etc/local.d">the relevant article in
the Gentoo wiki</a>.</p>
<h2 id="setting-the-threshold-after-each-start-up">Setting the threshold after each start-up<div class="permalink">[<a href="#setting-the-threshold-after-each-start-up">permalink</a>]</div></h2>
<p>Create the file <code>/etc/local.d/20-batery-threshold.start</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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/sh
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> battery in /sys/class/power_supply/*/charge_stop_threshold; <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>	echo <span style="color:#ae81ff">80</span> &gt; $battery;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">done</span>
</span></span></code></pre></div><p>Make it executable:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>doas chmod +x /etc/local.d/20-batery-threshold.start
</span></span></code></pre></div><p>Enable and start the <code>local</code> service:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>doas rc-update add local default
</span></span><span style="display:flex;"><span>doas service local start
</span></span></code></pre></div><h2 id="over-engineering-this-idea">Over-engineering this idea<div class="permalink">[<a href="#over-engineering-this-idea">permalink</a>]</div></h2>
<p>It would be useful to have a privileged daemon that lets me change this at
runtime (likely via a control socket). I could then have a widget in my status
bar that indicates the current threshold and lets me toggle it.</p>
<p>For now, the above approach is sufficient.</p>
]]></description></item><item><title>Setting up an Alpine Linux workstation</title><link>https://whynothugo.nl/journal/2023/11/19/setting-up-an-alpine-linux-workstation/</link><pubDate>Sun, 19 Nov 2023 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/11/19/setting-up-an-alpine-linux-workstation/</guid><description><![CDATA[<p>In the upcoming months I will travel to visit family and friends. I intend to
work remotely during some of those weeks (and will take some other weeks off
too). In preparation for this, I&rsquo;ve set up a new<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> <strong>ThinkPad T14s Gen 2i</strong>
laptop that I&rsquo;ll be using to work remotely.</p>
<p>This article covers the steps that I take when setting up Alpine on this
device. It is not intended to cover all possible installation options, but is a
reference of my particular setup.</p>
<p>My setup uses <code>btrfs</code> for the main partition, with a subvolume for the root
partition. This allows me to create snapshots of it and roll back to one of
these snapshots if necessary. I intend to build a system which allow atomic
updates on top of this.</p>
<!-- A table of content would be nice here, but gohugo makes everything so hard -->
<h2 id="a-minimal-intro-to-btrfs">A minimal intro to btrfs<div class="permalink">[<a href="#a-minimal-intro-to-btrfs">permalink</a>]</div></h2>
<p><em>Skip this section if you are familiar with btrfs.</em></p>
<p>Btrfs supports subvolume. A subvolume is created for a directory inside a btrfs
partition, and that subvolume can then be mounted as if it were a filesystem of
its own.</p>
<p>If a partition contains a directory <code>media/videos</code>, and one creates a subvolume
for that path, it is them possible to mount that <code>media/videos</code> directory into
any location without the need to mount the rest of the filesystem. Subvolumes
can somewhat be though of as sub-partitions, where the pool of free space is
shared amongst all of them.</p>
<p>Btrfs also supports copy-on-write, which allows creating a copy of a file or
directory without actually copying the data on disk. When <code>/home/hugo/orig</code> is
copied to <code>/home/hugo/dest</code>, the filesystem will only mark the latter is a copy
of the former. If later modifications are made to one of these, then the
filesystem will write the modified version into a new location, taking care to
leave the original file unchanged.</p>
<p>These two features combined are what allows implementing snapshots: a copy of a
subvolume at a given time. The subvolume can be further modified, but the
snapshot keeps an unchanged copy of the data.</p>
<!-- TODO: titles should be imperative -->
<h2 id="booting-into-the-installer">Booting into the installer<div class="permalink">[<a href="#booting-into-the-installer">permalink</a>]</div></h2>
<p><a href="https://alpinelinux.org/downloads/">Download Alpine&rsquo;s installation image</a>. Copy in into the USB drive
<a href="https://wiki.alpinelinux.org/wiki/Installation#Flashing_(direct_data_writing)_the_installation_image-file_onto_a_device_or_media">as explained in the wiki</a>. E.g.:</p>
<!-- sh formatting highlights the 'if' in a weird way. -->
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>doas dd if=alpine-extended-3.18.4-x86_64.iso of=/dev/sdX bs=512
</span></span></code></pre></div><p>Plug the USB drive into the laptop, turn on the device and immediately start
tapping F12 (about once per second until the laptop plays a short beep) to
enter the boot menu. Select the USB drive.</p>
<p>Once Alpine boots, log in as <code>root</code> (there is no password in the installation
media).</p>
<h2 id="preparing-the-disk">Preparing the disk<div class="permalink">[<a href="#preparing-the-disk">permalink</a>]</div></h2>
<p>Use <code>fdisk -l</code> to inspect local disks. The internal drive is <code>/dev/nvme0n1</code>.</p>
<p>The <code>setup-alpine</code> command will do fine for most of the setup, but partitions
and bootloader will be configured manually, since the intended setup diverges
from what the setup scripts support.</p>
<p><code>setup-alpine</code> will prompt for a few things:</p>
<ul>
<li>Keyboard: <code>us</code>.</li>
<li>Keyboard variation: <code>us-altgr-intl</code>.</li>
<li>Hostname: <code>device-name.whynothugo.nl</code>.</li>
<li>Network adapter: <code>wlan0</code>.</li>
<li>Wifi network.</li>
<li>Wifi password. Care must be taken to avoid typos here; apparently if this is
typed wrong the network ends up in a broken state and doesn&rsquo;t outright fail.</li>
<li>IP address: <code>dhcp</code>.</li>
<li>Networks: <code>done</code>.</li>
<li>Manual network configuration: <code>n</code>.</li>
<li>The default DNS is the one provided by the local router. This is fine for
now.</li>
<li><code>root</code> password: empty; this account will be <a href="#locking-the-root-user-account">locked</a> later in this process.</li>
<li>Time zone: <code>Europe/Amsterdam</code>.</li>
<li>User account: <code>hugo</code>. This user will be a member of the <code>wheel</code> group, and
has (by default) privileges to use  <code>doas</code>.</li>
<li>The defaults are fine for all remaining steps.</li>
</ul>
<p>Next, partition the disk manually (to create the btrfs subvolume for the root
and home partitions).</p>
<p><em>Also of note: the default swap partition created by the installer is way too
small and might not work when suspending to disk. When the system is
suspended-to-disk, it copies the contents of the RAM into the swap partition,
so the swap should be at least as large as the amount of RAM available. The
swap partition also needs to be encrypted, which the installation script do not
support.</em></p>
<p>Use <code>cfdisk</code> to partition the disk and create three partitions:</p>
<ul>
<li>A 600M <a href="https://wiki.archlinux.org/title/EFI_system_partition">EFI System Partition (ESP)</a>.</li>
<li>A swap equal to physical memory size.</li>
<li>A main partition with all remaining space.</li>
</ul>
<p>Next, format the partitions:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>apk add btrfs-progs cryptsetup
</span></span><span style="display:flex;"><span>modprobe btrfs
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>mkfs.vfat /dev/nvme0n1p1
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Create encrypted partitions for swap and main partition.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Hint: use the same passphrase for both so that they can be unlocked in unison.</span>
</span></span><span style="display:flex;"><span>cryptsetup luksFormat --label luks-swap /dev/nvme0n1p2
</span></span><span style="display:flex;"><span>cryptsetup luksFormat --label luks-main /dev/nvme0n1p3
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Unlock the newly created partitions:</span>
</span></span><span style="display:flex;"><span>cryptsetup open /dev/nvme0n1p2 swap
</span></span><span style="display:flex;"><span>cryptsetup open /dev/nvme0n1p3 main
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Format the inner layer of the encrypted partitions:</span>
</span></span><span style="display:flex;"><span>mkfs.btrfs --label main /dev/mapper/main
</span></span><span style="display:flex;"><span>mkswap /dev/mapper/swap
</span></span></code></pre></div><p>Temporarily mount the btrfs partition:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>mount /dev/mapper/root /mnt/
</span></span></code></pre></div><p>Create two subvolumes inside of 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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span><span style="color:#75715e"># This implicitly creates the directories too.</span>
</span></span><span style="display:flex;"><span>btrfs subvolume create /mnt/root
</span></span><span style="display:flex;"><span>btrfs subvolume create /mnt/home
</span></span></code></pre></div><p>And unmount the partition:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>umount /mnt
</span></span></code></pre></div><p>Now mount the subvolume that will be used as the root partition. <code>subvol=root</code>
here refers to the name of the subvolume created above (it is the directory
path relative to the root of the partition).</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>mount /dev/mapper/fs -o subvol<span style="color:#f92672">=</span>root /mnt/
</span></span></code></pre></div><p>Then mount the ESP and the home subvolume inside of the above:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>mkdir /mnt/home /mnt/efi
</span></span><span style="display:flex;"><span>mount /dev/nvmen1p1 /mnt/efi
</span></span><span style="display:flex;"><span>mount /dev/mapper/fs -o subvol<span style="color:#f92672">=</span>home /mnt/home
</span></span></code></pre></div><p>Finally, set up Alpine into the mounted partitions. This will also generates
<code>/mnt/etc/fstab</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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>setup-disk /mnt
</span></span></code></pre></div><h2 id="switching-into-the-installation-filesystem">Switching into the installation filesystem<div class="permalink">[<a href="#switching-into-the-installation-filesystem">permalink</a>]</div></h2>
<p><a href="https://man7.org/linux/man-pages/man1/chroot.1.html"><code>chroot</code></a> into the new installation. Also mount <code>/proc</code> and <code>/dev</code> inside the
new root:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>chroot /mnt
</span></span><span style="display:flex;"><span>mount -t proc proc /proc
</span></span><span style="display:flex;"><span>mount -t devtmpfs dev /dev
</span></span></code></pre></div><p>Enable the community and testing repositories, and switch to the Alpine Edge
branch. This is done by editing <code>/etc/apk/repositories</code> and replacing its
contents with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>http://dl-cdn.alpinelinux.org/alpine/edge/main
</span></span><span style="display:flex;"><span>http://dl-cdn.alpinelinux.org/alpine/edge/community
</span></span><span style="display:flex;"><span>http://dl-cdn.alpinelinux.org/alpine/edge/testing
</span></span></code></pre></div><p>Some regions may have networking issues reaching the global CDN mirror. If this
is the case, <a href="https://mirrors.alpinelinux.org/">a local mirror</a> should be used instead.</p>
<h2 id="setting-up-the-bootloader">Setting up the bootloader<div class="permalink">[<a href="#setting-up-the-bootloader">permalink</a>]</div></h2>
<!-- TODO: move the ESP to /boot/efi? -->
<p>When the firmware is done initialising, it will load a bootloader: a program
responsible for loading the actual operating system itself. I will be using a
UEFI bundle (nowadays also known as a UKI) to boot the system. This is a
executable binary which bundles the following:</p>
<ul>
<li>The kernel itself</li>
<li>The kernel&rsquo;s command line parameters</li>
<li>A small stub that execute the kernels with that command line</li>
<li>The <a href="https://wiki.archlinux.org/title/Arch_boot_process#initramfs">initramfs</a> (a small read-only filesystem with the necessary userspace
utilities to boot into the main system).</li>
</ul>
<p>There are additional notes on this setup <a href="https://wiki.alpinelinux.org/wiki/UEFI_Secure_Boot#Generating_Unified_Kernel_Image">in the Alpine Wiki</a>.</p>
<p>The stub itself is provided by the <code>gummiboot</code> package. It is considered
deprecated, but no solid alternative is available (I do have one in progress,
but still has some rough edges). The bundle itself is built by <code>efi-mkuki</code>, and
<code>secureboot-hook</code> will rebuild the bundle after each kernel upgrade.</p>
<p>Install the mentioned packages:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>apk add efi-mkuki secureboot-hook gummiboot
</span></span></code></pre></div><p>I use the <code>linux-edge</code> kernel:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>apk add linux-edge
</span></span><span style="display:flex;"><span>apk del linux-lts
</span></span></code></pre></div><p>Install <code>blkid</code> which will be used in a moment. This tool prints the <code>uuid</code> (and
a few other details) for a specified partition. This is the recommended way to
address a partition ambiguously:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>apk add blkid
</span></span></code></pre></div><p>Edit <code>/etc/kernel-hooks.d/secureboot.conf</code> with the following. See below for
how to determine the exact value for these UUIDs. <code>secureboot.conf</code> should look
something 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;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>cmdline=&#34;root=UUID=5021db58-cc3a-4829-a630-2d468f8d1761 ro rootflags=subvol=root cryptroot=UUID=0db973a0-1b95-4a23-a63f-cb6248fe2bf7 cryptdm=crypt modules=sd-mod,btrfs,nvme quiet rootfstype=btrfs hostname=destiny.whynothugo.nl&#34;
</span></span><span style="display:flex;"><span>signing_disabled=yes
</span></span><span style="display:flex;"><span>output_dir=&#34;/efi/EFI/Boot&#34;
</span></span><span style="display:flex;"><span>output_name=&#34;bootx64.efi&#34;
</span></span></code></pre></div><!-- TODO: set up secureboot keys -->
<p>Signing is disabled only temporarily until I install the proper keys.</p>
<p>The output path for the above is <code>/efi/EFI/Boot/bootx64.efi</code>, which is the
default path where UEFI firmware looks for a bootloader.</p>
<p><em>Note: While it is possible to configure additional paths for the firmware to
run executables, support for this varies, and configuration data is often lost
after firmware updates, so I prefer to avoid the approach. Note that this
particular setup is incompatible with booting multiple operating systems on the
same host.</em></p>
<ul>
<li>The <code>root</code> UUID can be determined with:</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span><span style="color:#75715e"># Append this data at the end of the file; this prevents having to copy it</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># manually. Edit the file, cut out the UUID and delete the rest of the line</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># appended by `blkid`</span>
</span></span><span style="display:flex;"><span>blkid /dev/mapper/main nvme0n1p3 &gt;&gt; /etc/kernel-hooks.d/secureboot.conf
</span></span></code></pre></div><ul>
<li>The <code>cryptroot</code> UUID can be determined with:</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span><span style="color:#75715e"># Same as above:</span>
</span></span><span style="display:flex;"><span>blkid /dev/nvme0n1p3 &gt;&gt; /etc/kernel-hooks.d/secureboot.conf
</span></span></code></pre></div><ul>
<li><code>hostname</code> is optional, but specifying it here ensures that the hostname is
set at the earliest moment possible.</li>
</ul>
<p>During boot, the bootloader will hand over control to the kernel. At this
stage, the only filesystem is the initramfs. The kernel needs the <code>cryptsetup</code>
user-space utilities in order to mount the encrypted root partition, so they
must be included in the initramfs.</p>
<p>Edit <code>/etc/mkinitfs/mkinitfs.conf</code> and add <code>crypsetup</code> to the <code>features</code>
variable. While editing this line, it is also safe to delete <code>virtio</code>, which is
used only in virtual machines. Also add <code>kms</code> to enable <a href="https://wiki.archlinux.org/title/Kernel_mode_setting">kernel mode
setting</a>.</p>
<p>Also disable the <code>mkinitfs</code> trigger. This trigger builds the initramfs into a
standalone file, but that won&rsquo;t be used for this setup; the initramfs is
included in the UEFI bundle:</p>
<p>The resulting file should look something 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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>features<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;ata base cdrom btrfs keymap kms mmc nvme raid scsi usb cryptsetup cryptkey resume&#34;</span>
</span></span><span style="display:flex;"><span>disable_trigger<span style="color:#f92672">=</span>yes
</span></span></code></pre></div><p>Remove GRUB. The generated configuration is broken for the current setup, so it
won&rsquo;t work as-is and will get in the way.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>apk del grub-efi grub
</span></span></code></pre></div><p>Finally, trigger the newly created kernel hook so that all the right files are
copied into <code>/efi</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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>apk fix kernel-hooks
</span></span></code></pre></div><h2 id="setting-zsh-as-the-default-shell">Setting <code>zsh</code> as the default shell<div class="permalink">[<a href="#setting-zsh-as-the-default-shell">permalink</a>]</div></h2>
<p>My shell of choice is <code>zsh</code>. It is ideal for interactive usage, having great
support for auto-completion and a few other interactive features.</p>
<p>Install <code>zsh</code> and set it as the default shell for my user:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>apk add zsh
</span></span><span style="display:flex;"><span>vi /etc/passwd <span style="color:#75715e"># locate user &#39;hugo&#39; and replace `/bin/ash` with `/bin/zsh`.</span>
</span></span></code></pre></div><h2 id="rebooting-into-the-installed-os">Rebooting into the installed OS<div class="permalink">[<a href="#rebooting-into-the-installed-os">permalink</a>]</div></h2>
<p>Unplug the installation media to prevent booting from it again and then reboot:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>exit <span style="color:#75715e"># leave the chroot</span>
</span></span><span style="display:flex;"><span>reboot
</span></span></code></pre></div><h2 id="installing-packages">Installing packages<div class="permalink">[<a href="#installing-packages">permalink</a>]</div></h2>
<p>Alpine keeps a list of &ldquo;wanted&rdquo; packages in <code>/etc/apk/world</code> (see: <a href="/journal/2023/02/18/in-praise-of-alpine-and-apk/">In praise
of Alpine and apk</a>). I trimmed the list from my previous system
with what I&rsquo;ll be wanting initially on this laptop.</p>
<p>The resulting <code>world</code> file <a href="https://paste.sr.ht/~whynothugo/94c2a31691eb15be659123f06187652862d0b2ae">is available here</a>.</p>
<p>This is a pretty large amount of packages. It includes everything that I&rsquo;ll
need on a daily use workstation and a few extras.</p>
<p>The <code>world</code> file generated during installation contained some firmware packages
that should be kept around:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>linux-firmware-i915
</span></span><span style="display:flex;"><span>linux-firmware-intel
</span></span><span style="display:flex;"><span>linux-firmware-mediatek
</span></span><span style="display:flex;"><span>linux-firmware-other
</span></span><span style="display:flex;"><span>linux-firmware-rtl_bt
</span></span></code></pre></div><p>Rather than overwrite the <code>world</code> file, concatenate both, removing duplicates:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>cat world-2023-10-25 /etc/apk/world | sort | uniq &gt; new_world
</span></span><span style="display:flex;"><span>mv new_world /etc/apk/world
</span></span></code></pre></div><p>And then upgrade all packages:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>apk update
</span></span><span style="display:flex;"><span>apk upgrade -a
</span></span></code></pre></div><p>If anything fails, <code>apk</code> can be instructed to fix the system (using
<code>/etc/apk/world</code> as a reference) using:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>apk fix
</span></span></code></pre></div><h2 id="configuring-the-user-session">Configuring the user session<div class="permalink">[<a href="#configuring-the-user-session">permalink</a>]</div></h2>
<p>The system is in a working state at this point, but <code>$HOME</code> is completely
empty, and logging in just provides a bare <code>zsh</code> shell.</p>
<p>I keep all my configuration files in my dotfiles repository. Clone that first:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>git clone https://git.sr.ht/~whynothugo/dotfiles .dotfiles
</span></span><span style="display:flex;"><span>cd .dotfiles
</span></span><span style="display:flex;"><span>git submodule update --init <span style="color:#75715e"># Pull git submodules. Mostly, neovim plugins.</span>
</span></span></code></pre></div><p>My git configuration is set to rebase when pulling from a remote. This will
affect any local repository created from this point forward, but not this one
because it was created before git&rsquo;s configuration was in-place. This needs to
be configured manually for this single repository:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>git config pull.rebase true
</span></span></code></pre></div><p>The helper that symlinks dotfiles into their &ldquo;real&rdquo; location is in the same
repository. It can be built and executed with <code>cargo</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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>cargo run
</span></span></code></pre></div><p>After logging out and logging in again sway fails to start and the session ends
immediately. Switch to another tty (sway only auto-starts on tty1) with
<kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>F2</kbd> to fix this.</p>
<p>The reason that sway won&rsquo;t start just yet is that it requires <code>seatd</code> in order
to access input/output interfaces. Enable this service and start it now:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>doas rc-update add seatd default
</span></span><span style="display:flex;"><span>doas service seatd start
</span></span></code></pre></div><p>In order to connect to <code>seatd</code>, my user account needs to be a member of the
<code>seat</code> group. This is a moment to add the user account to a few other groups
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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>doas adduser hugo seat <span style="color:#75715e"># For seatd</span>
</span></span><span style="display:flex;"><span>doas adduser hugo audio <span style="color:#75715e"># For audio hardware.</span>
</span></span><span style="display:flex;"><span>doas adduser hugo dialout <span style="color:#75715e"># For serial consoles.</span>
</span></span><span style="display:flex;"><span>doas adduser hugo kvm <span style="color:#75715e"># For hardware virtualisation capabilities.</span>
</span></span><span style="display:flex;"><span>doas adduser hugo qemu <span style="color:#75715e"># Ditto</span>
</span></span><span style="display:flex;"><span>doas adduser hugo gnupg <span style="color:#75715e"># Smart-cards.</span>
</span></span><span style="display:flex;"><span>doas adduser hugo power <span style="color:#75715e"># For powerctl (power management tool).</span>
</span></span><span style="display:flex;"><span>doas adduser hugo video <span style="color:#75715e"># For video devices. See below.</span>
</span></span></code></pre></div><p>The <code>video</code> group is required to access video input devices like cameras.
Unfortunately, it also grants access to video output/rendering hardware, which
is not required and a security liability. I reported this to both
<a href="https://gitlab.alpinelinux.org/alpine/aports/-/issues/15409">Alpine</a> and <a href="https://github.com/eudev-project/eudev/issues/268"><code>eudev</code></a>. I&rsquo;ll deal with it in greater
detail later.</p>
<p>As <a href="https://wiki.alpinelinux.org/wiki/Sway#Installation">indicated in the Alpine wiki</a>, <code>sway</code> also requires <code>udev</code> to be
set up:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>setup-devd udev
</span></span></code></pre></div><p>After logging out and back in again (still on tty2), running <code>sway</code> works. The
font size rather small, but the scale can be adjusted with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>swaymsg output <span style="color:#e6db74">&#39;*&#39;</span> scale 1.5
</span></span></code></pre></div><p>The actual name of this output can be determined with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>swaymsg -t get_outputs
</span></span><span style="display:flex;"><span>swaymsg -t get_inputs <span style="color:#75715e"># Prints input devices; used to locate the keyboard id.</span>
</span></span></code></pre></div><p>Also add a line to my sway configuration file (<code>~/.config/sway/config</code>) so that
this scale is applied again on subsequent runs:</p>
<!-- scfg doesn't get highlighted by gohugo -->
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>output <span style="color:#e6db74">&#34;IVO 0x057D Unknown&#34;</span> <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  scale 1.5
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><p>Configure the keyboard to map the <kbd>CapsLock</kbd> to <kbd>Esc</kbd>. <a href="https://git.sr.ht/~whynothugo/dotfiles/commit/d68306f961e0f9d170fcba031980dcb1a2e2fc09">Here
is the relevant commit</a>.</p>
<p>I use a work-in-progress helper to manage my user services. It&rsquo;s called <code>usvc</code>
and uses <code>s6-rc</code> under the hood. Before its first run, the services database
needs to be compiled at least once:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>usvc compile
</span></span></code></pre></div><p>Usually it is auto-started via sway, but failed to start when sway started
because its database was not compiled yet. It can easily be started manually:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>usvc run
</span></span></code></pre></div><h2 id="initialising-the-secret-store">Initialising the secret store<div class="permalink">[<a href="#initialising-the-secret-store">permalink</a>]</div></h2>
<p>Running <code>usvc run</code> shows an error for <a href="https://himitsustore.org/"><code>himitsud</code></a>; it fails to start due to
being uninitialised. It can be initialised with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>himitsu-store -i
</span></span></code></pre></div><p>Start <code>himitsud</code> again (this service won&rsquo;t auto-restart if the database is
missing, otherwise it would thrash CPU pointlessly):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>usvc start himitsud
</span></span></code></pre></div><h2 id="setting-up-abuild">Setting up <code>abuild</code><div class="permalink">[<a href="#setting-up-abuild">permalink</a>]</div></h2>
<p>I&rsquo;ll eventually want to build Alpine packages locally, which requires
setting up an <code>abuild</code> environment. This is already well-documented <a href="https://wiki.alpinelinux.org/wiki/Abuild_and_Helpers#Setting_up_the_build_environment">in the
Alpine wiki</a>, and is basically just:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>doas adduser hugo abuild
</span></span><span style="display:flex;"><span>abuild-keygen -a -i
</span></span></code></pre></div><h2 id="fixing-audio">Fixing audio<div class="permalink">[<a href="#fixing-audio">permalink</a>]</div></h2>
<p>This sound doesn&rsquo;t seem to be working. Running <code>pavucontrol</code> shows no input or
output devices. Checking <code>dmesg</code> seems to indicate missing firmware:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-console" data-lang="console"><span style="display:flex;"><span>&gt; dmesg | grep firmw
</span></span><span style="display:flex;"><span>[    1.096997] i915 0000:00:02.0: [drm] Finished loading DMC firmware i915/tgl_dmc_ver2_12.bin (v2.12)
</span></span><span style="display:flex;"><span>[   36.239398] iwlwifi 0000:00:14.3: loaded firmware version 77.2df8986f.0 QuZ-a0-hr-b0-77.ucode op_mode iwlmvm
</span></span><span style="display:flex;"><span>[   36.363387] Bluetooth: hci0: Minimum firmware build 1 week 10 2014
</span></span><span style="display:flex;"><span>[   36.365863] Bluetooth: hci0: Found device firmware: intel/ibt-19-0-4.sfi
</span></span><span style="display:flex;"><span>[   36.447917] sof-audio-pci-intel-tgl 0000:00:1f.3: Direct firmware load for intel/sof/sof-tgl.ri failed with error -2
</span></span><span style="display:flex;"><span>[   36.447919] sof-audio-pci-intel-tgl 0000:00:1f.3: error: sof firmware file is missing, you might need to
</span></span><span style="display:flex;"><span>[   36.447921] sof-audio-pci-intel-tgl 0000:00:1f.3: error: failed to load DSP firmware -2
</span></span><span style="display:flex;"><span>[   36.714932] psmouse serio1: trackpoint: Elan TrackPoint firmware: 0xa1, buttons: 3/3
</span></span><span style="display:flex;"><span>[   38.519487] Bluetooth: hci0: Waiting for firmware download to complete
</span></span></code></pre></div><p>Filtering <a href="https://pkgs.alpinelinux.org/contents">Alpine package</a> to those containing a file called <code>sof-tgl.ri</code>
indicates that the package <code>sof-firmware</code> contains this file.</p>
<p>I&rsquo;m not familiar with the organisation that maintains this package upstream,
but some quick searching online indicates that they&rsquo;re part of the ALSA project
and under the umbrella of the Linux Foundation, so I guess that installing this
package should be safe.</p>
<p>There&rsquo;s likely some way to force reloading of this firmware, but rebooting is
easy enough.</p>
<h2 id="setting-up-dhcp">Setting up DHCP<div class="permalink">[<a href="#setting-up-dhcp">permalink</a>]</div></h2>
<p>During reboot I noticed that the <code>networking</code> service blocks the start-up
process from continuing until the <code>dhcp</code> client has resolved an IP address.
This is annoying and unnecessary<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>. This behaviour can be disabled by removing
the line <code>iface wlan0 inet dhcp</code> from <code>/etc/network/interfaces</code>.</p>
<p>And then run the <code>dhcp</code> by itself.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>doas rc-update add dhcpcd default
</span></span></code></pre></div><p>When run like this, the DHCP client run for any network interface that is
connected and has signal, but will do so in the background without blocking the
boot process.</p>
<h2 id="configuring-iwctl-for-wireless-network-management">Configuring <code>iwctl</code> for wireless network management<div class="permalink">[<a href="#configuring-iwctl-for-wireless-network-management">permalink</a>]</div></h2>
<p>I use <code>iwd</code> to manage wireless networks. It conflicts with <code>wpa_supplicant</code>, so
disable the latter and enable the former:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>doas rc-update del wpa_supplicant boot
</span></span><span style="display:flex;"><span>doas rc-update add iwd default
</span></span><span style="display:flex;"><span>doas service wpa_supplicant stop
</span></span><span style="display:flex;"><span>doas service iwd start
</span></span></code></pre></div><p><code>iwctl</code> can be used to control <code>iwd</code>. Basic <code>iwctl</code> usage (via interactive
mode):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>help
</span></span><span style="display:flex;"><span>station list
</span></span><span style="display:flex;"><span>station wlan0 scan
</span></span><span style="display:flex;"><span>station wlan0 get-networks
</span></span><span style="display:flex;"><span>station wlan0 connect MyNetwork
</span></span><span style="display:flex;"><span># password prompt here
</span></span></code></pre></div><p>After running <code>get-networks</code>, <code>iwctl</code>  will often starts flashing, due to how
it keeps refreshing the list (this is likely a fixable bug).</p>
<p><code>iwctl</code> is also usable as a command line interface. Running the following
commands in a regular shell is equivalent to the above:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>iwctl station wlan0 scan
</span></span><span style="display:flex;"><span>iwctl station wlan0 get-networks
</span></span><span style="display:flex;"><span>iwctl station wlan0 connect MyNetwork
</span></span><span style="display:flex;"><span><span style="color:#75715e"># iwctl prompts for password</span>
</span></span></code></pre></div><p>On a separate terminal run <code>ip a</code> to confirm that network is connected (check
for an IP address in the local network) and <a href="https://git.sr.ht/~whynothugo/dotfiles/tree/3f722af25eaf8e64680238808c2535ac10f0fe74/item/home/.local/bin/net"><code>net</code></a> to check that the Internet
is reachable.</p>
<p>I would prefer a simpler interface which just shows a list of wifi networks.
Such a list would allow me to pick a network with the arrow keys (or maybe just
click on it or type-to-fuzzy-filter). I know of no such tool. Most UIs are
either entangled into entire Desktop Environments (and not usable elsewhere),
or are less convenient to use.</p>
<h2 id="configuring-the-firewall-and-sshd">Configuring the firewall and sshd<div class="permalink">[<a href="#configuring-the-firewall-and-sshd">permalink</a>]</div></h2>
<p><code>nftables</code> blocks all incoming connections by default.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>doas rc-update add nftables boot
</span></span></code></pre></div><p>The following rule allows incoming connections on <code>sshd</code>&rsquo;s default port, port
22. It should be installed into <code>/etc/nftables.d/sshd.nft</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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>table inet filter <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>	chain input <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>		tcp dport ssh accept comment <span style="color:#e6db74">&#34;Allow sshd&#34;</span>
</span></span><span style="display:flex;"><span>	<span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><p>And restart <code>nftables</code> 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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>doas service nftables restart
</span></span></code></pre></div><p>On another host, run <code>nmap</code> to confirm that all ports are filtered.:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-console" data-lang="console"><span style="display:flex;"><span>&gt; doas nmap -sS -T4 192.168.21.151
</span></span><span style="display:flex;"><span>doas (hugo@hyperion.whynothugo.nl) password:
</span></span><span style="display:flex;"><span>Starting Nmap 7.94 ( https://nmap.org ) at 2023-10-25 20:28 CEST
</span></span><span style="display:flex;"><span>Nmap scan report for destiny.lan (192.168.21.151)
</span></span><span style="display:flex;"><span>Host is up (0.100s latency).
</span></span><span style="display:flex;"><span>Not shown: 998 filtered tcp ports (no-response), 1 filtered tcp ports (port-unreach)
</span></span><span style="display:flex;"><span>PORT   STATE SERVICE
</span></span><span style="display:flex;"><span>22/tcp open  ssh
</span></span><span style="display:flex;"><span>MAC Address: 28:11:A8:1A:96:22 (Intel Corporate)
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span>Nmap done: 1 IP address (1 host up) scanned in 16.32 seconds
</span></span></code></pre></div><p>When doing this,  I also re-ran this test while running <code>python -m http.server</code>
on the laptop. This command exposes an HTTP server on port 8000. The result
was, as expected, the same.</p>
<p>Next, I want to copy a public ssh key from another host. The local IP of the
new device can be determined with <code>ip a | grep inet</code><sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>. For this example,
I&rsquo;ll use <code>192.168.1.5</code> as an IP. On the other host, run:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>cd ~/.ssh/keys/
</span></span><span style="display:flex;"><span>ssh-keygen -t ed25519-sk -f hugo@destiny.whynothugo.nl
</span></span><span style="display:flex;"><span>ssh-copy-id -i destiny.whynothugo.nl.pub hugo@192.168.1.5
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Hint: use ssh localhost on the new device to see the fingerprint; they should be the same.</span>
</span></span></code></pre></div><p>I came across <a href="https://gitlab.alpinelinux.org/alpine/aports/-/issues/15401">this weird bug</a> in <code>ssh-copy-id</code>. Apparently it is
harmless. Trying to connect via SSH into the new device should now 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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>ssh -i ~/.ssh/keys/destiny.whynothugo.nl  hugo@192.168.1.5
</span></span></code></pre></div><p>Next, back onto the new device, I edit <code>sshd</code>&rsquo;s configuration file with <code>doas vi /etc/ssh/sshd_config</code> to disable logging in via password 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;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span> # To disable tunneled clear text passwords, change to no here!
</span></span><span style="display:flex;"><span>-#PasswordAuthentication yes
</span></span><span style="display:flex;"><span>+PasswordAuthentication no
</span></span></code></pre></div><p>Finally, restart <code>sshd</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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>doas service sshd restart
</span></span></code></pre></div><h2 id="locking-the-root-user-account">Locking the root user account<div class="permalink">[<a href="#locking-the-root-user-account">permalink</a>]</div></h2>
<p>Logging in as root is discouraged. The account can be locked with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>doas passwd -l root
</span></span></code></pre></div><h2 id="connecting-to-irc">Connecting to IRC<div class="permalink">[<a href="#connecting-to-irc">permalink</a>]</div></h2>
<p>Running <code>senpai</code> and it fails with an authentication 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;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>Connecting to chat.sr.ht...
</span></span><span style="display:flex;"><span>Error (code 904): Registration failed: Authentication failed
</span></span><span style="display:flex;"><span>Error (code ACCOUNT_REQUIRED): Authentication required
</span></span><span style="display:flex;"><span>Error (code UNKNOWN_COMMAND): LISTNETWORKS Unknown subcommand
</span></span></code></pre></div><p>My IRC credentials are missing on this host, but I&rsquo;d expect <code>senpai</code> to warn
about this rather than trying to connect with an empty password and fail. <a href="https://lists.sr.ht/~delthas/senpai-dev/patches/46088">The
fix for this issue</a> is rather simple.</p>
<p>Configuring credentials for <code>senpai</code> is something that <a href="/journal/2023/07/05/senpai-a-modern-irc-terminal-client/#senpai">I&rsquo;ve already
covered</a> and won&rsquo;t repeat here.</p>
<h2 id="pushing-dotfiles-changes">Pushing dotfiles changes<div class="permalink">[<a href="#pushing-dotfiles-changes">permalink</a>]</div></h2>
<p>I will want to push local changes to the dotfiles repository. For this, an SSH
key needs to be copied from another host. As an alternative, a new SSH key
could be created an registered on the remote server.</p>
<p>The remote used by git for the dotfiles repository is a read-only URL. A
read-write URL should be configured for pushing:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>cd .dotfiles
</span></span><span style="display:flex;"><span>git remote set-url origin --push git@git.sr.ht:~whynothugo/dotfiles
</span></span></code></pre></div><p><em>Hint: only change the url used for pushing, so that interacting with the SSH
key is not required when pulling changes.</em></p>
<p>At this point, trying to push changes won&rsquo;t work because ssh cannot access the
yubikey (my SSH key is hardware-backed). In order for this to work, my user
needs to be a member of the <code>plugdev</code> group:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>doas adduser hugo plugdev
</span></span></code></pre></div><p>Note that the <code>libfido2</code> also needs to be installed. It provides the <code>udev</code>
rules that allow members of the <code>plugdev</code> group to access these hardware
security keys. It was included in the bulk installation of packages above.</p>
<h2 id="using-an-u2f-security-key">Using an U2F security key<div class="permalink">[<a href="#using-an-u2f-security-key">permalink</a>]</div></h2>
<p>Trying to log into a website which requires two-factory authentication via a
hardware security key requires that I am member of the <code>plugdev</code> group, which
was done above.</p>
<h2 id="using-my-totp-helper">Using my <code>totp</code> helper<div class="permalink">[<a href="#using-my-totp-helper">permalink</a>]</div></h2>
<p>I use a helper called <code>totp</code> to generate one-time authentication tokens on my
yubikey. It does not require me to be a member of the <code>plugdev</code> group to
operate, but it does require the &ldquo;middleware to access smart cards&rdquo; to be
running. It can be enabled and started 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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>doas rc-update add pcscd default
</span></span><span style="display:flex;"><span>doas service pcscd start
</span></span></code></pre></div><h2 id="configuring-the-power-button-and-laptop-lid">Configuring the power button and laptop lid<div class="permalink">[<a href="#configuring-the-power-button-and-laptop-lid">permalink</a>]</div></h2>
<p>Pressing the power button seems to do nothing. After some quick research and
asking around, and it turns out that this particular power button needs to be
held for two seconds before it registers a key press. Apparently this is a
hardware quirk.</p>
<p>By default, when the power button is pressed, <code>acpid</code> will execute (as root)
the script at <code>/etc/acpi/PWRF/00000080</code>. This is a pretty bad approach for an
interactive system<sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup>, since I would need to somehow signal to the
current user session that this happened. Or maybe multiple sessions? Which one?
It&rsquo;s easier for this to be handled as a key press event by whomever is assigned
the seat right now.</p>
<p>The easiest way to disable this behaviour is to replace the entire script with
a blank file. Deleting it might result in it being recreated on package update
/ reinstall.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>doas truncate -s <span style="color:#ae81ff">0</span> /etc/acpi/PWRF/00000080
</span></span></code></pre></div><p>Reload the service with <code>doas service acpid restart</code>. Now when pressing the
power button, <code>sway</code> will receive a key press events for the <code>XF86PowerOff</code>
button. I already have a binding that triggers my <code>sleep-and-lock</code> script, so
nothing extra needs to be done here.</p>
<!-- TODO: add link to sleep-and-lock article -->
<p>Next, configure sway to turn off the display when the lid is closed. It does
not put the system to sleep; I prefer to explicitly put the system to sleep
with the power button, since I sometimes want to close the lid but keep
programs running.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>bindswitch --reload lid:on output eDP-1 disable
</span></span><span style="display:flex;"><span>bindswitch --reload lid:off output eDP-1 enable
</span></span></code></pre></div><p>In future, I would like to auto-sleep the system when the lid is closed, but I
would first need some mechanism that allows me to quickly inhibit auto-sleep
before closing the lid. Perhaps <code>caffeine-ng</code> can somehow fit this use case.</p>
<h2 id="enabling-bluetooth">Enabling bluetooth<div class="permalink">[<a href="#enabling-bluetooth">permalink</a>]</div></h2>
<p>The only thing that needs to be done is to enable the system service:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>doas rc-update add bluetooth default
</span></span></code></pre></div><h2 id="running-ansible">Running ansible<div class="permalink">[<a href="#running-ansible">permalink</a>]</div></h2>
<p>Settings for a few desktop applications are handled via <code>ansible</code>, and the
relevant files are also in my dotfiles repository:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>cd $HOME/.dotfiles/ansible
</span></span><span style="display:flex;"><span>ansible-playbook main.yaml -v
</span></span></code></pre></div><h2 id="configuring-automatic-login">Configuring automatic login<div class="permalink">[<a href="#configuring-automatic-login">permalink</a>]</div></h2>
<div class="notice update">
  <header>Update 2024-03-21</header>
  <section>
  Switched to running via OpenRC. This prevents a login prompt from showing up
after the compositor exits during shutdown.
  </section>
</div>
<p>At this stage, when turning on the laptop it will first prompt for the disk
encryption password and seconds later for the local user&rsquo;s password. A password
at this point is unnecessary, a bit annoying, and doesn&rsquo;t add any security.</p>
<p>Additionally, when I instruct the system to shut down, the compositor will exit
and a login prompt will briefly show up while shutdown finishes. This login
prompt is <code>getty</code>, which <code>init</code> restarts automatically when a session ends,
even if the system is shutting down.</p>
<p>I will configure <code>greetd</code> to log me in automatically at start-up. I will run it
as an OpenRC service, so that it doesn&rsquo;t get restarted when the system is
shutting down.</p>
<p>Note that I&rsquo;ve already installed <code>greetd</code> and <code>greetd-agreety</code> when defining
packages in <code>/etc/apk/world</code> above.</p>
<p>First, I disabled running <code>getty</code> on <code>tty1</code> by commenting out the relevant line
in <code>/etc/inittab</code>. The original line looks something 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;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>tty1::respawn:/sbin/getty 38400 tty1
</span></span></code></pre></div><p>The following script runs <code>greetd</code> on <code>tty1</code> with its dedicated configuration
file. I saved it to <code>/etc/init.d/greetd.tty1</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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span><span style="color:#75715e">#!/sbin/openrc-run
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>name<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;greetd&#34;</span>
</span></span><span style="display:flex;"><span>description<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Greeter daemon&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>supervisor<span style="color:#f92672">=</span>supervise-daemon
</span></span><span style="display:flex;"><span>cfgfile<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/etc/greetd/tty1.toml&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>command<span style="color:#f92672">=</span>/usr/sbin/greetd
</span></span><span style="display:flex;"><span>command_args<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;--config </span>$cfgfile<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>required_files<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span>$cfgfile<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>start_pre<span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>	<span style="color:#75715e"># note that this user is the user from $cfgfile</span>
</span></span><span style="display:flex;"><span>	checkpath -d -m750 -o greetd:greetd /run/greetd/
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>depend<span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>	need localmount
</span></span><span style="display:flex;"><span>	provide display-manager
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><p>The configuration file referenced above (<code>/etc/greetd/tty1.toml</code>) also needs to
be created. The following will log in automatically on boot:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#66d9ef">[terminal]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">vt</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">1</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">switch</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">false</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[default_session]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">command</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;agreety --cmd &#39;$SHELL --login&#39;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">user</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;greetd&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[initial_session]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">user</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;hugo&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">command</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;$SHELL --login&#34;</span>
</span></span></code></pre></div><p>Note that only the initial session automatically logs me in. When that session
ends, <code>agreety</code> will prompt for a username and password. This ensures that the
system remains locked if the compositor crashes while locked.</p>
<p>Finally, enable the above service:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>rc-update add greetd.tty default
</span></span></code></pre></div><p>As of the next boot, the system will prompt for the passphrase to unlock the
disk and then log into the user session without the need for any further
interaction.</p>
<h2 id="tweaking-firefox">Tweaking Firefox<div class="permalink">[<a href="#tweaking-firefox">permalink</a>]</div></h2>
<h3 id="opening-new-tabs">Opening new tabs<div class="permalink">[<a href="#opening-new-tabs">permalink</a>]</div></h3>
<p>Double clicking on the tab bar doesn&rsquo;t open new tabs for some reason (it simply
does nothing). It works on my previous computer, but not on this new device. It
seems that the setting <code>browser.tabs.inTitlebar</code> affects this behaviour.</p>
<p>Here&rsquo;s a screenshot of Firefox with <code>browser.tabs.inTitlebar=2</code> (the default).
Changing this to <code>1</code> has no affect (as far as I can tell).</p>
<figure>
<img src="tabs-in-titlebar-2.png"
     alt="Screenshot of Firefox's about:config page showing that browser.tabs.inTitlebar is set to 2">
</figure>
<p>Changing the setting to <code>0</code> enables the behaviour that I want: double clicking
on the tab bar opens a new tab. It also changes the colour scheme of the tab
bar and hides the &ldquo;close&rdquo; button.</p>
<figure>
<img src="tabs-in-titlebar-0.png"
     alt="Screenshot of Firefox's about:config page showing that browser.tabs.inTitlebar is set to 0">
</figure>
<p>I searched online for more details about this setting but found little
information. Apparently this setting affected how the tab bar was rendered
when it was first moved to the top of the window (I think this was like a
decade ago). I suspect that its impact on the behaviour of double-clicking is
due to historial/legacy reasons of how Firefox evolved.</p>
<p>I am not sure if mapping double click to no-op with the new default was
intentional or a bug; I will report this upstream when I have the time.</p>
<h3 id="adding-bookmarks-with-tags">Adding bookmarks with tags<div class="permalink">[<a href="#adding-bookmarks-with-tags">permalink</a>]</div></h3>
<p>When adding a page as a bookmark in Firefox, it doesn&rsquo;t show the input field
for tags. The fix for this is to add a bookmark without tags, open the
Bookmarks Organizer (<kbd>ctrl</kbd>+<kbd>shift</kbd>+<kbd>o</kbd>) and add a
tag to an existing bookmark. The tags field is now shown when adding new
bookmarks.</p>
<h2 id="chronyd-blocks-at-startup">Chronyd blocks at startup<div class="permalink">[<a href="#chronyd-blocks-at-startup">permalink</a>]</div></h2>
<p><code>chronyd</code> takes care of keeping the system clock in sync. When the system
boots, <code>chronyd</code> will block start-up until it has resolved the time. This is
useful on systems without a hardware clock (to avoid the system booting as
1970-01-01), but annoying for this setup.</p>
<p>This behaviour can be disabled by editing <code>/etc/conf.d/chronyd</code> and setting
<code>FAST_STARTUP=yes</code>.</p>
<h2 id="setting-up-an-apk-cache">Setting up an <code>apk</code> cache<div class="permalink">[<a href="#setting-up-an-apk-cache">permalink</a>]</div></h2>
<p>When configured to use a cache directory, <code>apk</code> will keep local copies of
downloaded packages. This will prevent re-downloading any packages which are
uninstalled and later re-installed. This often happens when building packages
from source.</p>
<p>To set up an <code>apk</code> cache, simply run:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>doas setup-apkcache
</span></span></code></pre></div><p>The default value is fine for this kind of setup.</p>
<h2 id="disable-the-hostname-service">Disable the <code>hostname</code> service<div class="permalink">[<a href="#disable-the-hostname-service">permalink</a>]</div></h2>
<p>I include the system hostname in the kernel command line. The <code>hostname</code>
service sets the hostname from <code>/etc/hostname</code>. The latter is unnecessary, so
can be disabled:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>rc-update del hostname boot
</span></span></code></pre></div><p>This has little functional impact, and is merely a clean-up.</p>
<h2 id="pending-tasks">Pending tasks<div class="permalink">[<a href="#pending-tasks">permalink</a>]</div></h2>
<ul>
<li>Firefox profile</li>
<li>Configure credentials for <code>offlineimap</code> and <code>vdirsyncer</code></li>
</ul>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>The device is technically used, but it is &ldquo;new&rdquo; in the sense that I have
recently acquired it.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>This makes sense on servers which might require network to finalise
their start-up sequence, especially those that rely on <code>chronyd</code> running at
boot.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>I would really love to figure out an uncomplicated setup that would allow
me to just use mDNS for this. I have some projects ideas in mind here, but
not for anytime soon.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>I must emphasise that this is only a poor approach on interactive
systems. On a headless system it makes perfect sense to run an
administrator-defined script in reaction to the hardware power button being
pressed.&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>vdirsyncer status update, October 2023</title><link>https://whynothugo.nl/journal/2023/11/01/vdirsyncer-status-update-october-2023/</link><pubDate>Wed, 01 Nov 2023 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/11/01/vdirsyncer-status-update-october-2023/</guid><description><![CDATA[<p>After having an initial version of the configuration parser, I&rsquo;ve moved on to
working on the actual command line for vdirsyncer itself.</p>
<h1 id="replacing-boxdyn-storage-with-arcdyn-storage">Replacing <code>Box&lt;dyn Storage&gt;</code> with <code>Arc&lt;dyn Storage&gt;</code><div class="permalink">[<a href="#replacing-boxdyn-storage-with-arcdyn-storage">permalink</a>]</div></h1>
<p><em>This section is pretty technical and requires some understanding of Rust.</em></p>
<p>In my synchronisation algorithm, the type representing the pair to be
synchronised, <code>SyncPair</code> kept references to the <code>Storage</code> instances, and these
shared references had started to become a pain point due to Rust&rsquo;s lifetime
rules.</p>
<p>In particular, my <code>struct App</code> can&rsquo;t hold to the <code>Storage</code> and <code>SyncPair</code>
instances because one has references to the other (and Rust doesn&rsquo;t allow one
field of a <code>struct</code> to have references to another field of the <code>struct</code>).</p>
<p>It became clear that handling shared references was a pain, both for
development of vdirsyncer, and likely for other consumers of the <code>vstorage</code>
library.</p>
<p>I switched to using <code>Arc&lt;Storage&gt;</code> in most places, and it was clear that my
previous approach wasn&rsquo;t the right one. An <a href="https://doc.rust-lang.org/std/sync/struct.Arc.html"><code>Arc</code></a> allows sharing references to
data by using atomic reference counting. <code>Arc</code> itself is extremely cheap, and
<code>Storage</code> instances are only created once at start-up, so the near-unmeasurable
cost pays off instantly.</p>
<h1 id="testing-the-command-line-tool">Testing the command line tool<div class="permalink">[<a href="#testing-the-command-line-tool">permalink</a>]</div></h1>
<p>The initial version of the command line just reads the configuration file and
synchronises once. It is extremely minimal and my current goal is to iron out
all the little bugs lurking around.</p>
<p>I had already adapted my configuration file to the new format. The next step
was to make a full backup of all my calendars and contacts before proceeding.</p>
<p>During my first full sync, the grand majority of my items were in conflict.
This is because they existed on both sides with non-semantic differences. The
previous implementation handles these fine, but I intended to handle conflicts
further down the roadmap. When synchronising from a server into an empty
storage, conflicts are not an issue. But they do occur when synchronising a
storage with pre-existing data on both sides. This is my case personally, and
is also going to be the case for anyone switching over from the previous
implementation.</p>
<p>I&rsquo;ll note that while <em>most</em> of the synchronisation is in place, I&rsquo;ve left one
thing pending: conflict resolution. I&rsquo;ve been thinking about this as an edge
case that I can iron out after the initial alpha version, but that is actually
not the case.</p>
<p>The first point of conflict are newlines: while the icalendar specification
mandates that all lines end in <code>\r\n</code>, the majority of the files in my calendar
end in <code>\n</code>. Generally, tools are lenient to this type of issue, and this
should be no exception. I adapted my code to normalise all items as early as
possible.</p>
<p>After the above tweak, the second attempt at synchronising didn&rsquo;t go well
either. All my items exist on both sides (e.g.: on my local system and on the
remote server). Because they are &ldquo;new&rdquo; items (as this is the first
synchronisation ever), they are not assumed to be identical, and there the
result is a conflict: new items with the same <code>UID</code> on both sides.</p>
<p>I made some small changes so as to compare the hash of the items when there is
a conflict, and execute a <code>no-op</code> if the hashes match. Thanks to the previously
mentioned newline normalisation, the hashes match, and now almost all items are
detected as &ldquo;no change, no action required&rdquo;.</p>
<p>At this point, about ninety percent of my calendars are recognised as in-sync
properly. However, many items are still failing. The following is a portion of
an item that was in conflict:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span><span style="color:#f92672">-(de baño)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+(de baÃ±o)
</span></span></span></code></pre></div><p>This looks like an encoding issue. While handling different encodings is in
scope, I didn&rsquo;t expect to hit these so early (since both my local system and
server use UTF-8).</p>
<p>I wrote some tests trying to replicate this, but failed. Finally, after a lot
of debugging (and not the fun type), I narrowed down the issue to <a href="https://github.com/RazrFalcon/roxmltree/issues/108">this bug</a> in
my XML parsing library of choice, <code>roxmltree</code>.</p>
<p>I managed to temporarily work around this by <a href="https://git.sr.ht/~whynothugo/vdirsyncer-rs/commit/388c85c8172b3d0b81a10a451708e1c4c8805fea">pinning the exact version of the
parser</a> that I use. I was previously following the <code>master</code> branch due
to other recent fixes that I needed, but the latest stable version includes all
the fixes that I need and excludes this regression, so it works perfectly.</p>
<p>At this point, synchronisation works, but some items (10 out of 3298 calendar
items) are still in conflict. Some inspection indicates that fields like
<code>DTSTAMP</code> are the ones responsible for the conflict. The <code>DTSTAMP</code> field
indicates the last time that an item was edited.</p>
<p>I&rsquo;m already ignoring conflicts in the <code>PRODID</code> field. This field indicates the
last program that edited a calendar component, and some servers change it to
their own name. The result is that uploading a file and then fetching
immediately might yield a different <code>PRODID</code>, so ignoring changes to it is the
safest thing to do.</p>
<p>The same needs to be done for <code>DTSTAMP</code>. This is especially true for some
WebCal servers, which generate a new <code>DTSTAMP</code> each time that an item is
requested.</p>
<p>Indeed, there is <a href="https://github.com/pimutils/vdirsyncer/blob/b50f9def00b80c8a1c6170532f4d2209052de488/vdirsyncer/vobject.py#L10-L35">a whole list of properties</a> that need to be ignored
when comparing whether two events are the same or not. I intended to address
these later during development, since I expected them to be an issue later on.</p>
<h2 id="an-icalendar-parser">An iCalendar parser<div class="permalink">[<a href="#an-icalendar-parser">permalink</a>]</div></h2>
<p>When I wrote the code to ignore the <code>PROPID</code>, I simply wrote some code that
parses it and removes that line, but doesn&rsquo;t parse the whole iCalendar
component. It works, but extending it to other properties would start to become
too much of an ugly hack.</p>
<p>What needs to be done here is some basic parsing of the icalendar component, so
as to ignore those fields when comparing two items for semantic equality. I
already have a low-level icalendar parser in my roadmap. It seems best to first
write the proper parser, and then use that. I could use the same approach that
I used for <code>PROPID</code>, but the result would likely be some pretty ugly code, hard
to properly test and debug, and would ideally be replaced by the parser in
future anyway.</p>
<p>I am currently focused on this low level parser. I have shifted the &ldquo;initial
command line&rdquo; milestone to immediately after the parser is done.</p>
<h2 id="configuration-parsing">Configuration parsing<div class="permalink">[<a href="#configuration-parsing">permalink</a>]</div></h2>
<p>I have <a href="https://git.sr.ht/~whynothugo/vdirsyncer-rs/commit/0062ffb7ca95c7ea2aeca71480552d2d2afb99bf">committed</a> all the work done for the configuration parsing. This also
includes all TLS-related settings, including authentication based on client
certificates, using custom CA, and self-signed certificates by validating only
the signature.</p>
<p>Some of these scenarios need more testing, and at some point I would really
like a second pair of eyes having a look at the implementation, especially all
the security related code. I believe everything is correct and safe, but a
single pair of eyes is never enough for this kind of code.</p>
<h2 id="upcoming-month">Upcoming month<div class="permalink">[<a href="#upcoming-month">permalink</a>]</div></h2>
<p>This upcoming month I will be travelling to visit extended family in China. I
intend to work on and off every other week.</p>
]]></description></item><item><title>A configuration format for vdirsyncer v2</title><link>https://whynothugo.nl/journal/2023/10/05/a-configuration-format-for-vdirsyncer-v2/</link><pubDate>Thu, 05 Oct 2023 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/10/05/a-configuration-format-for-vdirsyncer-v2/</guid><description><![CDATA[<p>Settling down a configuration format for the upcoming vdirsyncer v2 has taken
more than I anticipated. These is a summary of my journey, considerations and
the current state.</p>
<h1 id="the-previous-format">The previous format<div class="permalink">[<a href="#the-previous-format">permalink</a>]</div></h1>
<p>My first approach was to retain the existing configuration format. I&rsquo;ll call
this one the &ldquo;legacy&rdquo; format, to keep language simple. I wrote a parser for it
but it was far from trivial and, honestly, extremely complicated code. The
configuration format itself is a bespoke format designed for vdirsyncer. The
general structure could be parsed as an <code>ini</code> file, while settings that took
multiple values look more like JSON.</p>
<p>So the parser I wrote reads the file as json and then deserialises some fields
as JSON. At this point, I ended up with a key-value of settings, from which I
need to extract the data itself into the &ldquo;real&rdquo; types that the application will
use.</p>
<p>It was a lot of code that did very little<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.</p>
<p>On top of this, the legacy configuration format doesn&rsquo;t quite contain all the
information needed. In particular, the <code>filesystem</code> storage might be one of two
types: <code>filesystem/icalendar</code> or <code>filesystem/vcard</code>. Due to the dynamic nature
of the Python implementation, treating both as the same works fine, but that is
not the case for the new implementation.</p>
<p>So changes needed to be made, even though quite minor. With this in mind, and
considering that this is a new development, keeping the legacy format felt a
bit like opting into technical debt.</p>
<h1 id="scfg-vs-toml">scfg vs toml<div class="permalink">[<a href="#scfg-vs-toml">permalink</a>]</div></h1>
<p>While considering new confirmation definitions, I narrowed my options down to
the following two:</p>
<ul>
<li><strong>scfg</strong>: a simple format, designed to be easy for humans to write and simple
for machines to parse.</li>
<li><strong>toml</strong>: a somewhat complex format. Parsing it is non-trivial, but
implementations exist in most mainstream languages.</li>
</ul>
<p>An upside of <code>toml</code> is that it is <strong>very similar</strong> to the legacy configuration
format, so it&rsquo;s possible to document the subtle differences clearly and make
migration easy for users.</p>
<p><code>scfg</code> is quite different. While not too hard to understand, it does imply that
users need to learn a rather different format when migrating. On the other
hand, it&rsquo;s very easy to understand for new users.</p>
<p>For reference, here is what a portion of my configuration file in the current
implementation:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#66d9ef">[pair calendars]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">a</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;calendars_local&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">b</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;calendars_fastmail&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">collections</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">[&#34;from b&#34;]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">metadata</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">[&#34;color&#34;, &#34;displayname&#34;]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">conflict_resolution</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">[&#34;command&#34;, &#34;nvim&#34;, &#34;-d&#34;]</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[storage calendars_local]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">type</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;filesystem&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">path</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;~/.local/share/calendars/&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">fileext</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;.ics&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[storage calendars_fastmail]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">type</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;caldav&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">url</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://caldav.fastmail.com/&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">username</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;hugo@whynothugo.nl&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">password.fetch</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">[&#34;command&#34;, &#34;hiq&#34;, &#34;-dFpassword&#34;, &#34;proto=caldavs&#34;, &#34;username=whynothugo@fastmail.com&#34;]</span>
</span></span></code></pre></div><p>The same thing in <code>toml</code> would look almost identical:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-toml" data-lang="toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">pair</span>.<span style="color:#a6e22e">calendars</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">a</span> = <span style="color:#e6db74">&#34;calendars_local&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">b</span> = <span style="color:#e6db74">&#34;calendars_fastmail&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">collections</span> = [<span style="color:#e6db74">&#34;from b&#34;</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">metadata</span> = [<span style="color:#e6db74">&#34;color&#34;</span>, <span style="color:#e6db74">&#34;displayname&#34;</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">conflict_resolution</span> = [<span style="color:#e6db74">&#34;command&#34;</span>, <span style="color:#e6db74">&#34;nvim&#34;</span>, <span style="color:#e6db74">&#34;-d&#34;</span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">storage</span>.<span style="color:#a6e22e">calendars_local</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">type</span> = <span style="color:#e6db74">&#34;filesystem/icalendar&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">path</span> = <span style="color:#e6db74">&#34;~/.local/share/calendars/&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">fileext</span> = <span style="color:#e6db74">&#34;.ics&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">storage</span>.<span style="color:#a6e22e">calendars_fastmail</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">type</span> = <span style="color:#e6db74">&#34;caldav&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">url</span> = <span style="color:#e6db74">&#34;https://caldav.fastmail.com/&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">username</span> = <span style="color:#e6db74">&#34;hugo@whynothugo.nl&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">password</span>.<span style="color:#a6e22e">fetch</span> = [<span style="color:#e6db74">&#34;command&#34;</span>, <span style="color:#e6db74">&#34;hiq&#34;</span>, <span style="color:#e6db74">&#34;-dFpassword&#34;</span>, <span style="color:#e6db74">&#34;proto=caldavs&#34;</span>, <span style="color:#e6db74">&#34;username=whynothugo@fastmail.com&#34;</span>]
</span></span></code></pre></div><p>Meanwhile, an <code>scfg</code> variation would look something 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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>pair calendars <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  a calendars_local
</span></span><span style="display:flex;"><span>  b calendars_fastmail
</span></span><span style="display:flex;"><span>  collections from b
</span></span><span style="display:flex;"><span>  metadata color displayname
</span></span><span style="display:flex;"><span>  conflict resolution <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>    command nvim -d
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>storage calendars_local <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  type filesystem/icalendar
</span></span><span style="display:flex;"><span>  path <span style="color:#f92672">=</span> ~/.local/share/calendars/
</span></span><span style="display:flex;"><span>  fileext <span style="color:#f92672">=</span> .ics
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>storage calendars_fastmail <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  type caldav
</span></span><span style="display:flex;"><span>  url https://caldav.fastmail.com/
</span></span><span style="display:flex;"><span>  username <span style="color:#f92672">=</span> hugo@whynothugo.nl
</span></span><span style="display:flex;"><span>  password <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>    command hiq -dFpassword proto<span style="color:#f92672">=</span>caldavs username<span style="color:#f92672">=</span>whynothugo@fastmail.com
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><h2 id="scfg"><code>scfg</code><div class="permalink">[<a href="#scfg">permalink</a>]</div></h2>
<p>Note how the <code>scfg</code> variant gets rid of quoting. While it&rsquo;s still possible to
quote values with spaces, it&rsquo;s not necessary. Honestly, this looks like the
most human-friendly option.</p>
<p>So this is the variant that I tried implementing first. I used <a href="https://git.sr.ht/~cdv/scfg-rs"><code>scfg-rs</code></a>, a
rust library for parsing <code>scfg</code> files.</p>
<p>This kind of worked. This library parses the file into an in-memory type that
feels like a <code>HashMap</code> with <code>Vec</code>s. Extracting the information from this
intermediate type into a proper <code>Config</code> format requires a additional code and
complexity of which I&rsquo;m not a fan.</p>
<p>In the end, I felt that this library is a good fit for simpler usages, but not
so much for this case. A <a href="https://serde.rs/"><code>serde</code></a> based approach would likely be a great fit.
While I did consider implementing such a thing, it&rsquo;s just a huge scope creep
for this project.</p>
<h2 id="toml">toml<div class="permalink">[<a href="#toml">permalink</a>]</div></h2>
<p>Before deciding between <code>scfg</code> and <code>toml</code>, I wanted to be sure that I had tried
out both properly. The <code>toml</code> implementation is substantially simpler; I mostly
just declared a few idiomatic types to represent my configuration, and added
<code>#[derive(Deserialize, Debug)]</code> to have <code>serde</code> deserialise this from the
<code>toml</code> file.</p>
<p>I feel a lot more comfortable moving
forward with this for now. In particular: I can move onto the next milestone
which is writing the command line itself instead of writing more code to unwrap
a configuration file.</p>
<p>Note that some version far in the future might end up supporting <code>scfg</code> as
well. For now, the focus is on moving forward and not on picking the one true
ultimate configuration format.</p>
<h1 id="specifying-collections">Specifying collections<div class="permalink">[<a href="#specifying-collections">permalink</a>]</div></h1>
<p>The config file needs to specify which collections to sync. This can take a few
shapes:</p>
<ul>
<li><code>null</code>: this is a special value which will no longer be supported.</li>
<li><code>[&quot;from a&quot;]</code> syncs all collections that exist on storage a.</li>
<li><code>[&quot;from b&quot;]</code> syncs all collections that exist on storage b.</li>
<li><code>[&quot;from a&quot;, &quot;from b&quot;]</code> syncs all collections that exist on either side.</li>
<li><code>[&quot;work&quot;]</code> syncs a collection with name &ldquo;work&rdquo;.</li>
<li><code>[&quot;from&quot;]</code> syncs a collection with name &ldquo;from&rdquo;.</li>
</ul>
<p><code>toml</code> doesn&rsquo;t even support <code>null</code>, but this use case is being dropped
entirely (more on that later), so it&rsquo;s not a problem at all.</p>
<p>I introduced a new option here, which is equivalent to <code>[&quot;from a&quot;, &quot;from b&quot;]</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;-webkit-text-size-adjust:none;"><code class="language-toml" data-lang="toml"><span style="display:flex;"><span><span style="color:#a6e22e">collections</span> = [<span style="color:#e6db74">&#34;all&#34;</span>]
</span></span></code></pre></div><p>An issue with this is that it’s not possible to specify a collection named
&ldquo;all&rdquo;. It was also impossible to specify a collection named &ldquo;from a&rdquo;, although
I don&rsquo;t think this has ever realistically been a problem.</p>
<p>The <code>&quot;from b&quot;</code> variant remains the same:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-toml" data-lang="toml"><span style="display:flex;"><span><span style="color:#a6e22e">collections</span> = [<span style="color:#e6db74">&#34;from b&#34;</span>]
</span></span></code></pre></div><p>However, specifying collections by their <code>id</code> now has an entirely different
format:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-toml" data-lang="toml"><span style="display:flex;"><span><span style="color:#a6e22e">collections</span> = [
</span></span><span style="display:flex;"><span>  { <span style="color:#a6e22e">id</span> = <span style="color:#e6db74">&#34;italki&#34;</span> }
</span></span><span style="display:flex;"><span>]
</span></span></code></pre></div><p>Note that the following is also valid<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-toml" data-lang="toml"><span style="display:flex;"><span><span style="color:#a6e22e">collections</span> = [
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;from a&#34;</span>,
</span></span><span style="display:flex;"><span>  { <span style="color:#a6e22e">id</span> = <span style="color:#e6db74">&#34;work&#34;</span> }
</span></span><span style="display:flex;"><span>]
</span></span></code></pre></div><p>The <code>id</code> part is to disambiguate exactly what the string itself means, which is
especially important due to a new addition:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-toml" data-lang="toml"><span style="display:flex;"><span><span style="color:#a6e22e">collections</span> = [
</span></span><span style="display:flex;"><span>  { <span style="color:#a6e22e">href</span> = <span style="color:#e6db74">&#34;/work&#34;</span> }
</span></span><span style="display:flex;"><span>]
</span></span></code></pre></div><p>The <code>id</code> syntax looks for a collection with a matching <code>id</code> (the &ldquo;id&rdquo; generally
being the name of the directory itself or the last component in a URL). The
<code>href</code> approach works on situations where discovery is not an option.</p>
<p>Finally, the legacy configuration format supported mapped synchronisation:
specifying a different collection on each side to be synchronised with each
other. The legacy format was:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#a6e22e">collections</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">[[&#34;bar&#34;, &#34;bar_a&#34;, &#34;bar_b&#34;], &#34;foo&#34;]</span>
</span></span></code></pre></div><p>The new format is a bit more verbose. In my honest opinion, it&rsquo;s not
necessarily simpler to write:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-toml" data-lang="toml"><span style="display:flex;"><span><span style="color:#a6e22e">collections</span> = [
</span></span><span style="display:flex;"><span>  { <span style="color:#a6e22e">mapped</span> = [ <span style="color:#e6db74">&#34;work&#34;</span>, { <span style="color:#a6e22e">id</span> = <span style="color:#e6db74">&#34;work&#34;</span> }, { <span style="color:#a6e22e">href</span> = <span style="color:#e6db74">&#34;/path/to/work&#34;</span> } ]}
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">#            ^^^^^^ this is an alias used only for logging.</span>
</span></span><span style="display:flex;"><span>]
</span></span></code></pre></div><p>For reference, this is the <code>scfg</code> version of the above:</p>
<!-- Set to bash so that the comment is highlighted -->
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>collections <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>    mapped work <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">#  ^^^^ this is the same alias as above.</span>
</span></span><span style="display:flex;"><span>        id work
</span></span><span style="display:flex;"><span>        href /path/to/work
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><p>This maps the collection on storage a with id <code>work</code> with a collection on
storage b with href <code>/path/to/work</code>. The legacy format did not allow specifying
collections by <code>href</code> at all, which can be an issue in some niche cases. Given
that the new sync algorithm supports it entirely, it is important that the
configuration file allows making use of this feature.</p>
<h1 id="the-null-collection">The <code>null</code> collection<div class="permalink">[<a href="#the-null-collection">permalink</a>]</div></h1>
<p>As I mentioned before, previously one could specify a <code>null</code> collection, and
this indicated that the configuration for a given storage points directly to a
collection and not to a storage with multiple collections.</p>
<p>Using a storage with a single collection is still possible, but the approach
has changed. Rather than specifying <code>null</code> as a collection, the <code>href</code> syntax
should be used instead to point directly to the collection. This works even in
situations where discovery doesn&rsquo;t work (it might simply be unsupported server
side).</p>
<p>The new approach keeps some abstractions in place for all scenarios, which
makes a lot of the under-the-hood logic much simpler.</p>
<h1 id="other-fields">Other fields<div class="permalink">[<a href="#other-fields">permalink</a>]</div></h1>
<p>Other fields will remain largely the same. I&rsquo;ve focused on the basic ones first
(mostly to allow simple usages), and will continue addressing others at a later
stage. In particular, custom TLS configuration is likely to come around after
an initial alpha version of the command line interface.</p>
<h1 id="current-state">Current state<div class="permalink">[<a href="#current-state">permalink</a>]</div></h1>
<p>As I&rsquo;ve mentioned above, I&rsquo;ve defined some idiomatic types that represent the
configuration itself, and the parser simply creates instances of those, which
are rather easy to operate with.</p>
<p>At this point, I&rsquo;m needing to translate these into the <em>actual</em> <code>Storage</code> and
<code>StoragePair</code> instances and trigger a synchronisation. It shouldn&rsquo;t be too
long before I have a working alpha version of vdirsyncer2.</p>
<p>That&rsquo;s mostly it for this month&rsquo;s update.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>I&rsquo;ve retained this code in the <a href="https://git.sr.ht/~whynothugo/vdirsyncer-rs/log/legacy-config"><code>legacy-config</code></a> branch. It will likely
be useful in future to write a tool to auto-migrate configuration formats.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>The initial version of this example was missing a trailing comma on the
first line, which made this example invalid <code>toml</code>. It&rsquo;s easy for
programmers to deal with, but I fear it has too many quirks for everyone
else.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>Debugging a non-functional pylsp</title><link>https://whynothugo.nl/journal/2023/09/05/debugging-a-non-functional-pylsp/</link><pubDate>Tue, 05 Sep 2023 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/09/05/debugging-a-non-functional-pylsp/</guid><description><![CDATA[<p><a href="https://github.com/python-lsp/python-lsp-server"><code>pylsp</code></a> wasn&rsquo;t working today for some reason. It wasn&rsquo;t jumping to definitions,
offering any auto-completions, nor formatting code. I had to figure out why.</p>
<p>Usually, LSPs are executed directly by the IDE with stdin/stdout piped directly
into an LSP client, which means that logging to stdout/stderr is not as simple
as usual.</p>
<p>Running the LSP manually on a TCP port makes following logs nice and simple.
The following command should be executed in the root of a python project:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>pylsp --tcp --port <span style="color:#ae81ff">9977</span> --verbose
</span></span></code></pre></div><p>I then had to configure neovim to use the language server via TCP instead of
spawning its own instance. This was also quite simple, just override the <code>cmd</code>
with a dedicated function.</p>
<p>A usual neovim LSP configuration looks something like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-lua" data-lang="lua"><span style="display:flex;"><span><span style="color:#66d9ef">local</span> lspconfig <span style="color:#f92672">=</span> require(<span style="color:#e6db74">&#39;lspconfig&#39;</span>)
</span></span><span style="display:flex;"><span>lspconfig.pylsp.setup({
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">-- additional LSP configuration here...</span>
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div><p>To connect via TCP use:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-lua" data-lang="lua"><span style="display:flex;"><span><span style="color:#66d9ef">local</span> lspconfig <span style="color:#f92672">=</span> require(<span style="color:#e6db74">&#39;lspconfig&#39;</span>)
</span></span><span style="display:flex;"><span>lspconfig.pylsp.setup({
</span></span><span style="display:flex;"><span>  cmd <span style="color:#f92672">=</span> vim.lsp.rpc.connect(<span style="color:#e6db74">&#34;127.0.0.1&#34;</span>, <span style="color:#ae81ff">9977</span>),
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">-- additional LSP configuration here...</span>
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div><p>Now execute neovim on the same directory where the LSP server was started. Logs
will immediately start showing out in the LSP server. In this particular case,
it turns out that I had a version of <code>jedi</code> which was too new for <code>pylsp</code> and
it refused to work with 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;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>2023-09-05 17:55:30,637 CEST - INFO - pylsp.python_lsp - Serving PythonLSPServer on (127.0.0.1, 9977)
</span></span><span style="display:flex;"><span>2023-09-05 17:55:34,279 CEST - INFO - pylsp.config.config - Failed to load pylsp entry point &#39;pylsp_mypy&#39;: (jedi 0.19.0 (/usr/lib/python3.11/site-packages), Requirement.parse(&#39;jedi&lt;0.19.0,&gt;=0.17.2&#39;), {&#39;python-lsp-server&#39;})
</span></span><span style="display:flex;"><span>2023-09-05 17:55:34,282 CEST - INFO - pylsp.config.config - Failed to load pylsp entry point &#39;pylsp_black&#39;: (jedi 0.19.0 (/usr/lib/python3.11/site-packages), Requirement.parse(&#39;jedi&lt;0.19.0,&gt;=0.17.2&#39;), {&#39;python-lsp-server&#39;})
</span></span><span style="display:flex;"><span>2023-09-05 17:55:34,282 CEST - INFO - pylsp.config.config - Failed to load pylsp entry point &#39;autopep8&#39;: (jedi 0.19.0 (/usr/lib/python3.11/site-packages), Requirement.parse(&#39;jedi&lt;0.19.0,&gt;=0.17.2&#39;))
</span></span><span style="display:flex;"><span>2023-09-05 17:55:34,283 CEST - INFO - pylsp.config.config - Failed to load pylsp entry point &#39;flake8&#39;: (jedi 0.19.0 (/usr/lib/python3.11/site-packages), Requirement.parse(&#39;jedi&lt;0.19.0,&gt;=0.17.2&#39;))
</span></span><span style="display:flex;"><span>2023-09-05 17:55:34,283 CEST - INFO - pylsp.config.config - Failed to load pylsp entry point &#39;folding&#39;: (jedi 0.19.0 (/usr/lib/python3.11/site-packages), Requirement.parse(&#39;jedi&lt;0.19.0,&gt;=0.17.2&#39;))
</span></span><span style="display:flex;"><span>2023-09-05 17:55:34,283 CEST - INFO - pylsp.config.config - Failed to load pylsp entry point &#39;jedi_completion&#39;: (jedi 0.19.0 (/usr/lib/python3.11/site-packages), Requirement.parse(&#39;jedi&lt;0.19.0,&gt;=0.17.2&#39;))
</span></span><span style="display:flex;"><span>2023-09-05 17:55:34,283 CEST - INFO - pylsp.config.config - Failed to load pylsp entry point &#39;jedi_definition&#39;: (jedi 0.19.0 (/usr/lib/python3.11/site-packages), Requirement.parse(&#39;jedi&lt;0.19.0,&gt;=0.17.2&#39;))
</span></span><span style="display:flex;"><span>2023-09-05 17:55:34,283 CEST - INFO - pylsp.config.config - Failed to load pylsp entry point &#39;jedi_highlight&#39;: (jedi 0.19.0 (/usr/lib/python3.11/site-packages), Requirement.parse(&#39;jedi&lt;0.19.0,&gt;=0.17.2&#39;))
</span></span><span style="display:flex;"><span>2023-09-05 17:55:34,283 CEST - INFO - pylsp.config.config - Failed to load pylsp entry point &#39;jedi_hover&#39;: (jedi 0.19.0 (/usr/lib/python3.11/site-packages), Requirement.parse(&#39;jedi&lt;0.19.0,&gt;=0.17.2&#39;))
</span></span><span style="display:flex;"><span>2023-09-05 17:55:34,283 CEST - INFO - pylsp.config.config - Failed to load pylsp entry point &#39;jedi_references&#39;: (jedi 0.19.0 (/usr/lib/python3.11/site-packages), Requirement.parse(&#39;jedi&lt;0.19.0,&gt;=0.17.2&#39;))
</span></span><span style="display:flex;"><span>2023-09-05 17:55:34,284 CEST - INFO - pylsp.config.config - Failed to load pylsp entry point &#39;jedi_rename&#39;: (jedi 0.19.0 (/usr/lib/python3.11/site-packages), Requirement.parse(&#39;jedi&lt;0.19.0,&gt;=0.17.2&#39;))
</span></span><span style="display:flex;"><span>2023-09-05 17:55:34,284 CEST - INFO - pylsp.config.config - Failed to load pylsp entry point &#39;jedi_signature_help&#39;: (jedi 0.19.0 (/usr/lib/python3.11/site-packages), Requirement.parse(&#39;jedi&lt;0.19.0,&gt;=0.17.2&#39;))
</span></span><span style="display:flex;"><span>2023-09-05 17:55:34,284 CEST - INFO - pylsp.config.config - Failed to load pylsp entry point &#39;jedi_symbols&#39;: (jedi 0.19.0 (/usr/lib/python3.11/site-packages), Requirement.parse(&#39;jedi&lt;0.19.0,&gt;=0.17.2&#39;))
</span></span><span style="display:flex;"><span>2023-09-05 17:55:34,284 CEST - INFO - pylsp.config.config - Failed to load pylsp entry point &#39;mccabe&#39;: (jedi 0.19.0 (/usr/lib/python3.11/site-packages), Requirement.parse(&#39;jedi&lt;0.19.0,&gt;=0.17.2&#39;))
</span></span><span style="display:flex;"><span>2023-09-05 17:55:34,284 CEST - INFO - pylsp.config.config - Failed to load pylsp entry point &#39;preload&#39;: (jedi 0.19.0 (/usr/lib/python3.11/site-packages), Requirement.parse(&#39;jedi&lt;0.19.0,&gt;=0.17.2&#39;))
</span></span><span style="display:flex;"><span>2023-09-05 17:55:34,284 CEST - INFO - pylsp.config.config - Failed to load pylsp entry point &#39;pycodestyle&#39;: (jedi 0.19.0 (/usr/lib/python3.11/site-packages), Requirement.parse(&#39;jedi&lt;0.19.0,&gt;=0.17.2&#39;))
</span></span><span style="display:flex;"><span>2023-09-05 17:55:34,284 CEST - INFO - pylsp.config.config - Failed to load pylsp entry point &#39;pydocstyle&#39;: (jedi 0.19.0 (/usr/lib/python3.11/site-packages), Requirement.parse(&#39;jedi&lt;0.19.0,&gt;=0.17.2&#39;))
</span></span><span style="display:flex;"><span>2023-09-05 17:55:34,284 CEST - INFO - pylsp.config.config - Failed to load pylsp entry point &#39;pyflakes&#39;: (jedi 0.19.0 (/usr/lib/python3.11/site-packages), Requirement.parse(&#39;jedi&lt;0.19.0,&gt;=0.17.2&#39;))
</span></span><span style="display:flex;"><span>2023-09-05 17:55:34,285 CEST - INFO - pylsp.config.config - Failed to load pylsp entry point &#39;pylint&#39;: (jedi 0.19.0 (/usr/lib/python3.11/site-packages), Requirement.parse(&#39;jedi&lt;0.19.0,&gt;=0.17.2&#39;))
</span></span><span style="display:flex;"><span>2023-09-05 17:55:34,285 CEST - INFO - pylsp.config.config - Failed to load pylsp entry point &#39;rope_autoimport&#39;: (jedi 0.19.0 (/usr/lib/python3.11/site-packages), Requirement.parse(&#39;jedi&lt;0.19.0,&gt;=0.17.2&#39;))
</span></span><span style="display:flex;"><span>2023-09-05 17:55:34,285 CEST - INFO - pylsp.config.config - Failed to load pylsp entry point &#39;rope_completion&#39;: (jedi 0.19.0 (/usr/lib/python3.11/site-packages), Requirement.parse(&#39;jedi&lt;0.19.0,&gt;=0.17.2&#39;))
</span></span><span style="display:flex;"><span>2023-09-05 17:55:34,285 CEST - INFO - pylsp.config.config - Failed to load pylsp entry point &#39;rope_rename&#39;: (jedi 0.19.0 (/usr/lib/python3.11/site-packages), Requirement.parse(&#39;jedi&lt;0.19.0,&gt;=0.17.2&#39;))
</span></span><span style="display:flex;"><span>2023-09-05 17:55:34,285 CEST - INFO - pylsp.config.config - Failed to load pylsp entry point &#39;yapf&#39;: (jedi 0.19.0 (/usr/lib/python3.11/site-packages), Requirement.parse(&#39;jedi&lt;0.19.0,&gt;=0.17.2&#39;))
</span></span><span style="display:flex;"><span>2023-09-05 17:55:34,290 CEST - INFO - pylsp.config.config - Disabled plugins: []
</span></span></code></pre></div><p>Looks like <code>pylsp</code> refuses to work with <code>jedi</code> &gt;= 0.19, but 0.19.0 is already
out and it&rsquo;s what I have installed.</p>
<p>This has been <a href="https://github.com/python-lsp/python-lsp-server/pull/416">addressed upstream</a> a while ago, although unreleased. I&rsquo;ve
<a href="https://gitlab.alpinelinux.org/alpine/aports/-/merge_requests/51103">backported the patch into the APKBUILD for community/py3-lsp-server</a>
in the meantime.</p>
]]></description></item><item><title>vdirsyncer status update, August 2023</title><link>https://whynothugo.nl/journal/2023/08/26/vdirsyncer-status-update-august-2023/</link><pubDate>Sat, 26 Aug 2023 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/08/26/vdirsyncer-status-update-august-2023/</guid><description><![CDATA[<p>I had family visiting during this past month, so I&rsquo;ve taken some time off to
spend with them. Progress has therefore been slower than usual.</p>
<h1 id="sub-tasks-missing-from-planning">Sub-tasks missing from planning<div class="permalink">[<a href="#sub-tasks-missing-from-planning">permalink</a>]</div></h1>
<p>As I <a href="/journal/2023/03/28/vdirsyncer-status-update-2023-03/#funding">mentioned a few months ago</a>, the <a href="https://nlnet.nl/">NLnet foundation</a> is
currently funding my work rewriting on vdirsyncer. As part of this process,
I&rsquo;ve shared a full plan with tasks that need to be done and an estimation for
them.</p>
<p>While I had a pretty solid plan on how to approach <a href="/journal/2022/04/18/a-vdirsyncer-rewrite/">re-writing
vdirsyncer</a>, some requirements did fly under my radar. For example,
the original plan included the work necessary for the caldav client, but I
didn&rsquo;t really take into account the DNS-based service discovery. The DNS-based
discovery didn&rsquo;t consume a huge amount of time &ndash; but it was still a divergence
from my original estimations.</p>
<p>When looking into the coming milestones, it has become clear that I&rsquo;ve missed
out on estimating several more tasks related to intermediate work. I have been
doing a lot of re-planning to make sure that I have a good plan and estimation
for this work.</p>
<p>Some items that I had overlooked are:</p>
<ul>
<li><strong>Creating an API to declare sync targets</strong>. I currently have code that can
synchronise two storages, but the API for defining these storages (as well as
which collections inside of them should be synchronised) was missing. The
rewrite has a strong separation between the library that does the work and
the command line interface, so an API is quite necessary, even at this stage.
I actually ended up refactoring almost all of the synchronisation code for
this (twice), and finally settled on a simple builder API for this. This API
is also usable by other frontends, including potentially a GUI application.</li>
<li><strong>Defining and documenting a configuration format</strong>: My next milestone is to
publish a command line tool for syncing (e.g.: an alpha vdirsyncer). This
obviously requires reading a configuration file (which specifies which
storages to sync). Defining and documenting this configuration format was not
something that I had planned for in scope. To be fair, I initially intended
to use the existing format, but it eventually turned out not to be a great
fit. I&rsquo;ve <a href="/journal/2023/07/17/vdirsyncer-status-update-july-2023/#configuration-changes">shared some notes on this topic in the past</a> and
intend to address this in more details in future.</li>
<li><strong>Icalendar parsing</strong> and <strong>JSCalendar parsing</strong>. JMAP support is in scope as
one of the final milestones. This requires converting Icalendar to/from
JSCalendar. This in turn requires a low level parser for both formats. These
parsers were not kept in scope, and none of the existing implementations fit
the bill. I have now added these parsers to my list of pending milestones.</li>
<li><strong>Functionality to repair non-compliant icalendar items</strong>: This is a feature
available in the previous vdirsyncer that&rsquo;s actually quite useful and slipped
under my radar (mostly because it&rsquo;s usually used as a one-time thing). This
mostly adds missing mandatory  fields (notably: <code>UID:</code>) and similar
irregularities.</li>
</ul>
<!-- TODO: mention grant being related here -->
<h1 id="coordinating-with-pimalaya">Coordinating with Pimalaya<div class="permalink">[<a href="#coordinating-with-pimalaya">permalink</a>]</div></h1>
<p>I&rsquo;ve also been coordinating further with <a href="https://soywod.me/en">@soywod</a>, who is working on the
<a href="https://sr.ht/~soywod/pimalaya/">Pimalaya</a> project. The goal of Pimalaya is to provide a suite of libraries and
applications for email, calendar and other personal information management
tools. We&rsquo;re obviously on very related fields, so coordinating to avoid
duplicating work is key for both of us.</p>
<!-- https://pimalaya.org/ -->
<p>As I&rsquo;ve mentioned above, a low-level icalendar parser is in scope for me, and
the intent is for it to be fully re-usable. Tentatively, it seems that it might
be usable for Pimalaya in future, although nothing in set in stone yet. My
plans after vdirsyncer involve other calendar-related tools, so we do aspire to
try and converge in our general direction.</p>
<h3 id="email-synchronisation">Email synchronisation<div class="permalink">[<a href="#email-synchronisation">permalink</a>]</div></h3>
<p>A topic that has come up many times is the idea of extending vdirsyncer to also
synchronise email (e.g.: between IMAP, Maildir, and potentially even JMAP). The
general idea of &ldquo;synchronise two storages while keeping a state file to resolve
mismatches&rdquo; applies well to both email and calendar. In fact, vdirsyncer&rsquo;s
original algorithm is inspired by the one used in <a href="https://www.offlineimap.org/">offlineimap</a></p>
<p>However, while some aspects of vdirsyncer are obviously reusable in this
context, email has a few nuances that make it quite different too. In
particular, emails have &ldquo;flags&rdquo; (e.g.: <code>seen</code>, <code>flagged</code>, <code>draft</code>, etc),
whereas calendar events have no equivalence. Additionally, flags change very
often, but messages are immutable.</p>
<p>I&rsquo;m still trying to wrap my head around a design that makes sense. Currently,
the <code>Storage</code> type is parametrised with the type of content it holds (e.g.:
<code>IcalendarItem</code>, which implements the trait <a href="https://mirror.whynothugo.nl/vdirsyncer/vstorage/base/trait.Item.html"><code>Item</code></a>). I&rsquo;m considering adding a
<code>flags</code> attribute to <code>Item</code>, which can be empty for <code>IcalendarItem</code> but have
actual flags for <code>EmailItem</code>. An important detail in this approach is that
whether an item type has flags is determined at compile-time, so the &ldquo;check and
synchronise flags&rdquo; part of the synchronisation algorithm could potentially
compile down to a no-op for <code>IcalendarItem</code>.</p>
<p>Another approach I&rsquo;ve considered is having multiple &ldquo;layers&rdquo; for each <code>Item</code>
type. <code>IcalendarItem</code> would only have the <code>data</code> layer, but <code>EmailItem</code> can
have a <code>message</code> and a <code>flags</code> layer, where one can mutate without the other.
Keep in mind that it&rsquo;s important to avoid re-synchronising an email just
because its flags have changed &ndash; we wouldn&rsquo;t want to re-download a 25MB email
just because it was marked as read.</p>
<p>However, these are still vague ideas in my head, which I&rsquo;ll continue refining.
, but are not in scope yet and there is still not a solid plan to implement
things things exactly this way yet. <em>If</em> this does happen, it will be after a
stable release of the rewrite has been made. I definitely don&rsquo;t want to block a
stable release with this milestone.</p>
<h1 id="configuration-parsing-and-api">Configuration parsing and API<div class="permalink">[<a href="#configuration-parsing-and-api">permalink</a>]</div></h1>
<p>A configuration might specify something like <em>&ldquo;sync all collections from
storage A&rdquo;</em>, but a <code>StoragePair</code> type (which contains all the rules for
creating a plan and eventually executing a synchronisation) needs explicitly
enumerated collections and their respective mappings.</p>
<p>A bit of glue code is needed between the reading the configuration and creating
the <code>StoragePair</code> instance. With the builder API for the <code>StoragePair</code> type, I
don&rsquo;t expect any major obstacles in this field.</p>
<p>Given that the configuration format is so decoupled from the application logic
itself, I expect that trying out different configuration formats in future
should not require huge changes.</p>
<h1 id="the-collectionid-type">The <code>CollectionId</code> type<div class="permalink">[<a href="#the-collectionid-type">permalink</a>]</div></h1>
<p>The <code>vstorage</code> crate has a few functions that take a &ldquo;collection id&rdquo; as a
parameter. A collection id is just a string with a few restrictions (no
slashes, can&rsquo;t be <code>..</code> or <code>.</code>).</p>
<p>My first instinct was to add a quick validation. A small function that takes
the string and returns whether it is valid as a collection id or not. This
works, but I just need to remember to call this in every single function that
takes a collection id as an input. It also means that any value that is
provided multiple times needs to be validated each time that it is received.</p>
<p>After some hesitation and experimenting back and forth, I eventually decided to
use a custom type for 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;-webkit-text-size-adjust:none;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">CollectionId</span> {
</span></span><span style="display:flex;"><span>    inner: String,
</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">impl</span> FromStr <span style="color:#66d9ef">for</span> CollectionId {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">type</span> Err <span style="color:#f92672">=</span> CollectionIdError;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">from_str</span>(s: <span style="color:#66d9ef">&amp;</span><span style="color:#66d9ef">str</span>) -&gt; Result<span style="color:#f92672">&lt;</span>CollectionId, CollectionIdError<span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// ...
</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">impl</span> TryFrom<span style="color:#f92672">&lt;</span>String<span style="color:#f92672">&gt;</span> <span style="color:#66d9ef">for</span> CollectionId {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Error</span> <span style="color:#f92672">=</span> CollectionIdError;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">try_from</span>(value: String) -&gt; <span style="color:#a6e22e">std</span>::result::Result<span style="color:#f92672">&lt;</span>Self, Self::Error<span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// ...
</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:#75715e">// ... A few other conversion helpers...
</span></span></span></code></pre></div><p>Note that the <code>inner</code> field is private, so creating instances of <code>CollectionId</code>
is only possible if the data has been properly validated.</p>
<p>To be honest, as I implemented this type and used it everywhere I kept
wondering if this wasn&rsquo;t an overkill. After completing the changes, it&rsquo;s clear
that it wasn&rsquo;t.</p>
<p>Now functions that take a collection id no longer take a <code>String</code> parameter;
they take a <code>CollectionId</code> parameter. This makes the compiler enforce that the
data is always validated before the function call. It also moves the
responsibility of validating data to the caller, so it simplifies the returned
error types (since they no longer need to account for the &ldquo;invalid collection
id&rdquo; variants).</p>
<p>This item constitutes a very small development (it only took less than a couple
of hours to get to its current version), but I&rsquo;m very happy with the ergonomics
that it provides.</p>
]]></description></item><item><title>Building and running sway-master</title><link>https://whynothugo.nl/journal/2023/08/04/building-and-running-sway-master/</link><pubDate>Fri, 04 Aug 2023 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/08/04/building-and-running-sway-master/</guid><description><![CDATA[<p>I wanted to run sway from upstream <code>master</code> branch to tests the yet-unreleased
fractional scaling support. This recipe also works for trying to build sway
with custom patches.</p>
<p>First, I needed to clone sway itself and <code>wlroots</code> as a subproject. Building
<code>wlroots</code> as a subproject is required since I need to use the current <code>master</code>
of <code>wlroots</code>. Using the distribution/system provided <code>wlroots</code> is unlikely to
work (due to it being the latest stable release).</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>git clone https://github.com/swaywm/sway.git
</span></span><span style="display:flex;"><span>cd sway
</span></span><span style="display:flex;"><span>mkdir subprojects
</span></span><span style="display:flex;"><span>cd subprojects
</span></span><span style="display:flex;"><span>git clone https://gitlab.freedesktop.org/wlroots/wlroots
</span></span><span style="display:flex;"><span>cd ..
</span></span></code></pre></div><p>Next up, install all the build dependencies for both sway and <code>wlroots</code>. Using
<code>-t .sway-dev</code> installs a virtual package called <code>.sway-dev</code> which has all the
packages listed below as dependencies. This makes cleaning up very simple later
on.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>doas apk add -t .sway-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  basu-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  cairo-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  eudev-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  gdk-pixbuf-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  glslang-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  hwdata-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  json-c-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  lcms2-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  libcap-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  libcap-utils <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  libdisplay-info-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  libevdev-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  libinput-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  libliftoff-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  libseat-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  libxcb-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  libxkbcommon-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  linux-pam-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  mesa-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  meson <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  ninja <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  pango-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  pcre2-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  pixman-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  scdoc <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  vulkan-loader-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  wayland-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  wayland-protocols <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  wlroots-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  xcb-util-image-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  xcb-util-renderutil-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  xcb-util-wm-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  xkeyboard-config-dev <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  xwayland-dev
</span></span></code></pre></div><p>As mentioned in the <a href="https://gitlab.freedesktop.org/wlroots/wlroots/-/wikis/Packaging-recommendations">packaging recommendations</a>, it&rsquo;s best to statically link
<code>wlroots</code>. This results in it being bundled into sway&rsquo;s binary, instead of sway
loading the system <code>wlroots</code><sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>meson -Dwlroots:default_library<span style="color:#f92672">=</span>static build
</span></span><span style="display:flex;"><span>meson compile -C build
</span></span><span style="display:flex;"><span>doas meson install -C build --skip-subprojects wlroots
</span></span></code></pre></div><p>Using <code>meson install</code> will install sway into <code>/usr/local</code>, and it won&rsquo;t
overwrite the distribution-provided sway or any of its files. Since
<code>/usr/local/bin/sway</code> has precedence over <code>/usr/bin/sway</code>, then this version
will be used by default when running <code>sway</code>.</p>
<p>Running <code>meson uninstall -C build</code> will remove this version of sway and
un-shadow the stable one installed via distribution packages.</p>
<div class="notice update">
  <header>Update 2024-04-24</header>
  <section>
  Added <code>--skip-subprojects wlroots</code> to avoid installing wlroots. It is
unnecessary in this case due to being statically linked.
  </section>
</div>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Loading the system <code>wlroots</code> is very likely to result in a crash, given
that sway is being built with a recent version of <code>wlroots</code>, and sway&rsquo;s
master branch is often not even compatible with the latest stable release
of <code>wlroots</code>.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>vdirsyncer status update, July 2023</title><link>https://whynothugo.nl/journal/2023/07/17/vdirsyncer-status-update-july-2023/</link><pubDate>Mon, 17 Jul 2023 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/07/17/vdirsyncer-status-update-july-2023/</guid><description><![CDATA[<h2 id="carddav-support">CardDav support<div class="permalink">[<a href="#carddav-support">permalink</a>]</div></h2>
<p>This month I&rsquo;ve worked on missing CardDav support in existing libraries.</p>
<p>Regarding <a href="https://git.sr.ht/~whynothugo/vdirsyncer-rs/tree/main/item/libdav"><code>libdav</code></a> itself, this has helped tidy up parts of the code that are
shared between the CalDav and CardDav. This wasn&rsquo;t very complicated; the latter
is very similar to the former, so it was just making sure that all of the
little differences were tweaked currently. The recent rewrite of the parsers
helped a lot since I could reuse most of the code from the CalDav
implementation.</p>
<p>I also have in mind eventually improving the <code>libdav</code>&rsquo;s WebDav client to make
it more usable as a stand-alone client. Even though that was not originally in
scope, I have some other projects in mind that could use this in future, and a
generic WebDav client could definitely be useful in the Rust ecosystem.</p>
<p>Regarding <a href="https://git.sr.ht/~whynothugo/vdirsyncer-rs/tree/main/item/vstorage"><code>vstorage</code></a>, this is the first storage that only handles items of a
type that <em>are not</em> calendars, which mostly confirms that the design of the
<code>Storage</code> types fits well.</p>
<p>Finally, I implemented live tests for CardDav servers. The code for live tests
is still &ldquo;just okay&rdquo;, and implementing CardDav consisted mostly of copying a
few tests and tweaking the bits that are different.</p>
<p>I&rsquo;m happy to say that these tests yielded very positive results. Tests pass
with all the same servers I&rsquo;d been testing so far. The only small issues
that I&rsquo;ve found are essentially the same issues that I found for CalDav on
these servers (I have reported most of them, but have a few still pending). To
be clear: these issues are merely scenarios where servers don&rsquo;t behave entirely
as expected, but don&rsquo;t affect the functionality that&rsquo;s required for
<code>vdirsyncer</code>.</p>
<h2 id="escaping-hrefs">Escaping <code>href</code>s<div class="permalink">[<a href="#escaping-hrefs">permalink</a>]</div></h2>
<p>A weird quirk of WebDav is that it uses XML to encode requests and responses,
but these XMLs include <code>href</code> elements which themselves are URL-encoded. These
can turn into a pain if not dealt with properly.</p>
<p>Fortunately, since <code>libdav</code> encapsulates all the WebDav implementation, it
makes it easy to enforce a clear API boundary. I&rsquo;ve updated all functions in
this library to clarify that all parameters and return values which include an
<code>href</code> are not URL-encoded. This means that, outside of <code>libdav</code>, one never has
to worry about whether an <code>href</code> is URL-encoded or not.</p>
<p>Internally, I&rsquo;ve enforced a rule where an <code>href</code> is decoded as soon as it is
parsed from the raw XML, and is encoded again only when actually serialising
a request. This also makes it less confusing what&rsquo;s going on internally.</p>
<h2 id="configuration-changes">Configuration changes<div class="permalink">[<a href="#configuration-changes">permalink</a>]</div></h2>
<p>Last month I mentioned that I&rsquo;ve been writing a parser to read the
configuration format in the same syntax as the Python implementation of
vdirsyncer. <a href="https://unterwaditzer.net/">@untitaker</a> provided me with some feedback on this and some
insight on the existing format: the intent was to make the configuration format
converge to toml and eventually make the configuration pure toml.</p>
<p>This really got me thinking that it is so close to toml that it might be a good
moment to change the configuration format. This change would make migration a
bit more work, but is also a good moment to lift some technical debt, add
support for a few missing corner cases, and clear some ambiguity.</p>
<p>I&rsquo;ve also been trying out <code>himalaya</code>, and its configuration syntax for
credentials and alike is quite clear, so I want to draw some inspiration from
it.</p>
<p>At this point, I&rsquo;m rethinking the configuration as pure toml and taking a lot
of notes on how the new format varies from the previous one. I intend to write
a clear migration guide for people to switch without much hassle. I&rsquo;ve
considered the idea of writing a migration tool, but I don&rsquo;t really know
whether it makes sense yet or not (this will depend on the feedback from people
when migrating over).</p>
<h2 id="spdx-headers">SPDX headers<div class="permalink">[<a href="#spdx-headers">permalink</a>]</div></h2>
<p>I&rsquo;ve followed a suggestion on behalf of the FSFE and added SPDX headers to
source files to facilitate re-usability by others wanting to copy an entire
file into a different project.</p>
<p>I&rsquo;m not convinced that anyone actually <em>wants</em> to do this at this stage, but
the effort isn&rsquo;t much I don&rsquo;t see any issues with this.</p>
<p>I considered using the <code>reuse</code> tool, which checks for this and other licensing
details, but it seems to be highly opinionated and brings into scope other
things which are not relevant to me.</p>
]]></description></item><item><title>Extending an expired GPG key</title><link>https://whynothugo.nl/journal/2023/07/13/extending-an-expired-gpg-key/</link><pubDate>Thu, 13 Jul 2023 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/07/13/extending-an-expired-gpg-key/</guid><description><![CDATA[<p>Slightly over a year ago, <a href="/journal/2022/07/11/using-a-yubikey-for-gpg/">I set up a new hardware-backed GPG key on my yubikey
device</a>. Today I needed to sign a release, and noticed my key expired
two days ago. It&rsquo;s time to renew it.</p>
<h2 id="possible-approaches">Possible approaches<div class="permalink">[<a href="#possible-approaches">permalink</a>]</div></h2>
<p>When a key expires, there are three alternatives on how to address this:</p>
<ul>
<li><strong>Generate a new key pair</strong>. This requires updating my public key everywhere
(e.g.: on services that use my public key, in the README for projects where I
sign releases, etc), which is somewhat of a nuisance.<br>
When using keys stored on-disk, this has a slight advantage in terms of
security: if the key was inadvertently leaked during the last year, then
switching to a new key reduces the impact of that leak. This is irrelevant
when using hardware backed keys.</li>
<li><strong>Update the expiration date of the existing key, and generate new subkeys</strong>.
This also requires redistributing the subkeys to people who verify
signatures.</li>
<li><strong>Update the expiration date of the existing key and its subkeys</strong>. This
seems to be the smoothest option. I&rsquo;ll be taking this approach.</li>
</ul>
<h2 id="updating-the-expiration-date">Updating the expiration date<div class="permalink">[<a href="#updating-the-expiration-date">permalink</a>]</div></h2>
<p>It turns out that updating the expiration date of keys is pretty simple on
recent GPG releases. First, update the expiration of the primary key:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>gpg --quick-set-expire 1204CA9FC2FFADEEDC2961367880733B9D062837 1y
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Provide the PIN</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Tap the Yubikey</span>
</span></span></code></pre></div><p>And then the subkeys. List all the fingerprints for the subkeys with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>gpg --list-secret-keys --verbose --with-subkey-fingerprints
</span></span></code></pre></div><p>And then actually update the expiration of the subkeys:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>gpg --quick-set-expire 1204CA9FC2FFADEEDC2961367880733B9D062837 1y 94248F3453FE6C15B5D57FA369799729DDF6BDD3 3DEBAC5D65DBADD5FA6A20DFF32635370237664C
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Provide the PIN</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Tap the Yubikey</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Provide the PIN again</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Tap the Yubikey again</span>
</span></span></code></pre></div><p>Check that everything looks correct:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>gpg --list-secret-keys
</span></span></code></pre></div><p>And push the keys to public keyservers:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>gpg --keyserver keyserver.ubuntu.com --send-keys 0x9D062837
</span></span><span style="display:flex;"><span>gpg --keyserver pgp.mit.edu --send-keys 0x9D062837
</span></span></code></pre></div><h2 id="sources">Sources<div class="permalink">[<a href="#sources">permalink</a>]</div></h2>
<ul>
<li><a href="https://www.gnupg.org/documentation/manuals/gnupg24/gpg.1.html">https://www.gnupg.org/documentation/manuals/gnupg24/gpg.1.html</a></li>
<li><a href="https://unix.stackexchange.com/questions/177291/how-to-renew-an-expired-keypair-with-gpg">https://unix.stackexchange.com/questions/177291/how-to-renew-an-expired-keypair-with-gpg</a></li>
<li><a href="https://gist.github.com/krisleech/760213ed287ea9da85521c7c9aac1df0">https://gist.github.com/krisleech/760213ed287ea9da85521c7c9aac1df0</a></li>
</ul>
]]></description></item><item><title>senpai: a modern IRC terminal client</title><link>https://whynothugo.nl/journal/2023/07/05/senpai-a-modern-irc-terminal-client/</link><pubDate>Wed, 05 Jul 2023 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/07/05/senpai-a-modern-irc-terminal-client/</guid><description><![CDATA[<p>I&rsquo;ve been writing some IRC-related experiments using a bouncer lately. I don&rsquo;t
have anything useful yet. My code so far connects to a bouncer,
enumerates bouncer networks, opens a second connection to the bouncer (you need
an additional connection per upstream network), and then crashes.</p>
<p>While trying to find some details on the <code>AUTHENTICATE</code> command using <code>SASL</code>
(do I know what I&rsquo;m doing at this point?), I ended up falling down a rabbit
hole where I came across a particular IRC client I that had somehow slipped
under my radar:</p>
<h1 id="senpai">Senpai<div class="permalink">[<a href="#senpai">permalink</a>]</div></h1>
<p>After testing it for a couple of days, I can say that <a href="https://sr.ht/~taiite/senpai">senpai</a> works
impressively well. I didn&rsquo;t have a configuration file the first time that I ran
it, and it prompted my for some basic connection details to create one for me.
Additional configuration is documented in <code>senpai(5)</code>.</p>
<p>I&rsquo;m using sourcehut&rsquo;s IRC bouncer, so my server is <code>chat.sr.ht</code>, username
<code>whynothugo</code> and the password is an OAuth token. The <a href="https://man.sr.ht/chat.sr.ht/quickstart.md">quickstart guide for
chat.sr.ht</a> has a handy link titled <a href="https://meta.sr.ht/oauth2/personal-token?grants=meta.sr.ht/PROFILE:RO">&ldquo;Visit the personal access token
issuance page with this specially-crafted link&rdquo;</a>. Any time that I need an
sourcehut OAuth token for IRC, this is the simplest path.</p>
<p>After a short test drive, I moved the token out of the configuration file and
into [<code>himitsu</code>]. <code>senpai</code> supports a <code>password-cmd</code> configuration directive,
which is exactly what it seems: a command that gets executed for <code>senpai</code> to
retrieve a password. I replaced <code>password</code> in my configuration with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>password-cmd hiq -dFpassword proto=irc address=chat.sr.ht nickname=whynothugo
</span></span></code></pre></div><p>Now when I start <code>senpai</code>, it will run <code>hiq</code>, which queries <code>himitsu</code> (the
secret store) for the bouncer password. I&rsquo;ll get a quick prompt to authorise
this and it connects right away.</p>
<p><code>senpai</code> is quite geared towards use with a bouncer. It starts up super fast,
and immediately shows all channel that I&rsquo;ve joined. It&rsquo;s a very fast an
effortless experience TBH.</p>
<!-- TODO: reconfirm this tomorrow morning  -->
<p>Switching between buffers can be done by clicking on the buffers on the
left-hand sidebar, or simply by typing <code>/buffer N</code>, where <code>N</code> is the buffers
number. <a href="https://todo.sr.ht/~taiite/senpai/83">There doesn&rsquo;t seem to be a shortcut for this</a>, but the buffer
names show up once I type <code>/buf</code>.</p>
<p>I haven&rsquo;t tried this on the road yet, so I&rsquo;m not sure how well it work with
slow connections. When putting the system to sleep and waking it up a few hours
later, I notice that it shows no indication of being offline, but if I send a
command to the server, it warns about being disconnected, and re-connects very
quickly. I got an impression that history re-sync immediately, so messages that
arrived while the system was asleep showed up fine.</p>
<h1 id="terminal-hints">Terminal hints<div class="permalink">[<a href="#terminal-hints">permalink</a>]</div></h1>
<p>I added this directive to the configuration file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>on-highlight-beep true
</span></span></code></pre></div><p>This makes <code>senpai</code> send a &ldquo;terminal bell&rdquo; whenever I&rsquo;m mentioned in a channel.
Terminal bells are that &ldquo;beep&rdquo; that you used to hear from the speaker inside
the computer CPU in the nineties, but nowadays terminals can map this to an
urgency hint.</p>
<p>I have the following in the configuration file for my terminal, <a href="https://codeberg.org/dnkl/foot/">foot</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;-webkit-text-size-adjust:none;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#66d9ef">[bell]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">urgent</span><span style="color:#f92672">=</span><span style="color:#e6db74">true</span>
</span></span></code></pre></div><p>This makes foot set the &ldquo;urgency hint&rdquo; for its window whenever an application
tries to &ldquo;ring the bell&rdquo;. The effect is that the window and its workspace are
highlighted in red in sway.</p>
<h1 id="notifications">Notifications<div class="permalink">[<a href="#notifications">permalink</a>]</div></h1>
<p><code>senpai</code> also supports showing notifications when one is mentioned, by running
a custom script when this happens. I tried putting the following script in
<code>~/.config/senpai/highlight</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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>echo -e <span style="color:#e6db74">&#34;\033]777;notify;senpai;You&#39;ve been mentioned on IRC\a&#34;</span>
</span></span></code></pre></div><p>Regrettably, this didn&rsquo;t work for me, even though the script was getting
executed. This script emits a terminal sequence which tells the terminal to
emit a notification. The terminal then relays it to the notification daemon.
This indirection makes notifications show up even if <code>senpai</code> is being run over
SSH on a remote system.</p>
<p>This didn&rsquo;t work because the <code>senpai</code> runs the script while sending its output
to <code>/dev/null</code> (this usually makes sense to avoid scripts from messing up
what&rsquo;s on screen). But in this case, this means that the escape sequence never
reaches the terminal. I figure out a hacky way around the issue and <a href="https://todo.sr.ht/~taiite/senpai/113">opened a
ticket</a> to discuss a cleaner fix, but a cleaner implementation is blocked
by <a href="https://github.com/gdamore/tcell/issues/499">the terminal library implementing the escape sequence upstream</a>.</p>
<p>In the meantime, I&rsquo;ve replaced <code>~/.config/senpai/highlight</code> with the following
instead:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>notify-send -a senpai -i foot <span style="color:#e6db74">&#34;senpai&#34;</span> <span style="color:#e6db74">&#34;New message from </span>$SENDER<span style="color:#e6db74">&#34;</span>
</span></span></code></pre></div><p>This won&rsquo;t work via SSH, but should be fine 99% of the time for me anyway.</p>
<!-- There's an `/NP` command to send the currently playing song. I tried this, but -->
<!-- if failed.I'm familiar with the error: [this issue in godbus][godbus]. I came -->
<!-- across it over a year ago, and again last week in some other application. -->
<!--  -->
<!-- [godbus]: https://github.com/godbus/dbus/issues/372 -->
<h1 id="nice-and-clean-code">Nice and clean code<div class="permalink">[<a href="#nice-and-clean-code">permalink</a>]</div></h1>
<p><code>senpai</code> had a quirk which I&rsquo;ve been finding quite annoying: when it starts up
it won&rsquo;t highlight channels which have received messages while it wasn&rsquo;t
running. It also won&rsquo;t highlight channels with mentions.</p>
<p>I decided to have a look at the code and try to fix this myself. I managed to
produce a patch for that, as well as a patch to add an &ldquo;unread&rdquo; ruler in
channels. Both of these have been merged upstream and will be out on the next
release.</p>
<p>I have to say that, from what I can tell so far, <code>senpai</code>&rsquo;s codebase is very
clean and easy to understand (it&rsquo;s written in go, in case you&rsquo;re wondering).</p>
<p>If you&rsquo;re on the lookout for a good IRC client, I recommend trying this out
(you&rsquo;ll have to use the version from git if you want my patches).</p>
<!-- Maybe wait until the patch is merged to publish this? -->
]]></description></item><item><title>vdirsyncer status update, June 2023</title><link>https://whynothugo.nl/journal/2023/06/30/vdirsyncer-status-update-june-2023/</link><pubDate>Fri, 30 Jun 2023 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/06/30/vdirsyncer-status-update-june-2023/</guid><description><![CDATA[<p>This month started out rather slow, with me taking a week off on vacation and
then coming back home only to be sick in bed for another week. But it ended up
being rather productive after all.</p>
<h2 id="configuration-parsing">Configuration parsing<div class="permalink">[<a href="#configuration-parsing">permalink</a>]</div></h2>
<p>My initial goal since last month was to implement a small binary that can work
as a drop-in replacement for vdirsyncer. With some basic sync scenarios
working<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>, the next step was to parse the configuration file.</p>
<p>The configuration format has a few quirks. In first place, the <em>names</em> of
storages and pairs are in section titles, which means section titles aren&rsquo;t a
static thing. In second place, property values are then treated as JSON, so I
need a second parser for that.</p>
<p>I&rsquo;m using <a href="https://docs.rs/rust-ini/latest/ini/"><code>rust-ini</code></a> to parse the configuration file itself (it&rsquo;s been a
perfect fit so far) and the famous <a href="https://docs.rs/serde_json/latest/serde_json/"><code>serde_json</code></a> to parse the property values.</p>
<p>I&rsquo;ve gotten to a point where most of my real configuration file parses
fine, and creates a <code>Config</code> object. I say “most” because one key detail is
missing: the CardDav storage.</p>
<p>So I&rsquo;ve temporarily paused development of this binary until I catch up with the
CardDav implementation.</p>
<h2 id="carddav">CardDav<div class="permalink">[<a href="#carddav">permalink</a>]</div></h2>
<p>So I set out to advance on the <a href="https://en.wikipedia.org/wiki/CardDAV">CardDav</a> (i.e.: address books) implementation.
Everything so far has mostly focused on CalDav (i.e.: calendars), while always
leaving the API definitions in place to make sure that CardDav would fit.</p>
<p>Implementing CardDav is a bit repetitive. It&rsquo;s <em>very</em> similar to CalDav but
with little differences all over the place. I&rsquo;m often tempted to try and
abstract the common parts, but it&rsquo;s often far harder than just having two very
similar looking functions.</p>
<p>However, for all the XML parsing aspect of things (reminder: both CalDav and
CardDav are XML-based), it did bother me how little could be reused from the
CalDav implementation. In fact, XML parsing was already a huge mess in itself.
Parsing worked (and pretty well I might say), but it was hard to understand, hard to
reuse parts, and quite non-trivial to parse any new structure (which was
required for CardDav).</p>
<p>I ended up rewriting all the XML parsing parts of <code>libdav</code>. And then again.</p>
<p>The first rewrite improved code re-usability, but I&rsquo;m not sure if it was
really easier to understand. This approach used the low-level parser from
<a href="https://docs.rs/quick-xml/latest/quick_xml/"><code>quick-xml</code></a>. Using the low-level parser was nice, because I could extract
only the necessary bits from the XML data that I received, and discard all the
noise. However, the price being paid for this (in terms of complexity) wasn&rsquo;t
worth it. Theoretically, performance could be stupendous with this approach,
but that would require investing a lot more time into this than what is
realistically ever going to happen.</p>
<p>After I was done, I was not happy with the result. It was a lot more code, and
a lot more complexity, and I had the impression that some bits did almost the
same work, but it was really hard to consolidate them into one thing.</p>
<p>I took a while to sit back and consider my options. It seemed like parsing the
whole XML structure into an actual in-memory tree (with pointers to the
original data, no copying!) would be superb approach. Converting data from this
tree into the final data type should be pretty simple. I could definitely do
this, but that&rsquo;s not what I signed up for: I&rsquo;m here to write a calendar (and
addressbook) synchronisation tool, not an XML tree generator.</p>
<p>Obviously, such a thing already existed. Indeed <a href="https://docs.rs/roxmltree/latest/roxmltree/"><code>roxmltree</code></a> seemed to fit the
bill perfectly and it also seems to be the best option around in terms of
performance. Great!</p>
<p>I set out to do this whole rewrite again, which took an entire day. And by
&ldquo;entire day&rdquo;, I mean, I started at 9:00hs and finished at around 23:00hs, just in
time for bed (don&rsquo;t worry, I did eat and stretch).</p>
<p>I&rsquo;m pretty happy with the result. It&rsquo;s pretty easy to extract data from an XML
tree, and instead of a huge mess of intertwined <code>Parser</code> and <code>Node</code>
implementations, I just have a few <code>parse_</code> functions (basically, one for each
query type). These functions are smaller in scope and even have a few nice
individual test each, yay.</p>
<p>Something that&rsquo;s left for later is <a href="https://todo.sr.ht/~whynothugo/vdirsyncer-rs/32">dealing with parsing non-UTF8
data</a>. So far, all the tests servers use UTF-8, but I&rsquo;m sure that
some exception exists out there.</p>
<h2 id="carddav-progress">CardDav progress<div class="permalink">[<a href="#carddav-progress">permalink</a>]</div></h2>
<p>With the XML parsing bits improved as much, I&rsquo;ve turned my attention back to
CardDav itself. It all looks in pretty good shape, and I&rsquo;m currently writing
some live tests to test functionality with live servers (currently: <code>xandikos</code>,
<code>radicale</code>, <code>baikal</code>, <code>nextcloud</code> and <code>cyrus-imap</code>). These are showing good
results so far, so I expect to close this phase pretty soon.</p>
<h2 id="some-notes-on-configuration-parsing">Some notes on configuration parsing<div class="permalink">[<a href="#some-notes-on-configuration-parsing">permalink</a>]</div></h2>
<p>My intent is for the configuration format to remain unchanged, but there are
some small subtleties that will have to change. I intend to clearly document
these, and have a clear migration guide in place before retiring the previous
tool.</p>
<!-- ## Collection properties dependant on storage type -->
<!--  -->
<!-- I've made a small change to the `vstorage` crate, so that the properties -->
<!-- available for collections are dependant on their data type. E.g.: Calendar -->
<!-- collections have some properties (e.g.: `colour`) which address book -->
<!-- collections do not. This has little end-user impact, but makes handling these a -->
<!-- bit cleaner. -->
<h2 id="notes-on-synchronising-email">Notes on synchronising email<div class="permalink">[<a href="#notes-on-synchronising-email">permalink</a>]</div></h2>
<p>I&rsquo;ve synced up with <a href="https://soywod.me/en">@soywood</a>, who is working on <a href="https://pimalaya.org/"><code>pimalaya</code></a> (open source
tools and libraries to manage emails), with regards to synchronising IMAP using
the <code>vstorage</code> library and using my same synchronisation algorithm for
synchronising emails.</p>
<p>A <code>vstorage</code> is generic on its item type and doesn&rsquo;t really care what type of
data it&rsquo;s handling or synchronising. It only cares that the data is a bunch of
bytes and that it has an API to extract a unique ID for each item.</p>
<p>In the case of IMAP, however, something else is relevant: flags. Flags are
per-message and might change while the message does not (flags are things like
&ldquo;seen&rdquo;, &ldquo;starred&rdquo;, etc). Flags are not part of the message blob itself. And
re-synchronising emails just because flags change can be expensive: imagine
having to re-download 10 email with 5MiB attachments just because they&rsquo;re now
marked as &ldquo;read&rdquo;.</p>
<p><code>vstorage</code> does have a concept for flags: <code>properties</code> (previously called
<code>metadata</code>). However, these are only applicable to collections, and not items.
IMAP flags apply only to items and not to collections.</p>
<p>It&rsquo;s not clear to me how to make this work nicely. One potential option is to
have a new concept of <code>ItemProperties</code> and shove flags in there, while defining
<code>ItemProperties</code> as &ldquo;nothing&rdquo; for CalDav and CardDav. This doesn&rsquo;t feel quite
right, so I intend to keep thinking on this before making any decision.</p>
<p>My main thought on the topic is: I intend to implement synchronising via JMAP
one day. JMAP can handle calendars, address books and emails, so it just
seems logical to synchronise emails at one point too. We&rsquo;ll see how that goes.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Basically, CalDav with HTTPS and Basic Auth works. None of the custom TLS
settings are implemented yet, nor are things like Digest Auth or Oauth.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>Understanding the XDG access portal</title><link>https://whynothugo.nl/journal/2023/06/20/understanding-the-xdg-access-portal/</link><pubDate>Tue, 20 Jun 2023 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/06/20/understanding-the-xdg-access-portal/</guid><description><![CDATA[<p>The upcoming version of Firefox 116 will use <a href="https://pipewire.org/">Pipewire</a> for camera access. This
sparked my curiosity and I wanted to know if my setup would work for this. In
recent times, Firefox has been moving rather quickly, adding dependency on
these Flatpak portals (even in scenarios where Flatpak is not involved). This
is no exception.</p>
<p>Access to the camera via pipewire is mediated by the camera portal, an
interface of the <code>xdg-desktop-portal</code>. I attempted to use a test script to see
if this would work on my machine, but the <code>xdg-desktop-portal</code> <a href="https://github.com/flatpak/xdg-desktop-portal/issues/1040">didn&rsquo;t even
expose the relevant interface</a> is my setup.</p>
<p>A friendly collaborator quickly answered and pointed out that the Camera portal
requires an <a href="https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.impl.portal.Access">access portal</a> running. The access portal is a simple portal that
shows a prompt to the user asking for confirmation to grant permission to
things like Location, Screenshot and, as you might have guessed, Camera access.</p>
<p>Only one implementation of the access portal exists: the GNOME one. I tried
running it on my machine, and it only seemed to work once, but did not prompt
me ever again after that.</p>
<p>I quickly wrote my own portal implementation (I need to improve its UI, but
I&rsquo;ll publish this at some point for all the non-GNOME users out there who&rsquo;ll
end up in the same situation). However, my access portal (<code>way-access-portal</code>,
since it&rsquo;s generic for any wayland compositor) was also not being invoked.</p>
<p>After some debugging and research, I found out that my impression (that the gtk
access portal had stopped working) was mistaken. The access portal is only
invoked once, and the <code>xdg-desktop-portal</code> saves the response into the
<code>xdg-permission-store</code>. The <code>xdg-permission-store</code> is another Flatpak
component, which basically saves the response to access requests into its tiny
database in <code>~/.local/share/flatpak/db/devices</code>.</p>
<p>I deleted the database, attempted to use the Camera Portal once more and voila,
my access portal worked!</p>
<p>The camera stream itself, however, didn&rsquo;t work with the test script. I&rsquo;m pretty
sure that this is an issue with the test script, but that&rsquo;s for another day.</p>
]]></description></item><item><title>vdirsyncer status update, May 2023</title><link>https://whynothugo.nl/journal/2023/05/27/vdirsyncer-status-update-may-2023/</link><pubDate>Sat, 27 May 2023 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/05/27/vdirsyncer-status-update-may-2023/</guid><description><![CDATA[<p>So far, <code>libdav</code> seems to be working pretty solid for interacting with CalDav
server, and <code>davcli</code> has been handy for interactively inspecting remote
collections.</p>
<p>There are a few reported issues for the previous vdirsyncer implementation that
are related URL decoding. These can probably be inspected well with <code>davcli</code>,
but I still need to write a proper guide that people can follow to do this. I
want to use this to confirm that the new implementation handles all these
scenarios fine (and debug them if necessary).</p>
<p>I tried running the <a href="/journal/2023/04/27/libdav-live-test-results/">live test suite</a> with iCloud, but that hasn&rsquo;t really work
well. It seems that  iCloud runs on an &ldquo;eventual consistency&rdquo; model. When
creating a collections and then listing existing collections, the newly created
collections is often not listed. Sometimes it takes a second to appearc,
sometimes it takes tens of seconds. Sometimes multiple collections showed up at
once, with some being from a previous test run.</p>
<p>I haven&rsquo;t continued to try and run live tests with iCloud; it doesn&rsquo;t seem
worth the effort for now. Testing on another 6 server implementations should
find any bugs on our side.</p>
<p>Finally, I&rsquo;ve been working on the synchronisation algorithm itself. A
noticeable difference from the previous implementation is that if synchronising
a single item fails, the whole operation will continue. The main implication of
this change is that if a sync conflict prevents updating a single calendar
event, the rest of a calendar will be synchronised properly. This change also
means that we have to handle synchronising two collections where some items may
previously be out of sync. I&rsquo;m pretty confident in the approach taken, and I
intend to write a proper explanation of how it work once it&rsquo;s been properly
battle tested.</p>
<p>That&rsquo;s mostly it for this month; it&rsquo;s been a slow one. My next goal is to
aggressively test the new sync algorithm.</p>
]]></description></item><item><title>Introducing davcli</title><link>https://whynothugo.nl/journal/2023/05/01/introducing-davcli/</link><pubDate>Mon, 01 May 2023 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/05/01/introducing-davcli/</guid><description><![CDATA[<p>I <a href="/journal/2023/03/28/vdirsyncer-status-update-2023-03/#caldav-auto-discovery">mentioned in the past</a> that <code>libdav</code> (the library which I&rsquo;ve
written to interact with caldav and carddav servers) implements <a href="/journal/2023/04/30/dns-based-discovery-for-caldav-and-carddav/">DNS-based
discovery</a>.</p>
<p>This should make any tools built on this library a lot easier to configure for
end users. One such tool so far is <a href="https://git.sr.ht/~whynothugo/vdirsyncer-rs/tree/main/item/davcli/README.md"><code>davcli</code></a>, a simple command line tool to
interact with caldav servers manually or via simple scripts. It is also useful
for hosting providers that want to validate that discovery is set up correctly.
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;-webkit-text-size-adjust:none;"><code class="language-console" data-lang="console"><span style="display:flex;"><span>&gt; DAVCLI_PASSWORD<span style="color:#f92672">=</span>XXX davcli caldav --server-url https://fastmail.com --username vdirsyncer@fastmail.com discover
</span></span><span style="display:flex;"><span>Discovery successful.
</span></span><span style="display:flex;"><span>- Context path: https://d277161.caldav.fastmail.com/dav/calendars
</span></span><span style="display:flex;"><span>- Calendar home set: https://d277161.caldav.fastmail.com/dav/calendars/user/vdirsyncer@fastmail.com/
</span></span></code></pre></div><p>Aside from discovery itself, <code>davcli</code> can be used to list calendars, list
components (events, todos, etc) inside calendars and fetch them. More
operations (e.g.: uploading new items) are in scope, but will be addressed at a
future time since the synchronisation aspect of vdirsyncer is a priority right
now.</p>
<p>I&rsquo;d appreciate hearing feedback from running <code>davcli</code> with it (especially if
you find any valid cases where <code>davcli</code> fails).</p>
<h1 id="demo-session">Demo session<div class="permalink">[<a href="#demo-session">permalink</a>]</div></h1>
<p>Here&rsquo;s a demo session using <code>davcli</code> on a test account:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-console" data-lang="console"><span style="display:flex;"><span>$ davcli --help
</span></span><span style="display:flex;"><span>Usage: davcli [OPTIONS] &lt;COMMAND&gt;
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span>Commands:
</span></span><span style="display:flex;"><span>  caldav
</span></span><span style="display:flex;"><span>          Operate on a CalDav server
</span></span><span style="display:flex;"><span>  carddav
</span></span><span style="display:flex;"><span>          Operate on a CardDav server
</span></span><span style="display:flex;"><span>  help
</span></span><span style="display:flex;"><span>          Print this message or the help of the given subcommand(s)
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span>[... other options trimmed for breverity ...]
</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;-webkit-text-size-adjust:none;"><code class="language-console" data-lang="console"><span style="display:flex;"><span>$ davcli caldav --help
</span></span><span style="display:flex;"><span>Operate on a CalDav server
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span>Usage: davcli caldav --server-url &lt;SERVER_URL&gt; --username &lt;USERNAME&gt; &lt;COMMAND&gt;
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span>Commands:
</span></span><span style="display:flex;"><span>  discover
</span></span><span style="display:flex;"><span>          Perform discovery and print results
</span></span><span style="display:flex;"><span>  find-calendars
</span></span><span style="display:flex;"><span>          Find calendars under the calendar home set
</span></span><span style="display:flex;"><span>  list-calendar-components
</span></span><span style="display:flex;"><span>          List calendar components under a given calendar collection
</span></span><span style="display:flex;"><span>  get
</span></span><span style="display:flex;"><span>          Fetches a single calendar component
</span></span><span style="display:flex;"><span>  help
</span></span><span style="display:flex;"><span>          Print this message or the help of the given subcommand(s)
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span>Options:
</span></span><span style="display:flex;"><span>      --server-url &lt;SERVER_URL&gt;
</span></span><span style="display:flex;"><span>          A base URL from which to discover the server.
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span>          Examples: `http://localhost:8080`, `https://example.com`.
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span>      --username &lt;USERNAME&gt;
</span></span><span style="display:flex;"><span>          Username for authentication
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span>  -h, --help
</span></span><span style="display:flex;"><span>          Print help (see a summary with &#39;-h&#39;)
</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;-webkit-text-size-adjust:none;"><code class="language-console" data-lang="console"><span style="display:flex;"><span>$ davcli caldav discover --help
</span></span><span style="display:flex;"><span>Perform discovery and print results
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span>Usage: davcli caldav --server-url &lt;SERVER_URL&gt; --username &lt;USERNAME&gt; discover
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span>Options:
</span></span><span style="display:flex;"><span>  -h, --help  Print help
</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;-webkit-text-size-adjust:none;"><code class="language-console" data-lang="console"><span style="display:flex;"><span>$ davcli caldav --server-url https://fastmail.com --username vdirsyncer@fastmail.com discover
</span></span><span style="display:flex;"><span>Discovery successful.
</span></span><span style="display:flex;"><span>- Context path: https://d277161.caldav.fastmail.com/dav/calendars
</span></span><span style="display:flex;"><span>- Calendar home set: https://d277161.caldav.fastmail.com/dav/calendars/user/vdirsyncer@fastmail.com/
</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;-webkit-text-size-adjust:none;"><code class="language-console" data-lang="console"><span style="display:flex;"><span>$ davcli caldav --server-url https://fastmail.com --username vdirsyncer@fastmail.com find-calendars
</span></span><span style="display:flex;"><span>/dav/calendars/user/vdirsyncer@fastmail.com/w6SUMkhoSb8JD3qF/
</span></span><span style="display:flex;"><span>/dav/calendars/user/vdirsyncer@fastmail.com/w9y5I6DKhBKIYLiB/
</span></span><span style="display:flex;"><span>/dav/calendars/user/vdirsyncer@fastmail.com/wJj87C2IVgAdzyLm/
</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;-webkit-text-size-adjust:none;"><code class="language-console" data-lang="console"><span style="display:flex;"><span>$ davcli caldav --server-url https://fastmail.com --username vdirsyncer@fastmail.com list-calendar-components /dav/calendars/user/vdirsyncer@fastmail.com/w6SUMkhoSb8JD3qF/
</span></span><span style="display:flex;"><span>/dav/calendars/user/vdirsyncer@fastmail.com/w6SUMkhoSb8JD3qF/eppLJLoI6kNs.ics
</span></span><span style="display:flex;"><span>/dav/calendars/user/vdirsyncer@fastmail.com/w6SUMkhoSb8JD3qF/qU2lNaMmAZVj.ics
</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;-webkit-text-size-adjust:none;"><code class="language-console" data-lang="console"><span style="display:flex;"><span>$ davcli caldav --server-url https://fastmail.com --username vdirsyncer@fastmail.com get /dav/calendars/user/vdirsyncer@fastmail.com/w6SUMkhoSb8JD3qF/eppLJLoI6kNs.ics
</span></span><span style="display:flex;"><span>BEGIN:VCALENDAR
</span></span><span style="display:flex;"><span>VERSION:2.0
</span></span><span style="display:flex;"><span>PRODID:-//hacksw/handcal//NONSGML v1.0//EN
</span></span><span style="display:flex;"><span>BEGIN:VEVENT
</span></span><span style="display:flex;"><span>UID:elgeVuhUQzZn
</span></span><span style="display:flex;"><span>DTSTAMP:19970610T172345Z
</span></span><span style="display:flex;"><span>DTSTART:19970714T170000Z
</span></span><span style="display:flex;"><span>SUMMARY:hello\, testing
</span></span><span style="display:flex;"><span>END:VEVENT
</span></span></code></pre></div>]]></description></item><item><title>DNS-based discovery for CalDav and CardDav</title><link>https://whynothugo.nl/journal/2023/04/30/dns-based-discovery-for-caldav-and-carddav/</link><pubDate>Sun, 30 Apr 2023 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/04/30/dns-based-discovery-for-caldav-and-carddav/</guid><description><![CDATA[<p><a href="https://www.rfc-editor.org/rfc/rfc6764">rfc6764</a> describes a mechanism for caldav service providers to expose exactly
where their server is located while requiring minimal input from the user.</p>
<p>The main intent here is that if you are hosting your email and calendar for
<code>example.com</code>, your users only need to provide their calendar application with
the domain <code>example.com</code> and it can automatically figure out exactly where the
caldav server is located. Let&rsquo;s assume, for this example, that the caldav
service is located at <code>https://dav.example.com/caldav/</code>.</p>
<p>First of all, a DNS record indicates the domain where the server runs. This can
be another domain, or a subdomain:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>_caldavs._tcp     SRV 0 1 443 dav.example.com.
</span></span></code></pre></div><p>And a separate record that indicates the path at which the caldav server runs
on that host:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>_caldavs._tcp    TXT path=/caldav
</span></span></code></pre></div><p>This prevents the user from having to figure out the exact path and port to
provide to the calendar application.</p>
<p>Some real life 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;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>_caldavs._tcp.posteo.de.	300	IN	SRV	0 0 8443 posteo.de.
</span></span><span style="display:flex;"><span>_caldavs._tcp.posteo.de.	300	IN	TXT	&#34;path=/&#34;
</span></span><span style="display:flex;"><span>_caldavs._tcp.fastmail.com.	2923	IN	SRV	0 1 443 d277161.caldav.fastmail.com.
</span></span></code></pre></div><p>Fastmail doesn&rsquo;t have a TXT record. Instead it uses another approach specified
in the RFC: the well-known path <code>/.well-known/caldav</code> redirects to the real
path where the caldav server runs.</p>
<p>There&rsquo;s nothing extremely complicated here, and this mechanism makes life a lot
easier for end users. Without it, users would have to dig through their
provider&rsquo;s documentation to find the exact URL and port and copy-paste all that
into their calendar application. It&rsquo;s not <em>terrible</em>, but far from a pleasant
experience.</p>
<p>If you are hosting a public caldav service and don&rsquo;t have discovery set up, I
encourage you to look into it. It&rsquo;s a one-time kind of setup that will make
life easier for many of your users (and even yourself when configuring new
devices).</p>
]]></description></item><item><title>libdav live test results</title><link>https://whynothugo.nl/journal/2023/04/27/libdav-live-test-results/</link><pubDate>Thu, 27 Apr 2023 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/04/27/libdav-live-test-results/</guid><description><![CDATA[<p>One of my goals for re-writing vdirsyncer was to write &ldquo;live tests&rdquo;. These
tests are series of operations against a real CalDav server, failing if the
server doesn&rsquo;t behave as expected (or if it doesn&rsquo;t support a specific
feature).</p>
<p>I&rsquo;ve had this working for a couple of weeks now, and while it always feels like
&ldquo;it could be better&rdquo; (the code quality isn&rsquo;t great, but it&rsquo;s also not my
worst), the reality is that it fulfils its goal and can be improved later as
needed. This is, after all, a test tool, and not something end-user facing.</p>
<h2 id="profiles">Profiles<div class="permalink">[<a href="#profiles">permalink</a>]</div></h2>
<p>Profiles are tiny files which contain all the information run these integration
tests with a real server. Here&rsquo;s the one for baikal as an 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;-webkit-text-size-adjust:none;"><code class="language-toml" data-lang="toml"><span style="display:flex;"><span><span style="color:#a6e22e">host</span> = <span style="color:#e6db74">&#34;http://localhost:8002&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">username</span> = <span style="color:#e6db74">&#34;baikal&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">password</span> = <span style="color:#e6db74">&#34;baikal&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">xfail</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">CreateAndDeleteCollection</span> = <span style="color:#e6db74">&#34;https://github.com/sabre-io/Baikal/issues/1182&#34;</span>
</span></span></code></pre></div><p>The <code>xfail</code> second lists tests that are known to fail with a given server. This
helps detect any <em>new</em> regressions, since a couple of tests are known to fail
due to servers not implementing some required features.</p>
<h2 id="results">Results<div class="permalink">[<a href="#results">permalink</a>]</div></h2>
<p>I&rsquo;ve tried to make the results as easy to read as possible, since I like to run
these tests again when I change something internally to make sure it doesn&rsquo;t
break with any servers.</p>
<p>These are the current 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;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>🗓️ Running tests for: live_tests/xandikos.profile
</span></span><span style="display:flex;"><span>- CreateAndDeleteCollection: ✅ passed
</span></span><span style="display:flex;"><span>- CreateAndForceDeleteCollection: ✅ passed
</span></span><span style="display:flex;"><span>- SetAndGetDisplayName: ✅ passed
</span></span><span style="display:flex;"><span>- SetAndGetColour: ✅ passed
</span></span><span style="display:flex;"><span>- CreateAndDeleteResource: ✅ passed
</span></span><span style="display:flex;"><span>- CreateAndFetchResource: ✅ passed
</span></span><span style="display:flex;"><span>- FetchMissingResource: ✅ passed
</span></span><span style="display:flex;"><span>- CheckAdvertisesSupport: ✅ passed
</span></span><span style="display:flex;"><span>✅ Tests 8 completed.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>🗓️ Running tests for: live_tests/baikal.profile
</span></span><span style="display:flex;"><span>- CreateAndDeleteCollection: ⚠️ expected failure: https://github.com/sabre-io/Baikal/issues/1182
</span></span><span style="display:flex;"><span>- CreateAndForceDeleteCollection: ✅ passed
</span></span><span style="display:flex;"><span>- SetAndGetDisplayName: ✅ passed
</span></span><span style="display:flex;"><span>- SetAndGetColour: ✅ passed
</span></span><span style="display:flex;"><span>- CreateAndDeleteResource: ✅ passed
</span></span><span style="display:flex;"><span>- CreateAndFetchResource: ✅ passed
</span></span><span style="display:flex;"><span>- FetchMissingResource: ✅ passed
</span></span><span style="display:flex;"><span>- CheckAdvertisesSupport: ✅ passed
</span></span><span style="display:flex;"><span>✅ Tests 8 completed.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>🗓️ Running tests for: live_tests/radicale.profile
</span></span><span style="display:flex;"><span>- CreateAndDeleteCollection: ✅ passed
</span></span><span style="display:flex;"><span>- CreateAndForceDeleteCollection: ✅ passed
</span></span><span style="display:flex;"><span>- SetAndGetDisplayName: ✅ passed
</span></span><span style="display:flex;"><span>- SetAndGetColour: ✅ passed
</span></span><span style="display:flex;"><span>- CreateAndDeleteResource: ✅ passed
</span></span><span style="display:flex;"><span>- CreateAndFetchResource: ✅ passed
</span></span><span style="display:flex;"><span>- FetchMissingResource: ✅ passed
</span></span><span style="display:flex;"><span>- CheckAdvertisesSupport: ✅ passed
</span></span><span style="display:flex;"><span>✅ Tests 8 completed.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>🗓️ Running tests for: live_tests/fastmail.profile
</span></span><span style="display:flex;"><span>- CreateAndDeleteCollection: ⚠️ expected failure: precondition failed
</span></span><span style="display:flex;"><span>- CreateAndForceDeleteCollection: ✅ passed
</span></span><span style="display:flex;"><span>- SetAndGetDisplayName: ✅ passed
</span></span><span style="display:flex;"><span>- SetAndGetColour: ⚠️ expected failure: https://github.com/cyrusimap/cyrus-imapd/issues/4489
</span></span><span style="display:flex;"><span>- CreateAndDeleteResource: ✅ passed
</span></span><span style="display:flex;"><span>- CreateAndFetchResource: ✅ passed
</span></span><span style="display:flex;"><span>- FetchMissingResource: ✅ passed
</span></span><span style="display:flex;"><span>- CheckAdvertisesSupport: ⚠️ expected failure: server does not adviertise caldav support (unreported)
</span></span><span style="display:flex;"><span>✅ Tests 8 completed.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>🗓️ Running tests for: live_tests/nextcloud.profile
</span></span><span style="display:flex;"><span>- CreateAndDeleteCollection: ⚠️ expected failure: server does not return etags (unreported)
</span></span><span style="display:flex;"><span>- CreateAndForceDeleteCollection: ✅ passed
</span></span><span style="display:flex;"><span>- SetAndGetDisplayName: ✅ passed
</span></span><span style="display:flex;"><span>- SetAndGetColour: ✅ passed
</span></span><span style="display:flex;"><span>- CreateAndDeleteResource: ✅ passed
</span></span><span style="display:flex;"><span>- CreateAndFetchResource: ✅ passed
</span></span><span style="display:flex;"><span>- FetchMissingResource: ✅ passed
</span></span><span style="display:flex;"><span>- CheckAdvertisesSupport: ⚠️ expected failure: https://github.com/nextcloud/server/issues/37374
</span></span><span style="display:flex;"><span>✅ Tests 8 completed.
</span></span></code></pre></div><p>As you can see, these tests cover all the &ldquo;primitive&rdquo; operations. Everything
else is built on top of them, so as long as they work, more complex flows
should work too. I&rsquo;ll of course refine as needed if I find any bugs along the
way.</p>
<p>The source is available in the <a href="https://git.sr.ht/~whynothugo/vdirsyncer-rs/tree/main/item/live_tests"><code>vdirsyncer-rs</code> repository</a>.</p>
]]></description></item><item><title>Tracking dotfiles</title><link>https://whynothugo.nl/journal/2023/04/18/tracking-dotfiles/</link><pubDate>Tue, 18 Apr 2023 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/04/18/tracking-dotfiles/</guid><description><![CDATA[<p>The term <em>dotfiles</em> refers to files with filenames starting with a dot. These
are treated as &ldquo;hidden&rdquo; files and usually not listed by default in most tools
unless an extra flag is provided, a &ldquo;show hidden files&rdquo; checkbox is checked or
some similar mechanism.</p>
<p>Additionally, the term dotfiles is nowadays often used to describe a specific
collection of these files: the ones that contain a user&rsquo;s configuration
associated helper scripts and some other related files. From the
<a href="https://wiki.archlinux.org/title/Dotfiles">Archwiki</a>:</p>
<blockquote>
<p>User-specific application configuration is traditionally stored in so called
dotfiles (files whose filename starts with a dot). It is common practice to
track dotfiles with a version control system such as Git to keep track of
changes and synchronize dotfiles across various hosts.</p>
</blockquote>
<p>I&rsquo;ve been keeping my dotfiles in a git repository since 2012, and it&rsquo;s been of
great value. For one, when I reconfigure a program I&rsquo;m not terrified of &ldquo;what
if this doesn&rsquo;t really work out&rdquo;: if the new approach is unconvincing, I can
easily roll back to the previous version. Any I can even inspect how something
was configured months ago for reference. This alone completely changes how I
approach improving workflow or making configuration changes.</p>
<p>That aside, it serves as a great mechanism to keep my configuration in sync
across devices; I can commit a change on my desktop and later pull that change
into my laptop (or Linux-based phone). At the time, I used this approach to
sync configuration of tools across my work computer and home computer.</p>
<h2 id="approaches-on-tracking-dotfiles">Approaches on tracking dotfiles<div class="permalink">[<a href="#approaches-on-tracking-dotfiles">permalink</a>]</div></h2>
<p>The idea is solid, and there&rsquo;s a lot of different approaches to achieve similar
results. Most of them have downsides, so here&rsquo;s an overview of everything I&rsquo;ve
tried in over a decade.</p>
<h3 id="tracking-all-of-home">Tracking all of <code>$HOME</code><div class="permalink">[<a href="#tracking-all-of-home">permalink</a>]</div></h3>
<p>My first approach was to just create a git repository in <code>$HOME</code>, add the
directories and files that I want, and ignore everything else. By &ldquo;ignore&rdquo; I
mean &ldquo;configure git to ignore&rdquo; these files so that they don&rsquo;t continuously show
up as untracked files. This unclutters git&rsquo;s output which would otherwise
always list irrelevant files.</p>
<p>Keeping this list of ignored files up to date was a pain. There&rsquo;s <em>always</em>
something new coming up. Of course, I could have ignored <code>*</code> and then un-ignore
everything else, but this results is directories in <code>.config</code> sometimes being
ignored-by-default. It&rsquo;s very easy to not notice that a specific directory was
not tracked until it&rsquo;s too late to go back.</p>
<p>Not great at all.</p>
<p>Another downside of this approach is that <code>$HOME</code> is now technically a git
repository, and so are all of its subdirectories. Shell integrations that show
the git-status of the current directory suddenly trigger on <em>any</em> directory,
slowing everything down and displaying meaningless output. If I run <code>git status</code> on a directory that <em>I think</em> is a repository, <code>git </code> yields confusing
results too because it&rsquo;s not really a repository the way I though it was; it is
actually part of the dotfiles repository.</p>
<p>All in all, this approach is confusing and had issues.</p>
<h3 id="homesick">Homesick<div class="permalink">[<a href="#homesick">permalink</a>]</div></h3>
<p>For years I used a tool called <a href="https://github.com/technicalpickles/homesick"><code>homesick</code></a>. The general idea is that the
dotfiles repository is in a dedicated directory, and <code>homesick</code> creates
<a href="https://en.wikipedia.org/wiki/Symbolic_link">symlinks</a> to these files in the &ldquo;real&rdquo; location in my home directory.</p>
<p>So, for example if my dotfiles repository is in <code>$HOME/.dotfiles</code>, then
<code>$HOME/config/git</code> is a symlink to <code>$HOME/dotfiles/.config/git</code></p>
<p>A nice upside of this symlinking approach is that it&rsquo;s easy to see which files
are tracked in my dotfiles repository. I can just run <code>ls -l $HOME/.config</code>,
and very easily visualise which files are symlinks. Those that are not symlinks
are not tracked.</p>
<p>This worked really well for years. The biggest pain points were a few major
updates of Ruby. <code>homesick</code> is written in Ruby, and sometimes when a new Ruby
update rolled out, <code>homesick</code> broke and it needed to be updated.</p>
<p>The last time it happened, it went unfixed for a long time (if it&rsquo;s ever been
fixed at all). I have no idea of what the issue was, or how to fix it either
(I&rsquo;m no Ruby expert). I guess I could have manually installed an older Ruby
version, but that has its own set of issues considering that this tool is used
to bootstrap new hosts.</p>
<h3 id="my-own-sync-tool">My own sync tool<div class="permalink">[<a href="#my-own-sync-tool">permalink</a>]</div></h3>
<p>I tried most other tools around, but none of them convinced me. Some of them
had more complicated abstractions or rules, others had too many dependencies or
tricky setup scenarios.</p>
<p>So I ended up writing <a href="https://git.sr.ht/~whynothugo/dotfiles/tree/004c9fa5b90e6ccfa6ce9c86b6efcfa9330a81e2/item/src/main.rs">my own very simple tool</a> that did a subset of what
<code>homesick</code> does. Specifically, it implemented the subset of features that I
use. This was initially in Python, and later in Rust (which helped iron out a
lot of edge cases). Every once in a while I find a tiny tweak or improvement
that I can make, but overall it&rsquo;s been really stable.</p>
<p>Most of what this tool does is find files in the dotfiles repository, and make
sure that symlinks exist in their expect location, pointing to the
in-repository version.</p>
<p>It works well, and does what it must, but has an issue that&rsquo;s present in all
the tools that use a &ldquo;symlinking&rdquo; approach, which is what the next section is
about:</p>
<h2 id="sandboxes-and-containers">Sandboxes and containers<div class="permalink">[<a href="#sandboxes-and-containers">permalink</a>]</div></h2>
<p>When a configuration file for a given program is in my dotfiles, it&rsquo;s stored in
a repository in <code>$HOME/.dotfiles</code>, and a symlink is placed under the real
location (e.g.: <code>$HOME/.config/git</code>) pointing to it.</p>
<p>This usually works, but breaks in a specific scenario: sandboxing. If an
application runs in a sandbox and is given access to its configuration location
(e.g.: <code>$HOME/.config/some-app</code>), it can &ldquo;see&rdquo; the symlink, but trying to
follow it fails due to <code>$HOME/.dotfiles</code> not being accessible inside the
sandbox.</p>
<p>This happens with all sorts of sandboxing, including Firejail, Flatpak or when
simply running a docker container with permissions to read <code>$HOME/.config</code>.</p>
<p>For example, I use neovim with some sandboxed/containerised language servers.
Language servers are given read-only access to the directory of the files being
edited. If I am editing <code>$HOME/.config/nvim/init.lua</code>, then the language server
can only access this directory. But because these files are symlinks, it tries
to read the target files (e.g.: where these symlinks point to), but fails
because, again, these are not accessible inside the sandbox.</p>
<p>Finally I should note: some very few tools don&rsquo;t play well with symlinks: when
they edit a file, they recreate it, so they remove the symlink and place a
regular file, which requires that I later resolve the conflict manually before
this can be sync into my dotfiles repository. This is a very rare though, and a
very minor problem.</p>
<h2 id="moving-forward">Moving forward<div class="permalink">[<a href="#moving-forward">permalink</a>]</div></h2>
<p>I don&rsquo;t think the symlink situation is great. As more tools run with an
isolated view of the filesystem (which is a great practice security-wise), the
issues around it will worsen.</p>
<p>So far I&rsquo;ve considered no longer using symlinks, but instead copying (or maybe
hard-linking) the files in-place out of the dotfiles repository. This likely
requires triggering conflict resolution manually when a file was edited in its
external location and a modified version is pulled by git. This scenario
shouldn&rsquo;t be very frequent and isn&rsquo;t terrible anyway; I can just trigger
<code>vimdiff</code> to handle this manually.</p>
<p>The main downsides of this approach are:</p>
<ul>
<li>It&rsquo;s very hard to visualise which files are tracked and which ones aren&rsquo;t. I
might add configuration for a new program in <code>.config</code>, and forever miss that
it hasn&rsquo;t been committed.</li>
<li>There&rsquo;s no tracking entire directories; only individual files.</li>
</ul>
<p>The second problem can be solved by keeping list of directories into which the
sync tool should automatically operate recursively.</p>
<p>For the first problem, however, I cannot see any obvious solution. Perhaps
writing a bunch of code to fine untracked directories? I suspect this would
permanently print out a lot of noise much like the situation with my first
approach to dotfile-tracking.</p>
]]></description></item><item><title>Installing postmarketOS on a OnePlus 6 with an encrypted filesystem</title><link>https://whynothugo.nl/journal/2023/04/12/installing-postmarketos-on-a-oneplus-6-with-an-encrypted-filesystem/</link><pubDate>Wed, 12 Apr 2023 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/04/12/installing-postmarketos-on-a-oneplus-6-with-an-encrypted-filesystem/</guid><description><![CDATA[<p>I&rsquo;ve been experimenting with <a href="https://postmarketos.org/">postmarketOS</a> for a long time now. My long-term
aspiration is to set up a phone that I can use as a daily driver. I need to
overcome a lot of obstacles to achieve this.</p>
<p>Today, my goal is to encrypt the main partition. So if I lose the phone, it
doesn&rsquo;t have all my messaging, email and credentials totally unencrypted. This
is a hard requirement before moving forward with a lot of other things.</p>
<p>Encrypting the main partition is a lot more complex on pmOS compared to a
regular computing device. On most other devices (desktop, laptop server or even
a Raspberry Pi) I would usually boot into a minimal environment with the tools
necessary for partitioning, encrypting installation, etc.</p>
<p>postmarketOS takes a rather distinct approach to installation: instead of
running the installation and setup process on the device, an entire disk image
must be build on another computer, and then flashed over onto the phone. This
approach works really well if you have a dozens or hundreds of devices and
makes things a bit simpler for devices that don&rsquo;t have physical keyboards.
Personally: I would have preferred a simple installation via SSH.</p>
<h2 id="preparing-a-workspace-vm">Preparing a workspace VM<div class="permalink">[<a href="#preparing-a-workspace-vm">permalink</a>]</div></h2>
<p>To create a device image, I need a privileged environment with <code>pmbootstrap</code>.
I&rsquo;m going to use a VM for this to avoid messing up my system.</p>
<p>First, create a disk image for the VM:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>qemu-img create workspace.img 20G
</span></span></code></pre></div><p>Then, <a href="https://alpinelinux.org/downloads/">download the Alpine &ldquo;virtual&rdquo; image</a>, and run a VM with both
the disk and the Alpine installation image installed (<a href="/journal/2022/07/01/quick-and-simple-vms-with-qemu/">notes on qemu
usage</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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>qemu-system-x86_64 --enable-kvm <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -nic user <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -nographic <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -m 1G <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -smp cores<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -drive file<span style="color:#f92672">=</span>alpine-virt-3.17.3-x86_64.iso,format<span style="color:#f92672">=</span>raw <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -drive file<span style="color:#f92672">=</span>workspace.img,format<span style="color:#f92672">=</span>raw
</span></span></code></pre></div><p>You might have to drop <code>--enable-kvm</code> if you hardware doesn&rsquo;t support KVM. If
you&rsquo;re on <code>aarch64</code> just use the <code>aarch64</code> image and <code>qemu-system-aarch64</code>. You
don&rsquo;t <strong>need</strong> to use <code>x86_64</code>, this is just what matches my architecture.</p>
<p>In the VM, use <code>setup-alpine</code> and go through the Alpine installation process.
Nothing special needs to be done here.I didn&rsquo;t bother setting up <code>ssh</code> here and
left an empty root password.</p>
<p>Then turn off the VM with <code>poweroff</code>, and start it again, this time without the
installation media and <a href="https://wiki.qemu.org/Documentation/9psetup">exposing a directory on the host via <code>9p</code></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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>qemu-system-x86_64 --enable-kvm <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -nic user <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -nographic <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -m 1G -smp cores<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -drive file<span style="color:#f92672">=</span>workspace.img,format<span style="color:#f92672">=</span>raw <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -virtfs local,path<span style="color:#f92672">=</span>/home/hugo/workspace/oneplus/output,mount_tag<span style="color:#f92672">=</span>output,security_model<span style="color:#f92672">=</span>mapped-xattr
</span></span></code></pre></div><p>Log in as root, and then edit <code>/etc/apk/repositories</code>. Uncomment the <code>edge</code>
repositories and remove everything else. E.g.:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>http://dl-cdn.alpinelinux.org/alpine/edge/main
</span></span><span style="display:flex;"><span>http://dl-cdn.alpinelinux.org/alpine/edge/community
</span></span><span style="display:flex;"><span>http://dl-cdn.alpinelinux.org/alpine/edge/testing
</span></span></code></pre></div><p>Run <code>apk update &amp;&amp; apk upgrade -a</code> and then <code>reboot</code> (the kernel likely gets
updated, so it&rsquo;s best to boot into the new one).</p>
<h2 id="building-the-device-images">Building the device images<div class="permalink">[<a href="#building-the-device-images">permalink</a>]</div></h2>
<p>Install <code>pmbootstrap</code>. It also expects either <code>sudo</code> or <code>doas</code> to be installed
and configured. <code>sudo</code> is the easier choice, since it comes pre-configured to
allow <code>root</code> to use it for anything (and I&rsquo;ll be using user <code>root</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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>apk add pmbootstrap sudo
</span></span></code></pre></div><p><code>pmbootstrap</code> uses <code>sudo</code> <strong>a lot</strong>. Simply building an image has 155
invocations of so it would need to be configured with <code>NOPASSWD</code>, plus a
dedicated user for it, and then need to <code>sudo -u newuser pmbootstrap</code> each
call. I&rsquo;m going to skip this pointless indirection entirely:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>alias pmbootstrap<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pmbootstrap --as-root&#34;</span>
</span></span></code></pre></div><p>Now initialise 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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>pmbootstrap init
</span></span></code></pre></div><p>The default work path is fine, and so is the <code>edge</code> channel. However, for
vendor and device pick the ones matching your phone. That&rsquo;s <code>oneplus</code> and
<code>enchilada</code> respectively for me. See <a href="https://wiki.postmarketos.org/wiki/Devices">pmOS#Devices</a> to find yours.</p>
<p>There&rsquo;s a variety of user interfaces available. <code>phosh</code> is a simple user
friendly one, but not very flexible, tweakable nor developer-oriented. I&rsquo;m
going with <a href="https://sxmo.org/"><code>sxmo-de-sway</code></a>. <code>sxmo</code> takes some learning to initially
understand (especially if you&rsquo;re coming from iOS/Android), but it&rsquo;s the most
hackable one and a very good target if you&rsquo;re wanting to develop applications
and test them on phones.</p>
<p>Additional packages can be specified at this time. 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;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>zsh,docs,git,neovim,font-noto-cjk,cargo,yubikey-manager,dino,telegram-desktop,bubblewrap
</span></span></code></pre></div><p>Now it&rsquo;s time to build the actual image. Contrary to what I initially expected
<code>pmbootstrap build</code> does not build the image (this command builds a single
package). It&rsquo;s <code>pmbootstrap install</code> that builds an image, and <code>pmbootstrap export</code> creates some handy symlinks to it.</p>
<p>To build an image with full disk encryption, use <code>pmbootstrap install --fde</code>.</p>
<p>Put together, it all looks something 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;-webkit-text-size-adjust:none;"><code class="language-console" data-lang="console"><span style="display:flex;"><span># pmbootstrap install --fde
</span></span><span style="display:flex;"><span>[... lots of output ommitted ...]
</span></span><span style="display:flex;"><span># pmbootstrap export
</span></span><span style="display:flex;"><span>[13:34:56] (rootfs_oneplus-enchilada) install device-oneplus-enchilada
</span></span><span style="display:flex;"><span>[13:34:58] (rootfs_oneplus-enchilada) install postmarketos-mkinitfs
</span></span><span style="display:flex;"><span>[13:35:00] (rootfs_oneplus-enchilada) mkinitfs postmarketos-qcom-sdm845
</span></span><span style="display:flex;"><span>[13:35:03] Export symlinks to: /tmp/postmarketOS-export
</span></span><span style="display:flex;"><span>[13:35:03]  * boot.img (Fastboot compatible boot.img file, contains initramfs and kernel)
</span></span><span style="display:flex;"><span>[13:35:03]  * initramfs-extra (Extra initramfs files in /boot)
</span></span><span style="display:flex;"><span>[13:35:03]  * initramfs (Initramfs)
</span></span><span style="display:flex;"><span>[13:35:03]  * vmlinuz (Linux kernel)
</span></span><span style="display:flex;"><span>[13:35:03]  * oneplus-enchilada.img (Rootfs with partitions for /boot and /)
</span></span><span style="display:flex;"><span>[13:35:03] NOTE: chroot is still active (use &#39;pmbootstrap shutdown&#39; as necessary)
</span></span><span style="display:flex;"><span>[13:35:03] DONE!
</span></span><span style="display:flex;"><span># ls -l /tmp/postmarketOS-export/
</span></span><span style="display:flex;"><span>total 0
</span></span><span style="display:flex;"><span>lrwxrwxrwx    1 root     root            74 Apr 12 13:30 boot.img -&gt; /root/.local/var/pmbootstrap/chroot_rootfs_oneplus-enchilada/boot/boot.img
</span></span><span style="display:flex;"><span>lrwxrwxrwx    1 root     root            75 Apr 12 13:30 initramfs -&gt; /root/.local/var/pmbootstrap/chroot_rootfs_oneplus-enchilada/boot/initramfs
</span></span><span style="display:flex;"><span>lrwxrwxrwx    1 root     root            81 Apr 12 13:30 initramfs-extra -&gt; /root/.local/var/pmbootstrap/chroot_rootfs_oneplus-enchilada/boot/initramfs-extra
</span></span><span style="display:flex;"><span>lrwxrwxrwx    1 root     root            81 Apr 12 13:30 oneplus-enchilada.img -&gt; /root/.local/var/pmbootstrap/chroot_native/home/pmos/rootfs/oneplus-enchilada.img
</span></span><span style="display:flex;"><span>lrwxrwxrwx    1 root     root            73 Apr 12 13:30 vmlinuz -&gt; /root/.local/var/pmbootstrap/chroot_rootfs_oneplus-enchilada/boot/vmlinuz
</span></span></code></pre></div><p>Now mount the directory that&rsquo;s exposed from the host. Note that <code>output</code> here
matches the value provided for <code>mount_tag</code> when specified when running <code>qemu</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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>mount -t 9p -o trans<span style="color:#f92672">=</span>virtio output /mnt
</span></span></code></pre></div><p>An copy the images out:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>cp /tmp/postmarketOS-export/boot.img /mnt/
</span></span><span style="display:flex;"><span>cp /tmp/postmarketOS-export/oneplus-enchilada.img /mnt/
</span></span></code></pre></div><p>It is now safe to turn off the virtual machine: <code>poweroff</code>.</p>
<h2 id="flashing-the-images-onto-the-device">Flashing the images onto the device<div class="permalink">[<a href="#flashing-the-images-onto-the-device">permalink</a>]</div></h2>
<p>I&rsquo;m assuming the device is already unlocked (otherwise check the Devices wiki
page linked above). Make sure the USB cable is not plugged into the phone and
then press and hold <kbd>VolUp</kbd>+<kbd>Power</kbd>. After it boots, it will
indicate &ldquo;Fastboot&rdquo; mode and once it&rsquo;s in the menu you can release first
<kbd>VolUp</kbd>, then <kbd>Power</kbd>. Now plug the USB cable onto the phone
and the other end of it to your other computer.</p>
<p>Flash the images onto the device and then reboot 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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>doas fastboot flash boot boot.img
</span></span><span style="display:flex;"><span>doas fastboot flash userdata oneplus-enchilada.img
</span></span><span style="display:flex;"><span>doas fastboot reboot
</span></span></code></pre></div><p>During the initial boot, the main partition will be extended to occupy all of
the disk.</p>
<p>The initial installation is now done.</p>
]]></description></item><item><title>Thoughts on sendmail in 2023</title><link>https://whynothugo.nl/journal/2023/03/30/thoughts-on-sendmail-in-2023/</link><pubDate>Thu, 30 Mar 2023 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/03/30/thoughts-on-sendmail-in-2023/</guid><description><![CDATA[<p>Sendmail is a classic <a href="https://en.wikipedia.org/wiki/Message_transfer_agent">mail transfer agent (MTA)</a> in the world of
Unix-like systems. It&rsquo;s design is simple, and worked well for many setups for
decades. For many scenarios it still works. For others, it does not.</p>
<h1 id="sendmail-as-a-system-wide-tool">Sendmail as a system-wide tool<div class="permalink">[<a href="#sendmail-as-a-system-wide-tool">permalink</a>]</div></h1>
<p>Sendmail is typically set up by a system administrator and configured to relay
emails from any local user via some administrator-defined mechanism. This
doesn&rsquo;t play well with typical multi-users systems as we see today, especially
if they each have their own different mailbox providers and accounts. One user
might want their email to be sent via their provider, <code>example.com</code>, while
another user uses <code>another-example.com</code> for their email services.</p>
<p>Even for single-user systems, <code>sendmail</code> needs access to the credentials to
forward emails via the user&rsquo;s email account. <code>sendmail</code> is then exposed to all
processes of the local system. So a developer working on and running an
application that sends emails might accidentally start sending emails via their
own account &ndash; without any prior confirmation. The same is true for misbehaving
programs and it&rsquo;s an incredibly juice target for exploits.</p>
<p>However, many tools have come to support simply running <code>sendmail</code> to send
emails, so maintaining its interface is quite valuable.</p>
<h1 id="we-can-do-better">We can do better<div class="permalink">[<a href="#we-can-do-better">permalink</a>]</div></h1>
<p>The solution should start becoming obvious by now. A new tools is needed with
a few specific requirements.</p>
<p>First of all, it needs to be per-user configurable. So each user can specify
their own email provider and credentials and have their messages relayed
through it.</p>
<p>In second place, the tool needs to prompt the user for some for of confirmation
before sending emails. This feature is very easy to get for free: assuming that
the tool relays emails via an SMTP that requires authentication, it will have
to read the credentials from the user&rsquo;s secret store. The secret store should
prompt the user before disclosing credentials, at which point the user has the
opportunity to confirm granting access to their SMTP.</p>
<p>Finally, the tool should retain the same API as <a href="https://man.openbsd.org/sendmail"><code>sendmail(8)</code></a>, so
any other program that usually works with <code>sendmail</code> will work with this
replacement.</p>
]]></description></item><item><title>vdirsyncer status update 2023-03</title><link>https://whynothugo.nl/journal/2023/03/28/vdirsyncer-status-update-2023-03/</link><pubDate>Tue, 28 Mar 2023 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/03/28/vdirsyncer-status-update-2023-03/</guid><description><![CDATA[<p>This is a long overdue update on the status on vdirsyncer&rsquo;s rewrite. I keep
thinking &ldquo;I&rsquo;ll just finish this one bit before publishing it&rdquo;, but that always
seems to be a day or two in the future, so here goes.</p>
<h2 id="funding">Funding<div class="permalink">[<a href="#funding">permalink</a>]</div></h2>
<p>I&rsquo;m very happy to announce that the <a href="https://nlnet.nl/">NLnet foundation</a> has agreed to <a href="https://nlnet.nl/project/vdirsyncer/">fund my
work on vdirsyncer</a>. Thanks to this, I&rsquo;ve been able to put a
lot more focus hours into this project, which would have not been possible
without this support. I&rsquo;m very thankful for their support and the opportunity
it provides.</p>
<h2 id="the-storages-module">The storages module<div class="permalink">[<a href="#the-storages-module">permalink</a>]</div></h2>
<p>My first focus was the <a href="/journal/2022/04/18/a-vdirsyncer-rewrite/#the-storage-module">storage implementations</a> themselves. Storages are
basically filesystem (e.g.: vdir), webcal (i.e.: an icalendar file over http),
CalDav, etc. As I mentioned before, my intent is to have a standalone
<code>vstorage</code> crate which has all the storage implementations. The goal is to
abstract away all the storage-specific quirks and details, so that when working
on vdirsyncer any storage provides the same API, regardless of how it works
under the hood.</p>
<p>I want all storage operations to be asynchronous: there will be a lot of
networking going on all the time, and blocking every time is just going to be
slow. Using worker threads is an option, but that doesn&rsquo;t really scale well.</p>
<p>My first approach was to define the <code>Storage</code> traits using Rust&rsquo;s experimental
<a href="https://rust-lang.github.io/rfcs/3185-static-async-fn-in-trait.html"><code>async trait</code> feature</a>. However these are not <a href="https://doc.rust-lang.org/reference/items/traits.html#object-safety">object
safe</a>, so application code wouldn&rsquo;t be able to operate on storages with the
right implementation defined at runtime. This is a no-go for my use-case, since
storage types (and therefore, their implementation) are determined at runtime
based on vdirsyncer&rsquo;s configuration. That aside, <code>async trait</code> is only
available in Rust nightly, and, while it&rsquo;s been so for a few years, there&rsquo;s no
indicator of this feature becoming stable anytime soon.</p>
<p>So I moved onto the <a href="https://docs.rs/async-trait/latest/async_trait/"><code>#[async_trait]</code></a> approach. This doesn&rsquo;t rely
on any nightly features and simply wraps the code to return a <code>Future</code> under
the hood. In order to have storages be interchangeable at runtime, I also had
to <code>Box</code> all the storage&rsquo;s return types. This adds a bit of indirection and is
a bit less efficient. I tried to work these performance issues for days, but
then realised that I was doing the &ldquo;too much premature optimisation&rdquo; thing we
developers sometimes do: the current approach is likely to yield results more
efficient than what any interpreted language could achieve anyway, and I&rsquo;m sure
we&rsquo;ll find other bottlenecks worth improving further down the line.</p>
<p>With that out of the way, I finished defining a first version of the traits
that all implementations will use as well as two of the implementations
themselves: filesystem and webcal. There&rsquo;s even a little example which fetches
a remote webcal and saves it into a local <code>vdir</code>. Not very exciting, but it&rsquo;s
written mostly to prove that the API and implementations work.</p>
<p>The next item on the agenda was the CalDav storage implementation, and storages
are paused at the moment until the next item is done&hellip;</p>
<h2 id="a-caldav-implementation">A CalDav implementation<div class="permalink">[<a href="#a-caldav-implementation">permalink</a>]</div></h2>
<p>In order to implement the storage type for CalDav itself I need a CalDav
client. I&rsquo;ve been working on this the past few weeks. A big time sink was
implementing auto-discovery of servers, context path, etc. This was actually
not on my radar ahead of time, but it seemed like a natural place to start.</p>
<h3 id="caldav-auto-discovery">CalDav auto-discovery<div class="permalink">[<a href="#caldav-auto-discovery">permalink</a>]</div></h3>
<p>Auto-discovery allows configuring a CalDav client with just a username,
password and a bare domain such as <code>example.com</code>. Auto-discovery will use DNS
to resolve the real server and port for the CalDav server for example.com
(which can be something like <code>https://caldav.example.com:8443/dav/</code>.</p>
<p>This helps mostly with hosted services; people who are self-hosting usually
know the under-the-hood details. I&rsquo;ve tested this with fastmail.com and
posteo.de &ndash; providing just the bare domain as input results in the client
communicating to the right server/port combination. Note that I&rsquo;m not making
anything up here; I&rsquo;m just following the specification for <a href="https://www.rfc-editor.org/rfc/rfc6764">Locating Services
for Calendaring Extensions to WebDAV (CalDAV) and vCard Extensions to WebDAV
(CardDAV)</a>.</p>
<h3 id="the-fun-of-parsing-xml">The fun of Parsing XML<div class="permalink">[<a href="#the-fun-of-parsing-xml">permalink</a>]</div></h3>
<p>CalDav (and WebDav) uses a lot of XML under the hood for representing requests
and responses. Pretty much anything that&rsquo;s not calendar data itself is modelled
in XML. For my initial approach I used <code>quick_xml</code> with its <a href="https://docs.rs/serde/latest/serde/"><code>serde</code></a>
capabilities, which (de)serialised everything for me into concrete Rust types,
all nice and simple.</p>
<p>Regrettably, <code>quick_xml</code>&rsquo;s <code>serde</code> implementation <a href="https://github.com/tafia/quick-xml/issues/218">doesn&rsquo;t support
namespaces</a>, and ignoring namespaces broke under a few
scenarios. It&rsquo;s not even clear <em>how</em> it could be updated to support namespaces,
so that turned out to be a dead end.</p>
<p>I had to switch to <code>quick_xml</code>&rsquo;s lower-level API for parsing XML manually. The
design of the lower-level parser API is very similar to a SAX parser, but with
inversion of control. Basically, the parser reads the underlying bytes and
provides a stream of <code>Event</code> instances indicating what it has found (new node,
text, etc). This allows modelling the higher-level parser as a sort of state
machine, which is what I&rsquo;ve done.</p>
<p>I thought I had all the XML parsing out of the day and then discovered a
scenario which forced me to refactor the whole thing. Each query gets a
multistatus object, which has multiple responses. Typically these responses
look 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;-webkit-text-size-adjust:none;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span><span style="color:#f92672">&lt;ns0:response&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;ns0:href&gt;</span>/user/calendars/Q208cKvMGjAdJFUw/qJJ9Li5DPJYr.ics<span style="color:#f92672">&lt;/ns0:href&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;ns0:propstat&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;ns0:status&gt;</span>HTTP/1.1 200 OK<span style="color:#f92672">&lt;/ns0:status&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;ns0:prop&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;ns0:getetag&gt;</span>&#34;adb2da8d3cb1280a932ed8f8a2e8b4ecf66d6a02&#34;<span style="color:#f92672">&lt;/ns0:getetag&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;ns1:calendar-data&gt;</span>calendar-data-goes-in-here<span style="color:#f92672">&lt;/ns1:calendar-data&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;/ns0:prop&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;/ns0:propstat&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/ns0:response&gt;</span>
</span></span></code></pre></div><p>But when no <code>prop</code> objects are included the <code>propstat</code> element is omitted, and
the <code>status</code> is one level higher:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span>  <span style="color:#f92672">&lt;ns0:response&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;ns0:href&gt;</span>/user/calendars/Q208cKvMGjAdJFUw/rKbu4uUn.ics<span style="color:#f92672">&lt;/ns0:href&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;ns0:status&gt;</span>HTTP/1.1 404 Not Found<span style="color:#f92672">&lt;/ns0:status&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;/ns0:response&gt;</span>
</span></span></code></pre></div><p>Upon re-reading the specification I don&rsquo;t know why I overlooked this the first
time, but after about a whole day, I refactored everything to work with both
cases, which seems to have been the last of all the XML parsing fun.</p>
<h3 id="testing-with-real-servers">Testing with real servers<div class="permalink">[<a href="#testing-with-real-servers">permalink</a>]</div></h3>
<p>After writing most of the primitive operations for my CalDav client (e.g.:
list/create/update/get/delete resources/collections), I started out some basic
testing.</p>
<p>I wrote a few integration tests which are relatively simple, for example:</p>
<ul>
<li>Create a collection</li>
<li>Create a couple of resources</li>
<li>Check that listing the collection returns two resources with the right names</li>
<li>Fetch one and check the data is what we saved</li>
<li>Delete both resources</li>
<li>List items in the collection and confirm that it is now empty</li>
</ul>
<p>This is very simple and straightforward, but running the same test with
different servers has helped iron out some bugs due to slight differences in
how they behave. I really appreciate having heterogeneous servers to test with
since it helps make my result a lot more resilient.</p>
<p>Having tested these primitives individually at this level of abstraction also
gives a lot more confidence in using this functionality in higher level code.</p>
<p>I&rsquo;ve also found a few bugs in servers running these tests.</p>
<p>The first bug was on Fastmail.com. They provide a free test account for
vdirsyncer&rsquo;s integration testing. Fastmail uses (and apparently contribute a
lot to) Cyrus IMAP. Cyrus IMAP, despite the name, is also a CalDav server. I
triggered the bug and reported it the next day. It turns out that <a href="https://github.com/cyrusimap/cyrus-imapd/pull/4442">a fix for
the issue had been pushed earlier that same day</a>.</p>
<!-- Side note: I wish the Fastmail guys would do an "running a business with open -->
<!-- source backend" kind of paper / interview/ write-up. -->
<p><a href="https://github.com/nextcloud/server/issues/37374">The second bug</a> is untriaged yet, but the reproducer looks
pretty solid. It mostly affects discovery: when asking the server &ldquo;what
features do you support&rdquo;, it doesn&rsquo;t mention CalDav. Everything else works, but
it just hurts discovery because vdirsyncer can&rsquo;t provide solid confirmation
that it&rsquo;s reached the correct server/port/path combination.</p>
<h3 id="a-helpertesting-command-line-interface">A helper/testing command line interface<div class="permalink">[<a href="#a-helpertesting-command-line-interface">permalink</a>]</div></h3>
<p>The CalDav client library is designed to be re-usable by other projects, and I
intend to include a small cli for interactive usage. This cli will just include
basic functionality like printing discovery data, list and fetch individual
items, and other primitives like that. The main use-case is for it to be used
as a debugging/development tool. In particular, I&rsquo;d like to ask others to test
discovery via the cli in order to find if there are any well-configured
services where discovery fails.</p>
<h2 id="libdav-v010"><code>libdav</code> v0.1.0<div class="permalink">[<a href="#libdav-v010">permalink</a>]</div></h2>
<p>I&rsquo;ve published a <a href="https://docs.rs/libdav/0.1.0/">version 0.1.0 of <code>libdav</code></a>. In reality, this is
mostly a milestone in the whole rewrite&rsquo;s development, but if you&rsquo;re in need of
one, feel free to give it a try. Do keep in mind that this is <strong>very</strong> early in
development so, (a) it likely still has bugs, and (b) the API might change over
time. Feedback, as always, is welcome.</p>
<p>Aside from CalDav, this has it own tiny WebDav client (given that CalDav is
just an extension to WebDav) with mostly the basics implemented for this use
case. I&rsquo;ve also laid out the groupwork for the CardDav client on top of it for
syncing contacts. That one is missing a few bits and some intense integration
testing.</p>
<h2 id="next-item-synchronisation">Next item: synchronisation<div class="permalink">[<a href="#next-item-synchronisation">permalink</a>]</div></h2>
<p>The next item on the agenda is synchronisation itself. I have some clear ideas
on how it&rsquo;s going to work (greatly inspired by the existing implementation, of
course).</p>
<h2 id="note-on-maintenance-status-of-vdirsyncer">Note on maintenance status of vdirsyncer<div class="permalink">[<a href="#note-on-maintenance-status-of-vdirsyncer">permalink</a>]</div></h2>
<p>I&rsquo;ve been putting less time into maintenance and bugfixes of the existing
codebase. Basically, every hour that I put into maintaining legacy code is an
hour that&rsquo;s not spent on the new codebase. As a single developer, I have to
choose one or finish neither.</p>
]]></description></item><item><title>AI-assisted computer interfaces of the future</title><link>https://whynothugo.nl/journal/2023/03/23/ai-assisted-computer-interfaces-of-the-future/</link><pubDate>Thu, 23 Mar 2023 23:08:15 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/03/23/ai-assisted-computer-interfaces-of-the-future/</guid><description><![CDATA[<p>I&rsquo;ve been rewatching <a href="https://en.wikipedia.org/wiki/Star_Trek:_The_Next_Generation">Star Trek TNG</a> lately, and a question that often comes to
mind is:</p>
<blockquote>
<p>If they have all these computerised sensors, and they have a computer capable
of converting information into voice, why do they need technicians constantly
interacting with each terminal to read information out to the team and to the
captain?</p>
</blockquote>
<!-- I think it's a question that's been asked a lot, and my mum even probably asked -->
<!-- this when I was a kid at some point too. -->
<p>This got me into thinking how we&rsquo;d design these terminals today, assuming that
we had such a ship full of sensors and that kind of computing power. What would
technicians/specialists be doing?</p>
<p>The first thing that comes mind is how AI assistants work today. They can spit
out a lot of suggestions and ideas, but many (most?) are (still?) plainly wrong
or useless, so their output need to be reviewed by a human. Specifically, by a
specialist in the field. Even for highly sophisticated AIs and computers, I
suspect this would remain the case.</p>
<p>So a possible user-interface for such scenarios is a split screen interface:
the left side surfacing what the computer AI thinks it&rsquo;s found, e.g.: readings
that stand out, sources of energy, possible life forms, etc. The right half has
a view into raw data, where the operator can cross reference readings manually
to confirm the AI&rsquo;s finding.</p>
<p>This kind of fits into the workflow seen on this sci-fi show (and others too).
The operator gets an indicator that the computer found something, but needs to
<em>interact</em> for a bit and then confirms the computers findings before reporting
these to their team.</p>
<p>Such an interface would also need specialists/technicians working these
terminals. It fits the workflow of this sci-fi show, but also seems to fit
where this kind of technology is headed. Running a survey where the data read
by sensors is too many for a human to analyse entirely would work quite well
this this approach: an AI points out all the oddities or potential findings of
interest, but the specialist does the final review/audit before reporting the
the result to their team.</p>
<p>I think this is an interesting user-interface model to consider for the future.
Where we can offload data that&rsquo;s too much for us to an AI, and only look at the
interesting points it finds. The AI points out what it&rsquo;s found and the operator
checks &ldquo;does this make any sense, or is this just a false positive?&rdquo;. You&rsquo;d
want false positive, because the alternative is false negatives, and you don&rsquo;t
want to miss out on interesting readings.</p>
]]></description></item><item><title>Notes on Podman</title><link>https://whynothugo.nl/journal/2023/03/15/notes-on-podman/</link><pubDate>Wed, 15 Mar 2023 11:21:05 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/03/15/notes-on-podman/</guid><description><![CDATA[<p>Podman is an alternative implementation of Docker which addresses some design
issues in Docker. The most obvious/notable difference is that Podman doesn&rsquo;t
require a daemon running permanently, which is nice, but not a huge deal. It
also has other design differences, but most of them are good ideas that have
little to no impact in practice.</p>
<p>A big feature of Podman was that it was the first to implement rootless
support. Rootless support means that there&rsquo;s no process running as root
involved (Docker&rsquo;s daemon typically runs as root). The non-rootles mode isn&rsquo;t
great for security, since any potential security issue would quickly turn into
privilege escalation. Nowadays, Docker has perfectly working rootless support,
so Podman no longer has the advantage on this particular item.</p>
<p>I experimented with Podman for many months, but finally concluded that it&rsquo;s not
worth investing any more of my time and energy.</p>
<p>Podman is meant to work as a Docker drop-in replacement, but fails to achieve
this goal for <code>docker-compose</code>. Effort has gone into <code>podman-compose</code> (which
tries to re-implement <code>docker-compose</code> for Podman) and into making Podman itself
work with <code>docker-compose</code>. Neither of these approaches work well, and for most
of the project where I use <code>docker-compose</code>, I could not rely on Podman and
needed to keep Docker around instead. This meant that during my months of
experimentation with it, I still needed to keep Docker around, so I had had two
products which did the same. But they each kept container images and other data
separate, so often I&rsquo;d end up pulling the same container twice (and have two
copies on disk): one for Podman and another for Docker.</p>
<p>I&rsquo;ve recently put an end to my experiments with Podman mostly because of this.
At this point, it&rsquo;s lost its main advantage and I still need Docker around and
running. I think there&rsquo;s a lot of interesting exploration left in the container
space on Linux, but imitating Docker is unlikely to be the way to go. An
imitation is unlikely to displace the existing implementation, and effort put
into writing new container engines is best directed at new ideas and designs,
and not just copying an existing one.</p>
<p>I do thank Podman did bring something good: it was first in implementing
rootless support and the precedent pushed Docker to follow. But that&rsquo;s about
it.</p>
]]></description></item><item><title>Using a Yubikey for both GPG and TOTP</title><link>https://whynothugo.nl/journal/2023/03/13/using-a-yubikey-for-both-gpg-and-totp/</link><pubDate>Mon, 13 Mar 2023 18:39:00 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/03/13/using-a-yubikey-for-both-gpg-and-totp/</guid><description><![CDATA[<p>I&rsquo;ve written before on how I use a Yubikey for <a href="/journal/2022/07/11/using-a-yubikey-for-gpg/">hardware-based GPG</a>
and 2FA on the web. I also use it for TOTP. That is, the Yubikey itself
generates those common &ldquo;authenticator codes&rdquo; like many other Authenticator
apps. But the secret seed is saved into hardware that does not support
revealing it, instead of being handled by a regular app on a network-connected
device.</p>
<p>A nasty issue I&rsquo;ve been dealing with is that when I signed something using GPG,
the key would no longer work for TOTP unless I killed the <code>gpg-agent</code>. And if I
used the key for TOTP, it wouldn&rsquo;t work for GPG until I killed <code>pcscd</code>.</p>
<p>For example, after having used gpg, trying to access the key for it TOTP
functionality yields:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-console" data-lang="console"><span style="display:flex;"><span>&gt; ykman oath info
</span></span><span style="display:flex;"><span>ERROR: Failed to connect to YubiKey.
</span></span></code></pre></div><p>And trying to use <code>gpg</code> after having used the key for TOTP resulted in <code>gpg</code>
asking me to insert the key (even if it was right there).</p>
<h1 id="the-cause-of-the-issue">The cause of the issue<div class="permalink">[<a href="#the-cause-of-the-issue">permalink</a>]</div></h1>
<p><code>pcscd</code> is a daemon allowing userspace applications to communicate with
smartcards (a Yubikey presents itself as a smartcard to the OS, so existing
tools just work with it). Tools that generate TOTP codes via the Yubikey (e.g.:
<code>ykman</code>) use <code>pcscd</code> to talk to the Yubikey itself.</p>
<p><code>gpg</code> however, uses <code>scdaemon</code> to talk to the Yubikey, and <code>scdaemon</code> tries to
use an &ldquo;exclusive lock&rdquo; on the key (this is because it caches data so wants to
make sure no other process is using the key). This is a problem, because if it
holds an exclusive lock, then it will work if and only if no other application
are holding a lock.</p>
<p>To be honest, I don&rsquo;t really get why any of these applications hold ANY lock on
the card when it&rsquo;s not being used.</p>
<h1 id="the-solution">The Solution<div class="permalink">[<a href="#the-solution">permalink</a>]</div></h1>
<p><code>scdaemon</code> needs to be configured to use only a shared lock. This can be done
by editing the file <code>$GNUPGHOME/scdaemon.conf</code> and adding the line:</p>
<pre><code>pcsc-shared
</code></pre>
<p>Note that, as the man page indicates, <a href="https://man.archlinux.org/man/scdaemon.1#pcsc-shared">this has some caveats</a>.</p>
<p>However, this was not enough in my case. This is because <code>scdaemon</code> can use its
built-in support to talk to cards directly rather than via <code>pcscd</code>. This can be
disabled by adding the follow extra directive to the same configuration file:</p>
<pre><code>disable-ccid
</code></pre>
<p>For more notes on this, see <a href="https://gitlab.alpinelinux.org/alpine/aports/-/issues/14701">the related issue in aports</a>.</p>
]]></description></item><item><title>In praise of Alpine and apk</title><link>https://whynothugo.nl/journal/2023/02/18/in-praise-of-alpine-and-apk/</link><pubDate>Sat, 18 Feb 2023 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/02/18/in-praise-of-alpine-and-apk/</guid><description><![CDATA[<p>Since the change of year, I&rsquo;ve been using Alpine Linux on my main computing
device (a new desktop PC that I assembled in December). These are some notes on
in, some niceties and caveats.</p>
<p>I used ArchLinux for over a decade before, so keep in mind that my main point
of reference/background is using Arch+pacman. However, this <em>is not</em> an &ldquo;Arch
vs Alpine&rdquo; article.</p>
<h1 id="my-woes-with-package-management">My woes with package management<div class="permalink">[<a href="#my-woes-with-package-management">permalink</a>]</div></h1>
<p>Over the years, I&rsquo;ve experimented with many ways of &ldquo;keeping a list of the list
of packages installed&rdquo;. The main goal is to be able to quickly recreate my
installation, and also understand how it changed over time (e.g.: being able to
roll back to a previous state with just the list of packages), but I also very
much prefer a declarative approach to installed packages (rather than an
imperative approach).</p>
<p>The idea behind a declarative approach is to basically have a list of packages
I want installed, and let the package manager install/remove anything so that
installed package match exactly that list (plus any required dependencies, of
course).</p>
<p>Over the last couple of years I tried a lot of approaches for this. One such
approach was a couple of  scripts that analysed system state and added/removed
packages to make it converge to the desired state. This kind of worked, but
meant a lot of maintenance and operational overhead and was orthogonal to how
pacman was designed to be used. I also tried having a meta-package, which has
my &ldquo;list of wanted packages&rdquo; as dependencies, and then remove anything no
required my it. Again, I needed extra scripts and complexity on top of the
package manager itself.</p>
<p>I&rsquo;ll mention that I also tried NixOS, but found it to be far too complex and
too distant from the KISS philosophy for my taste.</p>
<h1 id="apk-and-etcapkworld"><code>apk</code> and <code>/etc/apk/world</code><div class="permalink">[<a href="#apk-and-etcapkworld">permalink</a>]</div></h1>
<p><code>apk</code> (literally &ldquo;alpine package keeper) is Alpine&rsquo;s package manager. I was
very pleased to learn about <code>apk</code>&rsquo;s <code>/etc/apk/world</code>.</p>
<p>In essence, this file is a list of desired packages, and apk will install and
uninstall packages so that the system state converges to the list. Exactly what
I&rsquo;d been looking for, but built right into the package manager. Actually it&rsquo;s
not just built into the package manager: it&rsquo;s the very foundation of how it
works. <code>apk add</code> actually adds a package to that list, and then installs any
that are not present. <code>apk del</code> removes a package from that list, and only
uninstalls it if it&rsquo;s not a dependency for anything else.</p>
<p>Understanding this also explains why <code>apk</code> uses <code>add</code> and <code>del</code> rather than the
more traditional <code>install</code> and <code>uninstall</code>. I extremely pleased by this design
and how well it works. Additionally, one can edit the <code>world</code> file manually and
run <code>apk fix</code>, which will install/uninstall any packages required to converge
to the declared list.</p>
<p>I also admit it&rsquo;s a breath of fresh air to have &ldquo;normal&rdquo; verbs as subcommands
rather than <code>pacman -Syu</code>, even if I already know of most <code>pacman</code>&rsquo;s flags.</p>
<h1 id="the-edge-branch">The <code>edge</code> branch<div class="permalink">[<a href="#the-edge-branch">permalink</a>]</div></h1>
<p>Alpine has releases every six months (each called a &ldquo;branch&rdquo;, e.g.: the &ldquo;3.17
branch&rdquo;), but also has a special branch called <code>edge</code>. Alpine edge is basically
a rolling release with the latest versions of packages. I don&rsquo;t really like the
idea of point releases for a desktop system, and very much prefer a rolling
release distro, and Alpine edge is exactly that.</p>
<p>Software is updated and rolled out continuously on <code>edge</code>, and I really care
about this, especially given that I&rsquo;ll often submit patches (or just bug
reports) upstream to all kinds of projects, and want to see these changes on
my system today, not in a few months.</p>
<h1 id="the-aports-tree">The <code>aports</code> tree<div class="permalink">[<a href="#the-aports-tree">permalink</a>]</div></h1>
<p>All of Alpine&rsquo;s package definitions (<code>APKBUILD</code>s) are kept in a single git
repository called <code>aports</code>. This approach that is apparently becoming more and
more common these days across distributions, and I can understand why: it makes
maintenance a lot easier, and makes fetching the entire ports tree very simple
(just a single <code>git clone</code>).</p>
<p><code>APKBUILD</code> files look extremely similar to <code>PKGBUILD</code> files, so learning how
they work was trivial. They&rsquo;re actually so similar, that it bothers me to know
that they&rsquo;ll likely never converge into one single format. The main difference
is that <code>APKBUILD</code> files are <code>sh</code> scripts, and <code>PKGBUILD</code>s are <code>bash</code> scripts
(and rely on several bash-specific feature), but there are other differences
too.</p>
<p>The tooling for building packages is substantially different from Arch&rsquo;s. I
dare say it&rsquo;s a bit more straightforward and simpler. I&rsquo;m honestly feeling
comfortable with <code>abuild</code> very quickly, though it does bother me a lot that it
doesn&rsquo;t have a <code>man</code> page.</p>
<p>Given that the ports tree is a single git repository hosted on Alpine&rsquo;s GitLab
instance, anybody can submit patches very easily (via <a href="https://gitlab.alpinelinux.org/alpine/aports/-/merge_requests/">Merge Requests</a>),
which makes contributing back very easy for anyone interested.</p>
<h1 id="the-testing-repository">The <code>testing</code> repository<div class="permalink">[<a href="#the-testing-repository">permalink</a>]</div></h1>
<p>Aside from the usual packages, Alpine also has a <code>testing</code> package repository.
Packages in this repository are contributed and maintained by any member of the
community. I&rsquo;ve already submitted a few and am actually pretty happy with how
easy it is to contribute. Submissions are reviews by a human to check the
quality of the package and if they&rsquo;re good quality get merged in.</p>
<p>Binary packages become available shortly after change to <code>APKBUILD</code>s being
merged.</p>
<p>This somewhat reminds me of the <a href="https://wiki.archlinux.org/title/Arch_User_Repository">AUR</a>, though it&rsquo;s not quite the same. Alpine&rsquo;s
<code>testing</code> repository is more curated (e.g.: there&rsquo;s a review process), which
has both its upsides and downsides. It implies that package quality have quite
a bit more oversight, but it also implies you won&rsquo;t find packages for patched
versions of software, or <code>*-git</code> packages (ones that build from the latest
upstream source). It&rsquo;s easy to patch an <code>APKBUILD</code> locally and build a package
oneself, though a bit more work than finding a <code>PKGBUILD</code> where somebody has
already done it for you.</p>
<h1 id="subpackages">Subpackages<div class="permalink">[<a href="#subpackages">permalink</a>]</div></h1>
<p>Packages can contain subpackages (something that&rsquo;s definitely not exclusive to
Alpine). However, there&rsquo;s a few extra features built on top of them which I&rsquo;ve
found very nice.</p>
<p>First of all, if a package contains man pages and a <code>*-doc</code> subpackage, the man
pages are automatically moved into that subpackage. If a package contains
<code>*-zsh-completion</code> or <code>*-bash-completion</code> subpackage, the completion files are
moved into those. This keeps package sizes minimal, while making these extras
available to anyone who wants them.</p>
<p>The second feature is what makes it all round up though. If I install the
<code>docs</code> metapackage, then the <code>*-doc</code> subpackage is installed for any package
that I have installed. In other words, if I install <code>docs</code> and <code>darkman</code>, then
<code>darkman-doc</code> (e.g.: it&rsquo;s man page) is automatically installed. And so is every
other man page relevant to my setup.</p>
<p>Since I have <code>zsh</code> installed, all the <code>*-zsh-completion</code> subpackages are
installed too &ndash; so anything that I install automatically has zsh completion.</p>
<p>This strikes a great balance, where it&rsquo;s trivial to make a tiny Alpine
installation with no extras (e.g.: you don&rsquo;t want man pages on docker images or
embedded systems), but it&rsquo;s very easy to get these extras on a desktop or
laptop installation.</p>
<p>And it&rsquo;s all very nicely designed and works very seamlessly (this features uses
an <code>install_if=</code> field).</p>
<h1 id="glibc-vs-musl">glibc vs musl<div class="permalink">[<a href="#glibc-vs-musl">permalink</a>]</div></h1>
<p>Something that makes Alpine really different is that it uses <a href="https://musl.libc.org/"><code>musl</code></a> instead
of <code>glibc</code>. <code>musl</code> focuses on being lightweight, fast, simple and
standards-compliant. This results in a few practical implications:</p>
<p>The first is that using a different libc helps finds bugs in programs that
<em>assume</em> that every system uses <code>glibc</code>. This helps write portable software and
spot portability issues faster.</p>
<p>The second notable difference, is that binaries that link to <code>glibc</code> won&rsquo;t run.
This is mostly a pain for proprietary software, and, in particular, Steam.
Steam simply won&rsquo;t run natively on Alpine, although there&rsquo;s two workarounds:
(1) running it in a chroot/container with another Linux distro, or (2) running
it via Flatpak. I opted for the second choices, and it works fine without any
further hassles.</p>
<p>The third and final difference, is that musl has not multilib support. This
means no support for running 32-bit binaries on 64-bit systems. Again, this
realistically only affects proprietary software, which you can&rsquo;t rebuild for
another architecture. In particular, this can be a nuisance for games ran via
wine, where installers (and launchers) might be 32-bit, but the game itself
64-bit. The workaround are the same as above, and I use <a href="https://usebottles.com/">Bottles</a> via Flatpak
as a workaround which has also had no issues. Flatpak is, indeed, a great
solution to running proprietary software on Linux (just games in my case).</p>
<p>Note that the wine situation is likely to change in future, since wine is
looking to avoid using native 32-bit dependencies for 32-bit binaries and seeks
to solve the interoperability issue internally.</p>
<h1 id="alpine-is-bare-bones">Alpine is bare bones<div class="permalink">[<a href="#alpine-is-bare-bones">permalink</a>]</div></h1>
<p>Alpine installs just a base system out of the box and feels a bit like
installing a BSD more than a typical Linux distro. It&rsquo;s up to the user to
install anything that&rsquo;s needed, and this is one of the reasons it&rsquo;s so popular
as a base for docker images and servers: it&rsquo;s super minimal and small by
default.</p>
<p>The default shell, <code>login</code>, <code>grep</code> and other basic Unix utilities are provided
by <a href="https://www.busybox.net/about.html">BusyBox</a>. It&rsquo;s possible to install <code>pam</code>, <code>udev</code>, <code>dbus</code> and <code>polkit</code> , but
it needs to be done explicitly (most of it is pretty straightforward). I plan
on a separate article soon with all my notes on setting up a new desktop
system.</p>
<h1 id="regular-applications-work-as-usual">Regular applications work as usual<div class="permalink">[<a href="#regular-applications-work-as-usual">permalink</a>]</div></h1>
<p>Normally desktop applications (e.g.: <code>swaywm</code>, <code>firefox</code>, <code>foot</code>, <code>neovim</code>,
etc) all work the same with not issues. There&rsquo;s nothing special to mention
here; it&rsquo;s still just another Linux distro after all.</p>
<h1 id="supported-architectures">Supported architectures<div class="permalink">[<a href="#supported-architectures">permalink</a>]</div></h1>
<p>Alpine supports quite a few architectures, including ARM and RISCV. While
ArchLinuxARM (a.k.a.: ALARM) exists, I&rsquo;ve never quite been convinced by the
development model. ArchLinux and ArchLinuxARM don&rsquo;t work as a single project,
but feel more like a project and its fork. Patches on one aren&rsquo;t unstreamed to
the other nor made in an upstream-friendly and ALARM didn&rsquo;t feel to open to
contributions. I found obvious packaging issues, but never managed to
collaborate to get them fixed (and a lot of the build infrastructure is kind of
a mystery). I ran one system with Arch and another with ALARM for many months,
and I felt like I was running two entirely different distros (technically, they
are different distros).</p>
<p>Alpine supports multiple architectures upstream, so even packages that I submit
to <code>testing</code> are immediately available as binary packages on ARM.</p>
<h1 id="postmarketos">postmarketOS<div class="permalink">[<a href="#postmarketos">permalink</a>]</div></h1>
<p>Finally, I&rsquo;ve been experimenting with postmarketOS recently quite a bit. It&rsquo;s
an Alpine-based Linux distribution for smartphones (see their <a href="https://wiki.postmarketos.org/wiki/Devices">list of
devices</a>).</p>
<p>Using Alpine and pmOS has better synergy. Scripts and configurations that I
write for one work on the other, and the package manager is the same for both.</p>
<p>More details on this are a bit out-of-scope, but I&rsquo;ll have a separate article
on pmOS and my history with #LinuxMobile <a href="https://developer.valvesoftware.com/wiki/Valve_Time">soon</a>™.</p>
]]></description></item><item><title>OpenWrt with KPN fibre optics</title><link>https://whynothugo.nl/journal/2023/02/11/openwrt-with-kpn-fibre-optics/</link><pubDate>Sat, 11 Feb 2023 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/02/11/openwrt-with-kpn-fibre-optics/</guid><description><![CDATA[<p>I&rsquo;ve always preferred to use my own router at home when possible, and was
pleasantly surprised to learn that <a href="https://fsfe.org/news/2020/news-20200601-01.en.html">the EU actually has rules around router
freedom</a>. That is, each user is allowed to use their own hardware at
home, rather than being imposed which hardware they can use by their service
provider.</p>
<p>Additionally, the Netherlands has local legislation to reflect this, and KPN has
<a href="https://www.kpn.com/service/eigen-apparatuur.htm">a dedicated page</a> explaining that they support this too.</p>
<p>Okay, maybe <em>support</em> is not the right word. They allow it and provide all
necessary technical details, but any actual support requests should go to the
community forums. Makes sense; they can&rsquo;t realistically support any random
permutation of devices. I can&rsquo;t imagine trying to train tech support to handle
calls from people running OpenBSD on their router.</p>
<h2 id="why-use-ones-own-router">Why use one&rsquo;s own router<div class="permalink">[<a href="#why-use-ones-own-router">permalink</a>]</div></h2>
<p>Running one&rsquo;s own router for a home network has a few advantages. A big one is
security and privacy: rather than have a device managed by a third party on
one&rsquo;s private home network, there&rsquo;s just yet-another device managed by the
owner.</p>
<p>That aside, there&rsquo;s a few more quite practical usages. Because my own router is
the publicly-facing device, I can easily expose services (i.e.: port
forwarding), and have no issue with breaking through a black-box NAT. It&rsquo;s also
possible to run any lightweight services on the box itself.</p>
<p>Finally, a nice-to-have is having isolated guest or testing wireless networks.
These are isolated from the &ldquo;main&rdquo; home network without having to add a second
access point into the mix.</p>
<h2 id="what-is-openwrt">What is OpenWrt<div class="permalink">[<a href="#what-is-openwrt">permalink</a>]</div></h2>
<p><a href="https://openwrt.org/">OpenWrt</a> is a Linux distribution aimed at routers and access points. It
supports a variety of consumer hardware. It can be configured via SSH like a
traditional Unix-like system, or via LuCI, the web-based graphical interface.
It&rsquo;s pretty end-user-friendly to be honest, and mostly edits configuration in
<code>/etc/config</code> under the hood.</p>
<h2 id="technical-details">Technical details<div class="permalink">[<a href="#technical-details">permalink</a>]</div></h2>
<p>The page linked above has a link to a PDF with all the technical details,
though it&rsquo;s actually <strong>very</strong> verbose, since it includes details on using your
own fibre modem as well.</p>
<p>The important bits are:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>Internet: VLAN = 6 met prioriteit P-bit = 1
</span></span><span style="display:flex;"><span>Vast Bellen: VLAN = 7 met prioriteit P-bit = 5
</span></span><span style="display:flex;"><span>TV: VLAN = 4 (DHCP gebaseerd die ook IGMPv2 moet ondersteunen) met prioriteit P-bit = 5
</span></span></code></pre></div><p>And</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>Technische details Internet
</span></span><span style="display:flex;"><span>• PPPoE via VLAN 6 (802.1q).
</span></span><span style="display:flex;"><span>• PPPoE authenticatie PAP met een gebruikersnaam en wachtwoord (bijv. internet / internet).
</span></span><span style="display:flex;"><span>• Maximale pakket grote (mtu) 1500 bytes (rfc4638)
</span></span><span style="display:flex;"><span>• IPv4 adres + DNS servers via PPPoE verkrijgen
</span></span><span style="display:flex;"><span>• IPv6 adresreeks + DNS servers (IPv6) via DHCPv6-PD verzoek (in PPPoE). Een adres gebruiken uit reeks voor router.
</span></span></code></pre></div><p>Nothing unusual here, though I do admit I had to re-read on a lot of these
concepts which I hadn&rsquo;t touched in many years. Basically, it&rsquo;s just VLAN6 with
PPPOE, the default MTU (1500) and any username and password (e.g.:
<code>internet</code>/<code>internet</code>). IPv4, IPv6 and both sets of DNS also come via PPPOE.</p>
<h2 id="vlan-configuration-in-openwrt">VLAN configuration in OpenWrt<div class="permalink">[<a href="#vlan-configuration-in-openwrt">permalink</a>]</div></h2>
<p><a href="https://en.wikipedia.org/wiki/VLAN">VLAN</a> (virtual LAN) is a technique run different networks on a same physical
layer by tagging packets. The tag indicates to which VLAN each packet belongs.
This is standardised in <a href="https://en.wikipedia.org/wiki/IEEE_802.1Q">IEEE 802.1Q</a>. You don&rsquo;t need to understand much more
about VLAN for this setup.</p>
<p>The <code>Network</code> menu has a <code>Switch</code> entry which allows configuring the VLAN.</p>
<p>By default there are two VLANs; one will have just the WAN port enabled and the
other will have all the LAN ports enabled. The one with the WAN port needs to
be set to <code>VLAN ID = 6</code>. This row should be the one which has all LAN ports set
to <code>off</code> and the WAN port set to <code>tagged</code> (the CPU port should also remain
<code>tagged</code>. This mostly covers the VLAN side of things.</p>
<p>After saving in LuCI, it&rsquo;s possible to check that everything looks in order on
the device. <code>/etc/config/network</code> should have something 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;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>config switch
</span></span><span style="display:flex;"><span>	option name &#39;switch0&#39;
</span></span><span style="display:flex;"><span>	option reset &#39;1&#39;
</span></span><span style="display:flex;"><span>	option enable_vlan &#39;1&#39;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>config switch_vlan
</span></span><span style="display:flex;"><span>	option device &#39;switch0&#39;
</span></span><span style="display:flex;"><span>	option vlan &#39;1&#39;
</span></span><span style="display:flex;"><span>	option ports &#39;1 2 3 4 6t&#39;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>config switch_vlan
</span></span><span style="display:flex;"><span>	option device &#39;switch0&#39;
</span></span><span style="display:flex;"><span>	option vlan &#39;2&#39;
</span></span><span style="display:flex;"><span>	option vid &#39;6&#39;
</span></span><span style="display:flex;"><span>	option ports &#39;0t 5t&#39;
</span></span></code></pre></div><h2 id="pppoe-configuration-in-openwrt">PPPOE configuration in OpenWrt<div class="permalink">[<a href="#pppoe-configuration-in-openwrt">permalink</a>]</div></h2>
<p>In the <code>Network</code> menu pick <code>Interfaces</code>. If there are any existing WAN
configurations that are not PPPoE (e.g.: from previous configurations), you
probably want to delete them at this point.</p>
<p>Add a new interface, pick <code>Protocol=PPPoE</code> and <code>Device=eth0.6</code>. <code>eth0</code> is the
actual device, and <code>0.6</code> means &ldquo;VLAN6 on eth0&rdquo;. You probably want to leave
<code>Bring up on boot</code> enabled and set <code>username</code> and <code>password</code> to &ldquo;internet&rdquo;. Any
value should work, but leaving them empty does not work!</p>
<p>The relevant bits in <code>/etc/config/network</code> should have something 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;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>config interface &#39;pppoe&#39;
</span></span><span style="display:flex;"><span>        option device &#39;eth0.6&#39;
</span></span><span style="display:flex;"><span>        option proto &#39;pppoe&#39;
</span></span><span style="display:flex;"><span>        option username &#39;internet&#39;
</span></span><span style="display:flex;"><span>        option password &#39;internet&#39;
</span></span><span style="display:flex;"><span>        option ipv6 &#39;auto&#39;
</span></span><span style="display:flex;"><span>        option mtu &#39;1500&#39;
</span></span></code></pre></div><p>If you ever need to configure anything for the IPv6 aspect of this, it needs to
be in a <code>config interface 'pppoe_6'</code> block.</p>
<h2 id="configuring-custom-dns">Configuring custom DNS<div class="permalink">[<a href="#configuring-custom-dns">permalink</a>]</div></h2>
<p>By default, OpenWrt will pick up advertised DNS and use these as upstream. I
don&rsquo;t want this since I run my own DNS which does some basic adware filtering
(mostly for &ldquo;smart&rdquo; devices which can&rsquo;t run proper ad blockers).</p>
<p>The simplest way to disable using upstream DNS is to head over to <code>Network</code>
menu, <code>DHCP and DNS</code> and in the <code>Resolv and Hosts Files</code> tab uncheck <code>Ignore resolv file</code>.</p>
<p>This is equivalent to editing <code>/etc/config/dhcp</code> with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>config dnsmasq
</span></span><span style="display:flex;"><span>        # ... lots of other options ...
</span></span><span style="display:flex;"><span>        option noresolv &#39;1&#39;
</span></span></code></pre></div><p>Make sure to configure the actually desired DNS in the <code>DHCP and DNS</code> section.</p>
]]></description></item><item><title>vdirsyncer: looking for hosted Dav servers</title><link>https://whynothugo.nl/journal/2023/01/30/vdirsyncer-looking-for-hosted-dav-servers/</link><pubDate>Mon, 30 Jan 2023 15:48:50 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/01/30/vdirsyncer-looking-for-hosted-dav-servers/</guid><description><![CDATA[<h2 id="current-state-of-affairs">Current state of affairs<div class="permalink">[<a href="#current-state-of-affairs">permalink</a>]</div></h2>
<p>As part of vdirsyncer&rsquo;s test suite, I currently run tests against a multiple
CalDav/CardDav servers. The CI pipeline does this by downloading dockerised
versions of all the servers, starting them up and then running tests.</p>
<p>This isn&rsquo;t ideal. On CI, the radicale, xandikos and baikal containers get
downloaded on each test run which really doesn&rsquo;t scale<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>. Adding more and more
servers will just make the whole test suite slower, as well as more maintenance
work. I really don&rsquo;t want to set up a disposable container images for each
existing Dav server out there, and production-ready images tend to be too
involved for such use case.</p>
<p>Testing with a large variety of servers has proven to be a good approach. Such
heterogeneous testing environments help find quirks and bugs (both on
vdirsyncer and in the servers themselves). I want to continue testing with as
many servers as possible, but want to offload setting these up as much as
possible.</p>
<h2 id="looking-for-volunteer-hostmasters">Looking for volunteer hostmasters<div class="permalink">[<a href="#looking-for-volunteer-hostmasters">permalink</a>]</div></h2>
<p>I&rsquo;m currently calling out on anyone who&rsquo;s self-hosting a Dav server instance:
if you can create a spare account for testing on your server, that&rsquo;d be very
useful to use for testing. In general, vdirsyncer&rsquo;s test suite:</p>
<ul>
<li>Only requires a single dedicated users.</li>
<li>Creates collections and items and deletes them (assuming tests don&rsquo;t
fail/crash).</li>
<li>It&rsquo;s fine if all data gets flushed (e.g.: tests don&rsquo;t expect anything
specific).</li>
</ul>
<p>I also don&rsquo;t intend to run stress tests against hosted servers. So if you have
a tiny self-hosted instance, a spare user might be enough for me!</p>
<p>Aside from testing the current vdirsyncer codebase, I&rsquo;ll use this to properly
test the upcoming Rust implementation (more news on this soon!).</p>
<h3 id="authentication-other-than-http-basic-auth">Authentication other than HTTP basic auth<div class="permalink">[<a href="#authentication-other-than-http-basic-auth">permalink</a>]</div></h3>
<p>Servers configured with authentication other than HTTP basic auth would be most
welcome for testing. I currently don&rsquo;t have servers to test things like
<code>DIGEST AUTH</code> or authentication with TLS client certificates.</p>
<h3 id="nextcloud">Nextcloud<div class="permalink">[<a href="#nextcloud">permalink</a>]</div></h3>
<p>It should be noted I haven&rsquo;t mentioned NextCloud at all. Indeed, I currently
don&rsquo;t test with any NextCloud instance because I don&rsquo;t have access to one. I&rsquo;d
very much like for this to change.</p>
<h3 id="davmail">DavMail<div class="permalink">[<a href="#davmail">permalink</a>]</div></h3>
<p>A publicly accessible instance along with a working user for this would be a
very useful target for testing!</p>
<h3 id="radicale--xandikos--baikal">Radicale / Xandikos / Baikal<div class="permalink">[<a href="#radicale--xandikos--baikal">permalink</a>]</div></h3>
<p>These are the lowest priority servers (since I have a working setup). They&rsquo;re
currently run via the quick-and-dirty images maintained <a href="https://github.com/pimutils/vdirsyncer-devkit">in this
repository</a>.</p>
<h3 id="cyrus--fastmail">Cyrus / Fastmail<div class="permalink">[<a href="#cyrus--fastmail">permalink</a>]</div></h3>
<p>I want to thank the folks at <a href="https://www.fastmail.com/">Fastmail</a>, who have (for many years now) provided
a free account for testing vdirsyncer. They use [and contribute to] <a href="https://www.cyrusimap.org/imap/download/installation/http/caldav.html">Cyrus
IMAP</a> under the hook, so that&rsquo;s one open source Dav server that&rsquo;s
covered.</p>
<h3 id="other-servers">Other servers<div class="permalink">[<a href="#other-servers">permalink</a>]</div></h3>
<p>Generally speaking, for any Dav server that I haven&rsquo;t mentioned here, I welcome
a test account on a working server where I can test vdirsyncer. Even if nothing
works, at least we&rsquo;ll have clear knowledge that nothing works on that specific
server.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>These are currently 158MB (xandikos), 56MB (radicale) and 454MB
(baikal). They&rsquo;re also downloaded once per each test environment. Not
terrible, but far from great too.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>icalendar bug at NS International</title><link>https://whynothugo.nl/journal/2023/01/22/icalendar-bug-at-ns-international/</link><pubDate>Sun, 22 Jan 2023 16:16:52 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/01/22/icalendar-bug-at-ns-international/</guid><description><![CDATA[<p>Today I purchased train tickets to <a href="https://fosdem.org/2023/">FOSDEM</a> on <a href="https://www.nsinternational.com/">NS International</a>. The site has
a handy option to &ldquo;Add to Agenda&rdquo;, which generates an <code>icalendar</code> file which one
can add to their <del>calendar</del> agenda.</p>
<p>I downloaded it and tried to import it into my calendar tool of choice, khal.
<a href="https://github.com/pimutils/khal/issues/1217">It crashed</a>. I&rsquo;m a software developer who&rsquo;s deeply into calendaring
and I&rsquo;ve written plenty of things that deal with icalendar, so I had to dig in
deeper.</p>
<p>To start with, here&rsquo;s the icalendar object. I&rsquo;ve slightly changed times and
cities for privacy&rsquo;s sake (feel free to entirely skip 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;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>BEGIN:VCALENDAR
</span></span><span style="display:flex;"><span>VERSION:2.0
</span></span><span style="display:flex;"><span>PRODID:-//NS Internationaal B.V.//sales-v2//en
</span></span><span style="display:flex;"><span>BEGIN:VTIMEZONE
</span></span><span style="display:flex;"><span>TZID:Europe/Brussels
</span></span><span style="display:flex;"><span>LAST-MODIFIED:20221105T024525Z
</span></span><span style="display:flex;"><span>TZURL:http://www.tzurl.org/zoneinfo/Europe/Brussels
</span></span><span style="display:flex;"><span>X-LIC-LOCATION:Europe/Brussels
</span></span><span style="display:flex;"><span>X-PROLEPTIC-TZNAME:LMT
</span></span><span style="display:flex;"><span>BEGIN:STANDARD
</span></span><span style="display:flex;"><span>TZNAME:BMT
</span></span><span style="display:flex;"><span>TZOFFSETFROM:+0017
</span></span><span style="display:flex;"><span>TZOFFSETTO:+0017
</span></span><span style="display:flex;"><span>DTSTART:18800101T000000
</span></span><span style="display:flex;"><span>END:STANDARD
</span></span><span style="display:flex;"><span>BEGIN:STANDARD
</span></span><span style="display:flex;"><span>TZNAME:WET
</span></span><span style="display:flex;"><span>TZOFFSETFROM:+0017
</span></span><span style="display:flex;"><span>TZOFFSETTO:+0000
</span></span><span style="display:flex;"><span>DTSTART:18920501T001730
</span></span><span style="display:flex;"><span>END:STANDARD
</span></span><span style="display:flex;"><span>BEGIN:STANDARD
</span></span><span style="display:flex;"><span>TZNAME:CET
</span></span><span style="display:flex;"><span>TZOFFSETFROM:+0000
</span></span><span style="display:flex;"><span>TZOFFSETTO:+0100
</span></span><span style="display:flex;"><span>DTSTART:19141108T000000
</span></span><span style="display:flex;"><span>END:STANDARD
</span></span><span style="display:flex;"><span>BEGIN:STANDARD
</span></span><span style="display:flex;"><span>TZNAME:CET
</span></span><span style="display:flex;"><span>TZOFFSETFROM:+0200
</span></span><span style="display:flex;"><span>TZOFFSETTO:+0100
</span></span><span style="display:flex;"><span>DTSTART:19161001T010000
</span></span><span style="display:flex;"><span>RDATE:19421102T030000
</span></span><span style="display:flex;"><span>RDATE:19431004T030000
</span></span><span style="display:flex;"><span>RDATE:19440917T030000
</span></span><span style="display:flex;"><span>RDATE:19450916T030000
</span></span><span style="display:flex;"><span>RDATE:19461007T030000
</span></span><span style="display:flex;"><span>RDATE:19770925T030000
</span></span><span style="display:flex;"><span>RDATE:19781001T030000
</span></span><span style="display:flex;"><span>END:STANDARD
</span></span><span style="display:flex;"><span>BEGIN:STANDARD
</span></span><span style="display:flex;"><span>TZNAME:CET
</span></span><span style="display:flex;"><span>TZOFFSETFROM:+0200
</span></span><span style="display:flex;"><span>TZOFFSETTO:+0100
</span></span><span style="display:flex;"><span>DTSTART:19170917T030000
</span></span><span style="display:flex;"><span>RRULE:FREQ=YEARLY;UNTIL=19180916T010000Z;BYDAY=3MO;BYMONTH=9
</span></span><span style="display:flex;"><span>END:STANDARD
</span></span><span style="display:flex;"><span>BEGIN:STANDARD
</span></span><span style="display:flex;"><span>TZNAME:WET
</span></span><span style="display:flex;"><span>TZOFFSETFROM:+0100
</span></span><span style="display:flex;"><span>TZOFFSETTO:+0000
</span></span><span style="display:flex;"><span>DTSTART:19181111T120000
</span></span><span style="display:flex;"><span>RDATE:19191005T000000
</span></span><span style="display:flex;"><span>RDATE:19201024T000000
</span></span><span style="display:flex;"><span>RDATE:19211026T000000
</span></span><span style="display:flex;"><span>RDATE:19391119T030000
</span></span><span style="display:flex;"><span>END:STANDARD
</span></span><span style="display:flex;"><span>BEGIN:STANDARD
</span></span><span style="display:flex;"><span>TZNAME:WET
</span></span><span style="display:flex;"><span>TZOFFSETFROM:+0100
</span></span><span style="display:flex;"><span>TZOFFSETTO:+0000
</span></span><span style="display:flex;"><span>DTSTART:19221008T000000
</span></span><span style="display:flex;"><span>RRULE:FREQ=YEARLY;UNTIL=19271001T230000Z;BYDAY=SU;BYMONTHDAY=2,3,4,5,6,7,8;
</span></span><span style="display:flex;"><span> BYMONTH=10
</span></span><span style="display:flex;"><span>END:STANDARD
</span></span><span style="display:flex;"><span>BEGIN:STANDARD
</span></span><span style="display:flex;"><span>TZNAME:WET
</span></span><span style="display:flex;"><span>TZOFFSETFROM:+0100
</span></span><span style="display:flex;"><span>TZOFFSETTO:+0000
</span></span><span style="display:flex;"><span>DTSTART:19281007T030000
</span></span><span style="display:flex;"><span>RRULE:FREQ=YEARLY;UNTIL=19381002T020000Z;BYDAY=SU;BYMONTHDAY=2,3,4,5,6,7,8;
</span></span><span style="display:flex;"><span> BYMONTH=10
</span></span><span style="display:flex;"><span>END:STANDARD
</span></span><span style="display:flex;"><span>BEGIN:STANDARD
</span></span><span style="display:flex;"><span>TZNAME:CET
</span></span><span style="display:flex;"><span>TZOFFSETFROM:+0100
</span></span><span style="display:flex;"><span>TZOFFSETTO:+0100
</span></span><span style="display:flex;"><span>DTSTART:19770101T000000
</span></span><span style="display:flex;"><span>END:STANDARD
</span></span><span style="display:flex;"><span>BEGIN:STANDARD
</span></span><span style="display:flex;"><span>TZNAME:CET
</span></span><span style="display:flex;"><span>TZOFFSETFROM:+0200
</span></span><span style="display:flex;"><span>TZOFFSETTO:+0100
</span></span><span style="display:flex;"><span>DTSTART:19790930T030000
</span></span><span style="display:flex;"><span>RRULE:FREQ=YEARLY;UNTIL=19950924T010000Z;BYDAY=-1SU;BYMONTH=9
</span></span><span style="display:flex;"><span>END:STANDARD
</span></span><span style="display:flex;"><span>BEGIN:STANDARD
</span></span><span style="display:flex;"><span>TZNAME:CET
</span></span><span style="display:flex;"><span>TZOFFSETFROM:+0200
</span></span><span style="display:flex;"><span>TZOFFSETTO:+0100
</span></span><span style="display:flex;"><span>DTSTART:19961027T030000
</span></span><span style="display:flex;"><span>RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
</span></span><span style="display:flex;"><span>END:STANDARD
</span></span><span style="display:flex;"><span>BEGIN:DAYLIGHT
</span></span><span style="display:flex;"><span>TZNAME:CEST
</span></span><span style="display:flex;"><span>TZOFFSETFROM:+0100
</span></span><span style="display:flex;"><span>TZOFFSETTO:+0200
</span></span><span style="display:flex;"><span>DTSTART:19160501T000000
</span></span><span style="display:flex;"><span>RDATE:19400520T030000
</span></span><span style="display:flex;"><span>RDATE:19430329T020000
</span></span><span style="display:flex;"><span>RDATE:19440403T020000
</span></span><span style="display:flex;"><span>RDATE:19450402T020000
</span></span><span style="display:flex;"><span>RDATE:19460519T020000
</span></span><span style="display:flex;"><span>END:DAYLIGHT
</span></span><span style="display:flex;"><span>BEGIN:DAYLIGHT
</span></span><span style="display:flex;"><span>TZNAME:CEST
</span></span><span style="display:flex;"><span>TZOFFSETFROM:+0100
</span></span><span style="display:flex;"><span>TZOFFSETTO:+0200
</span></span><span style="display:flex;"><span>DTSTART:19170416T020000
</span></span><span style="display:flex;"><span>RRULE:FREQ=YEARLY;UNTIL=19180415T010000Z;BYDAY=3MO;BYMONTH=4
</span></span><span style="display:flex;"><span>END:DAYLIGHT
</span></span><span style="display:flex;"><span>BEGIN:DAYLIGHT
</span></span><span style="display:flex;"><span>TZNAME:WEST
</span></span><span style="display:flex;"><span>TZOFFSETFROM:+0000
</span></span><span style="display:flex;"><span>TZOFFSETTO:+0100
</span></span><span style="display:flex;"><span>DTSTART:19190301T230000
</span></span><span style="display:flex;"><span>RDATE:19200214T230000
</span></span><span style="display:flex;"><span>RDATE:19210314T230000
</span></span><span style="display:flex;"><span>RDATE:19220325T230000
</span></span><span style="display:flex;"><span>RDATE:19230421T230000
</span></span><span style="display:flex;"><span>RDATE:19240329T230000
</span></span><span style="display:flex;"><span>RDATE:19250404T230000
</span></span><span style="display:flex;"><span>RDATE:19260417T230000
</span></span><span style="display:flex;"><span>RDATE:19270409T230000
</span></span><span style="display:flex;"><span>RDATE:19280414T230000
</span></span><span style="display:flex;"><span>RDATE:19290421T020000
</span></span><span style="display:flex;"><span>RDATE:19300413T020000
</span></span><span style="display:flex;"><span>RDATE:19310419T020000
</span></span><span style="display:flex;"><span>RDATE:19320403T020000
</span></span><span style="display:flex;"><span>RDATE:19330326T020000
</span></span><span style="display:flex;"><span>RDATE:19340408T020000
</span></span><span style="display:flex;"><span>RDATE:19350331T020000
</span></span><span style="display:flex;"><span>RDATE:19360419T020000
</span></span><span style="display:flex;"><span>RDATE:19370404T020000
</span></span><span style="display:flex;"><span>RDATE:19380327T020000
</span></span><span style="display:flex;"><span>RDATE:19390416T020000
</span></span><span style="display:flex;"><span>RDATE:19400225T020000
</span></span><span style="display:flex;"><span>END:DAYLIGHT
</span></span><span style="display:flex;"><span>BEGIN:DAYLIGHT
</span></span><span style="display:flex;"><span>TZNAME:CEST
</span></span><span style="display:flex;"><span>TZOFFSETFROM:+0200
</span></span><span style="display:flex;"><span>TZOFFSETTO:+0200
</span></span><span style="display:flex;"><span>DTSTART:19440903T000000
</span></span><span style="display:flex;"><span>END:DAYLIGHT
</span></span><span style="display:flex;"><span>BEGIN:DAYLIGHT
</span></span><span style="display:flex;"><span>TZNAME:CEST
</span></span><span style="display:flex;"><span>TZOFFSETFROM:+0100
</span></span><span style="display:flex;"><span>TZOFFSETTO:+0200
</span></span><span style="display:flex;"><span>DTSTART:19770403T020000
</span></span><span style="display:flex;"><span>RRULE:FREQ=YEARLY;UNTIL=19800406T010000Z;BYDAY=1SU;BYMONTH=4
</span></span><span style="display:flex;"><span>END:DAYLIGHT
</span></span><span style="display:flex;"><span>BEGIN:DAYLIGHT
</span></span><span style="display:flex;"><span>TZNAME:CEST
</span></span><span style="display:flex;"><span>TZOFFSETFROM:+0100
</span></span><span style="display:flex;"><span>TZOFFSETTO:+0200
</span></span><span style="display:flex;"><span>DTSTART:19810329T020000
</span></span><span style="display:flex;"><span>RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
</span></span><span style="display:flex;"><span>END:DAYLIGHT
</span></span><span style="display:flex;"><span>END:VTIMEZONE
</span></span><span style="display:flex;"><span>BEGIN:VEVENT
</span></span><span style="display:flex;"><span>UID:4e3e3186-28c6-42d4-81cf-eb4f56d587f1
</span></span><span style="display:flex;"><span>DTSTAMP:20230122T142045Z
</span></span><span style="display:flex;"><span>SUMMARY:Trip from Utrecht to Bruxelles Central/Brussel Centraal
</span></span><span style="display:flex;"><span>DESCRIPTION:Trip from Utrecht to Bruxelles Central/Brussel Centraal
</span></span><span style="display:flex;"><span>LOCATION:Utrecht
</span></span><span style="display:flex;"><span>URL:https://www.nsinternational.com/en/traintickets-v3/
</span></span><span style="display:flex;"><span>DTSTART;TZID=Europe/Brussels:20230203T151500
</span></span><span style="display:flex;"><span>DTEND;TZID=Europe/Brussels:20230203T180600
</span></span><span style="display:flex;"><span>BEGIN:VALARM
</span></span><span style="display:flex;"><span>ACTION:DISPLAY
</span></span><span style="display:flex;"><span>TRIGGER;RELATED=START:PT-2H
</span></span><span style="display:flex;"><span>END:VALARM
</span></span><span style="display:flex;"><span>END:VEVENT
</span></span><span style="display:flex;"><span>BEGIN:VEVENT
</span></span><span style="display:flex;"><span>UID:28d25a55-84cd-4564-910b-088066fd5244
</span></span><span style="display:flex;"><span>DTSTAMP:20230122T142045Z
</span></span><span style="display:flex;"><span>SUMMARY:Trip from Bruxelles Central/Brussel Centraal to Utrecht
</span></span><span style="display:flex;"><span>DESCRIPTION:Trip from Bruxelles Central/Brussel Centraal to Utrecht
</span></span><span style="display:flex;"><span>LOCATION:Bruxelles Central/Brussel Centraal
</span></span><span style="display:flex;"><span>URL:https://www.nsinternational.com/en/traintickets-v3/
</span></span><span style="display:flex;"><span>DTSTART;TZID=Europe/Brussels:20230206T095000
</span></span><span style="display:flex;"><span>DTEND;TZID=Europe/Brussels:20230206T125000
</span></span><span style="display:flex;"><span>BEGIN:VALARM
</span></span><span style="display:flex;"><span>ACTION:DISPLAY
</span></span><span style="display:flex;"><span>TRIGGER;RELATED=START:PT-2H
</span></span><span style="display:flex;"><span>END:VALARM
</span></span><span style="display:flex;"><span>END:VEVENT
</span></span><span style="display:flex;"><span>END:VCALENDAR
</span></span></code></pre></div><p>The exact exception was:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>ValueError: Invalid iCalendar duration: PT-2H
</span></span></code></pre></div><p>This seems to be the line indicating when the alarm for the event should be
triggered (two hours before the event in this 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;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>TRIGGER;RELATED=START:PT-2H
</span></span></code></pre></div><p>I actually wrote a library to parse exactly this (durations as expressed in
icalendar components) a few months ago, <a href="https://docs.rs/icalendar-duration/0.1.1/icalendar_duration/">icalendar_duration</a>. I fed this value
(<code>PT-2H</code>) into my library, and received confirmation that it indeed had an
invalid format. At least now I knew the icalendar file was bad and not a bug in
the calendar application itself.</p>
<p>It wasn&rsquo;t entirely obvious what was wrong about it to me, so I went to check
the icalendar specification. <a href="https://www.rfc-editor.org/rfc/rfc5545#section-3.3.6">Section 3.3.6</a> specifies the
format for duration:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>Format Definition:  This value type is defined by the following notation:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    dur-value  = ([&#34;+&#34;] / &#34;-&#34;) &#34;P&#34; (dur-date / dur-time / dur-week)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    dur-date   = dur-day [dur-time]
</span></span><span style="display:flex;"><span>    dur-time   = &#34;T&#34; (dur-hour / dur-minute / dur-second)
</span></span><span style="display:flex;"><span>    dur-week   = 1*DIGIT &#34;W&#34;
</span></span><span style="display:flex;"><span>    dur-hour   = 1*DIGIT &#34;H&#34; [dur-minute]
</span></span><span style="display:flex;"><span>    dur-minute = 1*DIGIT &#34;M&#34; [dur-second]
</span></span><span style="display:flex;"><span>    dur-second = 1*DIGIT &#34;S&#34;
</span></span><span style="display:flex;"><span>    dur-day    = 1*DIGIT &#34;D&#34;
</span></span></code></pre></div><p>I had to read this over and over again for like 10 minutes until I figured out
what was wrong: The &ldquo;minus&rdquo; symbol goes before the <code>P</code> (this <code>P</code> indicates the
beginning of a duration, as opposed to an absolute time). That makes sense,
otherwise we&rsquo;d be able to write things like <code>P2DT-2H</code>, which means &ldquo;two days
and minus two hours&rdquo;, which is a super awkward way of expressing time.</p>
<p>So the issue seems to be a trivial; it&rsquo;s simply a bug in how NS International
is encoding the minus sign. I&rsquo;m not sure how different calendar applications
parse this. I assume at least one parses this correctly (e.g.: the one with
which the devs tested this). A common behaviour should be for an application to
ignore the alarm itself and treat the rest of the event as valid. This is
definitely what <code>khal</code> should be doing.</p>
<p>This issue could likely be solved by a developer at NS in a short time. The
hard part will be reaching a developer with access to these source in order to
fix this. I&rsquo;ll try and see if I can reach such a person, and the purpose of
this write-up is to have a quick link I can share with the hopes of it reaching
them.</p>
<p>Hopefully, next time that I, or some other train-traveler wants to import their
train-trip into an agenda, it actually works!</p>
]]></description></item><item><title>Setting up IRC redirects</title><link>https://whynothugo.nl/journal/2023/01/20/setting-up-irc-redirects/</link><pubDate>Fri, 20 Jan 2023 11:39:00 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/01/20/setting-up-irc-redirects/</guid><description><![CDATA[<div class="notice update">
  <header>Update 2023-10-23</header>
  <section>
  Added the <code>+i</code> mode. See below for details.
  </section>
</div>
<p>A few weeks ago I wanted to set up a few redirects on IRC. Mainly, I wanted to
redirect <code>#vdirsyncer</code>, <code>#khal</code> and <code>#todoman</code> to the <code>#pimutils</code> channel
(given that pimutils is the umbrella project for both of these).</p>
<p>These are the commands I had to run after authenticating:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>/msg ChanServ register #todoman
</span></span><span style="display:flex;"><span>/msg ChanServ set #todoman mlock +if #pimutils
</span></span><span style="display:flex;"><span>/msg ChanServ set #todoman guard on
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>/msg ChanServ register #vdirsyncer
</span></span><span style="display:flex;"><span>/msg ChanServ set #vdirsyncer mlock +if #pimutils
</span></span><span style="display:flex;"><span>/msg ChanServ set #vdirsyncer guard on
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>/msg ChanServ register #khal
</span></span><span style="display:flex;"><span>/msg ChanServ set #khal mlock +if #pimutils
</span></span><span style="display:flex;"><span>/msg ChanServ set #khal guard on
</span></span></code></pre></div><h2 id="reference">Reference<div class="permalink">[<a href="#reference">permalink</a>]</div></h2>
<ul>
<li><code>mlock</code> locks the mode to the specified one</li>
<li><code>guard</code> will retain the channel even if everyone leaves.</li>
<li><code>+i</code>: make channel invite only</li>
<li><code>+f</code>: forward to another channel</li>
</ul>
<p>The channel needs to be invite only; only users that cannot join the channel
are redirected.</p>
<h2 id="topic">Topic<div class="permalink">[<a href="#topic">permalink</a>]</div></h2>
<p>Setting a topic is important:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>/msg chanserv topic #pimutils Don&#39;t ask if you can ask questions, just ask. Be prepared to wait, your question will not be answered immediately.
</span></span></code></pre></div>]]></description></item><item><title>Notes on ruff</title><link>https://whynothugo.nl/journal/2023/01/20/notes-on-ruff/</link><pubDate>Fri, 20 Jan 2023 11:24:00 +0100</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/01/20/notes-on-ruff/</guid><description><![CDATA[<p><a href="https://github.com/charliermarsh/ruff"><code>ruff</code></a> is a tool to check and lint Python code. It&rsquo;s written in Rust and it
really is <em>blazing fast</em>. The first time I ran it on a real codebase I
immediately got an error and, for a split-second, though it was an execution
error. It had actually run successully in a few milliseconds and found an issue
in my code (a silly one, but a valid one).</p>
<h2 id="a-flake8-replacement">A <code>flake8</code> replacement<div class="permalink">[<a href="#a-flake8-replacement">permalink</a>]</div></h2>
<p><code>ruff</code> implements all the rules from <code>pyflakes</code> and <code>pycodestyle</code>, which
essentially implies it serves as a full replacement for <code>flake8</code>. Additionally,
it implements a lot of rules for <code>flake8</code> plugins (these need to be enabled
manually).</p>
<p>I&rsquo;ve transitioned a couple of projects from <code>flake8</code> to ruff and I&rsquo;m very happy
with the results. On top of re-implementing the rules in a very fast way,
<code>ruff</code> also implements &ldquo;severities&rdquo;: diagnostic may be Errors, Warnings, Info
or Hint. This helps a lot; when drafting code (e.g.: in a very rough state),
errors (e.g.: missing import, reading from undefined variable, etc) are not the
same as hints (e.g.: missing or extra whitespace, too many empty newlines, etc).</p>
<p>I actually <a href="https://github.com/PyCQA/flake8/issues/1762">suggested implementing severities for flake8</a>, but
this was deemed completely unacceptable; the authors believe that every single
user out there should re-implement this mapping themselves, which is an absurd
idea that doesn&rsquo;t scale.</p>
<p>Finally, <code>ruff</code> can auto-fix a lot of issues that existing tools could only
detect (for example: rewriting loops using comprehensions).</p>
<h2 id="an-isort-replacement">An <code>isort</code> replacement<div class="permalink">[<a href="#an-isort-replacement">permalink</a>]</div></h2>
<p>There&rsquo;s nothing truly broken with <code>isort</code> to be honest. It&rsquo;s a great tool. And
<code>ruff</code> implements all its functionality into the same codebase. It kinda makes
sense; both tools need to parse and entire codebase and analyse it. Adding a
new check to a single tool is simpler implementing two tools. Plus, combining
the auto-fixing of <code>flake8</code> rules with auto-fixing of imports into one action
makes code maintenance a lot simpler.</p>
<h2 id="a-pyupgrade-replacement">A <code>pyupgrade</code> replacement<div class="permalink">[<a href="#a-pyupgrade-replacement">permalink</a>]</div></h2>
<p>You can see where this is going, right? <code>ruff</code> also implement fixes done by
<code>pyupgrade</code>.</p>
<p>For those not familiar, <code>pyupgrade</code> allows specifying which version of python a
project is targeting, and will automatically update syntax for newer language
and drop obsolete syntax.</p>
<p>This usually removes a lot of redundant code, and updates code to newer
(easier to read) syntax.</p>
<p>Aside from fixing these automatically, <code>ruff</code> will show these potential fixes
as warnings when checking code, or when running via a LSP&hellip;</p>
<h2 id="a-dedicated-language-server-ruff-lsp">A dedicated language server: <code>ruff-lsp</code><div class="permalink">[<a href="#a-dedicated-language-server-ruff-lsp">permalink</a>]</div></h2>
<p><a href="https://github.com/charliermarsh/ruff-lsp"><code>ruff-lsp</code></a> is a language server built on top of <code>ruff</code>. It&rsquo;s very simple and
straightforward to <a href="https://github.com/neovim/nvim-lspconfig/blob/d228bcf/doc/server_configurations.md#ruff_lsp">configure in neovim</a> or any of many other code
editors. It provides diagnostics for all of <code>ruff</code>&rsquo;s checks, as well as actions
to apply fixes via an IDE&rsquo;s interface.</p>
<p>My experience so far with <code>ruff-lsp</code> has been very pleasant. A lot of useful
feedback when editing any Python file. It&rsquo;s fast, and has very sensible
defaults, so <strong>even for projects that don&rsquo;t have a ruff configuration the
diagnostics are quite valuable</strong> (e.g.: unused variable, unused import, missing
definition, all valid checks).</p>
<h2 id="summarising">Summarising<div class="permalink">[<a href="#summarising">permalink</a>]</div></h2>
<p>You can read more about <code>ruff</code> in <a href="https://notes.crmarsh.com/ruff-the-first-200-releases">today&rsquo;s post by the author</a>. If you&rsquo;re
interesting in switching a project from <code>flake8</code>+<code>isort</code>+<code>pyupgrade</code>, it&rsquo;s
usually pretty straighforward. <a href="https://github.com/pimutils/todoman/commit/6c9f8d4a0534c8b3a1edc8def2f6ee979970befb">Here&rsquo;s an example</a> of how I switched
<code>todoman</code> to use <code>ruff</code>.</p>
<p>For project with many developers (e.g.: those where you work in a team), be
sure to align with other devs. They <strong>will</strong> need to ensure that their workflow
is updated.</p>
]]></description></item><item><title>Notes on pre-commit</title><link>https://whynothugo.nl/journal/2023/01/12/notes-on-pre-commit/</link><pubDate>Thu, 12 Jan 2023 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2023/01/12/notes-on-pre-commit/</guid><description><![CDATA[<p><a href="https://pre-commit.com/"><code>pre-commit</code></a> is a tool to configure git pre-commit hooks. It allows defining
hooks in a simply syntax and runs them only if files of a specific type change
(e.g.: run <code>mypy</code> only if <code>*.py</code> files have changed).</p>
<p>Additionally, it&rsquo;ll stash unstaged changes when running, so any files that are
not being committed won&rsquo;t interefere with the hooks. This makes hooks fail if
you forgot to commit a file, and prevents hooks from failing if some
uncommitted file has broken code.</p>
<p>All of <code>pre-commit</code>&rsquo;s configuration is defined in <code>.pre-commit-config.yaml</code>.</p>
<h2 id="pre-commit-plugins">Pre-commit plugins<div class="permalink">[<a href="#pre-commit-plugins">permalink</a>]</div></h2>
<p>Pre-commit has a concept of plugins. These include a definition on how to
install a checker, and how to run it.</p>
<p>For example, this <a href="https://github.com/pre-commit/mirrors-mypy">mypy plugin</a> installs mypy (in a virtualenv, as with all
python-based plugins). If a project should require that mypy run with
additional dependencies installed, these need to be defined in the
configuration file too.</p>
<p>Here&rsquo;s an example snippet for the <code>mypy</code> configuration for todoman:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>  - <span style="color:#f92672">repo</span>: <span style="color:#ae81ff">https://github.com/pre-commit/mirrors-mypy</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">rev</span>: <span style="color:#e6db74">&#34;v0.991&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">hooks</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">id</span>: <span style="color:#ae81ff">mypy</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">additional_dependencies</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#ae81ff">types-atomicwrites</span>
</span></span><span style="display:flex;"><span>          - <span style="color:#ae81ff">types-tabulate</span>
</span></span><span style="display:flex;"><span>          - <span style="color:#ae81ff">types-freezegun</span>
</span></span><span style="display:flex;"><span>          - <span style="color:#ae81ff">types-pytz</span>
</span></span><span style="display:flex;"><span>          - <span style="color:#ae81ff">types-python-dateutil</span>
</span></span></code></pre></div><h2 id="duplication-of-definitions">Duplication of definitions<div class="permalink">[<a href="#duplication-of-definitions">permalink</a>]</div></h2>
<p>After using <code>pre-commit</code> for many months, I started to get frustrated at the
way dependencies for plugins need to be kept in sync. <code>mypy</code> and its
dependencies are already specified in <code>requirements-dev.txt</code>, which is used by
developers and contributor to bootstrap their development environment. The same
is true for most other plugins (<code>flake8</code>, for example).</p>
<p>The situation is the same for <code>nodejs</code>-based plugins (where the dependencies
constantly need to be kept in sync between <code>.pre-commit-config.yaml</code> and
<code>package.json</code>; the latter being used by IDEs and developer tools during
development).</p>
<h2 id="duplication-of-downloads-and-installations">Duplication of downloads and installations<div class="permalink">[<a href="#duplication-of-downloads-and-installations">permalink</a>]</div></h2>
<p>A nice feature of <code>pre-commit</code> is that it takes care of downloading and
installing all tools required by plugins into isolated environments. However,
this comes at a price; each time any of these change (or each time a developer
commits in a new project) all plugins are installed into their own environment.
This isn&rsquo;t terribly slow, but the problem is that the development environment
will usually already have these tools installed.</p>
<p>If I am working on a project that uses <code>flake8</code> for checking errors, I&rsquo;ll be
using a development environment with <code>flake8</code> installed, along with the right
set of <code>flake8</code>-plugins for that project. So having all these get re-installed
is an annoyance with arguable value.</p>
<p>I would very much like to see a <code>pre-commit</code> plugin that merely checks that the
environment being used matches the one defined in <code>requirements-dev.txt</code> (or
whatever convention this project is using). While <code>pre-commit</code>&rsquo;s approach does
ensure that the right version is used when running hooks, it does so by
installing its own copy, and doesn&rsquo;t even hint to the developer that they might
be using the wrong version for development (this also implies their IDE would
be using the wrong version, and them getting bogus diagnostics).</p>
<p>Finally, some plugins share dependencies with the main project itself, and
these dependencies need to be kept in sync. In my experience, I have always
seen this be address by leaving a comment <code># make sure these are updated in sync</code>, and developers manually updating the pinned versions in both places.</p>
<h2 id="using-the-system-plugin-language">Using the <code>system</code> plugin language<div class="permalink">[<a href="#using-the-system-plugin-language">permalink</a>]</div></h2>
<p>Pre-commit can handle installing tools and their dependencies in many languages
(e.g.: it can create virtualenvs for tools in Python, etc). Additionally,
there&rsquo;s a <code>system</code> language which means <em>&ldquo;expect this tool to be installed in
the host system&rdquo;</em>. This can be used to avoid re-installed dependencies that are
expected to be available on a development setup.</p>
<p>For example, for the above <code>mypy</code> example can be rewritten like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>  - <span style="color:#f92672">repo</span>: <span style="color:#ae81ff">local</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">id</span>: <span style="color:#ae81ff">mypy</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mypy</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">language</span>: <span style="color:#ae81ff">system</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">entry</span>: <span style="color:#ae81ff">mypy</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">types_or</span>: [<span style="color:#ae81ff">python, pyi]</span>
</span></span></code></pre></div><p>This will use whichever <code>mypy</code> is available in <code>$PATH</code>, which <strong>should</strong> be the
correct one if the developer has set up their system correctly, but will be the
right one on CI (assuming CI is configured to installed all development tools
to the correct version). Again, an extra hook that confirms this would be
pretty valuable.</p>
<p>This approach works very well on CI too; assuming that a pipeline installs
<code>requirements-dev.txt</code> before the CI run, then using <code>pre-commit run ...</code> will
use those versions of each tool.</p>
<h2 id="pre-commitci">pre-commit.ci<div class="permalink">[<a href="#pre-commitci">permalink</a>]</div></h2>
<p><a href="https://pre-commit.ci/">pre-commit.ci</a> is a service which runs <code>pre-commit</code> hooks as CI pipelines.
This is a proprietary service which runs only on proprietary forges (and, as
far as I know, has no public API).</p>
<p>It seems to cache plugins locally based on their definition, which makes it run
very quickly. The general idea seems pretty solid, but regrettably requires
fully buying into depending on proprietary tools and services for development.
Using <code>pre-commit</code> itself directly on any regular pipeline works well enough.</p>
<h2 id="final-thoughts">Final thoughts<div class="permalink">[<a href="#final-thoughts">permalink</a>]</div></h2>
<p>All in all, I think pre-commit does a good job at what it intends to do. I
think the &ldquo;install tools into an isolated environment&rdquo; feature is a bit of an
overkill that adds more complexity that it&rsquo;s worth (though it does play well
with its proprietary CI implementation).</p>
<p>It&rsquo;s ability to only run hooks if relevant files have changed and skipping
unstashed changes are both of great value on top of git&rsquo;s default hooks, and
using hooks with <code>language: system</code> seems to work around its major
shortcomings.</p>
]]></description></item><item><title>Selection / clipboard cheatsheet</title><link>https://whynothugo.nl/journal/2022/12/03/selection-cheatsheet/</link><pubDate>Sat, 03 Dec 2022 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2022/12/03/selection-cheatsheet/</guid><description><![CDATA[<h1 id="theres-multiple-selections">There&rsquo;s multiple selections:<div class="permalink">[<a href="#theres-multiple-selections">permalink</a>]</div></h1>
<ul>
<li>PRIMARY: is the old-school Unix one; select text to copy it. Middle-click to
paste it. Three-finger tap on a touchpad also pastes it.</li>
<li>CLIPBOARD: the well-known ctrl-c and ctrl-v (or <a href="/journal/2022/11/04/copying-with-super-c/"><kbd>Super</kbd>+<kbd>c</kbd></a>).</li>
<li>SECONDARY: historical, irrelevant, don&rsquo;t worry about this one.</li>
</ul>
<h1 id="vim--derivates">Vim (&amp; derivates)<div class="permalink">[<a href="#vim--derivates">permalink</a>]</div></h1>
<p>Vim has registers. They are accessed by prefixing <code>&quot;X</code>, where <code>X</code> is a register
name. These extend the ones above:</p>
<ul>
<li><code>&quot;-</code>: <code>/dev/null</code></li>
<li><code>&quot;*</code>: (<a href="https://vimhelp.org/options.txt.html#clipboard-unnamed">see <code>:h unnamed</code></a>), the PRIMARY SELECTION</li>
<li><code>&quot;+</code>: (<a href="https://vimhelp.org/options.txt.html#clipboard-unnamedplus">see <code>:h unnamedplus</code></a>), the CLIPBOARD SELECTION</li>
<li>There&rsquo;s also a lot of other registers which are historical and exotic IMHO</li>
</ul>
<h1 id="neovim-config-example">Neovim config example:<div class="permalink">[<a href="#neovim-config-example">permalink</a>]</div></h1>
<p>Copying to the PRIMARY selection:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-lua" data-lang="lua"><span style="display:flex;"><span><span style="color:#75715e">-- Copy to PRIMARY selection on mouse selection.</span>
</span></span><span style="display:flex;"><span>vim.keymap.set(<span style="color:#e6db74">&#34;v&#34;</span>, <span style="color:#e6db74">&#34;&lt;LeftRelease&gt;&#34;</span>, <span style="color:#e6db74">&#39;&#34;*ygv&#39;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- Yank (copy) to PRIMARY selection by default (e.g.: with `yy`).</span>
</span></span><span style="display:flex;"><span>vim.opt.clipboard <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;unnamed&#34;</span>
</span></span></code></pre></div><p>Copying to CLIPBOARD is also possible, but confusing in terminals:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-lua" data-lang="lua"><span style="display:flex;"><span><span style="color:#75715e">-- Copy to CLIPBOARD selection with ctrl+c.</span>
</span></span><span style="display:flex;"><span>vim.keymap.set(<span style="color:#e6db74">&#34;v&#34;</span>, <span style="color:#e6db74">&#34;&lt;C-c&gt;&#34;</span>, <span style="color:#e6db74">&#39;&#34;+ygv&#39;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- Paste from CLIPBOARD selection with ctrl+v.</span>
</span></span><span style="display:flex;"><span>vim.keymap.set(<span style="color:#e6db74">&#34;v&#34;</span>, <span style="color:#e6db74">&#34;&lt;C-v&gt;&#34;</span>, <span style="color:#e6db74">&#39;&#34;+p&#39;</span>)
</span></span></code></pre></div><p>Note that the usual mappings for <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>c</kbd>
(or <kbd>Cmd</kbd>+<kbd>c</kbd>) should work fine on terminals.</p>
]]></description></item><item><title>Copying with Super+C</title><link>https://whynothugo.nl/journal/2022/11/04/copying-with-super-c/</link><pubDate>Fri, 04 Nov 2022 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2022/11/04/copying-with-super-c/</guid><description><![CDATA[<p>Historically, <kbd>Ctrl</kbd>+<kbd>c</kbd> has been used to interrupt a process
on terminals. This applies on Linux, but also applied to BSDs and Unixes before
it. This is still true, even today, on pretty much any terminal emulator.</p>
<p>When mice became a thing, one could simply select text and then click
<kbd>mouse2</kbd> to paste it. <kbd>mouse2</kbd> was the right mouse button
back when mice had two buttons, and became the middle mouse button now that
mice typically have three buttons (or three-finger tap on touchpads).</p>
<p>When text is selected it becomes the <code>PRIMARY SELECTION</code>, and can be pasted
anywhere. Again, this still works on Linux nowadays as well as many other
platforms. This feature exists in parallel to the more popularly known
clipboard.</p>
<h2 id="the-clipboard">The clipboard<div class="permalink">[<a href="#the-clipboard">permalink</a>]</div></h2>
<p>The eighties came, and Apple refined this concept into the form commonly known
today. The action of &ldquo;copying&rdquo; text became explicit. One could select text for
a multitude of reasons (e.g.: to then click on the &ldquo;bold&rdquo; button), so having
&ldquo;copy&rdquo; be an explicit action made sense.</p>
<p>Apple defined some keyboard mapping for these actions which remain the same to
this today:</p>
<ul>
<li><kbd>Cmd</kbd>+<kbd>x</kbd> to cut <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></li>
<li><kbd>Cmd</kbd>+<kbd>c</kbd> to copy</li>
<li><kbd>Cmd</kbd>+<kbd>v</kbd> to paste</li>
</ul>
<p>You&rsquo;ll notice that someone did their homework and made sure to pick a key
combination that wasn&rsquo;t already used for something else.</p>
<p>Microsoft, faithful to its style, copied the whole idea, but changed it slightly:</p>
<ul>
<li><kbd>Ctrl</kbd>+<kbd>x</kbd> to cut</li>
<li><kbd>Ctrl</kbd>+<kbd>c</kbd> to copy</li>
<li><kbd>Ctrl</kbd>+<kbd>v</kbd> to paste</li>
</ul>
<p>You&rsquo;ll immediately notice an issue here: <kbd>Ctrl</kbd>+<kbd>c</kbd> already
had a different usage, so we have a collision. Of course, Windows is not like
all the other operating systems, and didn&rsquo;t have terminals, so this was never a
problem.</p>
<h2 id="modern-terminals">Modern terminals<div class="permalink">[<a href="#modern-terminals">permalink</a>]</div></h2>
<p>At some point in history, this same clipboard made its way into the graphical
interfaces of open source desktop environments (mainly GNOME and KDE) and
trickled into all other applications. However, the key mappings were copied
from windows.</p>
<p>Because terminals already had a use for <kbd>Ctrl</kbd>+<kbd>c</kbd>, they had
to use something else to paste from the clipboard. The following mappings were
chose, and remain the same on most non-Apple platforms even today use:</p>
<ul>
<li><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>x</kbd> to cut</li>
<li><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>c</kbd> to copy</li>
<li><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd> to paste</li>
</ul>
<p>This is terrible, mostly because copy-pasting is standard on most Linux apps
except terminals, where it has to be different because of this collision.</p>
<p>Using <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>c</kbd> to copy on terminals is
terrible ergonomics, and a permanent source of confusion. New users are always
confused (understandably so), and even seasoned users will occasionally get it
wrong (and kill a process trying to paste into it). It&rsquo;s also frequently a very
hard/painful combination to press on laptop keyboards, due to the size and
position of both modifiers.</p>
<h2 id="using-superc">Using Super+c<div class="permalink">[<a href="#using-superc">permalink</a>]</div></h2>
<p>Using <kbd>Super</kbd>+<kbd>c</kbd> to copy (much like has been the case on
Apple&rsquo;s OSs since the eighties) fixes this problem: all programs can use the
same mapping, and it&rsquo;s a lot easier to press than. However, changing this is
tricky. Asking every toolkit and app developer to change their mappings is not
feasible at all; most won&rsquo;t even think it&rsquo;s a good idea, and users would revolt
in uproar.</p>
<p>Luckily, there exists an escape hatch! There exist a few special keys called
<kbd>XF86Copy</kbd>, <kbd>XF86Paste</kbd>, <kbd>XF86Cut</kbd>. These are those
special media keys on some keyboards which have the actual copy, paste and cut
icons on them respectively, and do exactly that.</p>
<p>So it&rsquo;s possible to configure a system to simulate <kbd>XF86Copy</kbd> being
pressed when one presses <kbd>Cmd</kbd>+<kbd>c</kbd>. And this should work
pretty much everywhere. Both in terminals and GUI applications! A few
applications might not interpret <kbd>XF86Copy</kbd>, but that&rsquo;s a valid bug
report that one can reasonably expect to be addressed.</p>
<p>On wayland compositors, simulating this key can be achieved with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>wtype -P XF86Copy
</span></span></code></pre></div><p>So on swaywm one might configure:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>bindsym Mod4+x exec wtype -P XF86Cut
</span></span><span style="display:flex;"><span>bindsym Mod4+c exec wtype -P XF86Copy
</span></span><span style="display:flex;"><span>bindsym Mod4+v exec wtype -P XF86Paste
</span></span></code></pre></div><p>Care should be taken to remap any actions previously mapped to these
combinations.</p>
<p>If you try this out, I&rsquo;m very curious to hear your experience!</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><kbd>Cmd</kbd>, also known as <kbd>Command</kbd>, <kbd>Super</kbd>,
<kbd>Mod4</kbd> or <kbd>Windows Key</kbd>.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>Systemd, locking and sleeping</title><link>https://whynothugo.nl/journal/2022/10/26/systemd-locking-and-sleeping/</link><pubDate>Wed, 26 Oct 2022 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2022/10/26/systemd-locking-and-sleeping/</guid><description><![CDATA[<h2 id="locking-the-system">Locking the system<div class="permalink">[<a href="#locking-the-system">permalink</a>]</div></h2>
<p><code>logind</code> (part of systemd) emits events when the system is about to be locked
or go into sleep. Typically, one can configure <code>logind</code> so that closing the
laptop lid triggers a &ldquo;lock the session&rdquo; event. However, this just emits a
D-Bus signal and doesn&rsquo;t provide facilities for running any screen locker. One
needs a separate process listening for this event, and this process itself
needs to run a screen-locker and any other similar actions. This is the role
that a tools of mine, <a href="https://git.sr.ht/~whynothugo/systemd-lock-handler"><code>systemd-lock-handler</code></a>, has been fulfilling. When one
such event is emitted by systemd/logind, the <code>systemd-lock-handler</code> starts a
custom systemd user target (<code>sleep.target</code>). Users can associate services that
should run when this target starts (like a screen locker, or screen dimming,
etc).</p>
<p>It&rsquo;s hard to see any value being added by <code>logind</code> for this scenario; the
service that listens to the D-Bus signal could just as well be monitoring the
laptop lid or power button itself. The difference in complexity is immediately
obvious, no obvious added benefits.</p>
<p>Upon re-reading the docs for <a href="https://man.archlinux.org/man/sway.5.en">sway(5)</a>, I found <code>bindswitch</code>. It turns
out I can just tell <code>logind</code> to just ignore when I close the lid or press the
power button, and have <code>sway</code> execute a script when that happens instead.</p>
<p>That removes a lot of unnecessary abstractions. One could simply bind this to a
script that locks or dims the screen. Stupid simple.</p>
<h2 id="sleeping">Sleeping<div class="permalink">[<a href="#sleeping">permalink</a>]</div></h2>
<p><code>logind</code> also emits a <code>PrepareForSleep</code> event before the system goes to sleep
(either suspend or hibernate), and <code>systemd-lock-handler</code> handles this as well.
This is handy to lock the screen before the system goes into sleep mode, so the
system is already locked when it later resumes.</p>
<p>The documented approach for this kind of scenario is to <em>inhibit</em> suspension
permanently. Where there is an attempt to suspend the system, the inhibitor
should first run the screen locker, and then allow suspension to continue. This
is a terrible API, with too high a level of complexity for something rather
trivial.</p>
<p>Let&rsquo;s summarise how it&rsquo;s meant to be used:</p>
<ul>
<li>A dedicated service runs permanently, inhibiting suspension.</li>
<li>The service also needs to listen for <code>logind</code>&rsquo;s <code>PreparingForSleep</code> events.</li>
<li>When the system is about to suspend, a <code>PreparingForSleep</code> signal is emitted.</li>
<li>On receiving a <code>PreparingForSleep</code> event, the dedicated service runs the
screen locker, and then release the inhibition.</li>
</ul>
<p>The most obvious smell in this interface is the fact that a service needs to
run permanently, in order to do something before the system suspends. Instead,
the tool requesting the suspension could do this instead.</p>
<p>A solution for this is again simple; instead of using <code>systemd suspend</code>
directly, simply use a 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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>screen_locker --fork
</span></span><span style="display:flex;"><span>systemctl suspend
</span></span></code></pre></div><p>This tricky part is if Alice suspends the system via <code>ssh</code> and Bob using that
system interactively (e.g.: sitting in from of it). To be honest, this is a
rather unrealistic scenario anyway, and IIRC <code>logind</code> would not allow Alice to
suspend the system if she is only logged in remotely.</p>
<h3 id="issues-with-systemd-lock-handler">Issues with <code>systemd-lock-handler</code><div class="permalink">[<a href="#issues-with-systemd-lock-handler">permalink</a>]</div></h3>
<p>An issue with the current implementation is that, upon receiving a signal, it
starts a target, which will then trigger starting of the screen locker
<em>asynchronously</em>. This means that it&rsquo;s still possible for the locker to start
too late (e.g.: after the system comes back from sleep).</p>
<p>I&rsquo;m considering a v3.0.0 that will change how the tool works and simply run a set
of scripts <em>synchronously</em>, so it&rsquo;s simple to just run something like <code>swaylock -f</code> in it. It would also be possible to just run a systemd and wait for it to
be ready. The contents of the script are up to the user, but the general
approach reduces some pointless complexity and custom <code>systemd.target</code>s while
also ensuring that actions are executed <em>before</em> the system sleeps.</p>
<p>In hindsight, this approach is the obvious one, much simpler, and has far less
issues.</p>
<h2 id="conclusion">Conclusion<div class="permalink">[<a href="#conclusion">permalink</a>]</div></h2>
<p><code>systemd</code> is infamous for its complexity. I really appreciate a lot of the
features it bring (dependency resolution, cgroups, socket activation, user
services, and a few others). But when it comes to handling the system power
state and session locking, it is over-engineered, too complex, and does too
little. It requires dedicated services permanently running to listen for
specific events just to do something basic like &ldquo;run the screen locker before
suspending the system&rdquo;.</p>
<p>The only real benefit of having <code>logind</code> handle locking/sleeping when the lid
is closed or when the power button is pressed, is that the system will also go
to sleep if no user is logged in. This is an odd case, but, admittedly a valid
one; if I close my laptop lid without logging in, I&rsquo;d expect it to go to sleep
as usual. Ideally, however, the display manager to do this for me, but AFAIK,
no display manager actually does.</p>
]]></description></item><item><title>How the clipboard works</title><link>https://whynothugo.nl/journal/2022/10/21/how-the-clipboard-works/</link><pubDate>Fri, 21 Oct 2022 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2022/10/21/how-the-clipboard-works/</guid><description><![CDATA[<p>Reading how copy-paste works from the Wayland specification is non-trivial
unless you understand a lot of how desktop computing works and Wayland
internal. It took me quite a while to figure it all out, though once you get
there, it seems quite obvious.</p>
<p>Here&rsquo;s my attempt at explaining how it works for mere mortals.</p>
<h2 id="terminology">Terminology<div class="permalink">[<a href="#terminology">permalink</a>]</div></h2>
<p>Let me clarify that what we usually call &ldquo;clipboard&rdquo; is actually called a
&ldquo;selection&rdquo;. I&rsquo;ll use the term &ldquo;clipboard&rdquo; here anyway to keep this friendly,
but keep in mind that it&rsquo;s not the actual technical term.</p>
<h2 id="copying">Copying<div class="permalink">[<a href="#copying">permalink</a>]</div></h2>
<p>When you select some text and press ctrl+c, you&rsquo;d normally say that the program
&ldquo;copies&rdquo; data into the clipboard. In reality, no copying happens at this point.
What the program actually does is announce <em>&ldquo;I have the clipboard now, and have
data of type <code>text/plain</code>&rdquo;</em>. At this point, the previous application that owned
the clipboard (if any) is informed that they no longer own the clipboard.
From this point on, whenever another application gains focus, it&rsquo;ll be informed
that someone owns the clipboard and is offering data of type <code>text/plain</code>. This
happens when an application gains focus because only a foreground application
can access the clipboard.</p>
<p>However, we don&rsquo;t just copy text, we can copy anything. Let&rsquo;s copy a png image
on Firefox (e.g.: right click, copy image). At this point, Firefox announces
<em>&ldquo;I have the clipboard; I have data of type <code>image/png</code>, <code>text/x-moz-url</code><sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>
or <code>text/plain</code>&rdquo;</em>. This means that the data can either be pasted as a png image
(e.g.: raw image bytes) or as a URL (the URL to the image we&rsquo;ve copied) or as
text (again, the URL. This is the fallback for programs that don&rsquo;t support any
of the above two).</p>
<p>This all oversimplified one tiny bit; the copying process is not one step, but
a few (but the above introduction should help this make sense). In full detail:</p>
<ul>
<li>The application creates a <code>wl_data_source</code> object, indicating that it&rsquo;s going
to offer data to other applications.</li>
<li>The application adds the mime types that it can handle to the data source
(with <code>wl_data_source::offer</code>)</li>
<li>The application finally calls <code>wl_data_device::set_selection</code>, to indicate
<em>&ldquo;I&rsquo;m taking ownership of the clipboard, and the above created
<code>wl_data_source</code> is what I&rsquo;m offering&rdquo;</em>.</li>
</ul>
<h2 id="pasting">Pasting<div class="permalink">[<a href="#pasting">permalink</a>]</div></h2>
<p>Copying wasn&rsquo;t hard, now let&rsquo;s see how the other side works.</p>
<p>As mentioned above, applications are informed when another application owns the
clipboard.</p>
<p>So let&rsquo;s imagine an you&rsquo;ve copied an image in Firefox, and now switch to a
terminal. The compositor will inform the terminal <em>&ldquo;Somebody owns the clipboard
and is offering data of types <code>image/png</code>, <code>text/x-moz-url</code> or <code>text/plain</code>&rdquo;</em>.
If you try to paste, the terminal will ignore the types it doesn&rsquo;t know how to
handle, and will request the data of type <code>text/plain</code>. At this point, the
owner of the clipboard (Firefox) is informed that somebody wants to paste its
data and receives a file descriptor<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> into which is must write the data. The
terminal receives another file descriptor where it can read the data. Anything
written to the first one is read out the second, so the data is transferred
directly between applications with no middleman. It&rsquo;s basically a pipe; data
goes in one end, comes out the other.</p>
<h2 id="some-notes-on-this-design">Some notes on this design<div class="permalink">[<a href="#some-notes-on-this-design">permalink</a>]</div></h2>
<p>First of all, one has to understand that, under the hood, nothing is ever
&ldquo;copied into the clipboard&rdquo;. When we click copy, nothing is copied. The &ldquo;copy&rdquo;
semantic is only a user-interface concept. What really happens is &ldquo;the
application announces that it owns the clipboard; that the user has copied
something&rdquo;.</p>
<p>A big upside of this design is that no data is copied around unnecessarily. For
example, an image editor will offer data as <code>image/png</code>, <code>image/bmp</code>,
<code>image/jpeg</code>, etc. If the data had to be sent as soon as the &ldquo;copy&rdquo; action
happens, then the image would have to be encoded into all these formats right
away &ndash; but likely only one of these would ever be used. One could be copying a
600MB video, only to paste somewhere were a URL is will pasted.</p>
<p>This approach yields the greatest flexibility, but also keeps unnecessary work
and memory usage to a minimum.</p>
<p>There&rsquo;s a few other technical advantages to this design which are out-of-scope
here (like the compositor not needing to allocate huge amounts of memory for
clipboard data).</p>
<h2 id="an-issue-with-this-design">An issue with this design<div class="permalink">[<a href="#an-issue-with-this-design">permalink</a>]</div></h2>
<p>A big problem with this design is that if I copy an image (e.g.: from GIMP) and
then close that application, the clipboard selection is lost. I can no longer
paste it; it&rsquo;s gone forever.</p>
<p>This is a well-known issue on Linux desktop. There&rsquo;s a couple ways around it:</p>
<ul>
<li>When something is copied and all of a program&rsquo;s windows are closed, the
program can linger in the background, windowless, until is loses the
clipboard. This might be really hard to implement for some applications due
to how they&rsquo;re designed, and needs to be implemented by every single on. It
works for tools which focus to copying data though (this is why I do with
<a href="https://git.sr.ht/~whynothugo/shotman">shotman</a>).</li>
<li>A clipboard manager. Clipboard manager used a privileged API to always be
notified when any application takes ownership of the clipboard; when this
happens, they can copy all the data, and take ownership of the clipboard
themselves. The majority of the implementations out there are broken and only
handle text. Many dump the data to disk, which makes them a bit risky if you
ever copy sensitive data into your clipboard.
I&rsquo;ve written a clipboard manager that works (<a href="https://git.sr.ht/~whynothugo/clipmon">clipmon</a>), but it has one big
problem: if you copy an image that is offered in many different formats, it
has to copy all those formats. This adds a lot of unnecessary work (and
memory usage) for many scenarios.</li>
</ul>
<!--

-- This gets too into "problem fixing" mode, whereas this is just an
-- explanation article.

### A possible solution for this issue

While this is the only issue with this design, it's a very well known one and
extremely annoying. I do believe it should be addressed. I can imagine one way
of addressing this:

- Let clipboard-owners announce "I'm going to exit now".
- If a clipboard manager is running, it can, at this point, copy all the
  clipboard data into its own memory to keep it around, and then take ownership
  of the clipboard itself.
- If no clipboard manager is running, the compositor will simply remove
  ownership of the clipboard from the application.
- Once the original application has lost ownership of the clipboard, it knows
  it can finalise exiting.

This should work, but I wouldn't be surprised if the approach has some issue
I'm not seeing or breaks some design rule. The main issue is that applications
need to add explicit support to keep things in the clipboard once they exit.
Compositors need to implement this too, and it's a really unique behaviour
compared to everything else. It's also likely many applications will never
implement this, ever.

Perhaps applications should just linger in the background if they have the
clipboard until somebody else takes ownership of it.

-->
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Type starting with <code>x-</code> are non-standard. <code>x-moz-</code> means it&rsquo;s a
mozilla-defined one. There seems to be no standard mime type for a URL.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>A file descriptor is what you get when you open a file. You can think of
it as an object into which you can write or from which you can read. In this
case, there&rsquo;s no <em>real</em> file, but it&rsquo;s a useful abstraction when two
applications need to send data to each other directly.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>Shotman 0.2 development update part 2</title><link>https://whynothugo.nl/journal/2022/10/04/shotman-0.2-development-update-part-2/</link><pubDate>Tue, 04 Oct 2022 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2022/10/04/shotman-0.2-development-update-part-2/</guid><description><![CDATA[<p>After the initial proof of concept of the shotman rewrite, my next goal was to
have an implementation that could fully replace the previous version for daily
usage.</p>
<p>My main use of shotman is &ldquo;take screenshot of current window&rdquo;, and I have two
other (less frequent) use cases. They are all mapped to global hotkeys:</p>
<ul>
<li><kbd>Super</kbd>+<kbd>p</kbd> to screenshot the current window.</li>
<li><kbd>Super</kbd>+<kbd>Shift</kbd>+<kbd>p</kbd> to screenshot a region.</li>
<li><kbd>Super</kbd>+<kbd>Ctrl</kbd>+<kbd>p</kbd> to screenshot the entire
output.</li>
</ul>
<p>There&rsquo;s no &ldquo;take screenshot of another window&rdquo; use-case, since I can just focus
that other window and then take the press <kbd>Super</kbd>+<kbd>p</kbd>.</p>
<h2 id="current-window">Current window<div class="permalink">[<a href="#current-window">permalink</a>]</div></h2>
<p>As I&rsquo;d mentioned before, there&rsquo;s currently no Wayland API to request a
screenshot of the current window &ndash; actually, &ldquo;current window&rdquo; is somewhat of a
vague term. On a single-user desktop system, it&rsquo;s obviously the focused window,
but on a non-desktop or multi-user system it&rsquo;s not as obvious. An API to take a
screenshot of a <em>specific</em> window is also not yet available, although this is
currently <a href="https://gitlab.freedesktop.org/wlroots/wlr-protocols/-/issues/93">under discussion</a> and I plan on switching to it once
it&rsquo;s been stabilised and implemented.</p>
<p>In the meantime, I&rsquo;m using <code>swaymsg</code> to determine the position of the currently
focused window, and merely capturing a screenshot of that on-screen region. A
small caveat is that this will also capture any partially overlaid window in
that region. It also works only on <code>sway</code>, and not other compositors, but I
hope to improve on this when the above proposal has been stabilised (I don&rsquo;t
expect this to be too soon though).</p>
<h2 id="current-output">Current output<div class="permalink">[<a href="#current-output">permalink</a>]</div></h2>
<p>There&rsquo;s also no standard wayland API to take a screenshot of the current
output; there&rsquo;s only an API to take a screenshot of a single output, but the
output must be explicitly specified by the application. Again, the concept of
&ldquo;current output&rdquo; is obvious on a desktop/laptop, but vague on other systems
(e.g.: a car&rsquo;s HUD which has multiple displays but no &ldquo;currently focused&rdquo; one).
Shotman targets desktops/laptops and my intent is to support phones and tablets
in futures, but cars are definitely out of scope.</p>
<p>For now, I&rsquo;m using a bit of a hack. The thumbnail window already renders on the
current output. Technically, this is the &ldquo;preferred output&rdquo;, which on desktop
compositors usually matches &ldquo;the currently focused output&rdquo;. Since I can
determine the preferred output for rendering the thumbnail window, I can
request a screenshot of that same output. It works, and there&rsquo;s not obvious
pitfalls, despite it being a hack. I&rsquo;ve <a href="https://gitlab.freedesktop.org/wlroots/wlr-protocols/-/merge_requests/115">suggested</a> the idea
of extending the existing API to support capturing the preferred output
(without requiring hacks to determine it myself), but it&rsquo;s still unclear
whether this will happen or not.</p>
<p>The screencopy protocol is also being standardised beyond just wlroots
compositors, and I&rsquo;ve <a href="https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/124/diffs#note_1526902">mirrored</a> the same request there.</p>
<h2 id="region">Region<div class="permalink">[<a href="#region">permalink</a>]</div></h2>
<p>To screenshot a region, the user is expected to pick that region. I&rsquo;ve used
<a href="https://github.com/emersion/slurp"><code>slurp</code></a> for this, since it works perfectly for this use-case, and has
nothing sway-specific (it should work on any compositor that implements the
<code>layer-shell</code> protocol, which we already require anyway).</p>
<h2 id="results-so-far">Results so far<div class="permalink">[<a href="#results-so-far">permalink</a>]</div></h2>
<p>After finalising the changes to make this usable, I&rsquo;ve tagged version 0.2.0,
and switched to using it myself for my daily screenshot needs. I&rsquo;m sure I&rsquo;ll
encounter bugs soon and will address then as I find them.</p>
<p>The speed difference is undeniable, and it&rsquo;s mainly because shotman does
extremely little work, and copies data around as little as possible.</p>
<p>There&rsquo;s still quite a few code smells that need to be fixed. It&rsquo;s mostly good
hygiene and nothing that would have obvious issues, but some further (quite
minor) optimisations are still possible.</p>
<h2 id="moving-forward">Moving forward<div class="permalink">[<a href="#moving-forward">permalink</a>]</div></h2>
<p>In future, I want to add buttons for actions (which are currently are all
keybord-only). This is mostly for touch-displays, and once I&rsquo;ve added these,
I&rsquo;ll start experimenting on how this works on <a href="https://postmarketos.org/">postmarketOS</a>.</p>
<p>For more details on shotman and how its usage, see the README at the project&rsquo;s
repository:</p>
<p><a href="https://git.sr.ht/~whynothugo/shotman">https://git.sr.ht/~whynothugo/shotman</a></p>
]]></description></item><item><title>Shotman 0.2 development update part 1</title><link>https://whynothugo.nl/journal/2022/08/28/shotman-0.2-development-update-part-1/</link><pubDate>Sun, 28 Aug 2022 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2022/08/28/shotman-0.2-development-update-part-1/</guid><description><![CDATA[<div class="notice update">
  <header>Update </header>
  <section>
  This article was renamed to &ldquo;<em>part 1</em>&rdquo; when publishing <a href="/journal/2022/10/04/shotman-0.2-development-update-part-2/">part
2</a>.
  </section>
</div>
<p>These last couple of week I&rsquo;ve been putting a lot of time into <strong>shotman</strong>.
Shotman is a small GUI tool I wrote a few months ago. It shows a small preview
window when a screenshot is saved, and has controls to copy the screenshot into
clipboard or delete it right away.</p>
<p>The previous version was 0.1. This newly written version is to be 0.2.</p>
<h2 id="about-version-01">About version 0.1<div class="permalink">[<a href="#about-version-01">permalink</a>]</div></h2>
<p>The initial implementation was more of a prototype than anything else. It
worked, but was <em>slow</em>. It can take a couple of seconds to even render the
window. That aside, it had a lot of quirks regarding window sizing and
positioning, and its clipboard support is flaky (this was delegated to Qt, so
not much control under the hood here).</p>
<figure>
  <img src="shotman-0.1.png" alt="A screenshot shotman v0.1.">
  <figcaption>On the upper left: shotman v0.1, showing a screenshot preview.</figcaption>
</figure>
<h2 id="the-slowness">The slowness<div class="permalink">[<a href="#the-slowness">permalink</a>]</div></h2>
<p>The slowness is mostly due to the design; taking the screenshot was delegated
to <code>grimshot</code>, a bash script which in turn uses <code>grim</code> to actually take the
screenshot. The screenshot was taken, then encoded as png, saved to disk, then
loaded from disk and decoded by shotman, and finally rendered on screen by
shotman. This is a lot of back and forth to get pixels from the compositor back
into the same compositor.</p>
<p>The most obvious design change I&rsquo;ve made is how screenshots are taken; the new
version will request that the wayland compositor make a copy of the screen into
memory. Shotman will then create its window, and just tell the compositor to
render that same in-memory screenshot onto the new window. Shotman doesn&rsquo;t copy
the screenshot around at all &ndash; it doesn&rsquo;t even need to read it for rendering!</p>
<p>The result of this is that it&rsquo;s a lot faster. It takes well under 80ms from
startup to the point a window is rendered on screen on the same machine where
shotman would take between 1.6 seconds (at 2560x1600px). That&rsquo;s a 95% speed-up.</p>
<h2 id="the-thumbnail-window">The thumbnail window<div class="permalink">[<a href="#the-thumbnail-window">permalink</a>]</div></h2>
<p>On shotman 0.1, the rendered window was just a regular window, and required
configuring <code>sway</code> to make the window float and render properly.  This was a
very hacky approach, and wouldn&rsquo;t even work on other compositors.</p>
<p>The 0.2 shotman now renders itself using the <a href="https://wayland.app/protocols/wlr-layer-shell-unstable-v1"><code>wlr-layer-shell</code></a> wayland
protocol. This lets it render as a floating widget without any custom
compositor configuration, and allows it to work on any compositor exposing this
protocol (for now, mostly wlroots-based compositors).</p>
<h2 id="keyboard-control">Keyboard control<div class="permalink">[<a href="#keyboard-control">permalink</a>]</div></h2>
<p>Keyboard controls remain mostly the same (see <a href="https://git.sr.ht/~whynothugo/shotman#keyboard-usage">the relevant section of
README</a>). Usually, after taking a screenshot one would either dismiss
the window right away (<kbd>Esc</kbd>), or copy it to clipboard and then
dismiss. It&rsquo;s not meant to linger around.</p>
<p>It is not also possible to edit the image by pressing <kbd>e</kbd>. Currently,
<code>gimp</code> is hardcoded, though I expect to make this configurable in future.</p>
<p>A new &ldquo;nice to have&rdquo; is that the <kbd>h</kbd>, <kbd>j</kbd>, <kbd>k</kbd>,
<kbd>l</kbd>, keys can now be used to re-anchor the window onto different
corners of the screen quickly and easily.</p>
<p>A pending annoyance is that, once the window is unfocused, it cannot be
refocused with just the keyboard. This is because sway does not offer any
mechanism for a user to re-focus a layer-shell window (with good reason too;
this almost never makes sense for layer shell surface). I&rsquo;m considering
attaching a pop-up to an invisible layer-shell instead. Using the invisible
layer-shell surface allows me to continue anchoring the window in a specific
corner, but the floating pop-up can be re-focus via the usual &ldquo;focus floating&rdquo;
mapping.</p>
<h2 id="clipboard">Clipboard<div class="permalink">[<a href="#clipboard">permalink</a>]</div></h2>
<p>Regarding clipboard support, the new version handles it directly, which has
resulted in far more reliable results.</p>
<p>Normally, pressing <kbd>ctrl</kbd>+<kbd>c</kbd> copies the image into clipboard
(no need to &ldquo;select anything here, this window can only copy one thing). I&rsquo;m
also working on copying the filepath into clipboard, so that applications can
paste the file themselves too.</p>
<p>Copying the filepath should also provide an interesting mechanic with
<kbd>ctrl</kbd>+<kbd>x</kbd>, which should allow moving the original screenshot
file by just pasting it elsewhere.</p>
<h2 id="mouse-and-touch-controls">Mouse and touch controls<div class="permalink">[<a href="#mouse-and-touch-controls">permalink</a>]</div></h2>
<p>The original version also had buttons that could be clicked. I realised I&rsquo;d
last used these the day I wrote the tool and never again. However, I will be
implementing those in the new version as well, since I want shotman to be
usable either keyboard-only, pointer-only or touch-only. The touch-only
usability aspect is important since I want to be able to use this same tool to
take screenshots on Linux-based phones. A fast, reliable tool for this is much
needed, so I hope to be able to fill this gap early on.</p>
<p>Currently shotman runs on a Linux phone, but there&rsquo;s no convenient way to
interact with it due to being purely keyboard driven.</p>
<p>My intent is to include buttons for copying, deleting and closing, and also
allow swiping the window towards the border of the screen to dismiss it.
Fortunately, one of laptop has a touch display which should make testing this a
lot simpler.</p>
<h2 id="current-state-and-near-future-plans">Current state and near-future plans<div class="permalink">[<a href="#current-state-and-near-future-plans">permalink</a>]</div></h2>
<figure>
  <img src="shotman-0.2.png" alt="A screenshot shotman v0.1.">
  <figcaption>
    On the upper right: shotman v0.2, showing a screenshot preview.
    Still missing buttons (though honestly, the main focus is keyboard controls)
  </figcaption>
</figure>
<p>The newly written version work works well, fast and reliably. There are,
however, several pending items that still need to be addressed. The biggest one
is the ability to take a screenshot of a single window or a selected region.</p>
<p>There&rsquo;s also a lot of edge cases and smaller details that need to be addressed
all over the codebase, though this are more about cleaning up than about actual
usability issues.</p>
<p>Filenames format is now a bit less problematic:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>Screenshot.from.2022-08-28.at.10_49_36.465683503.png
</span></span></code></pre></div><p>This keeps files sorted in natural order (thanks to using ISO-like formats),
makes them very easy for humans to recognise, and avoids issues with tools and
websites that are picky about filename characters. Keeping the <code>ms</code> looks
weird, but avoid issues when multiple screenshots are taken within a second.
This last part might still change a bit in future.</p>
<p>I do want to look into animating the motion when the window is re-anchored from
one corner onto another &ndash; but only if this keeps complexity low, which is yet
to be seen.</p>
<p>Scaling support is still WIP, but should be ready soon.</p>
<h2 id="unfixable-bugs">Unfixable bugs<div class="permalink">[<a href="#unfixable-bugs">permalink</a>]</div></h2>
<p>There&rsquo;s two low-impact bugs that cannot be easily fixed:</p>
<ul>
<li>There&rsquo;s no wayland protocol to pick a window to screenshot, so this
functionality will remain sway-only once implemented. This should not be an
issue on mobile, where I would usually expect to take a screenshot of the
entire screen.</li>
<li>The first frame is rendered blurry and only the second frame will be sharp.
This is due to a bug in wayland protocols, where clients cannot know the
scale until <strong>after</strong> they&rsquo;ve rendered a window, forcing the first frame to
be imperfect.</li>
</ul>
<h2 id="why-not-existing-any-tools">Why not existing any tools?<div class="permalink">[<a href="#why-not-existing-any-tools">permalink</a>]</div></h2>
<p>Tools for taking screenshots already exist on Linux, but none of them fit the
bill quite right for this use case.</p>
<p>The most obvious contender is <code>grim</code>. It is a general-purpose tool for taking
screenshots, and also very easy to use in scripts. However, it was not quite
right for this use case; it will save the image to disk, which then needs to be
read again to render it on-screen. It&rsquo;s a great tool, but not right for this
particular use-case.</p>
<p>Shotman does one thing and one thing only, but does it well. It renders a GUI
when taking a screenshot, allowing copying, deleting or keeping it. A pending
feature is to press <code>e</code> to open the image in an editor (e.g.: GIMP), but that&rsquo;s
the end of its scope.</p>
<h2 id="upcoming-timeline">Upcoming timeline<div class="permalink">[<a href="#upcoming-timeline">permalink</a>]</div></h2>
<p>If time permits, I hope to have a release with all these new improvements in
the next week or two. I&rsquo;m not entirely sure about a timeline for the
touch-aspect, but after implementing that I hope to transition this project
into a stable phase.</p>
<p>In the meantime, have a look at shotman in its new home over at sourcehut:</p>
<p><a href="https://git.sr.ht/~whynothugo/shotman">https://git.sr.ht/~whynothugo/shotman</a></p>
]]></description></item><item><title>My packaging policy</title><link>https://whynothugo.nl/journal/2022/08/13/my-packaging-policy/</link><pubDate>Sat, 13 Aug 2022 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2022/08/13/my-packaging-policy/</guid><description><![CDATA[<p>The open source sphere has continuously had a lot of discussion regarding
packaging, and there&rsquo;s often an expectation that upstream developers should
distribution packages. I want to make it clear where I stand on this, what
users of my projects can expect, and what how packagers can contribute.</p>
<p>As a general rule, for projects I maintain, I&rsquo;ll only do source releases (e.g.:
tag a version as v2.0.1 and sign that tag or tarball). For a few, I&rsquo;ll provide
binary releases (this applies especially to compiled languages where
downloading the binary is actually meaningful). Anyone is free to download,
build and use these, since that&rsquo;s the whole point of open source software.</p>
<p>Obviously, this is rather inconvenient for many users: it&rsquo;s much more
convenient to use a <code>deb</code>, <code>apkg</code> or<code>rpm</code> package that they can easily install
and keep updated via their distribution&rsquo;s package manager.</p>
<p>Honestly, it&rsquo;s usually too much work for me (an most upstream developers) to
provide these packages. I don&rsquo;t use Debian and Fedora. I&rsquo;m not familiar with
all the quirks of rpm and deb. Even if I take the time to build these, I&rsquo;ll be
shipping a packages for platforms that I don&rsquo;t use. There would be zero quality
control. And that additional work is not something for which I want to sign up.
I want to focus on maintaining upstream.</p>
<p>Attempting to ship a single <code>deb</code> package as upstream also neglects the
different distributions and release channels out there. Some users are on
Debian-stable, others on Debian-testing, others on Mint, others on Ubuntu, etc.
Availability of dependencies vary, and each platform has its own quirks. While
expecting upstream to ship <code>deb</code> packages sounds simple, in reality there&rsquo;s a
very large permutation of targets for which one must build and test, and
shipping a single <code>deb</code> package is just opening the door for a plethora of bug
reports.</p>
<p>That said, I&rsquo;ve nothing against others packaging my tools for different
platforms. In fact, I encourage it! If someone with the right expertise wants
to build a Debian package for <code>caffeine-ng</code>, <code>darkman</code>, <code>vdirsyncer</code> or any
other project, I welcome the effort. My focus is on shipping releases ready to
be packaged, and let packagers do their jobs.</p>
<p>Additionally, if any packager finds obstacles, or needs to apply patches for
code to build on a given platform, please open an issue (or send a patch). I
don&rsquo;t mind giving guidance. But don&rsquo;t expect me to know all the quirks of every
distribution.</p>
<div class="notice update">
  <header>Update 2022-08-14</header>
  <section>
  Added a mention of how a single <code>deb</code> won&rsquo;t work on every Debian-based distro.
  </section>
</div>
]]></description></item><item><title>Please verify it's you</title><link>https://whynothugo.nl/journal/2022/07/28/please-verify-its-you/</link><pubDate>Thu, 28 Jul 2022 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2022/07/28/please-verify-its-you/</guid><description><![CDATA[<p>Welcome!</p>
<p>It looks like you haven&rsquo;t logged into your account in a really long time. We
need to verify it&rsquo;s you. To do so, we hope you still have that one thing that
people frequently change after a really long time: your phone number! Obviously
you can&rsquo;t have moved or simply switched phone provider over the years, right?</p>
<p>We&rsquo;ll send you an SMS, if you don&rsquo;t receive it with the code, you can&rsquo;t use
your account any more.</p>
<p>Cheers!</p>
]]></description></item><item><title>Meson for Python applications</title><link>https://whynothugo.nl/journal/2022/07/26/meson-for-python-applications/</link><pubDate>Tue, 26 Jul 2022 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2022/07/26/meson-for-python-applications/</guid><description><![CDATA[<p>I&rsquo;ve ported <a href="https://codeberg.org/WhyNotHugo/caffeine-ng">caffeine-ng</a> to use <a href="https://mesonbuild.com/">meson</a> rather than
<a href="https://wheel.readthedocs.io/en/stable/">python-wheel</a> for building and installing. Python&rsquo;s packaging ecosystem is
moving in a direction that will only support python packages, but not packages
that need to install data files outside python&rsquo;s package location. This last
scenario <a href="https://github.com/pypa/installer/issues/128">is deprecated</a> and will become unsupported in future. It already
doesn&rsquo;t work when using python wheels, as discussed on that issue.</p>
<p>caffeine-ng, like many other desktop applications, ships with data files
including manual pages, desktop entry files, icons, translations, etc. It&rsquo;s
clear that Python&rsquo;s build / packaging ecosystem has difference goals in mind
and isn&rsquo;t a good fit here. This isn&rsquo;t a big deal though: tools for building and
installing projects like this already exist.</p>
<p>While it is still possible to distribute a python package that ships data files
in arbitrary locations (e.g.: man pages), it is (and always has been) somewhat
ugly and hacky. The project maintainer needs to write the setup script that
manually copies all the files to the right paths, and then write the
application code to <em>find</em> such files at runtime. Given that Python packages
can be installed into a virtualenv, this becomes even more of a pain.</p>
<p>caffeine-ng itself also has multiple non-python dependencies. There&rsquo;s no way
for a python package/wheel to announce these; it can only raise an exception,
and hope that the user (or packager) knows how to figure that out. There&rsquo;s no
escape hatch for this, so it seems that shipping via this particular
distribution mechanism isn&rsquo;t ideal for us.</p>
<h3 id="meson">Meson<div class="permalink">[<a href="#meson">permalink</a>]</div></h3>
<p>Meanwhile, <code>meson</code> is a much better fit. It can deal with installing a Python
module, but also has explicit support for man pages, building translations, and
all the other functionality needed here. It&rsquo;s also widely used for projects in
a variety of languages, so distribution packagers will usually be more
familiar. It also makes installation easier for end users, since they don&rsquo;t
need to worry about <code>pip</code> and other Python-specific tools that don&rsquo;t really
target end-users.</p>
<p>Something that I appreciate greatly, is that uninstalling also works <strong>well</strong>
by just using <code>ninja -C build uninstall</code>.</p>
<p>When working on a project, uninstalling via  <code>meson</code> will remove files that it
previously installed if the new project build removes references to that file.
It&rsquo;s always nice to keep things nice and clean, and this was never fun with
old-school Makefiles.</p>
<p>Finally, because meson is not Python-specific, it can verify that non-Python
dependencies are installed and at compatible versions &ndash; something entirely
out-of-scope for Python packages.</p>
<p>Porting this project to meson took a few hours, but most of the time sink was
due to my having to familiarise myself with it. Similar future endeavours will
definitely take far less. If you&rsquo;re interested in learning a bit of what meson
looks like and what it can do, I encourage you to take a look at caffeine-ng&rsquo;s
<a href="https://codeberg.org/WhyNotHugo/caffeine-ng/src/commit/c7b1800fa1eb544588406ff93eebae3b232024bd/meson.build">meson.build</a> file; it&rsquo;s a rather complete example (with all sorts of data
files), though likely not the most refined one.</p>
]]></description></item><item><title>Announcing caffeine-ng v4.0.0</title><link>https://whynothugo.nl/journal/2022/07/12/announcing-caffeine-ng-v4.0.0/</link><pubDate>Tue, 12 Jul 2022 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2022/07/12/announcing-caffeine-ng-v4.0.0/</guid><description><![CDATA[<p><a href="https://codeberg.org/WhyNotHugo/caffeine-ng">caffeine-ng</a> is a small tool that shows an icon on the desktop status bar to
temporarily disable screenlockers, screensavers, and screen dimming. It is
typically used to temporarily avoid the screen turning off for things like
watching a movie, a videoconference and alike.</p>
<p>Version 4.0.0 of caffeine-ng is now available. Many of the changes in this
release are thanks to the contributors who have both written and tested many
of these improvements, so a huge thanks to all those who have made this
possible!</p>
<h2 id="whats-new-in-v400">What&rsquo;s new in v4.0.0<div class="permalink">[<a href="#whats-new-in-v400">permalink</a>]</div></h2>
<p>A major change that might break some scripts or integrations is that the
command line portion has been rewritten in <a href="https://palletsprojects.com/p/click/"><code>click</code></a>, a rather popular
Python library for handling everything command line related. Now, when typing a
wrong command (or using <code>--help</code>), usage instructions and help messages are a
lot more helpful and provide much better UX. Figuring out which options exist
and what they do should be much simpler; just append <code>--help</code> to any
[sub]command when in doubt.</p>
<p>That aside, support for inhibiting via Xfce&rsquo;s presentation mode has been merged
in, as well as multiple improvements to Pulseaudio support. A common usage of
caffeine-ng to automatically avoid the system turning off when playing music.
This feature works, but still needs improving.</p>
<p>For all the little details, see the <a href="https://codeberg.org/WhyNotHugo/caffeine-ng/src/branch/main/CHANGELOG.rst">changelog</a>.</p>
<h2 id="moving-to-codeberg">Moving to codeberg<div class="permalink">[<a href="#moving-to-codeberg">permalink</a>]</div></h2>
<p>I have recently also finalised moving this project to codeberg. Codeberg&rsquo;s
interface is very familiar to GitHub users, so this change should be the least
disruptive to contributors in general.</p>
<p>The new home is now <a href="https://codeberg.org/WhyNotHugo/caffeine-ng">https://codeberg.org/WhyNotHugo/caffeine-ng</a>. Please update
your bookmarks. The previous home will remain as a read-only mirror, mostly to
avoid breaking existing links. Another upside of Codeberg for this particular
scenario, is that all issues and PRs can be copied over, retaining all history
in the new home and allowing discussions to continue smoothly.</p>
<p>This forge is entirely open source, and move is part of my initiative to have
<a href="/journal/2022/04/29/open-source-platforms-for-open-source-software/">open source projects hosted purely on open source platforms</a>.</p>
<p>I&rsquo;d like to thank the gitea developers for writing this open source forge, and
the codeberg community for hosting this service for public usage.</p>
<h2 id="availability">Availability<div class="permalink">[<a href="#availability">permalink</a>]</div></h2>
<p><a href="https://codeberg.org/WhyNotHugo/caffeine-ng#installation">Installation instructions</a> remain the same as always. The <a href="https://aur.archlinux.org/packages/caffeine-ng">AUR
package</a> has been updated for ArchLinux users. The release tags are signed
with my new GPG key, <a href="https://pgp.mit.edu/pks/lookup?op=get&amp;search=0x7880733B9D062837">0x9D062837</a>.</p>
<h2 id="future-improvements">Future improvements<div class="permalink">[<a href="#future-improvements">permalink</a>]</div></h2>
<p><code>caffeine-ng</code> is far from perfect. These are some of the main items I currently
have in sight:</p>
<h3 id="using-the-mpris-api">Using the MPRIS API<div class="permalink">[<a href="#using-the-mpris-api">permalink</a>]</div></h3>
<p>This would allow caffeine inhibiting screen saver or suspension as soon as any
compatible media player is running. While this isn&rsquo;t 100% of the media player
out there, it would still improve the experience in many scenarios by a lot.</p>
<p>I&rsquo;m currently not planning on working on this myself, so if anyone wants to
contribute, I&rsquo;m happy to provide guidance. For discussion and details on this
particular topic, see the <a href="https://codeberg.org/WhyNotHugo/caffeine-ng/issues/113">the existing issue</a>.</p>
<h3 id="wayland-support">Wayland support<div class="permalink">[<a href="#wayland-support">permalink</a>]</div></h3>
<p>I am still not 100% certain how to achieve this. The relevant <a href="https://wayland.app/protocols/idle-inhibit-unstable-v1">wayland
protocol</a> allows telling the compositor &ldquo;inhibit screen locking
while a given window<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> is open&rdquo;, so it will work for whitelist applications
and fullscreen applications, but there&rsquo;s no obvious solution to inhibit the
screensaver when it&rsquo;s manually requested by the user. I do plan on researching
this a bit in the near future, since I&rsquo;d like to make use of this feature
myself.</p>
<h3 id="exposing-an-api">Exposing an API<div class="permalink">[<a href="#exposing-an-api">permalink</a>]</div></h3>
<p>Being able to control caffeine via command line or scripts has often been
requested, and I&rsquo;d personally want to have a waybar widget which can show the
current state and control it.</p>
<p>I&rsquo;m thinking of implementing this via a simple unix socket, but I&rsquo;m hesitant of
whether this is a good idea or not. My biggest concern is that this feels like
<a href="https://xkcd.com/927/">there will be &ldquo;yet another screensaver inhibition API&rdquo;</a>, but the
reality is that there isn&rsquo;t one that works everywhere anyway.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>A given <em>surface</em>, to be precise.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>Using a Yubikey for GPG</title><link>https://whynothugo.nl/journal/2022/07/11/using-a-yubikey-for-gpg/</link><pubDate>Mon, 11 Jul 2022 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2022/07/11/using-a-yubikey-for-gpg/</guid><description><![CDATA[<p>I&rsquo;ve written recently on <a href="/journal/2022/05/07/how-i-secure-my-setup-with-a-yubikey/">how I use a Yubikey as a hardware security token for
two factor authentication</a>.</p>
<p>One item I was missing was GPG, and this was mostly because setting up GPG is a
bit tricker to set up and I simply hadn&rsquo;t had the time. My previous key
recently expired, so this is a good time to address that.</p>
<p>This article explains the basics of how Yubikey + GPG works, and how to get
started.</p>
<h1 id="first-glance-at-the-device">First glance at the device<div class="permalink">[<a href="#first-glance-at-the-device">permalink</a>]</div></h1>
<p><a href="https://developers.yubico.com/yubikey-manager/"><code>yubikey-manager</code></a> is required. When installed correctly, running
<code>ykman info</code> should print the following:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-console" data-lang="console"><span style="display:flex;"><span>Device type: YubiKey 5C NFC
</span></span><span style="display:flex;"><span>Serial number: 12345678
</span></span><span style="display:flex;"><span>Firmware version: 5.2.7
</span></span><span style="display:flex;"><span>Form factor: Keychain (USB-C)
</span></span><span style="display:flex;"><span>Enabled USB interfaces: OTP, FIDO, CCID
</span></span><span style="display:flex;"><span>NFC transport is enabled.
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span>Applications	USB          	NFC
</span></span><span style="display:flex;"><span>FIDO2       	Enabled      	Enabled
</span></span><span style="display:flex;"><span>OTP         	Enabled      	Disabled
</span></span><span style="display:flex;"><span>FIDO U2F    	Enabled      	Enabled
</span></span><span style="display:flex;"><span>OATH        	Enabled      	Enabled
</span></span><span style="display:flex;"><span>YubiHSM Auth	Not available	Not available
</span></span><span style="display:flex;"><span>OpenPGP     	Enabled      	Enabled
</span></span><span style="display:flex;"><span>PIV         	Enabled      	Enabled
</span></span></code></pre></div><p>There&rsquo;s three &ldquo;Enabled USB interfaces&rdquo; here:</p>
<ul>
<li><strong>OTP</strong>: This is the basic OTP functionality of the yubikey. Use with
compatible applications (e.g.: <code>Yubikey Authenticator</code>) it can generate
classic OTP tokens. These are the same kind used by mainstream websites like
Twitter and alike. Regrettably, there is little tooling around this
functionality, it <em>seems</em> to be tied to this specific brand only, and it can
only be used with the mentioned app, which is not very convenient, and
somewhat buggy.</li>
<li><strong>FIDO</strong>: This is basically <code>webauthn</code>, and it&rsquo;s basically the second factor when
logging into website where I&rsquo;m prompted for &ldquo;tap your key to log in&rdquo;. See my
<a href="/journal/2022/05/07/how-i-secure-my-setup-with-a-yubikey/">previous article</a> for details on this.</li>
<li><strong>CCID</strong> (<a href="https://en.wikipedia.org/wiki/CCID_(protocol)">chip card interface device</a>): This interface allows the
Yubikey to be used as a smartcard. The protocol is pretty standard and is
used by devices which provide on-device keys for things like signing or
encrypting. This is what I&rsquo;ll be using for GPG. Good to see that it&rsquo;s on by
default.</li>
</ul>
<!-- Note: `pcscd` needs to be installed and running -- and it MUST run as root, which is -->
<!-- a detail I kinda dislike for setting this up. -->
<p>For GPG usage, the Yubikey works as a &ldquo;smart card&rdquo;, so I will use this term in
future when referring to it. I will use the term &ldquo;key&rdquo; to refer to actual GPG
keys (e.g.: the blob of bytes that is a key).</p>
<h1 id="preparing-a-gpg-key">Preparing a GPG key<div class="permalink">[<a href="#preparing-a-gpg-key">permalink</a>]</div></h1>
<p>It&rsquo;s possible to create the key on-device, which is the safest approach, given
it cannot be extracted. However, I want to set up a backup device, which needs
to have the same key material. Because I can&rsquo;t extract the key it from one
smart card, I can&rsquo;t put it in the backup one, so I need to generate the key on
a computer and the import it into both smart cards. To do this, it&rsquo;s best to do
it on an offline machine. I booted a separate laptop with no network to do
this. For full-on paranoid mode, use a Tails live image on a computer with no
storage or wireless adapters.</p>
<p>Usually, generating the private key only needs to be done <strong>once</strong>. On later
years, it&rsquo;s possible to simply extend the expiration of the existing key<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.</p>
<h3 id="key-generation">Key generation<div class="permalink">[<a href="#key-generation">permalink</a>]</div></h3>
<p>Plug in the smart card and then generate a key with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>gpg --expert --full-gen-key
</span></span></code></pre></div><p>I want an ed25519 key, so pick <code>ECC and ECC</code> first, then <code>Curve 25519</code>. Real
name is not really important, but I just put my own. The key is going to be
tied to my identity anyway. I&rsquo;m not 100% sure how important email is here, but
also provided the right one &ndash; this&rsquo;ll make the public key easier to map to my
email address.</p>
<p>The output will include a line that looks something 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;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>gpg: key 0x7880733B9D062837 marked as ultimately trusted
</span></span></code></pre></div><p>The key&rsquo;s id in this case is <code>0x7880733B9D062837</code>.</p>
<h3 id="subkeys">Subkeys<div class="permalink">[<a href="#subkeys">permalink</a>]</div></h3>
<p>GPG keys can have one or more subkeys. These are additional key pairs that are
tied to the master key and can be stored, revoked and expire separately. Each
subkey can have one or more of the following capabilities:</p>
<ul>
<li><code>S</code>, signing: Used to sign arbitrary data.</li>
<li><code>C</code>, certification: Used to sign other keys. We usually call this &ldquo;key signing&rdquo;.</li>
<li><code>E</code>, encryption: Used to encrypt data.</li>
<li><code>A</code>, authentication: This seems to be used by SSH. It&rsquo;s relatively new, and
I&rsquo;m not 100% familiar.</li>
</ul>
<p>Inspecting the key with <code>gpg --list-secret-keys</code> indicates that the generated
key has an <code>SC</code> subkey (Signing, Certification) and an <code>E</code> subkey (Encryption).</p>
<!-- TODO: is $KEYID optional here if there's just ONE? -->
<p>Let&rsquo;s create an Authentication key too with <code>gpg --expert --edit-key $KEYID</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;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>addkey
</span></span><span style="display:flex;"><span>(11) ECC (set your own capabilities)
</span></span><span style="display:flex;"><span>(S) Toggle the sign capability
</span></span><span style="display:flex;"><span>(A) Toggle the authenticate capability
</span></span><span style="display:flex;"><span>(Q) Finised
</span></span><span style="display:flex;"><span>(1) Curve 25519
</span></span></code></pre></div><p>The key should now look something 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;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>sec ed25519/7880733B9D062837
</span></span><span style="display:flex;"><span>    created: 2022-07-10  expires: 2023-07-10  usage: SC
</span></span><span style="display:flex;"><span>    trust: ultimate      validity: ultimate
</span></span><span style="display:flex;"><span>ssb cv25519/69799729DDF6BDD3
</span></span><span style="display:flex;"><span>    created: 2022-07-10  expires: 2023-07-10  usage: E
</span></span><span style="display:flex;"><span>ssb ed25519/F32635370237664C
</span></span><span style="display:flex;"><span>    created: 2022-07-10  expires: 2023-07-10  usage: A
</span></span><span style="display:flex;"><span>[ultimate] (1). Hugo Osvaldo Barrera &lt;hugo@whynothugo.nl&gt;
</span></span></code></pre></div><h3 id="exporting-the-keys">Exporting the keys<div class="permalink">[<a href="#exporting-the-keys">permalink</a>]</div></h3>
<p>At this point, the keys need to be exported. When the keys are later imported
by GPG into the primary smart card, GPG will delete its copy of the secret key
material. I&rsquo;ll then re-import these for the second/backup smart card:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>gpg --armor --export-secret-keys $KEYID &gt; master.key
</span></span><span style="display:flex;"><span>gpg --armor --export-secret-subkeys $KEYID &gt; subkeys.key
</span></span><span style="display:flex;"><span>gpg --armor --export $KEYID &gt; public.key
</span></span></code></pre></div><p>Make sure this is an ephemeral filesystem, it&rsquo;s a bad idea to keep these files
around after finishing this whole process.</p>
<p>Additionally, <code>public.key</code> MUST be copied onto any system so that GPG can
identify the private key on the smart card. Failing to save the public key
will result in the smart card&rsquo;s private key being unusable.</p>
<h1 id="importing-the-keys-into-the-yubikey">Importing the keys into the Yubikey<div class="permalink">[<a href="#importing-the-keys-into-the-yubikey">permalink</a>]</div></h1>
<p>GPG&rsquo;s smart card support works here:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>gpg --edit-card
</span></span></code></pre></div><p>First of all, let&rsquo;s change the admin PIN (default is <code>12345678</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;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>admin
</span></span><span style="display:flex;"><span>passwd
</span></span><span style="display:flex;"><span>3 - change Admin PIN
</span></span></code></pre></div><p>Next, let&rsquo;s change the user PIN (default is <code>123456</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;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>1 - change PIN
</span></span></code></pre></div><p>Now let&rsquo;s move the keys onto the device:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>key 1 # selects the first subkey
</span></span><span style="display:flex;"><span>keytocard
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>key 1 # de-selects the first subkey
</span></span><span style="display:flex;"><span>key 2 # selects the second subkey
</span></span><span style="display:flex;"><span>keytocard
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>key 2 # de-selects the second subkey
</span></span><span style="display:flex;"><span>key 0 # selects the master key
</span></span><span style="display:flex;"><span>keytocard
</span></span></code></pre></div><h1 id="importing-the-keys-into-the-backup-yubikey">Importing the keys into the BACKUP Yubikey<div class="permalink">[<a href="#importing-the-keys-into-the-backup-yubikey">permalink</a>]</div></h1>
<p>Importing the keys into the PRIMARY Yubikey has deleted them from GPG&rsquo;s local
storage, so they need to be imported again. It&rsquo;s best to completely delete
<code>.gnupg</code> to avoid any inconsistencies left over from the previous key 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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>rm -rm ~/.gnupg <span style="color:#75715e"># You&#39;re using an ephemeral system, right!?</span>
</span></span><span style="display:flex;"><span>gpg --import master.key
</span></span><span style="display:flex;"><span>gpg --import subkeys.key
</span></span><span style="display:flex;"><span>gpg --import public.key
</span></span></code></pre></div><p>The steps from the previous section need to be repeated, but with the backup
card plugged in this time.</p>
<h1 id="extra-steps">Extra steps<div class="permalink">[<a href="#extra-steps">permalink</a>]</div></h1>
<p>Finally, configure the Yubikey to require a touch to sign or decrypt anything.
This avoids any random program from using the key without manual approval.</p>
<p>I couldn&rsquo;t do this on the air-gapped machine that did the key generation,
because it need <code>ykman</code>. Doing this on the daily desktop is fine:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>ykman openpgp keys set-touch aut off
</span></span><span style="display:flex;"><span>ykman openpgp keys set-touch sig on
</span></span><span style="display:flex;"><span>ykman openpgp keys set-touch enc on
</span></span></code></pre></div><p>Import the public key here too, so the smart card&rsquo;s key can be used on this
device:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>gpg --import public.key
</span></span></code></pre></div><p>It&rsquo;s a good idea to upload the public key to a keyserver at this point, so it
cannot be lost. It <strong>is</strong> a public key after all.</p>
<p>Finally, mark the key as trusted with <code>gpg --edit-key 0x7880733B9D062837</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;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>trust
</span></span><span style="display:flex;"><span>5 = I trust ultimately
</span></span></code></pre></div><!-- TODO: disable GPG nfc? -->
<h1 id="extending-the-expiration-date">Extending the expiration date<div class="permalink">[<a href="#extending-the-expiration-date">permalink</a>]</div></h1>
<p>The key will expire in a year, so its expiration needs to be extended around
that time. I&rsquo;ll try to publish a follow-up on how to extend the key expiration
when the time comes.</p>
<div class="notice update">
  <header>Update 2023-07-13</header>
  <section>
  See [Extending an expired GPG key][extend].
  </section>
</div>
<h1 id="configuring-git-to-use-this-key">Configuring git to use this key<div class="permalink">[<a href="#configuring-git-to-use-this-key">permalink</a>]</div></h1>
<p>When signing tags, <code>git</code> will use a key matching the email configured, so if
the right email is configured via <code>git config --global user.email hugo@whynothugo.nl</code> then git will use the correct key.</p>
<p>If previous keys exist, it&rsquo;s best to explicitly specify the key 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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>git config --global user.signingkey 0x9D062837
</span></span></code></pre></div><h1 id="disclaimer">Disclaimer<div class="permalink">[<a href="#disclaimer">permalink</a>]</div></h1>
<p>I mention Yubikey a lot here and in other articles. This is because I own this
particular brand, however, there are several alternatives which can be
considered. I&rsquo;ve heard that the <a href="https://shop.nitrokey.com/shop/product/nk3cn-nitrokey-3c-nfc-148">Nitrokey 3</a> is a good FOSS alternative, but
have not had a chance to try it out. Researching alternatives is an exercise
for the reader.</p>
<!-- Here's some useful links for that: -->
<!--  -->
<!-- - [FIDO® Certified Products](https://fidoalliance.org/certification/fido-certified-products/) -->
<!-- - [Yubico](https://www.yubico.com/), the company that makes Yubikeys. -->
<!-- - https://thetis.io/ -->
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><a href="https://web.archive.org/web/20210228103524/https://riseup.net/en/security/message-security/openpgp/best-practices/#use-an-expiration-date-less-than-two-years">https://web.archive.org/web/20210228103524/https://riseup.net/en/security/message-security/openpgp/best-practices/#use-an-expiration-date-less-than-two-years</a>&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>Quick and simple VMs with qemu</title><link>https://whynothugo.nl/journal/2022/07/01/quick-and-simple-vms-with-qemu/</link><pubDate>Fri, 01 Jul 2022 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2022/07/01/quick-and-simple-vms-with-qemu/</guid><description><![CDATA[<p>I don&rsquo;t use VMs very often, so there&rsquo;s no chance I can remember all the dozens
of command line flags for <a href="https://www.qemu.org/"><code>qemu</code></a>.</p>
<p>I end up using <a href="https://virt-manager.org/"><code>virt-manager</code></a> most of the time. It&rsquo;s a GUI for managing
QEMU (and other VM backends) and has dozens of checkboxes and buttons, which
come in handy for really complex virtual hardware configurations where I&rsquo;d need
to know a dozen obscure command line flags to replicate the same results.
<code>virt-manager</code> is also very handy for VMs with a graphical interface (e.g.: to
test something on Fedora or Ubuntu), but not so much when the only need is a
simple console.</p>
<p>After taking less than half an hour of reading, it turns out <code>qemu</code> is far
easier than I suspected. Should have done this years ago.</p>
<h2 id="basic-vm">Basic VM<div class="permalink">[<a href="#basic-vm">permalink</a>]</div></h2>
<p>The most basic VM simply starts a console with the alpine ISO:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>qemu-system-x86_64 -nographic -drive file<span style="color:#f92672">=</span>alpine-virt-3.15.4-x86_64.iso,format<span style="color:#f92672">=</span>raw
</span></span></code></pre></div><p>By default qemu shows output in a separate window (this is the VM&rsquo;s &ldquo;monitor).
This is rather annoying because it hijacks mouse, and doesn&rsquo;t have the
terminal&rsquo;s regular copy-paste support. <code>-nographic</code> prevents qemu from showing
a separate window for graphic output and uses the current terminal console
instead (it simply connects the current terminal to the VM&rsquo;s serial port).</p>
<h2 id="cpu-and-memory">CPU and Memory<div class="permalink">[<a href="#cpu-and-memory">permalink</a>]</div></h2>
<p>By default, the VM has a single CPU core and 128MB of RAM. This can be changed
very easily:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>qemu-system-x86_64 -m 1G -smp cores<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span> -drive file<span style="color:#f92672">=</span>alpine-virt-3.15.4-x86_64.iso,format<span style="color:#f92672">=</span>raw
</span></span></code></pre></div><h2 id="network">Network<div class="permalink">[<a href="#network">permalink</a>]</div></h2>
<p>The internet is a kinda useful thing, and it&rsquo;s nice to be able to use it inside
the VM. There&rsquo;s the <code>-net</code> and <code>-netdev</code> flags, but those are really
complicated &ndash; mostly useful for more advanced configurations. For the most
basic setup (userspace NAT), <code>-nic</code> is the &ldquo;new&rdquo; flag which makes things dead
simple<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>qemu-system-x86_64 -nic user -nographic -drive file<span style="color:#f92672">=</span>alpine-virt-3.15.4-x86_64.iso,format<span style="color:#f92672">=</span>raw
</span></span></code></pre></div><p>After booting, network itself needs to be configured in the guest. For alpine,
use:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>ip link set eth0 up
</span></span><span style="display:flex;"><span>udhcpc eth0
</span></span></code></pre></div><h2 id="putting-it-all-together">Putting it all together<div class="permalink">[<a href="#putting-it-all-together">permalink</a>]</div></h2>
<p>The following example defines CPU, memory and network:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>qemu-system-x86_64 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -nic user <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -nographic <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -m 1G <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -smp cores<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -drive file<span style="color:#f92672">=</span>alpine-virt-3.15.4-x86_64.iso,format<span style="color:#f92672">=</span>raw
</span></span></code></pre></div><h2 id="architecture">Architecture<div class="permalink">[<a href="#architecture">permalink</a>]</div></h2>
<p>QEMU supports A LOT of architectures. For ARM64, use <code>qemu-system-aarch64</code>.
Make sure the ISO image&rsquo;s architecture matches the VM&rsquo;s architecture.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><a href="https://wiki.qemu.org/Documentation/Networking#The_-nic_option">QEMU Networking Documentation</a>&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>The Murena Lies</title><link>https://whynothugo.nl/journal/2022/05/31/the-murena-lies/</link><pubDate>Tue, 31 May 2022 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2022/05/31/the-murena-lies/</guid><description><![CDATA[<p>I usually abstain from writing <strong>just</strong> to criticise someone&rsquo;s work. Rather
than say &ldquo;X is wrong&rdquo;, I prefer to say &ldquo;X could improve by doing Y&rdquo;.</p>
<p>However, in the case of <a href="https://murena.com/">Murena</a>, there&rsquo;s just too much shameless lying to the
public. I don&rsquo;t think this kind of activity should be allowed to continue.</p>
<h1 id="live-stream">Live stream<div class="permalink">[<a href="#live-stream">permalink</a>]</div></h1>
<p>Murena had a live stream today, announcing their privacy centric tools and
services. On this stream, they explained how many modern companies continuously
spy on users, and send all sorts of granular data to advertising companies
(Google, Facebook and many others).</p>
<p>The same page where this stream was happening (<a href="https://launch.murena.com/">https://launch.murena.com/</a>)
included a lot of these very &ldquo;analytics&rdquo; that Murena claims to stand against.
It even used multiple of Google&rsquo;s APIs. So yeah, Google knew of everyone
watching this stream.</p>
<p>Pretty hard to trust this company based on their initial presentation. I can&rsquo;t
imagine how you can &ldquo;accidentally&rdquo; integrate Google into your livestream.</p>
<div class="notice update">
  <header>Update 2022-06-04</header>
  <section>
  Since I keep getting questions around this, here&rsquo;s a screenshot of the Network
inspector during the live stream. I&rsquo;ve censored the video itself since I don&rsquo;t
feel comfortable exposing any individual person that was on-stream for this
article.
  </section>
</div>
<figure>
  <a href="murena-cropped.png">
    <img src="murena-cropped.png"
         alt="Screenshot showing the livestream page and a network inspector."
         style="border-radius: 0;">
  </a>
  <figcaption>
    <a href="murena-cropped.png">
      Link to image
    </a>
  </figcaption>
</figure>
<h1 id="eos-opt-in-to-google-online-services-for-you">eOS opt-in to Google online services for you<div class="permalink">[<a href="#eos-opt-in-to-google-online-services-for-you">permalink</a>]</div></h1>
<p>eOS is an Android OS created by e.foundation. The title on their website is
&ldquo;deGoogled unGoogled smartphone&rdquo;.</p>
<p>I read a lot of negative comments on eOS online, so eventually reached out to
them to confirm what I&rsquo;d been hearing.</p>
<p>e.foundation&rsquo;s staff confirmed that the phone includes multiple applications
that talk to Google&rsquo;s services out-of-the box.</p>
<p>Honestly, I&rsquo;m amazed of the shamelessness of this organisation mis-representing
itself in such a way. Then again, the average consumer doesn&rsquo;t understand
enough about tech or privacy for this to be a problem. Anyone who want privacy
but is not an expect of the subject will likely fall for this and buy from them
anyway.</p>
<h1 id="the-obvious-solution">The obvious solution<div class="permalink">[<a href="#the-obvious-solution">permalink</a>]</div></h1>
<p>The obvious solution is to <strong>not</strong> include analytics, tracking, and Google
integrations in the page where you&rsquo;re streaming a presentation talking about
the privacy issues of analytics and Google spying on everyone.</p>
<p>The obvious solution is to <strong>not</strong> enable integration with Google&rsquo;s services
out-of-the-box on a product tagged as &ldquo;unGoogle deGoogled&rdquo;. If you insist on
installing them by default, then at least require users to opt-in to these kind
of services.</p>
<h1 id="just-another-cloud-thats-the-same">Just another cloud that&rsquo;s the same<div class="permalink">[<a href="#just-another-cloud-thats-the-same">permalink</a>]</div></h1>
<p>Murena offers cloud hosting. Their services are based on unencrypted, and they
have access to all user personal and private data.</p>
<p>They promise not to snoop &ndash; but so do many other companies that are simply
lying.</p>
<p>Given what we&rsquo;ve seen so far, there&rsquo;s zero reason to trust any of their
promises of privacy. And promises is all they offer; their services don&rsquo;t
encrypt on device, or anything alike.</p>
<p>Real privacy comes from tools that don&rsquo;t allow <strong>third parties</strong> to snoop on
one&rsquo;s personal data. The typical example is encrypting data with a key that
only the owner knows. Privacy does not come from a tool that allows third
parties to snoop on one&rsquo;s data and a promise that they won&rsquo;t.</p>
<h1 id="the-sad-state-of-afairs">The sad state of afairs<div class="permalink">[<a href="#the-sad-state-of-afairs">permalink</a>]</div></h1>
<p>We&rsquo;ve lived many years in a world where sustainable, open-srouce ethical phone
were not something that anyone was talking about. It was a void waiting to be
filled.</p>
<p>What worries me deeply, is that this void is just filled by organisations lying
about their values. Organisations that <strong>claim</strong> to be privacy centric and
ethical, but in reality, are more of the same.</p>
<p>It worries me that the guys actually working on open source, privacy-centric
tools, won&rsquo;t get any attention because the attention is going to companies like
Murena.</p>
<p>Then again, we live in a world where companies like Facebook and Google claim
that &ldquo;they care about our privacy&rdquo;, so blatant lies to the public are the norm.</p>
<p>Dunno why I&rsquo;m so surprised.</p>
]]></description></item><item><title>Status update, May 2022</title><link>https://whynothugo.nl/journal/2022/05/27/status-update-may-2022/</link><pubDate>Fri, 27 May 2022 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2022/05/27/status-update-may-2022/</guid><description><![CDATA[<h1 id="vdirsyncer-rewrite">vdirsyncer rewrite<div class="permalink">[<a href="#vdirsyncer-rewrite">permalink</a>]</div></h1>
<p>I&rsquo;ve been working on some foundational code for the <a href="/journal/2022/04/18/a-vdirsyncer-rewrite/">vdirsyncer rewrite</a>.</p>
<p>I have some basic traits that all storages will implement, and have a working
<code>filesystem</code> storage, which reads a <a href="https://vdirsyncer.pimutils.org/en/stable/vdir.html"><code>vdir</code></a>. The filesystem storage
itself also helps validate that the API is sound enough before starting to
write the other storage implementations.</p>
<p>I considered exposing <code>async</code> APIs, but this turned out to not be a great idea.
Async functions in trait are not something fully stabilised. I gave it a shot
using <a href="https://crates.io/crates/async-trait"><code>async-trait</code></a>, and <em>kind of</em> worked. An obvious fault was
that the generated documentation (e.g.: with <code>cargo doc</code>) for the methods was
painful to read. For example, this simple 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;-webkit-text-size-adjust:none;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#75715e">#[async_trait]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">trait</span> Storage: Sized {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">discover_collections</span>(<span style="color:#f92672">&amp;</span>self) -&gt; Result<span style="color:#f92672">&lt;</span>Self::Discovery<span style="color:#f92672">&gt;</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Would render like this in documentation:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">trait</span> Storage: Sized {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">discover_collections</span><span style="color:#f92672">&lt;</span><span style="color:#a6e22e">&#39;life0</span>, <span style="color:#a6e22e">&#39;async_trait</span><span style="color:#f92672">&gt;</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">&#39;life0</span> self
</span></span><span style="display:flex;"><span>    ) -&gt; <span style="color:#a6e22e">Pin</span><span style="color:#f92672">&lt;</span>Box<span style="color:#f92672">&lt;</span><span style="color:#66d9ef">dyn</span> Future<span style="color:#f92672">&lt;</span>Output <span style="color:#f92672">=</span> Result<span style="color:#f92672">&lt;</span>Self::Discovery<span style="color:#f92672">&gt;&gt;</span> <span style="color:#f92672">+</span> Send <span style="color:#f92672">+</span> <span style="color:#a6e22e">&#39;async_trait</span><span style="color:#f92672">&gt;&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">where</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">&#39;life0</span>: <span style="color:#a6e22e">&#39;async_trait</span>,
</span></span><span style="display:flex;"><span>        Self: <span style="color:#a6e22e">&#39;async_trait</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Maybe there&rsquo;s a workaround to patch this somehow during documentation
generation. I honestly didn&rsquo;t look; dealing with this kind of details on
unstable features is not something in which I want to invest time for this
project.</p>
<p>While standing back to analyse the situation and usability it became very clear
that shipping an <code>async</code> library would not be ideal. All users using the API
would need to deal with the complexity of async, and this would likely become a
deal breaker when using the library from other languages (which is an important
goal of this project).</p>
<p>I&rsquo;ll be addressing the <a href="https://en.m.wikipedia.org/wiki/Webcal"><code>WebCal</code></a> storage next. This one has the quirk
of always having a single collection, so I&rsquo;ll need to think a few details
around it. It&rsquo;s probably the simplest one, but I also have good use for it
right now, and will help validate that the current storage API has no obvious
pitfalls when networking (with async out of the way, I highly doubt it).</p>
<p>Almost everything so far is pretty draft and being iterated upon rather
quickly, but I do expect to have <em>some</em> presentable code soon, though nowhere
near an alpha or alike.</p>
<h1 id="darkman">darkman<div class="permalink">[<a href="#darkman">permalink</a>]</div></h1>
<p>The latest release of <code>darkman</code> (1.3.1) has proved itself quite solid. It
ironed out the last issues that were coming up with the Portal implementation,
and now plenty of applications pick up the current dark/light mode properly.
This includes GTK applications, Firefox, Telegram, and a few miscellaneous
others.</p>
<p>The good old &ldquo;run this script&rdquo; escape hatch still exists for everything else.</p>
<p>I&rsquo;ve also consolidated its documentation into a man page, which is now also
<a href="https://darkman.whynothugo.nl/">easily accessible online</a>.</p>
<p>A bit thanks goes to all the contributors who&rsquo;ve reported bugs in various
different scenarios and setups.</p>
<h1 id="keepassxc">KeePassXC<div class="permalink">[<a href="#keepassxc">permalink</a>]</div></h1>
<p>I&rsquo;ve been consolidating a lot of Password Management into
<a href="https://keepassxc.org/">KeePassXC</a>. It works pretty nicely, but needed <a href="https://github.com/keepassxreboot/keepassxc/issues/8018">a small
patch</a> to work cleanly in sandboxed contexts (e.g.: with Firejail or
alike).</p>
<p>I&rsquo;ll likely try and tweak a few other rough edges I came across (especially in
the browser integration side), or at least try and report them clearly. But my
experience has been pretty positive. It&rsquo;s nice to be embracing a fully
functional open source password manager.</p>
<p>Regrettably, I do see some deal breakers in using KeePassXC for teams or vaults
that need to be shared and actively mutated by multiple users. However, I do
think it&rsquo;s pretty good for personal usage.</p>
]]></description></item><item><title>How I secure my setup with a YubiKey</title><link>https://whynothugo.nl/journal/2022/05/07/how-i-secure-my-setup-with-a-yubikey/</link><pubDate>Sat, 07 May 2022 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2022/05/07/how-i-secure-my-setup-with-a-yubikey/</guid><description><![CDATA[<h1 id="yubikeys">YubiKeys<div class="permalink">[<a href="#yubikeys">permalink</a>]</div></h1>
<p>I have a pair of <a href="https://www.yubico.com/nl/product/yubikey-5c-nfc/">YubiKey 5C NFC</a>, which I use for authentication a
lot. They&rsquo;re small USB-C authentication devices which can generate multiple
types of keys and are usable for different types of authentication.</p>
<figure>
  <img src="yubikey.png"
       alt="A stock photo of the hardware token, which a golden button and a male USB-C port"
       style="box-shadow: none;">
  <figcaption>There's also a USB-A version if USB-C ports aren't your thing.</figcaption>
</figure>
<p>The keys generated on-device cannot be extracted, which means that the only way
to steal the keys is to physically steal the device itself.</p>
<p>This article includes some of the practices I follow and recommend, as well as
some warnings of things not to do.</p>
<h1 id="two-factor-authentication">Two factor authentication<div class="permalink">[<a href="#two-factor-authentication">permalink</a>]</div></h1>
<p>The obvious usage is 2FA. On supported websites, when logging in, they&rsquo;ll
prompt me to press the button on the key. This is very simple and convenient to
use, and no need to copy numbers from a dedicated hardware device.</p>
<figure>
  <img src="sentry.png"
       alt="A screenshot of Sentry prompting for confirmation on the 2FA device"
       style="border-radius: 0;">
  <figcaption>This is what the 2FA prompt looks like on Sentry.</figcaption>
</figure>
<p>It&rsquo;s pretty important to have some form of backup in case the key is lost. In my
case, I keep a second off-site. I recommend a similar arrangement whenever
possible.</p>
<h1 id="ssh-keys">SSH Keys<div class="permalink">[<a href="#ssh-keys">permalink</a>]</div></h1>
<p>I use a YubiKey to generate my SSH keys. This has been supported by SSH for a
while now (since <a href="https://www.openssh.com/releasenotes.html#8.2">version 8.2</a>), and they can be generated using:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>ssh-keygen -t ed25519-sk -C <span style="color:#e6db74">&#34;A key for some service&#34;</span> -f id_someservice
</span></span></code></pre></div><p>This generates a key on the YubiKey device itself. The key itself cannot be
extracted, but also requires the on-disk file to be used. This means that if
someone snatches my YubiKey, they still can&rsquo;t use my SSH key. And if someone
extracts the file from disk, they still need the YubiKey.</p>
<p>Using keys inside the YubiKey also reduces the need for an <code>ssh-agent</code>. The
agent mostly keeps the keys in-memory, so one doesn&rsquo;t have to type the
passphrase over and over. Since there&rsquo;s no passphrase, this is no longer an
issue. On top of that, one <em>does</em> have to tap the Yubikey in order to use the
keys, so they can&rsquo;t be used without a manual confirmation (which is just a tap
of a button). The tap also cannot be spoofed by someone with remote access to
the system. This yields stronger security than a passphrase, and far more
convenience.</p>
<p>Of course, if someone extracts the file from disk AND steals the YubiKey, then
it&rsquo;s game over. There are, after all, plenty of scenarios that we just can&rsquo;t
avoid&hellip;</p>
<h1 id="side-note">Side note<div class="permalink">[<a href="#side-note">permalink</a>]</div></h1>
<p>There&rsquo;s obvious attacks to some of these practices, like <em>&ldquo;What if the
adversary has a forensics lab that can crack a YubiKey?&rdquo;</em> or <em>&ldquo;What if an
organisation has broken the encryption on the keys?&rdquo;</em>.</p>
<p>Before you start to plan against how you&rsquo;d prevent the NSA from looking at your
email, please keep in mind that they could just as a well buy a wrench:</p>
<p><a href="https://xkcd.com/538/"><img src="security.png" alt="xkcd 538"></a></p>
<h1 id="sudo-prompts">Sudo prompts<div class="permalink">[<a href="#sudo-prompts">permalink</a>]</div></h1>
<p>Typing a user password to <code>sudo</code> is annoying, repetitive and error prone.
Instead, I&rsquo;ve set up <code>sudo</code> to use my YubiKey instead. This is what a <code>sudo</code>
prompt looks like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-console" data-lang="console"><span style="display:flex;"><span>$ sudo whoami
</span></span><span style="display:flex;"><span>🔐 Confirm user presence
</span></span><span style="display:flex;"><span>root
</span></span></code></pre></div><p>The <code>Confirm user presence</code> prompt indicates that one should press the
YubiKey&rsquo;s button. Note that, in order to even <em>run</em> sudo, one needs to first be
logged into an unlocked interactive session, which is something else entirely.</p>
<p><strong>Hint</strong>: If you want to set this up, see <a href="https://github.com/Yubico/">pam-u2f</a>.</p>
<h1 id="polkit-prompts">Polkit prompts<div class="permalink">[<a href="#polkit-prompts">permalink</a>]</div></h1>
<p>I have polkit set up to also prompt with a YubiKey. All the same details apply,
but it&rsquo;s just at GUI prompt rather than a CLI prompt.</p>
<h1 id="system-login">System login<div class="permalink">[<a href="#system-login">permalink</a>]</div></h1>
<p>My system prompts for a disk decryption password on boot, so getting to a
non-root shell requires this passphrase.</p>
<p>There&rsquo;s nothing crazy and special here, but I do want to point out that
<strong>logging into the system or unlocking the disk with a hardware key alone is a
terrible idea</strong>. Imagine you set up your system so that you can log into it
with just a hardware key. Now imagine that you&rsquo;re on vacation in a hotel, and
someone bursts in and steals all your electronics. The hardware that&rsquo;s unlocked
<strong>and</strong> they key will, more often than not, remain physically close.</p>
<p>It&rsquo;s the same idea as carrying a suitcase with a lock, and having the key for
the lock tied to the suitcase. This particular suitcase though, has access to
all your online accounts, email, banking, etc.</p>
<p>For disk encryption, a good old passphrase is usually still the best idea. It
should be one of the few passwords you need to remember anyway, and it should
<strong>obviously</strong> be unique.</p>
<h1 id="screen-locker">Screen locker<div class="permalink">[<a href="#screen-locker">permalink</a>]</div></h1>
<p>Most of the same arguments above apply to a screen locker. Unlocking the screen
requires a password. If the screen locker would somehow crash, then the
compositor won&rsquo;t unlock the screen so no stranger can use the laptop.</p>
<p>Configuring a screen locker to use a YubiKey is also a kinda bad idea. If a
random attacker were to steal your stuff, they&rsquo;ll take both things together.
It&rsquo;s again a case of &ldquo;leaving the key in the lock&rdquo;.</p>
<p>Again, usually a password or passphrase is best here. If your hardware has
reliable fingerprint sensors or alike, that might be a good idea, but it
depends <strong>a lot</strong> on the hardware.</p>
<p>Personally, my local user&rsquo;s password is <em>only</em> used for unlocking the screen,
which makes it a bit safer, since no other process or program ever handles it.</p>
<!-- TODO: Publish that draft on 2FA -->
<h1 id="password-manager">Password Manager<div class="permalink">[<a href="#password-manager">permalink</a>]</div></h1>
<p>It&rsquo;s ideal to use a password manager. Too many experts have written on this, so
I won&rsquo;t repeat their points, but mostly summarise:</p>
<ul>
<li>Reusing passwords is dangerous.</li>
<li>Humans are terrible at coming up with unique passwords.</li>
<li>Humans are even worse at remembering MANY unique passwords.</li>
</ul>
<p>My advise is to remember the most important passwords (system, email, password
manager, etc), and then keep all the rest on a password manager.</p>
<p>Some password manager can be unlocked with a hardware token (e.g.: A YubiKey).
I personally don&rsquo;t use this; I prefer to type the password manager&rsquo;s main
passphrase often. Typing it daily makes sure I won&rsquo;t forget it.</p>
<h1 id="summary">Summary<div class="permalink">[<a href="#summary">permalink</a>]</div></h1>
<ul>
<li>Use a hardware token for 2FA whenever possible.</li>
<li>A hardware token is a good mechanism to authorise special operations, like
<code>sudo</code>.</li>
<li>However, it&rsquo;s a bad idea to use a hardware token a sufficient mechanism to
gain access to the system.</li>
</ul>
]]></description></item><item><title>Open source platforms for open source software</title><link>https://whynothugo.nl/journal/2022/04/29/open-source-platforms-for-open-source-software/</link><pubDate>Fri, 29 Apr 2022 19:11:35 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2022/04/29/open-source-platforms-for-open-source-software/</guid><description><![CDATA[<p>There&rsquo;s something that&rsquo;s been low-key bothering me for many months now: the
fact that so much open source software is hosted on proprietary platforms and
has such a strong dependency on non-open source non-free forges.</p>
<p>Now, sometimes we must be pragmatical, and use the closed sourced software when
we don&rsquo;t have a choice. But when it comes to products like GitHub, there&rsquo;s
plenty of choices. And most of them are, feature-wise and UI-wise, superior.
GitHub merely has one leading point: it has more users, hence, a stronger
network effect.</p>
<p>The network effect has value: it&rsquo;s easier for people to jump in and contribute.</p>
<p>Recently, Drew DeVault <a href="https://drewdevault.com/2022/03/29/free-software-free-infrastructure.html">blogged on this same topic</a>. While he&rsquo;s the
author of a competing forge, but I think his points have merit on their own,
regardless of personal position (by the way, huge shout-out to SourceHut, which
as a wonderfully simple yet powerful CI/Builds system). One particular idea
expressed by him resonated with me the most: each time we pick a closed sourced
platform as a home for an open source project, we&rsquo;re also saying to open source
forges &ldquo;you&rsquo;re not good enough&rdquo;.</p>
<p>This situation sucks. As a software developer, I&rsquo;m often annoyed about how hard
it is to come across career opportunities in the open source ecosystem &ndash; but
on the other hand, I&rsquo;m also feeding products competing with open source
forges, products who don&rsquo;t care about ethics, respecting people, privacy, or
anything else other increasing users and more and more profits.</p>
<p>It&rsquo;s only natural to want one&rsquo;s project or endeavour to grow (plus, most of us
appreciate more income!), but growth is only feasible up to a point. After
reaching said point, it is simply not possible to continue growing a user-base
without damaging the product, the ecosystem, or both. Failing to understand
this is failing to understand how to build sustainable industries. Sadly, this
is all too common in the modern world.</p>
<p>So after months (probably years) of this bothering me I&rsquo;ve decided that I&rsquo;ll be
moving all projects off proprietary platforms and onto open source ones. This
isn&rsquo;t a simple undertaking: there&rsquo;s a lot of documentation and background
lurking in issues and pull request discussions. A lot of people rely on
projects I maintain, and one can&rsquo;t suddenly one day change where everything is
located.</p>
<p>However, these are all problems that can be solved.</p>
<p>I&rsquo;ll be publishing plans for individual projects in the coming weeks. For the
moment, I&rsquo;m considering Codeberg for most migrations, mostly because it&rsquo;s easy
to carry over Issues and Pull Requests, and the UI should be familiar to
existing contributors. Currently there&rsquo;s only a beta CI, but I intend to
continue experimenting with integrating Sourcehut&rsquo;s Builds with Codeberg.</p>
<p>Smaller projects will come first, since they have the least chance of negative
impact, and larger projects will follow. I&rsquo;ll be starting with GitHub projects
first, then gradually moving over to GitLab. Open-core is simply not open
source, and I invite you to see <a href="https://media.libreplanet.org/u/libreplanet/m/why-i-forked-my-own-project-and-my-own-company-31c3/">this excellent talk</a> by Frank
Karlitschek (founder of Nextcloud) on the topic if you&rsquo;d like to learn more
about the subject.</p>
]]></description></item><item><title>Fairphone: please lead by example!</title><link>https://whynothugo.nl/journal/2022/04/29/fairphone-please-lead-by-example/</link><pubDate>Fri, 29 Apr 2022 09:29:22 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2022/04/29/fairphone-please-lead-by-example/</guid><description><![CDATA[<p>The FSFE published <a href="https://fsfe.org/activities/upcyclingandroid/openletter.en.html">an open letter</a> a couple of days ago, asking for
legislation to allow users to freely user their own devices, contrary to the
status quo, where devices are locked down and controlled fully by the
manufacturer rather than the end-user.</p>
<p>I whole agree with this stance and applaud this request: if I can&rsquo;t install the
software I want on devices I paid for, do I really even own them? For the last
couple of decades, buying a computer from MOST manufacturers (or building one&rsquo;s
own) allowed the owner to install their operating system of choice on the
device, without permission from the manufacturer or third party. The right to
use our phones freely the same way is incredibly important, especially in a
world where large corporations are focusing in controlling what users can or
cannot do, while tracking all kind of details of their private lives.</p>
<p>I was amazed that Fairphone was one of the companies signing this letter. The
hypocrisy!</p>
<p>Several months ago, I bought a Fairphone device, on which they <em>claim</em> one can
install any OS. This turned out to be rather distant from the truth. The devices
has a locked bootloader out-of-the box. To use the device in ANY WAY (even to
install another OS), one must enter into a contractual agreement with Google,
the advertising corporation known to <del>spy on</del> track people far beyond the
point any most of us are comfortable with.</p>
<p>I reached out to the <a href="https://forum.fairphone.com/t/finalising-the-setup-wizard-without-consenting-to-googles-tos/81716">community support</a> and official support from
Fairphone on this. They confirmed they had no plans to fix this issue. Google
was in control of the phone I had paid for, and in order to use it, I would
have to sign the contract with Google. I could not use an alternative OS &ndash; at
least not without first using this highly restricted one that shipped out of
the box, but I could not use that one without signing the contract with Google.</p>
<p>This was of course, not acceptable to me, so I decided to return this device,
defective by design. Fortunately, before I could return the device, a friend of
mine offered to get it off my hands since they admired the build quality of it.
They didn&rsquo;t mind signing a contract a contract with the excuse that nobody
could prove they had done so (this is also what Fairphone suggested I should
do).</p>
<p>However, this also was a dead end. Even after having accepted the terms,
unlocking the bootloader on the device simply yielded an &ldquo;inner unknown error&rdquo;.
Fairphone&rsquo;s support now suggested that device actually needs to be internet
connected since, apparently, the user needs Google&rsquo;s permission to do this.
IMHO, even though I had paid for the phone, Google was the ultimate owner of
them. Using the phone required entering a contractual agreement with Google,
and an approval via their servers (also sending them confirmation that the
person in possession of it had signed the contract). Ultimately, Google was
the de-facto owner of this device, even though I had paid for it.</p>
<p>So, please, guys at Fairphone, have a read at this beautiful letter you&rsquo;ve
under-signed today, and consider leading the market by example. Show the EU
that what you&rsquo;re asking them to legislate is doable!</p>
<p>Please also consider using free open source software instead of shipping a
device with proprietary software, including with all of Google&rsquo;s spyware!
Again, please read the letter you&rsquo;ve under-signed! And remember: unlike
proprietary, corporate-controlled software, open source software <em>can</em> actually
be sustainable!</p>
<h2 id="efoundation">e.foundation<div class="permalink">[<a href="#efoundation">permalink</a>]</div></h2>
<p>A small side-note: Fairphone has partnered with <a href="https://e.foundation/">e.foundation</a>,
who&rsquo;s website title reads &ldquo;e Foundation - deGoogled unGoogled smartphone
operating systems and online services [&hellip;]&rdquo;. They <em>do</em> sell unlocked devices
(at least &ldquo;some of them&rdquo;, as I&rsquo;ve been told).</p>
<p>I read a lot of negative comments an opinions on them online, but, ultimately,
decided to reach out to confirm what I was hearing. In summary: they ship
devices with eOS, an Android distribution that builds on top of <a href="https://source.android.com/">AOSP</a>
and <a href="https://lineageos.org/">LineageOS</a> by <strong>adding integrations with Google&rsquo;s services</strong>.</p>
<p>eOS includes multiple pieces of software on their operating system that do talk
to Google out-of-the-box. F-Droid classifies this software as having an <a href="https://f-droid.org/en/docs/Anti-Features/">anti
feature</a> called &ldquo;Non-Free Network Services&rdquo;, and, I quote:</p>
<blockquote>
<p>This Anti-Feature is applied to apps that promote or depend entirely on a
Non-Free network service.</p>
</blockquote>
<p>In this case, the non-free network that&rsquo;s being promoted is Google&rsquo;s services.
The only reply I got from e.foundation was that they include this because not
adding integration with Google would &ldquo;decrease a lot the usability&rdquo;. Fun fact:
this is the same excuse Google uses to add all their services all over the
place and record every little detail on you that they can.</p>
<p>I&rsquo;ll remind you of the title on e.foundation&rsquo;s website one more time just in
case:</p>
<blockquote>
<p>e Foundation - deGoogled unGoogled smartphone operating systems and online
services [&hellip;]</p>
</blockquote>
<p>For the record, I&rsquo;m currently using LineageOS, a free and open source Android
distribution, maintained by the community and without integration with Google.
If what e.foundation promises sounds good to you, consider having a look at
LineageOS.</p>
]]></description></item><item><title>A vdirsyncer rewrite</title><link>https://whynothugo.nl/journal/2022/04/18/a-vdirsyncer-rewrite/</link><pubDate>Mon, 18 Apr 2022 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2022/04/18/a-vdirsyncer-rewrite/</guid><description><![CDATA[<p>I&rsquo;ve been planning a full rewrite of vdirsyncer for quite some time now. So far
work on vdirsyncer (both on the original version and on this rewrite) has
happened during free time and is mostly unpaid. My intention is to seek
sponsors for this rewrite, so that I can dedicate more hours to this project
and deliver a well polished product to the open source ecosystem.</p>
<p>By becoming a sponsor, you will allow me to focus on this endeavour and spend
much more time on it than I ever could on &ldquo;another side project&rdquo;.</p>
<p>This is a detailed description of everything that&rsquo;s been planned so far. Given
the considerable length of the project, progress will be delivered through
several intermediate milestones. This also ensures a more modular design with
multiple reusable parts.</p>
<h2 id="what-is-vdirsyncer">What is vdirsyncer<div class="permalink">[<a href="#what-is-vdirsyncer">permalink</a>]</div></h2>
<p><code>vdirsyncer</code> is a command line tool to synchronise two calendars and contacts
between a variety of supported storages (including CalDav and local
filesystem). A very popular usage scenario is synchronizing a server with a
local directory and using other applications to to view or edit the local
entries.  Vdirsyncer can also syncronise between two remote storages, or two
local storages.</p>
<h2 id="the-current-implementation">The current implementation<div class="permalink">[<a href="#the-current-implementation">permalink</a>]</div></h2>
<p>There&rsquo;re a few hard-to-solve problems with the current design.</p>
<p>Much of the current design was written while learning all the quirks of
involved standards, and without fully knowing the final shape vdirsyncer would
have. It <em>evolved</em> to where it is, and maintenance has become pretty hard and
painful: different internal bits are way too intertwined and it&rsquo;s not possible
to alter one part without affecting the others. In other words, there&rsquo;s a
strong lack of modularity.</p>
<p>On the distribution side, shipping python applications to end users has also
become a pain point. The python version on each distribution varies, as does
the version of many individual dependencies, making supporting all of them a
dependency hell. Additionally, due to the fact that distributions ship
individual libraries as separate packages, packaging vdirsyncer also implies
packaging all its dependencies, which is often a burden for distribution
package maintainers. There&rsquo;s also no easy way to ship a single binary to
end-users, which could be very useful for special debug builds, or builds with
special changes to individual users trying to inspect an unusual scenario.</p>
<p>The current vdirsyncer was designed first as a command line tool, which makes
it hard to be integrated into larger systems or to be used programmatically as
part of larger applications. There has been much interest in reusing it in many
context, both as a whole or some of its parts, but this is generally not
possible.</p>
<h2 id="goals">Goals<div class="permalink">[<a href="#goals">permalink</a>]</div></h2>
<p>The goal is to ship a solid version of vdirsyncer well designed from the start,
and to implement most of its functionality as a library. I&rsquo;ll still ship a
command line tool, though it will actually just be a thin wrapper delegating
everything to the library itself, keeping most of the underlying implementation
reusable.</p>
<p>Rust has been chosen for this implementation, since its expressive type system
allows implementing this in very safe ways. Rust is well-known for making it
easier to write code that can account for all possible scenarios, rather than
having to use exception-handling as a flow-control mechanism. Using Rust
libraries from other languages (e.g.: from Python) is also a solved problem,
which implies that developers who are not using Rust can still leverage this
work.</p>
<p>The first milestone will be a storages library, which will allow interacting
with different calendar/contacts storages without having to worry about
internals or protocol implementations. It will expose raw entries without
parsing their contents.</p>
<p>The second milestone will leverage the above and implement synchronisation
between any two storages (including one-way and other features currently
supported).</p>
<p>The third milestone will be the vdirsyncer cli itself, which will be a single
binary that mostly wraps around the above two libraries.</p>
<p>Finally, a fourth milestone will be to parse CalDav/CardDav items from storages
and expose typed entries. The goal of this is to make the storages libraries
well usable by programs (web, desktop, mobile) that need to interact with these
storages. These can be used by all sorts of calendar or contacts applications
with little to no overhead.</p>
<p>I intend to also make this an educational endeavour, documenting progress, and
potentially doing live streams if there is interest in it. These items aren&rsquo;t
set in stone; I&rsquo;d like to hear more from [potential] sponsors. Monthly status
updates (as are very common for funded open source work) will also happen.</p>
<p>Writing a GUI for vdirsyncer is not in my initial goals - although I do intend
to provide all the foundations necessary to build one.</p>
<h2 id="technical-details">Technical details<div class="permalink">[<a href="#technical-details">permalink</a>]</div></h2>
<p>The following is a general idea of what libraries would look like. I&rsquo;ve been
refining these for months now, and while the final results may be subtly
different, they should not stray too much from this design.</p>
<h3 id="the-storage-module">The <code>storage</code> module<div class="permalink">[<a href="#the-storage-module">permalink</a>]</div></h3>
<p>The first key item is the <code>storage</code> module with multiple <code>Storage</code>
implementations. The intent is to have one type per supported storage type
(e.g.: <code>Vdir&lt;Ics&gt;</code>, <code>Vdir&lt;Vcf&gt;</code>, <code>SingleFileCalendar</code>, <code>CalDav</code>, <code>CardDav</code>,
<code>GoogleCalDav</code>, <code>EteSync&lt;Ics&gt;</code>, etc), all of them having the same API (by
implementing a common trait).</p>
<p>Each one is initialised purely with a URL describing it (e.g.:
<code>caldav://user:password@host:port/path</code> or <code>file+ical:///path/to/calendar.ics</code>)
and a URL fragment indicating whether it is read only or not (e.g.:
<code>#read_only=true</code>). Applications using the library can avoid exposing the URL
to users if they prefer, but for an internal representation, it is the simplest
thing to handle.</p>
<p>Each <code>Storage</code> implementation handles all the reading/writing/communicating
with the actual storage itself, both for data and metadata. In-memory will be
implemented internally at this layer too.</p>
<p>While vdirsyncer will be the obvious user of this module, other tools can use
it for reading storages too. For example, a calendar app can read a local
<code>Vdir&lt;Ics&gt;</code> or even a <code>CalDav</code> storage. Providing a base for other developers
to write calendar and contacts apps is an important goa.</p>
<h3 id="the-syncpair-type">The <code>SyncPair</code> type<div class="permalink">[<a href="#the-syncpair-type">permalink</a>]</div></h3>
<p>A <code>SyncPair</code> represents synchronisation settings between two storages. It
contains the set of synchronisation rules like direction (e.g.: <code>a-&gt;b</code>,
bi-directional, etc) and conflict resolution rules (if any).</p>
<p>A <code>SyncPair</code> reads both storages and can determine which operations to execute
to sync both ways.</p>
<p>A <code>SyncPair</code> also needs a <code>KnownState</code> to operate, which is the previous state of
both collections (this allows understanding which one has changed and which one
hasn&rsquo;t when we find discrepancies between the two).</p>
<p>The <code>KnownState</code> will be passed to the <code>SyncPair</code> on creation, and can be read
from different forms of storage. Our cli tool will save to disk as we currently
do, but potential library users should be able to serialise it into a database
or however they deem fit.</p>
<h3 id="library">Library<div class="permalink">[<a href="#library">permalink</a>]</div></h3>
<p>An important aspect of doing a rewrite is avoiding raising exceptions as a
flow-control mechanism &ndash; when something fails it will return an appropriate
error, allowing the calling layer to handle this properly. That is, no
<code>panic!()</code> or exposing any unsafe behaviour.</p>
<p>This means, amongst other things, that if a single collection has a fatal issue,
vdirsyncer will continue working on everything else. This also enables writing
things like long-lived processes that use vdirsyncer as a library. Having a daemon
that does automatic syncing has also been a highly demanded feature.</p>
<p>Note that while this would also have technically been possible in Python, the
language doesn&rsquo;t really have clear constructs for returning errors that MUST be
handled.</p>
<h3 id="cli-tool-eg-vdirsyncer-2">Cli tool (e.g.: <code>vdirsyncer 2</code>)<div class="permalink">[<a href="#cli-tool-eg-vdirsyncer-2">permalink</a>]</div></h3>
<p>So far everything described is libraries, and the final piece is the small command line
tool that reads a configuration file and calls this library with the right
parameters.</p>
<h2 id="audience">Audience<div class="permalink">[<a href="#audience">permalink</a>]</div></h2>
<p>This development seeks to enrich the open source ecosystem by providing not
just a tool, but also libraries that can boost other developments by getting
the complicated bits out of the way. Therefore, the target audience of this
development shall include:</p>
<ul>
<li>Current users of vdirsyncer who want a lighter version that has less
dependency issues.</li>
<li>Developers of servers who need to handle calendar/contacts collections.</li>
<li>Developers of contacts or calendar applications for desktop or mobile.</li>
<li>Engineers working on hosted solutions involving synchronisation calendars or contacts.</li>
</ul>
<p>Note that developers don´t just imply Rust developers, since libraries
wrapping Rust code can be created in many languages, including Python and the
commonly used mobile development languages.</p>
<h1 id="contributing">Contributing<div class="permalink">[<a href="#contributing">permalink</a>]</div></h1>
<p>If you would like to sponsor this project, see my page on
<a href="https://liberapay.com/WhyNotHugo/">liberapay</a>. Don&rsquo;t hesitate to reach out if you have additional
inquiries or would like to collaborate in some other way.</p>
]]></description></item><item><title>Introducing darkman v1.0.0</title><link>https://whynothugo.nl/journal/2022/02/25/introducing-darkman-v1.0.0/</link><pubDate>Fri, 25 Feb 2022 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2022/02/25/introducing-darkman-v1.0.0/</guid><description><![CDATA[<p>I&rsquo;m pleased to announce the release of darkman 1.0. Darkman is a session
service that transitions a desktop environment between colour schemes. It
switches to dark mode at sundown, and back to light mode at sunrise.</p>
<p>This small project started in 2020, out of frustration with dark mode being too
hard to read during sunny days, and light mode being blinding at night-time.</p>
<p>Darkman runs as a session service. It sets a timer for the next sunrise/sundown
based on the current location and sleeps 99% of the time.</p>
<p>There&rsquo;s a few different features that have gradually sprung up over these many
months:</p>
<ul>
<li><strong>scripts</strong>: darkman will run scripts in <code>/.local/share/dark-mode.d</code> (and
<code>/.local/share/light-mode.d</code>). These scripts can be used to reconfigure
software that doesn&rsquo;t support transitions, change the settings for
applications, change the current GTK theme, etc. There&rsquo;s a few example ones
in the <a href="https://gitlab.com/WhyNotHugo/darkman">git repo</a>.</li>
<li><strong>XDG portal</strong>: darkman implements the <code>org.freedesktop.impl.portal.Settings</code>
D-Bus API, so applications that use the standard desktop portals to determine
the current preference will also pick up the correct value. Support for this
is gradually picking up.</li>
<li><strong>D-Bus API</strong>: darkman exposes its own D-Bus API to query the current mode
and also change it. This API is designed for unsandboxed clients.</li>
<li><strong>command line</strong>: <code>darkman set MODE</code> and <code>darkman get</code> can be used for
scripts (it simply uses the above D-Bus API).</li>
<li><strong>location</strong>: the current location will be determined using geoclue. In cases
where using geoclue is not an option, it&rsquo;s possible to disable it and
configure the location manually via the [optional] config file.</li>
</ul>
<p>Handling systems going into sleep mode was also tricky &ndash; regular timers stop
ticking when the system goes to sleep, so had to find the right timer that
doesn&rsquo;t run off-schedule when system goes to sleep and wakes up again.</p>
<p>Darkman runs on Linux. I&rsquo;ve only used it on Linux desktop, but there&rsquo;s no
reason why it wouldn&rsquo;t work on Linux mobiles. If you give it a shot I&rsquo;d be glad
to hear your results! Darkman should run on BSDs, but has not been tested so
far.</p>
<p>Check out the <a href="https://gitlab.com/WhyNotHugo/darkman/-/blob/main/README.md#installation">README for installation instructions</a> for setup
instructions.</p>
<p>Patches are welcome! If you find this useful, consider <a href="https://ko-fi.com/whynothugo">leaving a tip</a>.</p>
]]></description></item><item><title>I like that Signal is looking for sponsors</title><link>https://whynothugo.nl/journal/2021/12/14/i-like-that-signal-is-looking-for-sponsors/</link><pubDate>Tue, 14 Dec 2021 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2021/12/14/i-like-that-signal-is-looking-for-sponsors/</guid><description><![CDATA[<p>Recently <a href="https://signal.org/">Signal</a> has started <a href="https://signal.org/blog/become-a-signal-sustainer/">asking people to sponsor
them</a>, and I&rsquo;m actually
really glad that this is happening.</p>
<p>There are many companies out there offering &ldquo;free services&rdquo;, but, generally,
companies that offer free instant messaging are often doing it to build a
detailed profile on you, what you believe, how you behave, and how you feel
about an important topic. They do this so they can then sell advertising (or,
often, propaganda). Or they&rsquo;ll use this data to figure out how to manipulate
large amounts of people in harmful ways &ndash; just for the sake of profits!</p>
<p>And this is an inescapable reality: if you&rsquo;re not giving them any money to
provide the service, then somebody else is giving them money to provide that
service, and you&rsquo;re not really the client: you&rsquo;re the just product they&rsquo;re
selling.</p>
<p>Signal has always focused on security, privacy, and no spying on users. They&rsquo;ve
designed their systems to work in a way that user&rsquo;s don&rsquo;t even send readable
data to them, so they don&rsquo;t even handle any delicate information.</p>
<p>And there&rsquo;s something very unintuitive that&rsquo;s worrying about this: if they
don&rsquo;t sell our data, then how will they continue operating? How do I know they
won&rsquo;t just close shop, or sell out because they have no more cash influx?</p>
<p>Well, if they ask their users to sponsor them, they have an income They don&rsquo;t
need to worry about going broke, so they can focus on implementing more features
that people want &ndash; since that should make them more likely to sponsor Signal.
And it makes it a bit clearer who the client is.</p>
<p>Let&rsquo;s not be naive here: a service provide <em>can</em> charge their users and also
focus on spying on them. This particular case does not seem to be so.</p>
<p>This has the potential to turn Signal into a <em>sustainable</em> private messaging
client, and so far, all the chat clients out there have struggled on this
aspect: making a sustainable business model that both users and provider
believe can survive long-term.</p>
<p>I mean, developers need to pay bills like anyone else, so I can understand them
unconditionally trying to have <em>some</em> income. I much prefer that to be done by
asking their users to sponsor them rather than advertising companies paying
them to spy on us.</p>
]]></description></item><item><title>Running Eve-Online on Linux/Wayland</title><link>https://whynothugo.nl/journal/2021/12/07/running-eve-online-on-linux/wayland/</link><pubDate>Tue, 07 Dec 2021 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2021/12/07/running-eve-online-on-linux/wayland/</guid><description><![CDATA[<p>Initially, the launcher would render only a black window, and nothing else.
I noticed that moving it off-screen and back again, it would have updated
content, but remained frozen.</p>
<p>As a coincidence, I noticed that when moved onto the edge of the screen, parts
near the edge would render, and would respond to input events, whereas other
parts didn&rsquo;t.</p>
<p>This made it obvious that the game was doing something funny with window
positioning. I&rsquo;ve a dual screen setup, with one screen on the top-right of the
other, so neither of them are at the 0x0 coordinates. The launcher assumes the
current monitor is in the upper-left position, and won&rsquo;t render or react to
events otherwise. I&rsquo;ve no idea if this is a wine issue, an xwayland issue, or
an eve-launcher issue, but changing the position of my monitor fixed it.</p>
<p>After that, I had to copy over the file as mentioned in other posts on
ProtonDB. I&rsquo;m using Flatpak, so the base path varies a little:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>cd .var/app/com.valvesoftware.Steam/  <span style="color:#75715e"># this line only applicable if using Flatpak</span>
</span></span><span style="display:flex;"><span>cd .local/share/Steam/steamapps/common/Eve<span style="color:#ae81ff">\ </span>Online/SharedCache/
</span></span><span style="display:flex;"><span>cp tq/bin64/eve_client_launchdarkly_client.pyd tq/
</span></span><span style="display:flex;"><span>chmod <span style="color:#ae81ff">400</span> tq/eve_client_launchdarkly_client.pyd
</span></span></code></pre></div><p>Trying to switch the game window itself from fullscreen to non-fullscreen
resulted in all mouse events having an offset of a few pixels. I tried
switching the game to Windowed mode via the game menu, but initially had the
same issue. However, fullscreening that window and un-fullscreenining it back
fixed it.</p>
<p>It seems the game won&rsquo;t correctly handle being fullscreen externally (e.g.: by
a global hotkey); it&rsquo;s best if you limit yourself to only doing this via the
in-game menu.</p>
]]></description></item><item><title>The issue with flatpak's permissions model</title><link>https://whynothugo.nl/journal/2021/11/26/the-issue-with-flatpaks-permissions-model/</link><pubDate>Fri, 26 Nov 2021 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2021/11/26/the-issue-with-flatpaks-permissions-model/</guid><description><![CDATA[<p>There seems to be a lot of discussion of whether Flatpak is terrible or is
great, whether it&rsquo;s the future or whether it&rsquo;s complete trash.</p>
<p>I think Flatpak does a lot of very useful things, and requires more work in
other aspects. I&rsquo;m not sure what the The One True Package Manager™ will be, but
I&rsquo;m sure we can all learn <em>some</em> lessons from flatpak.</p>
<h1 id="isolation">Isolation<div class="permalink">[<a href="#isolation">permalink</a>]</div></h1>
<p>Flatpak itself does a pretty good job of isolating applications. A package can
be set up to have no access to the filesystem and several other resources, and
it won&rsquo;t have access to them. It builds on a lot of existing system APIs,
rather than try to reinvent the wheel in this aspect.</p>
<p>I really like this aspect of Flatpak, to be honest, and I hope stronger
isolation continues to be the trend.</p>
<p>So, <strong>if permissions are set up correctly</strong> and the package misbehaves (be it
malice or a bug), you don&rsquo;t have to worry about it sending your photos to
strangers, stealing your SSH keys, or installing a keylogger on your terminal.</p>
<p>And that&rsquo;s a big <em>if</em>&hellip;</p>
<h2 id="permissions-cannot-be-rejected">Permissions cannot be rejected<div class="permalink">[<a href="#permissions-cannot-be-rejected">permalink</a>]</div></h2>
<p>The permissions model in Flatpak is broken, in that packagers declare what
permissions they want, and users must accept all of them to install the
package.</p>
<p>During package installation, there&rsquo;s no option to &ldquo;say no&rdquo; to a permission. If
a package wants to read-write all files on your home directory and use your
webcam and USB ports, then you MUST grant it permissions to continue
installation, even if you know they won&rsquo;t be needed for your use case.</p>
<p>This is a big pain point right now. A lot of package on Flathub require
too broad permissions since they try to cover all possible use cases
out-of-the-box. So it&rsquo;s very common for packages to have enough permissions to
steal your SSH keys, or install a keylogger without any hassle. This defeats
the purpose of even using the isolation in the first place.</p>
<p>Of course, the alternative would be to break some edge-cases for some users,
and that&rsquo;s not a nice thing to do either. There&rsquo;s no correct answer here
(convenience vs security), and, ultimately, it should be left up to the user to
decide. This is one of the few things that iOS actually gets right: when
installing an application, it has no permissions by default, and the OS later
prompts for things like access to photos, camera access, etc. The user is free
to accept or refuse.</p>
<p>With Flatpak, users <em>can</em> manually opt out of permissions, by either using the
command line, or a third party app called Flatseal (BTW: it&rsquo;s a must-have if
you want to use Flatpak). Both of these solutions require installing the
application (granting all permissions), <em>and then</em> manually opting out.</p>
<p>Opt-out is never the right approach in terms of permissions or consent.</p>
<h2 id="poor-default-permissions">Poor default permissions<div class="permalink">[<a href="#poor-default-permissions">permalink</a>]</div></h2>
<p>Flatpak and Flathub are slightly distinct things. Flatpak is the package
manager and runtime that fetches and runs packages. Flathub is a public package
repository with community maintained packages.</p>
<p>Because users can&rsquo;t answer yes/no to individual package permissions,
maintainers are always left in the awkward situation: do they make sure it
&ldquo;works for everyone in all edge cases&rdquo;, or do they focus on security and
isolation? Results vary on a per-package basis.</p>
<p>Ultimately though, this issue is more of a symptom of the previous one than an
issue in itself.</p>
<h2 id="explanation-on-permissions">Explanation on permissions<div class="permalink">[<a href="#explanation-on-permissions">permalink</a>]</div></h2>
<p>Currently, FLatpaks merely declare the permissions they require, but there&rsquo;s no
explanation as to why. The package definition format has no mechanism to
explain this, and it often leaves users wondering &ldquo;Why does this app need
unrestricted access to my entire filesystem?&rdquo;. There&rsquo;s plenty of issues of
people asking &ldquo;Why does X need Y permissions?&rdquo;.</p>
<p>So maybe it make sense to switch permissions from a list:</p>
<pre><code>[
    &quot;--socket=x11&quot;,
    &quot;--socket=wayland&quot;,
    &quot;--socket=pulseaudio&quot;,
    &quot;--share=network&quot;,
    &quot;--device=all&quot;,
    &quot;--filesystem=xdg-download&quot;,
    &quot;--talk-name=org.freedesktop.Notifications&quot;,
],
</code></pre>
<p>To an actual map of explanations:</p>
<pre><code>{
    &quot;--socket=x11&quot;: &quot;To render windows on X11 desktops.&quot;,
    &quot;--socket=wayland&quot;: &quot;To render windows on Wayland desktops.&quot;,
    &quot;--socket=pulseaudio&quot;: &quot;For microphone and headphone access for videocalls&quot;,
    &quot;--share=network&quot;: &quot;To communicate with the server and send/receive messages&quot;,
    &quot;--device=all&quot;: &quot;For webcam access for videocalls&quot;,
    &quot;--filesystem=xdg-download&quot;: &quot;To save received files&quot;,
    &quot;--talk-name=org.freedesktop.Notifications&quot;: &quot;To show notifications&quot;,
},
</code></pre>
<p>The format might be terrible, but the general idea is clear: give users a clean
explanation on why you&rsquo;re asking for permission, and enough context to
understand if they need it. Because, if they <em>don&rsquo;t</em> need it, then users should
always opt to click the now-non-existent &ldquo;Refuse&rdquo; button.</p>
]]></description></item><item><title>How disk encryption works</title><link>https://whynothugo.nl/journal/2021/09/03/how-disk-encryption-works/</link><pubDate>Fri, 03 Sep 2021 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2021/09/03/how-disk-encryption-works/</guid><description><![CDATA[<p><em>Note: this article avoids being too technical and is rather geared towards
non-technical users.</em></p>
<h2 id="without-disk-encryption">Without disk encryption<div class="permalink">[<a href="#without-disk-encryption">permalink</a>]</div></h2>
<p>Historically, computers used to ask you for a username and password after you
turned them on.</p>
<p>This was mostly an authentication mechanism to prevent strangers sitting in front
of your computer from using it. However, they could still open the computer,
remove the disk, and access all your information without further obstacles.</p>
<p>The password prompt was a <em>soft</em> protection, akin to a security guard outside
the entrance to a room with an open window on the other side. The guard keeps
out the polite ones coming in through the door, but does nothing for those
crawling through the window.</p>
<p>For laptops, this is especially worrying; if you lose your laptop, the new
owner could access any of its contents. Skipping or changing your user account
password is trivial.</p>
<p>Any photos, banking passwords, message history, business plans, etc, are all
very easily accessible.</p>
<h2 id="encryption">Encryption<div class="permalink">[<a href="#encryption">permalink</a>]</div></h2>
<p>Encryption in this context can be summarised as &ldquo;scrambling your data so that
only someone with the decryption key can access it&rdquo;.</p>
<p>The &ldquo;key&rdquo; is usually based on a password / passphrase for which you&rsquo;re prompted
at startup. This varies a bit per operating system and implementation:</p>
<ul>
<li>On iOS, the key is unlocked from a dedicated chip using your PIN code when
you turn on the phone.</li>
<li>On Linux (and others), the key is derived from the passphrase you provide.</li>
<li>Some devices require both a passphrase and a special hardware token.</li>
<li>Other more specialised implementations or approaches exist.</li>
</ul>
<p>A noticeable outlier is Windows Home (including Windows 10 Home). Microsoft is
of the stance that private individuals have no important information on their
systems, and have no need for privacy, so only includes this feature in Windows
Pro, Enterprise and other [expensive] tiers.</p>
<p>However, outside the Windows-bubble, disk encryption has been widely adopted
for over a decade.</p>
<h3 id="less-secure-encryption">Less-secure encryption<div class="permalink">[<a href="#less-secure-encryption">permalink</a>]</div></h3>
<p>Some systems also encrypt the entire disk, but store the key on the device, and
use other specialised hardware to attempt to prevent strangers from using the
device or accessing the key.</p>
<p>These tend to be more secure than nothing, but are less secure than a key
derived from a passphrase, since they give attackers a new attack vector.</p>
<h2 id="multi-user-systems">Multi-user systems<div class="permalink">[<a href="#multi-user-systems">permalink</a>]</div></h2>
<p>There are really two ways disk encryption can happen:</p>
<ul>
<li>Full Disk Encryption: the whole disk is encrypted.</li>
<li>Home Encryption: only your user profile (and documents and data) is encrypted.</li>
</ul>
<p>The first tier (FDE) is the safest and most solid. Nothing is readable by a
stranger holding your laptop. However, the whole device is encrypted with one
key, so if two people use the same device, then they share the decryption key,
and one can, ultimately, steal the other&rsquo;s private information.</p>
<p>Home encryption however, encrypts each user&rsquo;s profile (their &ldquo;Home directory&rdquo;)
with a different key. This means that if two people share a computer, there&rsquo;s
still no way for one person to read the other person&rsquo;s information. This is
good in families, but even more so when sharing devices in work environments.</p>
<p>Home encryption does have some caveats: it&rsquo;s more complex, and your base system
is decrypted, which <em>can</em> results in someone snooping what&rsquo;s installed, and
other curiosities, but they won&rsquo;t be able to access any private data. Other
mechanisms are also needed to prevent someone from tampering with your operating
system (if they have physical access to your computer).</p>
<h2 id="conclusion">Conclusion<div class="permalink">[<a href="#conclusion">permalink</a>]</div></h2>
<p>Turn on disk encryption on all your devices. <strong>There&rsquo;s no reason not to do so</strong>,
unless you&rsquo;re fine with strangers having full, unrestricted access to any data
you keep on them.</p>
<p>Full disk encryption is best for a single-user device, or devices where you
blindly trust other users. Home encryption is best for shared devices.</p>
]]></description></item><item><title>My desktop-wide microphone mute toggle</title><link>https://whynothugo.nl/journal/2021/06/16/my-desktop-wide-microphone-mute-toggle/</link><pubDate>Wed, 16 Jun 2021 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2021/06/16/my-desktop-wide-microphone-mute-toggle/</guid><description><![CDATA[<p>I use a global hotkey (<kbd>Super</kbd><sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>+<kbd>m</kbd>) to toggle my microphone
between muted and unmuted.</p>
<p>It&rsquo;s been very handy so far. Different videocall applications all have
different hotkeys to mute / unmute oneself, and this avoids me having to keep a
mental map of all the different mappings.</p>
<p>It&rsquo;s also been super handy when pair-programming; I can mute while typing and
unmute when I need to talk, saving my co-programmer the pain of hearing <a href="https://www.hyperxgaming.com/netherlands/us/keyboards/alloy-origins-core-tenkeyless-mechanical-gaming-keyboard">my
rather loud keyboard</a>.</p>
<!-- more -->
<h2 id="discord">Discord<div class="permalink">[<a href="#discord">permalink</a>]</div></h2>
<p>I recently wanted to game online with some friends, and, again, since my
keyboard is pretty loud, it made sense to try to have a global hotkey for
toggling my mic (so that my co-gamers don&rsquo;t hate me).</p>
<p><em>Note: Voice auto-detection isn&rsquo;t an option since, apparently, my keyboard is
as loud as my voice.</em></p>
<p>Discord supports a global hotkey on Windows and macOS, but the way it&rsquo;s
implemented relies on <del>security issues</del> &ldquo;features&rdquo; present on those platform
that are not present in Linux desktop. On these other platforms, applications
can freely act as  keylogger and intercept keystrokes when they&rsquo;re not in
foreground. This is not possible on Wayland (the display server protocol on
Linux<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>) for obvious reasons.</p>
<p>Global hotkeys have a rather different philosophy: you configure the compositor
itself to grab certain hotkeys, and execute an action in response. If Discord
had something like a <a href="https://en.wikipedia.org/wiki/D-Bus">D-Bus API</a>, configuring such a thing would be
possible. Sadly, Discord support no such thing.</p>
<p>However, the toggle works as is. It work on any application since it mutes the
microphone at a system level. It&rsquo;s just a shame that other can&rsquo;t easily
determine if I&rsquo;m deliberately muted or not.</p>
<h2 id="indicator">Indicator<div class="permalink">[<a href="#indicator">permalink</a>]</div></h2>
<p>Aside from being globally accesible, this technique is also globally visible.
There&rsquo;s an icon on my status bar indicating the microphone status at all times:</p>
<figure>
  <img src="closed.png"
       alt="The statusbar showing an icon that blends in.">
  <figcaption>The muted ("closed") indicator.</figcaption>
</figure>
<figure>
  <img src="open.png"
       alt="The statusbar showing an icon with a red background that stands out.">
  <figcaption>The unmuted ("open") indicator.</figcaption>
</figure>
<p>The design of the icon is vaguely inspired on the iOS icon used when
applications are using a microphone in the background (e.g.: during background
calls).</p>
<p>It works, and it&rsquo;s just <a href="https://git.sr.ht/~whynothugo/dotfiles/tree/adf6af99/item/home/.local/lib/waybar-mic">a tiny script</a>. No polling either, it
actually sleeps 99% of the time, and only wakes up for changes on microphone
state.</p>
<h2 id="hotkey-implementation">Hotkey implementation<div class="permalink">[<a href="#hotkey-implementation">permalink</a>]</div></h2>
<p>The global hotkey is just <a href="https://git.sr.ht/~whynothugo/dotfiles/tree/5837972/item/home/.config/sway/apps.conf#L21-24">a one-liner</a> which just calls
ponymix. If you use sway, this can be freely copy-pasted. If you use a
different compositor, just map global hotkeys to a similar command.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Also known as <kbd>Meta</kbd>, <kbd>⌘</kbd>, <kbd>Command</kbd> or <kbd>Windows</kbd>.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Older Linux desktops use Xorg, which <em>does</em> allow applications to freely
snoop on keystrokes and what others are rendering.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description></item><item><title>A simple boot setup with SecureBoot</title><link>https://whynothugo.nl/journal/2021/06/11/a-simple-boot-setup-with-secureboot/</link><pubDate>Fri, 11 Jun 2021 00:00:00 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2021/06/11/a-simple-boot-setup-with-secureboot/</guid><description><![CDATA[<p>I use a pretty simple setup for booting my systems.</p>
<ul>
<li>The hardware firmware (UEFI) loads a signed bootloader (<code>systemd-boot</code> in my
case, but <code>gummiboot</code> is basically the same for non-systemd systems).</li>
<li>The bootloader loads a signed executable that bundles the kernel,
initrd and the cmdline (I&rsquo;ll call this the bundle&quot; from here on).</li>
<li>The <code>initrd</code> prompts for the encryption passphrase, mounted the decrypted
disk, and then boots the actual OS.</li>
</ul>
<p>That&rsquo;s the basic boot process. Note that the bootloader is a single binary, and
the bundle is another single binary. There are no moving parts, just both
binaries are protecting from tampering by being signed. There are no extra
modules, configuration files, or alike.</p>
<p>The OS itself has some extra steps before i see an actual desktop.</p>
<ul>
<li>Upon booting, <code>tty1</code> automatically logs into my user account and starts up
<code>sway</code> (the compositor).</li>
<li><code>sway</code> itself starts all other user-session services (launcher, etc).</li>
</ul>
<p>For the remainder of this artile, it is assumed that the EFI ESP partition is
mounted in <code>/efi</code>. There is no <code>/boot</code> partition.</p>
<h2 id="systemd-boot">systemd-boot<div class="permalink">[<a href="#systemd-boot">permalink</a>]</div></h2>
<p><em>(reminder: <code>gummiboot</code> is basically the same tool for non-systemd setups)</em></p>
<p>First of all, copy the bootloader into the EFI partition and register it with
the UEFI firmware. Both things can be done with one command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>bootctl install
</span></span></code></pre></div><p>On startup, systemd-boot will automatically look for signed bundles in
<code>EFI/Linux</code>. Since I usually have my primary bundle and a backup one, I
manually configure it to prefer the main one:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span><span style="color:#75715e"># The bundle is located in /efi/EFI/Linux/arch.efi:</span>
</span></span><span style="display:flex;"><span>bootctl set-default arch.efi
</span></span></code></pre></div><p><code>systemd-boot</code> requires no additional configuration.</p>
<p>I don&rsquo;t configure a menu with a timeout since I want the system to boot
directly without any prompt. In case I need to boot from the recovery one, the
menu can be triggered by spamming <kbd>Space</kbd> during start-up.</p>
<p>Note that the menu will only allow picking another stub to boot from, but
overriding the <code>cmdline</code> is not possible since it&rsquo;s embedded in the bundle.
This prevents random strangers from executing arbitrary commands.</p>
<h2 id="sbctl">sbctl<div class="permalink">[<a href="#sbctl">permalink</a>]</div></h2>
<p><code>sbctl</code> is a tool to manage SecureBoot keys and generate signed bundles with
the <code>initrd</code>, <code>cmdline</code> and kernel. Keeping all items in a single signed bundle
is important. As a counter-example: if the <code>cmdline</code> is just a file in the same
partition, it can be altered, which would allow easily executing unsigned code.</p>
<p>The UEFI firmware first needs to be configured to use a key that will be
generated and managed by <code>sbctl</code>.</p>
<h3 id="prepare-the-firmware">Prepare the firmware<div class="permalink">[<a href="#prepare-the-firmware">permalink</a>]</div></h3>
<ul>
<li>Reboot into the firmware/UEFI menu.</li>
<li>Disable Secure Boot.</li>
<li>Turn on custom key mode (this implies &ldquo;using user-provisioned keys&rdquo;).</li>
<li>Turn on setup mode (this implies &ldquo;allow altering the user-provisinged keys&rdquo;).</li>
</ul>
<h3 id="set-up-the-os">Set up the OS<div class="permalink">[<a href="#set-up-the-os">permalink</a>]</div></h3>
<ul>
<li>Reboot into ArchLinux (or your distro of choice).</li>
<li>Create and enroll keys:</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>sudo sbctl create-keys
</span></span><span style="display:flex;"><span>sudo sbctl enroll-key
</span></span></code></pre></div><h3 id="generate-the-bundles">Generate the bundles<div class="permalink">[<a href="#generate-the-bundles">permalink</a>]</div></h3>
<p><code>sbctl</code> will read the cmdline from <code>/etc/kernel/cmdline</code>, so make sure to put
your current cmdline there. This can be done with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>cat /proc/cmdline | sudo tee /etc/kernel/cmdline
</span></span></code></pre></div><p>My partitions has the right <code>Partition Type</code> set, so systemd knows what
partition to pick up based on its id. This follows the <a href="https://systemd.io/DISCOVERABLE_PARTITIONS/">Discoverable Partitions
Specification</a>. The Partition Type can be safely
updated for partitions, even if currently mounted (since this updates the
<a href="https://en.wikipedia.org/wiki/GUID_Partition_Table">GPT</a>, not the partition itself).</p>
<p>Then, generate the <code>sbctl</code> bundle:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>sbctl bundle --sign /boot/EFI/Linux/arch.efi
</span></span></code></pre></div><p>The bundle will use the existing <code>initrd</code> and <code>kernel</code>. So each time
<code>mkinitcpio</code> generates a new <code>initrd</code>, the bundle needs to be rebuilt.
Fortunately, <code>sbctl</code> includes pacman hooks to re-build the bundle when you
update the kernel or the <code>initrd</code>.</p>
<p>The file <code>/etc/kernel/cmdline</code> is read on each re-generation, so don&rsquo;t delete
it. It&rsquo;s also the file you want to update when you need to make changes to your
cmdline.</p>
<!-- FIXME: signing before installing makes sure a signed binary gets installed -->
<p>Finally, you should also sign the bootloader itself:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span><span style="color:#75715e"># This is the entry explicitly configured in the UEFI settings.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># The entry for it MAY be deleted during firmware updates.</span>
</span></span><span style="display:flex;"><span>sbctl sign --save /efi/EFI/systemd/systemd-bootx64.efi
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># This is default fallback in case there are no entries in the UEFI.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># If you dualboot, some aggressive OSs may overwrite it:</span>
</span></span><span style="display:flex;"><span>sbctl sign --save /efi/EFI/BOOT/BOOTX64.EFI
</span></span></code></pre></div><h2 id="turning-on-secureboot">Turning on SecureBoot<div class="permalink">[<a href="#turning-on-secureboot">permalink</a>]</div></h2>
<ul>
<li>Reboot into the firmware/UEFI menu.</li>
<li>Enable Secure Boot (this implies &ldquo;check that everything is signed&rdquo;).</li>
<li>Leave custom key mode on (e.g.: continue using the user-provisioned keys).</li>
<li>Turn off setup mode (e.g.: disallow further altering the configured keys).</li>
</ul>
<h2 id="reboot-and-cleanup">Reboot and cleanup<div class="permalink">[<a href="#reboot-and-cleanup">permalink</a>]</div></h2>
<p>You should be able to reboot with SecureBoot turned on. The amount of moving
parts in the whole boot process are extremely few, and it&rsquo;s also very
low-maintenance.</p>
<p>I&rsquo;d recommend also removing all other unnecessary boot entries from the
firmware using:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>sudo efibootmgr --delete-bootnum  --bootnum 00XX
</span></span></code></pre></div><p>If you&rsquo;re unsure which one you should leave, delete all of them and re-run
<code>bootctl install</code> afterwards.</p>
<h2 id="breakage-recovery">Breakage recovery<div class="permalink">[<a href="#breakage-recovery">permalink</a>]</div></h2>
<!-- FIXME: This is obsolete -->
<p>This setup is unlikely to break by itself &ndash; but as with any Arch setup, it may
break due to the user fiddling with things (e.g.: a broken cmdline).</p>
<p>I have <a href="https://git.sr.ht/~whynothugo/sb-backup">a small tool</a> that creates the above-mentioned backup bundle
after every successful boot. If I break the main bundle and it doesn&rsquo;t boot,
then that image is kept around, and I can reboot off it.</p>
<p>As mentioned above, tapping <kbd>Space</kbd> during startup shows the boot
entry menu.</p>
<h2 id="credit">Credit<div class="permalink">[<a href="#credit">permalink</a>]</div></h2>
<p><code>sb-backup</code> relies on <a href="https://github.com/Foxboron/go-uefi"><code>go-uefi</code></a> and <a href="https://github.com/Foxboron/sbctl"><code>sbctl</code></a>, so thanks to
the author for doing all the hard lifting.</p>
<h2 id="update-1">Update 1<div class="permalink">[<a href="#update-1">permalink</a>]</div></h2>
<p>It seems that using SecureBoot with a discrete GPU can be problematic in some
cases. Since the GPU&rsquo;s firmware is not signed, it may fail to load in some
cases due initialisation order, and may result in a bricked motherboard.</p>
<p>I&rsquo;d recommend you do some serious research before trying this if you have a
discrete GPU. See <a href="https://www.reddit.com/r/archlinux/comments/nyunrx/a_simple_boot_setup_with_secureboot/h1m7xe6/?context=3">this reddit thread</a> for more details.</p>
]]></description></item><item><title>Zoom screen sharing on ArchLinux</title><link>https://whynothugo.nl/journal/2020/06/14/zoom-screensharing-on-archlinux/</link><pubDate>Sun, 14 Jun 2020 20:23:46 +0200</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2020/06/14/zoom-screensharing-on-archlinux/</guid><description><![CDATA[<p>Zoom doesn&rsquo;t support screen sharing on Linux unless you&rsquo;re using GNOME or X11.</p>
<p>Also, Zoom only runs via XWayland (a compatibility layer for older
applications). XWayland doesn&rsquo;t really support desktop scaling, which is why it
looks so blurry:</p>
<figure>
  <img src="zoom.png" style="width: 570px"
       alt="A screenshot of zoom's error message.">
  <figcaption>
  </figcaption>
</figure>
<!-- more -->
<p>This is a very funny message, and not really something that&rsquo;s an option for
desktop users. Saying &ldquo;please use a different desktop&rdquo; is not really
realistically applicable advise.</p>
<p>Changing something like the desktop environment on an OS is a great recipe for
disaster.</p>
<p>Downgrading to x11 is a non-trivial matter; the window manager needs to be
changed too, then all the desktop settings, and finally, all the desktop
applications need to be tweaked. Honestly, I wouldn&rsquo;t even know where to start.</p>
<p>Even if I did go through that huge undertaking, I&rsquo;d end up with a system that
supports Zoom&rsquo;s screen sharing, but wouldn&rsquo;t properly support my hardware
(e.g.: HiDPI displays) or many modern applications.</p>
<h2 id="a-solution">A solution<div class="permalink">[<a href="#a-solution">permalink</a>]</div></h2>
<p>Fortunately, there&rsquo;s a workaround; just pipe the current screen into a virtual
webcam, and then share you video using that device instead of your real webcam.
It&rsquo;s hack, but a hack is the best option there is when you have to use crappy
software like Zoom.</p>
<p>If you&rsquo;re not using sway, you&rsquo;ll need to find something to record your desktop
(e.g.: an alternative to <code>wf-recorder</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;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span><span style="color:#75715e"># Install the v4l2 loopback driver</span>
</span></span><span style="display:flex;"><span>sudo pacman -S v4l2loopback-dkms
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Load the kernel module</span>
</span></span><span style="display:flex;"><span>sudo modprobe v4l2loopback
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Finally, pipe the screen output into the virtual webcam:</span>
</span></span><span style="display:flex;"><span>wf-recorder --muxer<span style="color:#f92672">=</span>v4l2 --codec<span style="color:#f92672">=</span>rawvideo --pixel-format<span style="color:#f92672">=</span>yuv420p --file<span style="color:#f92672">=</span>/dev/video2
</span></span></code></pre></div><p>Now jump back to zoom, and change the video output to the &ldquo;Dummy video device&rdquo;.</p>
<p>Extra tips:</p>
<ul>
<li>Remember to <strong>turn off video mirroring</strong>, or everything will be mirrored,
which you generally don&rsquo;t want when sharing the screen.</li>
<li>In zoom, if you set the camera to <strong>Original Ratio</strong>, your full screen will be
shared, but the quality will be really crap (it&rsquo;s unlikely the other party
will be able to read anything).</li>
<li>If you set it to <strong>16:9</strong>, the sides of your screen will be cropped, but the
quality should be readable. Your pick. 😞</li>
<li>Remember to kill <code>wf-recorder</code> once you&rsquo;re done or your screen will be shared
next time you join a call.</li>
</ul>
<p>&ldquo;Enable HD&rdquo; does not seem to change anything. Probably because the codec is
420p. Sadly, other codecs don&rsquo;t seem to work, I&rsquo;ve no idea <em>why</em> better quality
codecs won&rsquo;t work, I think this is a limitation of ffmpeg, but it might be
wf-recorder, or v4l2loopback 🤷🏻.</p>
]]></description></item><item><title>What is git?</title><link>https://whynothugo.nl/journal/2020/03/25/what-is-git/</link><pubDate>Wed, 25 Mar 2020 17:55:58 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2020/03/25/what-is-git/</guid><description><![CDATA[<p><code>git</code> is one of the most important tools for a developer nowadays regardless of
what programming language is being used.</p>
<p>Regrettably, it seems that nobody explains what git <em>is</em> to new developers at
any point. Over the years, I&rsquo;ve mentored many fellow developers of different
levels of experience, and it&rsquo;s clear that git is something that&rsquo;s not explain
well enough (if at all) in educational settings. Those who were familiar were
mostly self-taught or had learned from colleagues.</p>
<p>Many courses seem to cover git, but only cover very advanced topics: branching,
tags, git-flow, and alike. This is pretty much like teaching a medicine student
how to do heart surgery on their first week &ndash; sure, it&rsquo;s important, but it&rsquo;s
definitely not the first topic that should be covered, nor will they be able to
successfully assimilate this skill during their first week anyway.</p>
<!-- more -->
<h2 id="what-is-it-for">What is it for?<div class="permalink">[<a href="#what-is-it-for">permalink</a>]</div></h2>
<p>We&rsquo;re all (hopefully) familiar with the idea of saving a file with code; when
saved, it&rsquo;s written to disk, and replaces the previous version. Sometimes we
break code, and want to look back at previous versions to understand what we
changed and how we broke it. Sometimes we look at lines of code and wonder
&ldquo;what was the author thinking when writing this?&rdquo;.</p>
<p>Git allows saving &ldquo;snapshots&rdquo; of history for an entire codebase (e.g.: multiple
files with code in them). While the files with code look the same, git keeps a
history of changes in a <code>.git</code> directory so we can look back upon changes,
compare them, and even roll back entirely.</p>
<p>Each &ldquo;snapshot&rdquo; or &ldquo;past version&rdquo; of the code that is saved is called a
&ldquo;commit&rdquo;. Commits aren&rsquo;t created automatically, they must be done explicitly
(e.g.: by using the <code>git commit</code> command). Commits are accompanied by a
descriptive message of what changed and why, so anyone inspecting history in
future can figure out what was going on.</p>
<h2 id="so-whats-a-commit">So what&rsquo;s a commit?<div class="permalink">[<a href="#so-whats-a-commit">permalink</a>]</div></h2>
<p>Internally, git doesn&rsquo;t keep a copy each file for each version. Instead, a
commit is just a set of differences of what&rsquo;s changed between one version and
the previous one.</p>
<p>Say you have this code in your &ldquo;version 1&rdquo; (and you&rsquo;ve committed 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;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">do_stuff</span>(obj):
</span></span><span style="display:flex;"><span>    obj<span style="color:#f92672">.</span>do_something()
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">&#39;done!&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">True</span>
</span></span></code></pre></div><p>And you have this code in &ldquo;version 2&rdquo;:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">do_stuff</span>(obj):
</span></span><span style="display:flex;"><span>    obj<span style="color:#f92672">.</span>do_something()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">True</span>
</span></span></code></pre></div><p>When you commit &ldquo;version 2&rdquo;, git merely records &ldquo;this version deleted line #3
of <code>somefile.py</code>). When you ask it to show this commit, it&rsquo;ll render it
something 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;-webkit-text-size-adjust:none;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span><span style="color:#75715e">@@ -1,4 +1,3 @@
</span></span></span><span style="display:flex;"><span> def do_stuff(obj):
</span></span><span style="display:flex;"><span>     obj.do_something()
</span></span><span style="display:flex;"><span><span style="color:#f92672">-    print(&#39;done!&#39;)
</span></span></span><span style="display:flex;"><span>     return True
</span></span></code></pre></div><p>Notice that tiny <code>-</code> at the beginning? That means &ldquo;this line was deleted&rdquo;.
(side note: git doesn&rsquo;t <em>literally</em> store this, it&rsquo;s more efficient, but this
is the general idea).</p>
<p>A bit more formally, a commit is a set of changes applied (a.k.a.: a &ldquo;delta&rdquo;)
to a codebase, with an attached human-friendly message.</p>
<h2 id="how-do-i-create-a-new-commit">How do I create a new commit<div class="permalink">[<a href="#how-do-i-create-a-new-commit">permalink</a>]</div></h2>
<p>So, let&rsquo;s assume you&rsquo;re working on some files with code (maybe an assignment?).
The first thing you want to do it initialise the current directory as a git
repository.</p>
<p>This basically means &ldquo;tell git to set up it&rsquo;s stuff here&rdquo;. A repository is
basically a directory, but you can push it (&ldquo;send my commits it to a repository
on a remote machine&rdquo;), or pull it (&ldquo;copy commits from a remote machine&rdquo;).</p>
<p>Initialising is done running <code>git init</code> with the official/cli client, but your
IDE may have some other way of doing it.</p>
<p>Once you&rsquo;re working on a repository, you need to follow a few steps to create a
commit:</p>
<ul>
<li>Use <code>git add</code> to add files you want to save to your commit. Technically, one
adds the changes in the files. So when updating a file previously committed,
one should add changes individually to have a quick review of what&rsquo;s being
added and make sure it all makes sense. This can be done with <code>git add -p</code>,
where git will prompt to confirm adding each individual change.</li>
<li>Keep in mind that you&rsquo;re adding changes &ndash; so if you deleted a file, you can
still use <code>git add myfile.py</code> to remove it. To help wrap your mind around it,
consider that you&rsquo;re &ldquo;adding changes&rdquo;. In this case, &ldquo;the changes&rdquo; is
deleting a file.</li>
<li>Use <code>git commit</code> to create the commit. Git will prompt for a commit messages,
and that brings me to the next topic&hellip;</li>
</ul>
<h2 id="the-importance-of-commit-messages">The importance of commit messages<div class="permalink">[<a href="#the-importance-of-commit-messages">permalink</a>]</div></h2>
<p>If you work on a same codebase for days/weeks/months, you&rsquo;ll many times want to
look back. &ldquo;<em>Oh, how was this code written in the old version where it didn&rsquo;t
crash?</em>&rdquo;, or &ldquo;<em>Why did I write this line, what was I thinking?</em>&rdquo;.</p>
<p>The key to writing commit messages, is to quickly describe what you did, and
why you did anything that might not seem obvious. When you work with a team,
they&rsquo;ll appreciate the attention to detail. When you work alone, your future
self will thank you even more.</p>
<p>My general guideline is to think <em>&ldquo;What would be useful to my future self when
trying to understand why I made this commit?&rdquo;</em>.</p>
<!-- TODO: add some good and bad examples? I'm sure I can just link to some
good resource already out there for that -->
<p>Sometimes we don&rsquo;t leave comments in the code like <em>&ldquo;added this check because
otherwise X and Y happen&rdquo;</em> because too many of those end up hurting readability
and aren&rsquo;t always important enough. A commit messages is the perfect place for
this. It&rsquo;ll never be in the way, but will forever be findable.</p>
<p>As you use git and continue working on more complicated codebases, you should
learn to appreciate the ability to look back and figure out how and why you did
things.</p>
<h2 id="final-words-on-commits">Final words on commits<div class="permalink">[<a href="#final-words-on-commits">permalink</a>]</div></h2>
<p>Think of commits as checkpoints. Did you break everything and lose? No problem,
yo can go back to the last checkpoint. Or use <code>git diff</code> to see exactly what
you&rsquo;ve change since you did it.</p>
<p>You can also use <code>git stash</code>. Stashing &ldquo;moves aside&rdquo; all the changes you&rsquo;ve
made, so you can test your last commit and see if something worked (or not).
This is great if you want to figure out if you&rsquo;ve broken something, or if it
was always broken. <code>git stash pop</code> will restore the stashed changes.</p>
<h2 id="want-to-know-more">Want to know more?<div class="permalink">[<a href="#want-to-know-more">permalink</a>]</div></h2>
<p>Thee intention with this article is to teach what git is for, and why you
should care. If I&rsquo;ve achieve my goal, you&rsquo;ll probably want to learn more and
incorporate it into your workflow &ndash; learning it pays off pretty fast, and
you&rsquo;ll save so much time so fast that it&rsquo;s a great investment early in one&rsquo;s
career as a developer.</p>
<p>A great starting guide is the official git book. It&rsquo;s pretty short (in under 30
pages you&rsquo;ll have everything you need to know and lots more!), and very hands
on:</p>
<p><a href="https://git-scm.com/book/en/v2">https://git-scm.com/book/en/v2</a></p>
<hr>
<div class="notice update">
  <header>Update </header>
  <section>
  This article was slightly re-worded in July 2022 and September 2022 to improve
readability and clarify some items.
  </section>
</div>
]]></description></item><item><title>Spotify review</title><link>https://whynothugo.nl/journal/2016/11/29/spotify-review/</link><pubDate>Tue, 29 Nov 2016 11:23:59 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2016/11/29/spotify-review/</guid><description><![CDATA[<p>Since copying music to my iPhone is a bit of pain, I decided to stop being a
dinosaur, and get into this new world of on-demand music streaming.</p>
<p>Regrettably, it seems that these services are really below alpha quality - and
amazingly, manage to have millions of customers anyway (but hey, stuff is
frequently made popular due to marketing and not due to good quality).</p>
<!-- more -->
<h2 id="spotify-website">Spotify website<div class="permalink">[<a href="#spotify-website">permalink</a>]</div></h2>
<p>The website is so riddled with bugs it&rsquo;s incredible. Here&rsquo;s one:</p>
<ul>
<li>Create account A</li>
<li>Logout</li>
<li>Visit the registration page, and create account B</li>
<li>You&rsquo;re now signed into account A!!!</li>
</ul>
<p>I managed to reproduce this several times, and it keeps on happening.</p>
<p>Also, if you create an account using Facebook, it&rsquo;ll assign an erroneous email
which you can&rsquo;t change. For me, it used &ldquo;<a href="mailto:hugo@osvaldobarrera.com.ar">hugo@osvaldobarrera.com.ar</a>&rdquo;. While
it&rsquo;s true that this was my email lots of years back, it&rsquo;s not even present in
Facebook. I actually created my Facebook profile <em>after</em> that email died, so
I&rsquo;ve no idea where Spotify got it from.</p>
<p>There&rsquo;s also lots of localization errors. It seems Spotify&rsquo;s entire i18n system
is completely broken, and will often render different bits of the same page in
different languages.</p>
<p>I ignored these errors (and many others that I honestly don&rsquo;t recall so won&rsquo;t
mention here) and proceded to try it anyway, because I felt quite generous.</p>
<h2 id="the-web-based-player">The web-based player<div class="permalink">[<a href="#the-web-based-player">permalink</a>]</div></h2>
<p>The web-player is pretty much a dead end unless you live in 2009. It requires
that you install the ancient flash player. No HTML5 support, for some reason
that&rsquo;s beyond me.</p>
<p>This reminds me of Grooveshark, which had an HTML5 player, but regrettably, it
died.</p>
<div class="notice update">
  <header>Update 2017-05-22</header>
  <section>
  Looks like Spotify&rsquo;s web player can now use Widevine, a DRM plug-in compatible
with major browsers/platforms. Still no luck if you don&rsquo;t use one of those, and
still DRM-tied, but there&rsquo;s a chance.
  </section>
</div>
<hr>
<h2 id="the-desktop-app">The desktop app<div class="permalink">[<a href="#the-desktop-app">permalink</a>]</div></h2>
<p>The desktop app is buggy as hell. It too had localization errors where I&rsquo;d see
screens in multiple languages.</p>
<p>More importantly though, the search does not work, so you can&rsquo;t find tracks.
That&rsquo;s as much as a dead end as it gets.</p>
<figure>
  <img src="search-error.png"
       alt="A screenshot of the search error.">
  <figcaption>
    I tried the app in different systems, and the results are consistently the
    same.
  </figcaption>
</figure>
<p>There&rsquo;s also a huge bar on the right hand side of the screen to link Spotify
with Facebook. If you don&rsquo;t use or care for Facebook, it&rsquo;ll stay there, taking
up space uselessly, with no &ldquo;hide&rdquo; button.</p>
<h2 id="the-mobile-app">The mobile app<div class="permalink">[<a href="#the-mobile-app">permalink</a>]</div></h2>
<p>Even after all the above issues, I decided I&rsquo;d try the mobile app. I usually
give up fast, but for some reason, I went on.</p>
<p>The mobile app <em>also</em> has localization issues, with different bits of the UI in
different languages.</p>
<figure class="narrow">
  <img src="sign-up.png"
       alt="The sign up page showing an error in a different language.">
  <figcaption>
    It pisses me that my user name was used, but that's not actually a bug.
    <br>
    It'd be nice if the error was in the same language as the rest of the
    screen though.
  </figcaption>
</figure>
<figure class="narrow">
  <img src="mixed-languages.png"
       alt="A screen mostly in english, except for a title in spanish..">
  <figcaption>
    More mixed languages.
  </figcaption>
</figure>
<p>I tried to play a track. &ldquo;Bohemian Rhapsody&rdquo;. Search worked, but when I tried
to play it, Spotify would just play <em>random</em> music by the same artists, not the
track I picked. Selecting a track requires a premium account (so, basically,
free accounts can only listen to random stuff).</p>
<figure class="narrow">
  <img src="premium-required.png"
       alt="A screen explaining that only premium users can select tracks.">
  <figcaption>
    The "only premium users can choose what they listen to" is actually
    documented.
  </figcaption>
</figure>
<p>The language issues continue:</p>
<figure class="narrow">
  <img src="premium-in-spanish.png"
       alt="A screen showing I need a premium account - in spanish..">
  <figcaption>
    I need a premium account for this. In Spanish.
  </figcaption>
</figure>
<figure class="narrow">
  <img src="premium-in-english.png"
       alt="A screen stating I've activated premium mode - in english.">
  <figcaption>
    Okay, I activated my "7 day free trail". The trail is in English. Maybe
    proper i18n is only for premium users?
  </figcaption>
</figure>
<p>So, now that I can finally actually play music, there&rsquo;s a larger issue. Opening
any album only has a huge &ldquo;shuffle&rdquo; button. There&rsquo;s no clear way to play it in
order (and some albums make no sense shuffled, why would anyone have this
chaotic default is beyond me).</p>
<p>It <a href="https://community.spotify.com/t5/iOS-iPhone-iPad/How-to-Disable-Shuffle-Play-on-the-iPhone/td-p/993199">seems that</a> there&rsquo;s actually no way to disable this insane
behaviour, but taping on the first track will start playing it, and play the
rest in order. However, there&rsquo;s a catch. Adding other stuff to the queue plays
it immediately after the selected track, overriding the rest of the initial
album.<br>
The workaround is adding a track, then its album, then deleting the duped first
track, and then adding more stuff. Ugh, horrible.</p>
<p>I also discovered that the iPhone app can remote-control the desktop app, so
it&rsquo;s possible to play music in the desktop version, even if its search is
non-functional.</p>
<p>Note: My iPhone is configured in English (UK). Siri as well.  Everything
everywhere is set to English.</p>
<h2 id="collection">Collection<div class="permalink">[<a href="#collection">permalink</a>]</div></h2>
<p>Spotify&rsquo;s collection is pretty large. About four out of every five
albums/tracks I&rsquo;ve looked for were there.</p>
<p>There&rsquo;s a button on the desktop app to link a local folder and listen music
from it. Clicking the button does nothing, regrettably.</p>
<h2 id="cost">Cost<div class="permalink">[<a href="#cost">permalink</a>]</div></h2>
<p>The price varies substantially from region to region, but is relatively cheap
where I live. YMMV.</p>
<p>There&rsquo;s also an offer with a 86% (yes, eighty six percent!) discount the first
three months, which probably helps you get hooked at a very cheap price and
then forget that you&rsquo;re paying for it.</p>
<p>Finally, if you and your family members live together, you only need to pay 21%
of the total price each (being six members). Sadly, the terms and conditions
specify that it&rsquo;s <em>only</em> for family members that live under the same household.</p>
<p>Live in separate households? Share your household with friends? Not included.
I&rsquo;m not sure how this applies unmarried couples.</p>
<p>In any case, I&rsquo;m pretty sure that lots of families will ignore this, especially
since it&rsquo;s in very small print, and it&rsquo;s impossible to enforce.</p>
<h2 id="veredict">Veredict<div class="permalink">[<a href="#veredict">permalink</a>]</div></h2>
<p>The only thing that&rsquo;s actually usable is the mobile app, if you&rsquo;re willing to
pay. All the rest is garbage, and free accounts make no sense, since you can&rsquo;t
pick what you listen to.</p>
]]></description></item><item><title>Using FreeOTP with Battle.net</title><link>https://whynothugo.nl/journal/2016/11/23/using-freeotp-with-battle.net/</link><pubDate>Wed, 23 Nov 2016 23:00:34 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2016/11/23/using-freeotp-with-battle.net/</guid><description><![CDATA[<p>Some battlet.net users <a href="http://us.battle.net/forums/en/bnet/topic/12188259158">have requested</a>, over and over to use other
apps as a battle.net 2FA. These include FreeOTP, Authy, and possible others
(Google Authenticator, AFAIK, cannot be used since it lacks the ability to
configure the amount of digits).</p>
<p>After some searching the web, I found out all the pieces of the puzzle are out
there, but nobody built it entirely, so here goes!</p>
<!-- more -->
<p>First of all, install <code>bna</code> using <code>pip</code>. AFAIK, <code>python</code> and <code>pip</code> should be
part of the base OS in just about any OS/distro except windows. On windows,
you&rsquo;ll have to download and install <a href="https://www.python.org/downloads/">python</a> (the latest version is
fine).:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>pip install bna
</span></span></code></pre></div><p>Then, generate a new authenticator:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-console" data-lang="console"><span style="display:flex;"><span>$ bna --new
</span></span><span style="display:flex;"><span>Success. Your new serial is: US-1611-2467-4116 06184679
</span></span></code></pre></div><p>Visit the <a href="https://us.battle.net/account/management/authenticator.html">authenticator page</a> at battle.net, and like the new
authenticator using the provided serial. Obviously, don&rsquo;t copy the above, but
the output of your own.</p>
<p>Now to link that to your phone! You can get all the information via the CLI:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-console" data-lang="console"><span style="display:flex;"><span>$ bna --otpauth-url
</span></span><span style="display:flex;"><span>otpauth://totp/Battle.net:US161124674088:?secret=KMN3RTIFHLNDEMIHDIVXVGXK2GB33VW5&amp;issuer=Battle.net&amp;digits=8
</span></span></code></pre></div><p>In the above example:</p>
<ul>
<li><code>US161124674088</code> is the generated serial. You <em>may</em> want to store it
somewhere very safe for posterity&rsquo;s sake, but it&rsquo;s not really that
important (I think).</li>
<li><code>KMN3RTIFHLNDEMIHDIVXVGXK2GB33VW5</code> is your actual secret. You&rsquo;ll need to
provide this to FreeOTP (or whatever you&rsquo;re using).</li>
<li><code>digits=8</code> indicates that your authenticator app should always generate 8
digits. This <em>MUST</em> be set to 8 or it won&rsquo;t work.</li>
</ul>
<p>If you&rsquo;ve <code>qrencode</code> installed, you can also run:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>bna --otpauth-url | qrencode -o ~/BNA-qrcode.png
</span></span></code></pre></div><p>Which will generate a QR code which you can scan in your home. I tried to scan
it, but FreeOTP didn&rsquo;t generate anything for me. Not sure if it&rsquo;s a glitch or
what.</p>
<p>That it! Just a reminder, <em>keep your secret and serial numbers secret</em> anyone
who has access to these can basically spoof your 2FA codes!</p>
]]></description></item><item><title>Using letsencrypt with HKPK</title><link>https://whynothugo.nl/journal/2016/02/07/using-letsencrypt-with-hkpk/</link><pubDate>Sun, 07 Feb 2016 21:15:40 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2016/02/07/using-letsencrypt-with-hkpk/</guid><description><![CDATA[<p>HKPK (<a href="https://tools.ietf.org/html/rfc7469">RFC7469</a>) is a standard that tells browser to cache a certain
TLS certificate&rsquo;s signature, and validate that future visits use that
certificate (or a defined backup).</p>
<p>I intended on enabling this on my servers, but since letsencrypt renews your
certificates every few months, it would mean updating this setting on my nginx
configuration. It also means that if something catastrophic happens (like a
disk failure), the certificate would be lost, but browsers would still expect
to see that same one.</p>
<!-- more -->
<p>After some quick searching, I didn&rsquo;t find anything in letsencrypt&rsquo;s docs on how
to quickly do this.</p>
<p>The solution is merely to pin the root certificates - or the intermediate ones.</p>
<p>Pinning the intermediate one (and the backup intermediate certificate) seem
like a good compromise: There&rsquo;s no need to update anything upon renewals, but
in case of lost certificates, there would be no issue using new ones.</p>
<p>To do this, I tailored the  Public-Key-Pins header on my server (in my case,
nginx) sends.</p>
<p>You can copy-paste these into nginx&rsquo;s settings if that&rsquo;s what you&rsquo;re using:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>add_header Public-Key-Pins &#39;pin-sha256=&#34;YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=&#34;; pin-sha256=&#34;sRHdihwgkaib1P1gxX8HFszlD+7/gTfNvuAybgLPNis=&#34;; max-age=5184000; includeSubDomains&#39;;
</span></span></code></pre></div><p>This also sets the <code>max-age</code> to six months, which is generally a good
compromise.</p>
<p>I you don&rsquo;t want to copy-paste security-related settings from a random website,
you can get these hashes yourself.</p>
<p>First of all, grab letsencrypt&rsquo;s <a href="https://letsencrypt.org/certificates/">intermediate certificates</a>.
Once you have them, you need to get the key&rsquo;s SHA256 hash (do this for both):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>openssl x509 -noout -in lets-encrypt-x1-cross-signed.pem -pubkey | <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>openssl rsa -pubin -outform der | <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>openssl dgst -sha256 -binary | <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>base64
</span></span></code></pre></div><p>Note that these are the same hashes in the above:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>add_header Public-Key-Pins &#39;pin-sha256=&#34;HASH1=&#34;; pin-sha256=&#34;HASH2&#34;; max-age=5184000; includeSubDomains&#39;;
</span></span></code></pre></div><p>Now tell nginx to reload your settings, and you&rsquo;re done. Visitors for you
domain will pin the intermediate certificate for six months, blocking any MITMs
done with rogue certificates, etc.</p>
]]></description></item><item><title>I'm giving up on IM</title><link>https://whynothugo.nl/journal/2015/09/22/im-giving-up-on-im/</link><pubDate>Tue, 22 Sep 2015 02:08:42 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2015/09/22/im-giving-up-on-im/</guid><description><![CDATA[<p>I&rsquo;ve been using XMPP as my primary IM protocol for years now. I&rsquo;ve used a few
other things on the side, but I&rsquo;ve always advertised it as my primary mean of
communication. And it&rsquo;s really worked for a long time: lots of developers and
people in FLOSS circles use XMPP, and Google Talk federated as XMPP too, so
that worked for less tech-inclined users.</p>
<!-- more -->
<p>In recent years, I also ended up having to use Facebook&rsquo;s IM a bit more as
well, since a lot of people I know <em>only</em> used that to online communication. It
wasn&rsquo;t a big hassle, since they used to expose their IM as XMPP too.</p>
<h2 id="xmpp-kinda-died">XMPP kinda died<div class="permalink">[<a href="#xmpp-kinda-died">permalink</a>]</div></h2>
<p>Yup, it&rsquo;s time to admit it&rsquo;s died. Even as a long time proponent of open,
federated protocols, <em>and</em> XMPP, I can&rsquo;t just deny the truth.</p>
<p>These last few years, though, we&rsquo;ve been going back to the dark ages: All these IM
providers closed up their networks behind unfederated (and sometimes
proprietary) protocols. Lots of new protocols/networks started appearing, and
people started migrating left and right to all of them.</p>
<p>Most of these new protocols have only a subset of the features that XMPP had to
offer, but XMPP had a big problem: most features were optional extensions, and
finding a client that had them all, was non trivial. You then had to make sure
the other person was in the same situation.</p>
<p>So convincing people that XMPP is superior is hard: the protocol and its
extensions <em>can</em> offer more, but in reality, it&rsquo;s not so superior (mostly due
to lack of client/server features, not the protocol itself).</p>
<p>There&rsquo;s no client I can recommend to people, and many Google Talk users have
moved on to Hangouts, which does not federate, and are thus, unreachable. Less
reachable people makes it hard to attract users, regardless of technical merit.</p>
<h2 id="theres-no-sucesor">There&rsquo;s no sucesor<div class="permalink">[<a href="#theres-no-sucesor">permalink</a>]</div></h2>
<p>But that&rsquo;s not all of it. Even after giving up on free and open IM protocols,
and admitting you need to move elsewhere to keep in touch with fellow humans,
there&rsquo;s really nowhere to go. People have moved to diverse IM networks, and
different groups of people use different things - and they&rsquo;ll even claim &ldquo;but
everyone uses XXX&rdquo;, where XXX can be replaced by:</p>
<ul>
<li>Hangouts</li>
<li>Facebook Messenger</li>
<li>Skype</li>
<li>Whatsapp</li>
<li>Lime</li>
</ul>
<p>(Note: This list goes on quite a bit. I just can&rsquo;t be bothered to think further
about it).</p>
<p>Telegram users are the only one who seem to admit that it&rsquo;s not very popular,
but they chose it because they like it and/or it&rsquo;s FLOSS and openly documented
(even though the protocol itself is unfederated). And there&rsquo;s also quite a few
there.</p>
<p>So basically, IM is back to where it was on 1999. Lots of islands with lots of
users in each, no single network where you can reach everybody, and closed
protocols which means no multi-protocol clients.</p>
<p>Things like Pidgin (which I&rsquo;ve used for around a decade) have stagnated, and
don&rsquo;t support these newer proprietary protocols. The only solution seems to be
to keep open several browser webapps at all time, draining CPU and Battery. All
just in case somebody who&rsquo;s on that particular network will try to reach me.</p>
<p>Can I blame all those people? Not really. They couldn&rsquo;t have chosen better.
XMPP failed marketing-wise, and the implementations are lacking, and there&rsquo;s
not real decent second choice. People just fled to what looked nicer at the
time.</p>
<p>The saddest part of all this, is that there&rsquo;s no lesson to be learnt. It&rsquo;s just
history repeating itself, and we&rsquo;ll all pay the price for this in a few years
time.</p>
<p>Maybe next time, we can educate people as to what happens with isolated
networks, proprietary protocols, and just picking what looks nicest. For now,
it&rsquo;s either go with the flock, or stop talking to people around you.</p>
]]></description></item><item><title>Start small, then grow</title><link>https://whynothugo.nl/journal/2015/04/06/start-small-then-grow/</link><pubDate>Mon, 06 Apr 2015 02:33:45 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2015/04/06/start-small-then-grow/</guid><description><![CDATA[<p>All of us developers who love what we do have started lots of side-projects.</p>
<p>And almost all of us have equally as many side-projects abandoned on some
<code>workspace</code> or <code>projects</code> directory, rotting, with no hope of every achieving
completion. New projects are dumped there periodically, into a pit of
abandonment and decay.</p>
<!-- more -->
<p>I wanted to find my way out of this endless, and pointless loop. I <em>needed</em> to
get things done, and make the best of my time.  Sure: it&rsquo;s fun to start a new
project, but it&rsquo;s even cooler to get it done and have something completed to
share with the world.</p>
<p>I decided I&rsquo;d do something that a so many people advise (and what lots of
companies do successfully, at some scale): make a very minimal program, with
the basic functionality, and grow from there.<br>
We software engineers understand things perfectly well: maybe too well, so we
analyse and design very single aspect of a software program and design a huge
monster that would take ages to implement.</p>
<p>I&rsquo;d been needing a TODO manager for some time (talk about getting things
done!), so I though I&rsquo;d do something that could write TODO events as
<a href="https://tools.ietf.org/html/rfc5545">icalendar</a> files, and also list them.</p>
<p>My old approach (and, I believe, the usual approach for developers) would have
been to list all the functionality, prepare a very complete model with all the
necessary features, contemplate dozens of edge use-cases, etc (this includes
supporting every single icalendar property, creation, deletion, copying, etc).</p>
<p>I ignored my instinct, and started small: I wrote a small python script that
listed icalendar tasks from a directory, and then another script with a very
simple <a href="http://urwid.org/reference/widget.html">urwid</a> interface to write new tasks.</p>
<p>There, it worked. For the first time I had something I could <em>use</em> (outside of
work, that is)! It&rsquo;s hard to describe how pleasing this is to complete
something, even if a minor milestone.</p>
<p>I used this inspiration to stay on track for a couple more hours to polish
these scripts, add a simple cli interface to them, a small settings file, some
documentation, and voilà: <a href="/journal/2015/03/30/introducing-todoman/">todoman</a> was born.</p>
<p>Having something that <em>works</em> increases motivation to continue working on it.
It&rsquo;s a tool that I now use on a daily basis (and, apparently, a few other
people too). However, I feel that the greater achievement, was actually
getting something completely done. Reaching that 1.0 milestone (actually,
todoman is already at 1.2.1), and understanding how to develop side-projects in
future, without them ending in a pile of incomplete projects.</p>
<p>Remember: start with a minimal working version, and grow from there. It doesn&rsquo;t
matter how basic it is, small and functional is better than huge and
incomplete.</p>
<p>Hopefully, this will be far from the last, and hopefully, this may help you
stop having incomplete side-projects, or hobby-projects dying all the time.</p>
]]></description></item><item><title>Introducing Todoman</title><link>https://whynothugo.nl/journal/2015/03/30/introducing-todoman/</link><pubDate>Mon, 30 Mar 2015 13:57:23 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2015/03/30/introducing-todoman/</guid><description><![CDATA[<p>Inspired on <a href="http://www.getmemo.org/">memo</a> and <a href="https://github.com/geier/khal/">khal</a>, <strong><a href="https://gitlab.com/hobarrera/todoman">todoman</a></strong> is a simple todo
manager, (or task manager), designed to take note and keep track of pending
tasks, that runs as a cli application on almost any Unix-like system (this
includes Linux, BSD and probably other OSs from the Unix family).</p>
<!-- more -->
<p>Todoman is MIT licensed and saves tasks as <a href="https://tools.ietf.org/html/rfc5545">icalendar</a>, meaning you
can use a <a href="http://en.wikipedia.org/wiki/CalDAV">CalDav</a>-complaiant too to sync your calendar (for example,
<a href="https://github.com/untitaker/vdirsyncer">vdirsyncer</a>). You can also then sync this to your mobile phone,
tablet, or any other device with a CalDav-compliant client.</p>
<p>Todoman has a very simple usage:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-py" data-lang="py"><span style="display:flex;"><span>todo
</span></span><span style="display:flex;"><span>todo new
</span></span><span style="display:flex;"><span>todo edit ID
</span></span><span style="display:flex;"><span>todo show ID
</span></span><span style="display:flex;"><span>todo done ID
</span></span></code></pre></div><p>And images are worth a thousand words:</p>
<figure>
  <img src="todoman-1.jpeg"
       alt="Screenshot of the main view."
       style="width: auto;">
  <figcaption>Sample output of running <code>todo</code>.</figcaption>
</figure>
<p>Exclamation marks indicate urgent (high priority) tasks. All the rest is pretty
self-explanatory.</p>
<figure>
  <img src="todoman-2.jpeg"
       alt="Screenshot of the edition view."
       style="width: auto;">
  <figcaption>The todo edition screen; sample output of running <code>todo edit 1</code>.</figcaption>
</figure>
<p>The source code is available at <a href="https://gitlab.com/hobarrera/todoman">its repository</a> and a <a href="https://github.com/hobarrera/todoman">github</a>
mirror. Go ahead and try it now:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>$ curl -L https://git.barrera.io/hobarrera/todoman/repository/archive.tar.gz<span style="color:#ae81ff">\?</span>ref<span style="color:#ae81ff">\=</span>v1.2.0 -o todoman.tar.gz
</span></span><span style="display:flex;"><span>$ tar -xzf v1.1.0.tar.gz
</span></span><span style="display:flex;"><span>$ cd todoman.git/
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># python setup.py install</span>
</span></span></code></pre></div><p>You&rsquo;ll also need to copy and update the configuration file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>$ mkdir  ~/.config/todoman/
</span></span><span style="display:flex;"><span>$ cp todoman.conf.sample ~/.config/todoman/todoman.conf
</span></span><span style="display:flex;"><span>$ vim ~/.config/todoman/todoman.conf
</span></span></code></pre></div><p>There&rsquo;s also already a <a href="https://aur.archlinux.org/packages/todoman/">package</a> for ArchLinux.</p>
<p>Feedback is welcome, especially bug reports, which you can email, comment, or
<a href="https://gitlab.com/hobarrera/todoman/issues">open a issue</a>.</p>
<div class="notice update">
  <header>Update 2015-03-31</header>
  <section>
  Link above now points to release 1.2.0.
  </section>
</div>
<div class="notice update">
  <header>Update 2015-08-30</header>
  <section>
  Todoman has moved to [gitlab.com][gitlab], rather than my own hosted
instance. The above links have been updated.
  </section>
</div>
<p><code>todoman</code> has also been available via PyPI for some time now, so may also be
installed running:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>pip install todoman
</span></span></code></pre></div>]]></description></item><item><title>Open source your website</title><link>https://whynothugo.nl/journal/2015/03/17/open-source-your-website/</link><pubDate>Tue, 17 Mar 2015 17:15:28 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2015/03/17/open-source-your-website/</guid><description><![CDATA[<p>Unless your business&rsquo;s value is actually on your website code itself, there&rsquo;s
little reason not to share your site&rsquo;s code.</p>
<p>I understand why facebook or gmail won&rsquo;t release the code to their site (I
understand, without condemning nor condoning), but if you&rsquo;ve got a blog, an
institutional website, a three-page site that merely links to &ldquo;download our
app&rdquo;, there&rsquo;s little reason <em>not</em> to share the source with the public.</p>
<!-- more -->
<p>All of us web developers (or just curious designers) have come across some site
and wondered &ldquo;Hmm, wonder how they did X and Y with just jekyll&rdquo;, or wondered
what plugin they used on Z platform to achieve something.</p>
<p>For example, I&rsquo;ve followed <a href="http://paulstamatiou.com/">@Stammy</a>&rsquo;s articles for
ages, and as the site change, I&rsquo;ve more than once wondered how certain details
were done with Jekyll (he&rsquo;s mentioned once that he&rsquo;s using jekyll to generate
it). Sure, I could have search online for a while and would eventually find it,
but if I&rsquo;m already at a site, it would be pretty nice to just be able to dig up
its source and see that immeditaly.</p>
<p>I&rsquo;ve shared my site&rsquo;s code even though it&rsquo;s rather poor from the start. The
reason? Someone might wonder how a small site like this is layed out using
jekyll, or how I placed X and Y pieces together. Someone wanting to learn from
example has yet another working one too. I won&rsquo;t get less job if somebody finds
out how I do this stuff. It&rsquo;s no easier to steal my (rather poor) design
either. But it might make someone&rsquo;s like slightly easier.</p>
<p>And of course, we all probably use <a href="http://www.git-scm.com/">git</a>, or something
alike to keep track of our website anyway (if not: what are you waiting for!?),
so why not just flip a switch and make that repo public.</p>
<p>Do <em>you</em> have a reason why you need to keep your site&rsquo;s source secret?</p>
<hr>
<p>A final note: this line of though came to me today when working on a small
website for a client, that&rsquo;s basically a few pages that show some stats, and a
link to their app. When I asked &ldquo;Why&rsquo;s the repository for it public? It only
uses your public API that you want other devs to use anyway.&rdquo; his answer was
he&rsquo;d think about it.</p>
<p>Maybe it&rsquo;s time to turn the &ldquo;open by default&rdquo; switch on for these sort of
things?</p>
]]></description></item><item><title>Opensmtpd and dovecot with a shared SQL database</title><link>https://whynothugo.nl/journal/2015/02/15/opensmtpd-and-dovecot-with-a-shared-sql-database/</link><pubDate>Sun, 15 Feb 2015 15:04:03 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2015/02/15/opensmtpd-and-dovecot-with-a-shared-sql-database/</guid><description><![CDATA[<p>This article will describes how to achieve a flexible and scalable email setup
using opensmtpd and dovecot. For single-user or single-domain setups, this is
an overkill, but feel free to read ahead, you may still find something useful.</p>
<!-- more -->
<h2 id="introduction">Introduction<div class="permalink">[<a href="#introduction">permalink</a>]</div></h2>
<p>I&rsquo;ve used opensmtpd and dovecot for years now, and have been hosting email for
several domains for a large portion of that time.</p>
<p>For small sites the text-based backends work fine, however, as the amount of
users, domains and virtual users grows, it&rsquo;s not easy to keep track. Data needs
to be duplicated between smtpd&rsquo;s credentials table, the virtual users table,
and dovecot&rsquo;s mailbox table.</p>
<p>I finally decided it was time to consolidate all the data in one place. I chose
sqlite to begin with, but this can be moved onto postgresql if it needs to
scale even more.</p>
<h2 id="opensmtpd-and-dovecot-interaction">Opensmtpd and dovecot interaction<div class="permalink">[<a href="#opensmtpd-and-dovecot-interaction">permalink</a>]</div></h2>
<p>If you attemp to use virtualusers (and you&rsquo;ll want to if you&rsquo;re handling many
users in many domains), when receiving emails, opensmtpd maps email addresses
to usernames (which can contain no <code>@</code> sign).  Dovecot then stores emails in
mailboxes based on these usernames. Both these things make mapping many virtual
users from different domains a bit compex.</p>
<p>I decided that the usernames I&rsquo;ll be mapping to will take the form of
<code>username_domain</code> (eg: <code>hugo_barrera.io</code>). This makes a few initial settings a
bit complicated, but is infinitely flexible (the underscore sign in illegal for
email addresses, so there&rsquo;s no change for collision).</p>
<h2 id="database-schema">Database schema<div class="permalink">[<a href="#database-schema">permalink</a>]</div></h2>
<p>I designed two database schemas, a normalized one, and a single-table one. I
decided to keep the latter, since it makes inserts simpler <em>and</em> it&rsquo;s easier
to show-and-tell, but if you read through this entire article, you&rsquo;ll be able
to use whatever tableset you like.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">TABLE</span> users (
</span></span><span style="display:flex;"><span>  username TEXT <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">domain</span> TEXT <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
</span></span><span style="display:flex;"><span>  mailbox TEXT <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
</span></span><span style="display:flex;"><span>  password TEXT <span style="color:#66d9ef">NULL</span>
</span></span><span style="display:flex;"><span>);
</span></span></code></pre></div><p>Passwords are blowfish encrypted. Opensmtpd uses this by default and dovecot
also supports this (it refers to this scheme as <code>BLF-CRYPT</code>).</p>
<p>In different contexts, each column is used for something different:</p>
<ul>
<li>For email submission <code>username</code>, <code>domain</code> and <code>password</code> are used to validate
authentication. The usernames smtpd receives from the client take the form of
<code>hugo@barrera.io</code>.</li>
<li>Opensmtpd needs to know what domains it&rsquo;s receiving emails for. For this, it
just sees if there&rsquo;s <em>any</em> entry in the <code>users</code> table where this domain is
present. It would make no sense to keep a separate list of domains: if
there&rsquo;s no address for a domain, then I&rsquo;re not accepting email for this
domain.</li>
<li>For email receiption, <code>username@domain</code> is used to map to the recipient mailbox
(ie: the <code>mailbox</code> column). Several addresses can map to a single user, and
wildcards can also be used too. The <code>@</code> is replaced with an <code>_</code> when querying
this table as well.</li>
<li>For dovecot&rsquo;s authentication, it needs to know if <code>username@domain</code> is a real
user, or just an aliased address. This is simply determined by checking if
<code>password != NULL</code>.</li>
</ul>
<p>Passwords should be encrypted using either <code>doveadm pw -s BLF-CRYPT</code> or
<code>smtpctl encrypt</code>. The output of both seems interchangeable.</p>
<h2 id="dkim">DKIM<div class="permalink">[<a href="#dkim">permalink</a>]</div></h2>
<p>DKIM signing is done with DKIMProxy. There&rsquo;s a bunch of examples out there, so
I won&rsquo;t go into detail about that. Basically, opensmtp will send emails to
DKIMProxy, accepts them back, tags them, and then relays them out.</p>
<h2 id="opensmtpd-configuration">Opensmtpd configuration<div class="permalink">[<a href="#opensmtpd-configuration">permalink</a>]</div></h2>
<p>First of all, we need to configure opensmtpd to receive email and read all the
data from the sqlite database. Here&rsquo;s my <code>smtpd.conf</code> as a reference:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span># === TLS Certificates === #
</span></span><span style="display:flex;"><span>pki mx1.barrera.io certificate &#34;/path/to/certs/mx1.barrera.io.crt&#34;
</span></span><span style="display:flex;"><span>pki mx1.barrera.io key         &#34;/path/to/certs/mx1.barrera.io.key&#34;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>pki smtp.barrera.io certificate &#34;/path/to/certs/smtp.barrera.io.crt&#34;
</span></span><span style="display:flex;"><span>pki smtp.barrera.io key         &#34;/path/to/certs/smtp.barrera.io.key&#34;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span># === Tables === #
</span></span><span style="display:flex;"><span>table domains sqlite:/etc/mail/sqlite.conf
</span></span><span style="display:flex;"><span>table virtuals sqlite:/etc/mail/sqlite.conf
</span></span><span style="display:flex;"><span>table userinfo sqlite:/etc/mail/sqlite.conf
</span></span><span style="display:flex;"><span>table credentials sqlite:/etc/mail/sqlite.conf
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span># === Listen === #
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>listen on lo0
</span></span><span style="display:flex;"><span>listen on lo0 port 10028 tag DKIM
</span></span><span style="display:flex;"><span>listen on egress port smtp       tls                            hostname &#34;mx1.barrera.io&#34;
</span></span><span style="display:flex;"><span>listen on egress port submission tls-require auth &lt;credentials&gt; hostname &#34;smtp.barrera.io&#34;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span># === Handle Messages === #
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>accept from any for local virtual &lt;virtuals&gt; \
</span></span><span style="display:flex;"><span>  userbase &lt;userinfo&gt; deliver to lmtp &#34;/var/dovecot/lmtp&#34;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>accept from any for domain &lt;domains&gt; virtual &lt;virtuals&gt; \
</span></span><span style="display:flex;"><span>  userbase &lt;userinfo&gt; deliver to lmtp &#34;/var/dovecot/lmtp&#34;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span># === Sign/relay === #
</span></span><span style="display:flex;"><span>accept tagged DKIM for any relay
</span></span><span style="display:flex;"><span>accept for any relay via smtp://127.0.0.1:10027
</span></span></code></pre></div><p>This is all rather self-explanatory if you&rsquo;re familiar with <code>smtpd.conf</code>&rsquo;s
sytax. I use lmtp via a unix socket because I believe it&rsquo;s slightly faster, but
a network socket works fine too.</p>
<p><code>sqlite.conf</code> is a bit more complex:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>dbpath                  /etc/mail/smtpd.sqlite
</span></span><span style="display:flex;"><span>query_credentials       SELECT username||&#39;@&#39;||domain, password FROM users WHERE (username||&#39;@&#39;||domain)=?;
</span></span><span style="display:flex;"><span>query_domain            SELECT domain FROM users WHERE domain=? LIMIT 1;
</span></span><span style="display:flex;"><span>query_userinfo          SELECT 7000, 7000, &#39;/var/empty&#39; FROM users WHERE (username||&#39;_&#39;||domain)=?;
</span></span><span style="display:flex;"><span>query_alias             SELECT replace(mailbox, &#39;@&#39;, &#39;_&#39;) FROM users WHERE ? LIKE (username||&#39;@&#39;||domain);
</span></span></code></pre></div><ul>
<li><code>query_credentials</code> is used to validate user credentials. Opensmtpd will pass
the provided email (in the form of username@domain), and this will find
mapping rows. Since aliases have <code>password = NULL</code>, those rows will return
false.</li>
<li><code>query_domain</code> is used when querying if we receive emails for a domain or
not. The logic is rather simple: if there&rsquo;s an address for a domain, then
we accept email for it and viceversa.</li>
<li><code>query_userinfo</code> is used for email delivery, and to check if a mailbox
exists. If there&rsquo;s a mapping for it, then it exists.</li>
<li><code>query_alias</code> returns the <code>mailbox</code> for a <code>username@domain</code>, as
described above.</li>
</ul>
<p>On the smtpd side, that&rsquo;s basically it. Here&rsquo;s some sample data:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">INSERT</span> <span style="color:#66d9ef">INTO</span> users <span style="color:#66d9ef">VALUES</span>(<span style="color:#e6db74">&#39;hugo&#39;</span>, <span style="color:#e6db74">&#39;barrera.io&#39;</span>, <span style="color:#e6db74">&#39;hugo@barrera.io&#39;</span>, <span style="color:#e6db74">&#39;$2b$08$CEWRsxzLeTziYlq58gJvd.35RQ0fK2jP9RW8AisoxAznmmN6GsdvK&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">INSERT</span> <span style="color:#66d9ef">INTO</span> users <span style="color:#66d9ef">VALUES</span>(<span style="color:#e6db74">&#39;%&#39;</span>, <span style="color:#e6db74">&#39;barrera.io&#39;</span>, <span style="color:#e6db74">&#39;hugo@barrera.io&#39;</span>, <span style="color:#66d9ef">NULL</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">INSERT</span> <span style="color:#66d9ef">INTO</span> users <span style="color:#66d9ef">VALUES</span>(<span style="color:#e6db74">&#39;contact&#39;</span>, <span style="color:#e6db74">&#39;example.com&#39;</span>, <span style="color:#e6db74">&#39;contact@example.com&#39;</span>, <span style="color:#e6db74">&#39;$2b$08$CEWRsxzLeTziYlq58gJvd.35RQ0fK2jP9RW8AisoxAznmmN6GsdvK&#39;</span>);
</span></span></code></pre></div><ul>
<li><code>hugo@barrera.io</code> is an actual mailbox. It&rsquo;ll get emails for himself, or
anyone matching <code>%@barrera.io</code> (remember that <code>%</code> is the SQL wildcard
character).</li>
<li><code>contact@example.com</code> is another mailbox. That user will get email sent to
him and just that.</li>
</ul>
<p>If you&rsquo;re going to have several overlapping delivery patterns, you probably
want to have a priority column in the table, and add <code>ORDER BY priority</code> and
<code>LIMIT 1</code> to some queries.</p>
<h2 id="dovecot">Dovecot<div class="permalink">[<a href="#dovecot">permalink</a>]</div></h2>
<p>Dovecot includes <code>conf.d/auth-sql.conf.ext</code>. I&rsquo;ve modified it as follows:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>mail_location = maildir:~/Maildir
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>passdb {
</span></span><span style="display:flex;"><span>  driver = sql
</span></span><span style="display:flex;"><span>  args = /etc/dovecot/dovecot-sql.conf.ext
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>userdb {
</span></span><span style="display:flex;"><span>  driver = sql
</span></span><span style="display:flex;"><span>  args = /etc/dovecot/dovecot-sql.conf.ext
</span></span><span style="display:flex;"><span>  override_fields = uid=vmail gid=vmail
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><ul>
<li><code>mail_location</code> tell dovecot where relative to the user&rsquo;s home the email is,
and to use maildir. You can change this as you prefer.</li>
<li><code>override_fields</code> tells dovecot that email is always handled by the uid/gid
<code>vmail:vmail</code>.</li>
</ul>
<p>The other values are the defaults. Of course, <code>dovecot-sql.conf.ext</code>
does require more changes so as to retrieve user data from the shared SQL
database:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>driver = sqlite
</span></span><span style="display:flex;"><span>connect = /etc/mail/smtpd.sqlite
</span></span><span style="display:flex;"><span>default_pass_scheme = BLF-CRYPT
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>password_query = \
</span></span><span style="display:flex;"><span>  SELECT password \
</span></span><span style="display:flex;"><span>  FROM users \
</span></span><span style="display:flex;"><span>  WHERE mailbox = replace(&#39;%u&#39;, &#39;_&#39;, &#39;@&#39;) AND password NOT NULL
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>user_query = \
</span></span><span style="display:flex;"><span>  SELECT &#39;/home/vmail/&#39;||domain||&#39;/&#39;||username AS home \
</span></span><span style="display:flex;"><span>  FROM users \
</span></span><span style="display:flex;"><span>  WHERE mailbox = replace(&#39;%u&#39;, &#39;_&#39;, &#39;@&#39;) AND password NOT NULL
</span></span></code></pre></div><ul>
<li><code>connect</code> needs to point to the same db that smtpd is using.</li>
<li><code>default_pass_scheme</code> <em>must</em> be <code>BLF-CRYPT</code> since that&rsquo;s what smtpd uses.</li>
<li><code>password_query</code> is used to authenticate users (eg: those attempting to open
their IMAP mailbox).</li>
<li><code>user_query</code> is used in two scenarios:</li>
</ul>
<ol>
<li>To determine where to  deliver messages destined to <code>user_domain</code>. This is
done by finding the real user who owns this mailbox (password <em>must</em> be
null for aliased mailboxes!).</li>
<li>To determine what messages to serve to a user reading his email (eg: via
IMAP). In this case, the usernames have the format of <code>user@domain</code>.</li>
</ol>
<h2 id="final-notes">Final notes<div class="permalink">[<a href="#final-notes">permalink</a>]</div></h2>
<p>Setting the whole thing up is a bit complicated, but adding new users is a
breeze. If there&rsquo;s a need to grow, the sqlite db can become a postgresql db. By
using lmtp, dovecot and opensmtpd can move into different machines, giving even
more scalability.
Further scaling, however, will require multiple dovecot backend and some
changes to the sql schema.</p>
<p>Please feel free to point out any issues, potential improvements, or comments,
I&rsquo;ll try to update this appropiately.</p>
]]></description></item><item><title>Performing backups the right way</title><link>https://whynothugo.nl/journal/2014/08/06/performing-backups-the-right-way/</link><pubDate>Wed, 06 Aug 2014 20:33:45 +0000</pubDate><author>hugo@whynothugo.nl (Hugo Osvaldo Barrera)</author><guid>https://whynothugo.nl/journal/2014/08/06/performing-backups-the-right-way/</guid><description><![CDATA[<p>For years I&rsquo;ve had a single task on my TO-DO list: backup photos. I had an awful solution years ago, and only recently did a permanent, proper solution.</p>
<p>Doing backups the right way means taking several items into consideration, and should not be done lightly. Trusting poor backup solutions will result in a false sense of security where you might loose everything suddenly, and not even realize it until it&rsquo;s too late!</p>
<!-- more -->
<p>These are items you should keep in mind when designing your backup solution:</p>
<ul>
<li>
<p><strong>Automation</strong><br>
Backups that require manual internention are backups that you&rsquo;ll forget to perform. Period. Backups need to be automated as much as possible so as to be sure to always have a very recent one.</p>
</li>
<li>
<p><strong>Transparent</strong><br>
Backups shouldn&rsquo;t be a hastle to my everyday life. A popup every day at any time is a PITA, and needless. and will most likely end up in me disabling the process permanently. I don&rsquo;t really need a &ldquo;everything is working fine&rdquo; popup anyway, which leads me to:</p>
</li>
<li>
<p><strong>Fail-safe</strong><br>
If anything goes wrong, I need to be aware of it ASAP. Ideally, I need an email notification of what went wrong and as much information as possible.</p>
</li>
<li>
<p><strong>Efficient</strong><br>
Particularly, in space. I&rsquo;ve 17GiB worth of photos (including family photos, etc). I want a snapshot of each day&rsquo;s files easily accesible, but don&rsquo;t want to use up 170GiB in just 10 days by brute-copying everything.<br>
I also want the bandwidth usage to be efficient, since I usually pay for it and/or it interferes with my actual bandwidth availability.</p>
</li>
<li>
<p><strong>Maintainable</strong><br>
Or, to put it another way: <em>simplicity</em>.  Huge scripts mixing dozen of applications may work, but in a weeks time, I&rsquo;ll forget how it works, and will be unable maintain it, reuse it, and, worst of all, fix it if it breaks.</p>
</li>
<li>
<p><strong>Secure</strong><br>
Everything needs to be transfered securely. Dead simple.</p>
</li>
</ul>
<h2 id="what-i-did">What I did<div class="permalink">[<a href="#what-i-did">permalink</a>]</div></h2>
<p><strong>Automation</strong>, <strong>transparency</strong> and <strong>fail-safe</strong> are all covered by using <code>cron</code>. My backups run on a daily basis, in case of error cron emails the output, and it does not in any way interfere with my work at all.<br>
<code>cron</code> sends an email to the current user with the output of what it runs. My backup script is silent in case of success, but will leak all errors to <code>stdout</code>, which <code>cron</code> will then email to me. By simple creating an MX record for my machine&rsquo;s hostname, and an alias on my mail server, I get an email in my inbox if anything goes wrong.<br>
If you don&rsquo;t have your own domain (or don&rsquo;t control the host&rsquo;s domain), you can set up a local <a href="https://opensmtpd.org/">smtpd server</a>, and forward the emails to yourself.</p>
<p><strong>Efficiency</strong> is covered by <code>rsync</code>. <code>rsync</code> is really efficient when it comes to bandwidth usage by only transfering chages over the network and not the entire 17GiB.<br>
As for disk-usage, by using the the <code>--link-dest</code> flag, <code>rsync</code> creates the directory tree for each day, but hardlinks files which have not been altered since the previous day. The result is a 3.4MiB usage increase per day, but the entire file tree for each day available. I can also randomly delete any day, and all other backups are unaffected.</p>
<p><strong>Security</strong> is also covered by <code>rsync</code>, by simply making it use <code>ssh</code> for transfers (which is actually the default). If you use ssh of course, you&rsquo;ll have to create a special ssh key pair, and make the private key available to cron, unencrypted. You&rsquo;ll probably want full-disk encryption to protect it.</p>
<p>As for <strong>Maintainability</strong>, I had to deal with that myself. Instead of over-complicated solutions, I wrote a 26-line shell script, of which 6 lines are actually whitespace, and 13 are comments. Pretty easy to maintain.</p>
<h2 id="how-i-did-it">How I did it<div class="permalink">[<a href="#how-i-did-it">permalink</a>]</div></h2>
<p>First of all, here&rsquo;s the above mentioned script:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/sh
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">#</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Backs up photos into the fileserver.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Pre-existing files are hardlinked to yesterday&#39;s copy, so the size</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># increase equals the size of new files, while keeping daily snapshots.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>set -e
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Since other scripts sync cron tasks across machines, I want to make sure</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># this one only runs on hyperion</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> <span style="color:#66d9ef">$(</span>hostname -s<span style="color:#66d9ef">)</span> !<span style="color:#f92672">=</span> hyperion <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span> exit 1; <span style="color:#66d9ef">fi</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>SSH<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;ssh -i </span>$HOME<span style="color:#e6db74">/.ssh/backup@hades&#34;</span>
</span></span><span style="display:flex;"><span>REMOTE<span style="color:#f92672">=</span>backup@hades.barrera.io
</span></span><span style="display:flex;"><span>TODAY<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>date -I<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Using -H is too expensive, and I don&#39;t use hardlinks in this directory.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Use -x to avoid copying .enc.mount directories (fuse-mounted encrypted</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># data).</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Sync the files:</span>
</span></span><span style="display:flex;"><span>rsync -aqx -e <span style="color:#e6db74">&#34;</span>$SSH<span style="color:#e6db74">&#34;</span> /home/hugo/photos/ $REMOTE:data/photos/$TODAY/ <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --link-dest<span style="color:#f92672">=</span>../latest/
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Link today&#39;s as latest:</span>
</span></span><span style="display:flex;"><span>$SSH $REMOTE <span style="color:#e6db74">&#34;rm data/photos/latest &amp;&amp; ln -sf </span>$TODAY<span style="color:#e6db74"> data/photos/latest&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Stupidly simple, right?</p>
<ul>
<li>Lines 8 make sure the script bails upon error instead of continuing (ie: whenever something exists non-zero).</li>
<li>Line 14 forces ssh to use a specific ssh key which has been added on the host with very limited permissions, and only for the user &ldquo;backup&rdquo;, who owns backups and nothing else. Nobody else has access to <code>backup</code>&rsquo;s files either.</li>
<li>Line 23 does the actual copying. The files are copied to data/photos/2014-08-06, with hardlinks to ../latest (on the server side). As you&rsquo;ll see in a moment, latest is a symlink to the latest <em>successfull</em> backup (the very first day, it was an empty directory, of course).</li>
<li>Line 27 updated the symlink latest to point to today&rsquo;s backup (the script would have bailed if there had been an error). Tomorrow, the files will be compared to today&rsquo;s files to determine what&rsquo;s changed.</li>
</ul>
<p>And, of course, the single line in <code>crontab</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;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>35   5    *    *    *    /bin/sh /home/hugo/.config/cron/scripts/backup-photos.sh
</span></span></code></pre></div><p>A final item was securing the backup. I own the destination server, and the <code>$HOME</code> for user <code>backup</code> is mounted onto an encrypted drive. Should that server reboot, I&rsquo;d get an email, and would need to log in and re-mount that drive manually. This may result in a single backup failing, but I would get <strong>2</strong> emails if it came to that. And a server having been offline is an indicator of a more serious problem.</p>
]]></description></item></channel></rss>