<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>~jlucas/</title>
<link>https://jlucas.codeberg.page/</link>
<description>Posts by jlucas</description>
<language>en-us</language>
<lastBuildDate>Thu, 12 Mar 2026 00:00:00 +0000</lastBuildDate>
<atom:link href="https://jlucas.codeberg.page/index.xml" rel="self" type="application/rss+xml" />
<item>
<title>I Finally Have an RSS Feed</title>
<link>https://jlucas.codeberg.page/posts/20260312-rss.html</link>
<pubDate>Thu, 12 Mar 2026 00:00:00 +0000</pubDate>
<guid>https://jlucas.codeberg.page/posts/20260312-rss.html</guid>
<description><![CDATA[
<p>Subscribe to my RSS feed at <a href="/index.xml">this link</a>. Full post content included.</p>
<p>That's all. Bye now.</p>
<p>For those who stuck around:</p>
<h2>Custom Static Site Generator Part II</h2>
<p>As explained in <a href="/20250117-ssg.html">this post</a>, this site is powered by my custom hacky
SSG. So I had to do some more scripting to implement this new RSS feed
generator. It's really simple as expected (and as the name implies), I just had
to adjust some of my original code so that it could be reused, and fill in some
XML templates:</p>
<pre><code class="language-sh">cat &lt;&lt;-EOF
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;rss version=&quot;2.0&quot; xmlns:atom=&quot;http://www.w3.org/2005/Atom&quot;&gt;
	&lt;channel&gt;
		&lt;title&gt;~jlucas/&lt;/title&gt;
		&lt;link&gt;${url}/&lt;/link&gt;
		&lt;description&gt;Posts by jlucas&lt;/description&gt;
		&lt;language&gt;en-us&lt;/language&gt;
		&lt;lastBuildDate&gt;$(date -Rd &quot;${posts%%-*}&quot;)&lt;/lastBuildDate&gt;
		&lt;atom:link href=&quot;${url}/index.xml&quot; rel=&quot;self&quot; type=&quot;application/rss+xml&quot; /&gt;
		$(items)
	&lt;/channel&gt;
&lt;/rss&gt;
EOF
</code></pre>
<p>This is the outer template. Mostly all constants except for the lastBuildDate
tag, which I am formatting based on the date prefix of the latest post, which
happens to be the first entry in the space separated <code>$posts</code> variable.</p>
<p>The template for the actual posts is expanded from the <code>items</code> function, which
simply fills in another template in a loop:</p>
<pre><code class="language-sh">for fname in $posts; do
	cat &lt;&lt;-EOF
	&lt;item&gt;
		&lt;title&gt;$(gettitle &quot;${srcdir}/posts/${fname}&quot;)&lt;/title&gt;
		&lt;link&gt;${url}/posts/${fname%md}html&lt;/link&gt;
		&lt;pubDate&gt;$(date -Rd &quot;${fname%%-*}&quot;)&lt;/pubDate&gt;
		&lt;guid&gt;${url}/posts/${fname%md}html&lt;/guid&gt;
		&lt;description&gt;&lt;![CDATA[
			$(tail +3 &quot;${srcdir}/posts/${fname}&quot; | render)
		]]&gt;&lt;/description&gt;
	&lt;/item&gt;
	EOF
done
</code></pre>
<p>The <code>gettitle</code> function was explained in <a href="/20250117-ssg.html">part 1</a>, and the <code>render</code> function
simply wraps the code to preprocess and convert the markdown content to HTML.</p>
<p>Then just add it to the Makefile and it's done:</p>
<pre><code>index.xml: $(POSTS) $(SCRIPTS)
	src/ssg/genrss.sh &gt; $@
</code></pre>
<p>You can check out the full code <a href="https://codeberg.org/jlucas/pages/src/branch/master/src/ssg">here</a>.</p>
<p>Expect a Part III whenever I manage to integrate some nice (static, server side)
syntax highlighting to fix those ugly code blocks.</p>
]]></description>
</item>
<item>
<title>Introducing pacc: A Better UNIX Password Manager</title>
<link>https://jlucas.codeberg.page/posts/20260101-pacc.html</link>
<pubDate>Thu, 01 Jan 2026 00:00:00 +0000</pubDate>
<guid>https://jlucas.codeberg.page/posts/20260101-pacc.html</guid>
<description><![CDATA[
<h2>TL;DR</h2>
<ul>
<li>I wrote the <a href="https://codeberg.org/jlucas/pacc">pacc</a> password manager, inspired by <a href="https://www.passwordstore.org/">pass</a>.</li>
<li>Unlike <a href="https://www.passwordstore.org/">pass</a>, <a href="https://codeberg.org/jlucas/pacc">pacc</a> does not use public-key cryptography, which makes no
sense to use when encrypting to yourself.</li>
<li>Unlike <a href="https://www.passwordstore.org/">pass</a>, <a href="https://codeberg.org/jlucas/pacc">pacc</a> uses an encrypted vault file which does not expose
entry names in plain text.</li>
</ul>
<h2>Introduction</h2>
<p>I had been using the <a href="https://www.passwordstore.org/">pass</a> password manager (aka password-store) for a few
years, and it was great in terms of user experience. However it has some design
flaws that impact security, which should be a primary focus when we're talking
about a <em>password manager</em>. So I wrote <a href="https://codeberg.org/jlucas/pacc">pacc</a>, which aims to keep the familiar
user experience of <a href="https://www.passwordstore.org/">pass</a>, but not at the cost of security.</p>
<p><a href="https://codeberg.org/jlucas/pacc">pacc</a> is neither a fork nor a rewrite. While the CLI is inspired by <a href="https://www.passwordstore.org/">pass</a>,
it was built from scratch, and functions entirely differently under the hood.</p>
<h2>The Problem</h2>
<p>While <a href="https://www.passwordstore.org/">pass</a> claims to be simple (which it is from the user's perspective), it
is a shell script that relies on the old complex <a href="https://gnupg.org/">GPG</a>. Even disregarding any
flaws in GPG itself, it is just not the right tool for the job, because it uses
public-key cryptography, which makes no sense in a password manager. Now let me
give you a little cryptography crash course to show you why:</p>
<h3>Cryptography 101</h3>
<p>Let's say Alice wants to send Bob an encrypted message.</p>
<p>Using symmetric-key cryptography, Alice and Bob share a secret key, which is
used for both encrypting and decrypting the messages. It is impossible for
anyone to decrypt their messages without the key. But how do they establish a
secret key? That's where public-key cryptography comes in.</p>
<p>With public-key cryptography, Alice and Bob have a public and a private key
each. They can share their public keys over an insecure channel, and through
some mathematical witchcraft, securely exchange a symmetric key for their chat
session.</p>
<p>Public-key cryptography isn't stronger than symmetric-key cryptography. Quite
the opposite, actually. In this case it is only used to solve the key exchange
problem, after which a symmetric-key algorithm is used.</p>
<h3>Getting back to pass</h3>
<p>Do you see the problem here? A password manager is used to encrypt <em>your</em>
passwords, so that they can only be decrypted by <em>you</em>. There are no other
parties involved, meaning <em>you</em> are both Alice <em>and</em> Bob. With <a href="https://www.passwordstore.org/">pass</a>, you are
using public-key cryptography to exchange a key with <em>yourself</em>! A chain is no
stronger than its weakest link, and <a href="https://www.passwordstore.org/">pass</a> adds a weaker link for no good
reason.</p>
<h3>Encryption Coverage</h3>
<p>Another problem is that <a href="https://www.passwordstore.org/">pass</a> stores each entry in a file, using the
(unencrypted) file path as the entry name. There is no strict rule for how you
should name your entries, but the recommended way is to use &quot;the title of the
website or resource that requires the password&quot;. You might also want to throw
the username in there, or some other useful information. All of this is left
exposed in plaintext in your file system, as well as other metadata such as
timestamps.</p>
<h2>The Solution</h2>
<p>And so I wrote <a href="https://codeberg.org/jlucas/pacc">pacc</a>, which solves the previously mentioned problems, has a
similar CLI (including (pseudo-)directory operations), and is also faster and
uses less disk space. How?</p>
<ul>
<li>Encryption/decryption is done using a symmetric key, derived from a
passphrase. Key files are also supported. No silly key exchange involved.</li>
<li>It stores all password entries (including names) in a single encrypted file
(vault).</li>
<li>It's written in C, uses <a href="https://openssl-library.org/">OpenSSL</a> for cryptography, and a simple custom vault
format.</li>
</ul>
<h3>Why the name?</h3>
<p>Because time spent naming it is time not spent developing it. Also because
<em>pass... but in C... whatever, let's go with pacc</em>.</p>
<h3>What will I miss from pass?</h3>
<p>While some features might be added in the future, others are out of scope, due
to unjustified complexity or reliance on <a href="https://www.passwordstore.org/">pass</a>'s design flaws. Here are some
examples:</p>
<ul>
<li>Anything that requires bypassing the <code>pass</code> script and accessing the files in
~/.password-store directly: Though that is convenient, it is based on the flaw
of having plaintext entry names, and so <a href="https://codeberg.org/jlucas/pacc">pacc</a> doesn't support it.</li>
<li>Git integration: While you can version-control your vault, it isn't built into
<a href="https://codeberg.org/jlucas/pacc">pacc</a>, and it isn't possible to see which entries have changed (because
they are all contained in one encrypted binary vault).</li>
<li>Extensions: There is currently no extension support, though I plan on
implementing some features from <a href="https://www.passwordstore.org/">pass</a> extensions, such as OTP support,
either as built-ins, or compile-time options, or maybe suckless style patches.
Suggestions and contributions are welcome.</li>
</ul>
<p>Additionally, some features such as <a href="https://www.passwordstore.org/">pass</a>'s QR code options weren't added
since they can be easily achieved by piping the output of <a href="https://codeberg.org/jlucas/pacc">pacc</a> to another
program.</p>
<h3>How to migrate?</h3>
<p>To switch from <a href="https://www.passwordstore.org/">pass</a> to <a href="https://codeberg.org/jlucas/pacc">pacc</a>, initialize a new vault using <code>pacc init -i</code>
(omit -i for default parameters), then simply run the included <code>pass2pacc</code>
script to import all your password-store entries.</p>
<h3>How to use?</h3>
<p>For most operations, if you've been using <a href="https://www.passwordstore.org/">pass</a>, you'll feel right at home.
The biggest differences are in the init command, for obvious reasons. Run <code>pacc help</code> and <code>pacc help SUBCOMMAND</code> do learn more. Also there are bash and zsh
completions.</p>
<h2>Conclusion</h2>
<p><a href="https://www.passwordstore.org/">pass</a> is cool and all, but it puts UNIX philosophy above security. <a href="https://codeberg.org/jlucas/pacc">pacc</a>
aims to be as close to <a href="https://www.passwordstore.org/">pass</a> as possible without compromising security,
though it ended up with many fundamental differences under the hood due to the
fact that many advantages of <a href="https://www.passwordstore.org/">pass</a> depend on its design flaws.</p>
<p>Also, public-key cryptography is great when we need it. Not for storing
passwords.</p>
<p>And it should go without saying but I mean no disrespect to the <a href="https://www.passwordstore.org/">pass</a>
project and its contributors and users. Your threat model, your choice.</p>
<!-- Links -->
]]></description>
</item>
<item>
<title>You've been using PGP wrong</title>
<link>https://jlucas.codeberg.page/posts/20250713-pgp-airgap.html</link>
<pubDate>Sun, 13 Jul 2025 00:00:00 +0100</pubDate>
<guid>https://jlucas.codeberg.page/posts/20250713-pgp-airgap.html</guid>
<description><![CDATA[
<p>Unless you have been using it right, in which case you can stop reading now.</p>
<p>TL;DR: Your master key should be airgapped. All your keys should expire. You
should have revocation certificates in case you lose your master key. You should
back it up safely so you don't lose it.</p>
<p>This post assumes you know what PGP is and have a rough understanding of how
public key cryptography works. Also it is based on <a href="https://wiki.debian.org/GnuPG/AirgappedMasterKey">this tutorial</a> which you
should read for more details.</p>
<h2>Quick PGP Refresher</h2>
<p>To understand what we'll be working with, here are the core concepts that you
should understand:</p>
<ul>
<li><strong>Master key</strong>: the key that you use to sign subkeys and identities;</li>
<li><strong>Subkeys</strong>: the keys that you use to encrypt/decrypt and sign/verify
arbitrary data;</li>
<li><strong>Identities</strong>: names and email addresses associated with the master key.</li>
</ul>
<h2>Airgapped Master Key</h2>
<p>Most of the actions that you do with PGP only require subkeys. You only really
need your master key when you want to add new subkeys or identities, or change
something about the existing ones.</p>
<p>That means you can (and should) keep only your subkeys on your day-to-day
devices, and access the master key only on a secure isolated offline device.
Once you are done with your master key stuff, you just export everything except
the master key itself to a removable storage device and import it on your other
devices. That way, if one of your devices gets compromised you can just revoke
the affected subkeys, while the master key remains secure.</p>
<p>If you haven't done this with your current key, you might want to consider
retiring it and starting fresh, generating a new key from the airgapped system.</p>
<h3>Example Setup</h3>
<p>Use an old SBC or laptop that you have lying around, that will never be
connected to a network ever again. Run a live OS on it. You will need a
persistent partition for storing the private master key, which could be on a USB
stick or on the device itself, as long as it stays offline. Export the subkeys
to a <em>different</em> USB stick. You should make backups of the master key, but be
sure to only access them from the airgapped system.</p>
<h3>In Practice</h3>
<p>Alright, that sounds fun, but how do you do it? I'm assuming you know how to
create a PGP key, so go ahead and do that on your airgapped system. After that,
you'll want to export only the subkeys, which can be done with the command:</p>
<pre><code class="language-sh">gpg --export-secret-subkeys -o secret-subkeys.gpg
</code></pre>
<p>Then you move <code>secret-subkeys.gpg</code> to your USB stick and import it normally on
your other devices. Also you should export the public keys whenever you change
identities or expiration dates or whatever.</p>
<h3>Revocation Certificates</h3>
<p>You should keep a few of these around in case you lose your master key. To
generate them, run the following command from your airgapped system (replace
$KEY with your actual key ID):</p>
<pre><code class="language-sh">gpg --gen-revoke $KEY
</code></pre>
<p>In order to actually revoke the key, import the resulting certificate so that it
is merged with your key. Then you need to publish it to a keyserver or share it
in some other way so that people know it is revoked. Keep this certificate
somewhere safe. Anyone with access to it can revoke your master key.</p>
<h2>Rotating Keys</h2>
<p>Don't hold on to your subkeys for eternity. It is good practice to rotate them
every once in a while. For that, set them to expire, and generate new ones to
replace them once they're approaching EOL. The lifetime of your keys will depend
on your threat model. Rotating them more frequently is better for security, but
it is very inconvenient.</p>
<p>As for the master key, things get a bit more complicated. Rotating a master key
would mean revoking it and starting the whole process from scratch, having to
send it to everyone and rebuild trust each time. So if you are confident that
your airgapped system is truly airgapped, and that the key is too strong to have
been cracked since it was created, then it might be best to keep it around.
Either way, you should also set an expiry date on it, and extend it when needed
instead of rotating. This will ensure the key eventually expires if you lose it
or die or something.</p>
<h2>Conclusion</h2>
<p>Airgapping your master key greatly increases security, at the cost of having to
boot into the airgapped system every now and then to edit and export stuff.
I hope this made you rethink your PGP OPSEC. Read the <a href="https://wiki.debian.org/GnuPG/AirgappedMasterKey">linked tutorial</a> if
you want an actual step-by-step guide.</p>
]]></description>
</item>
<item>
<title>PinePhone To The Rescue...?</title>
<link>https://jlucas.codeberg.page/posts/20250329-pinephone.html</link>
<pubDate>Sat, 29 Mar 2025 00:00:00 +0000</pubDate>
<guid>https://jlucas.codeberg.page/posts/20250329-pinephone.html</guid>
<description><![CDATA[
<p>Yes, <a href="/posts/20250209-phones.html">I hate phones</a>. Yes, I own a phone. Yes, it is a <a href="https://pine64.org/devices/pinephone/">PinePhone</a>.</p>
<p>This is Part 2 of my <a href="/posts/20250209-phones.html">previous article</a> about why phones suck, and I
encourage you to read Part 1 before taking any of this as advice, since this is
not quite the PinePhone review you are looking for. A lot of what I say here
about the PinePhone applies to any phone running any of the Linux distros you
would typically run on a PinePhone.</p>
<h2>Introduction</h2>
<p>The TL;DR for Part 1 is that it's bad that society expects you to carry a phone
all the time and install a bunch of proprietary apps for things that shouldn't
even require electricity. Therefore the answer for the title is: no, the
PinePhone won't magically solve all of society's problems, but it <em>can</em> be quite
an upgrade in terms of freedom and privacy if you are not expecting to have full
spyware compatibility.</p>
<h2>Some Positive Notes</h2>
<p>I got carried away and ended up making this too much of a Part 2 on hating
phones instead of focusing on the PinePhone, so let's insert some nice things
here before it all gets ugly.</p>
<p>Linux phones get it right when it comes to making mobile-friendly interfaces:
They run full desktop Linux systems, with a friendly mobile interface on top
(optional). You still have access to all the desktop stuff you could possibly
need, including root access, terminal access (including Linux virtual
terminals), any desktop app that is totally not meant for a phone, SSH daemon,
partitioning and mounting internal/removable storage, literally everything. Most
of the &quot;mobile apps&quot; shipped with these distros are just desktop apps that adapt
to small screens. This shows that it is possible to have a functioning mobile
device without all the dumb Android limitations. <em>Phones are computers</em>.</p>
<p>Another great thing is that the PinePhone can boot from a microSD card out of
the box. That means you can easily switch the operating system without having to
sacrifice your child into a volcano and do the bootloader unlock dance.</p>
<h2>My Setup</h2>
<p>To give you an idea of the freedom you get, here is how I have my PinePhone set
up:</p>
<ul>
<li>OS: Arch Linux ARM. There is a community maintaining a <a href="https://github.com/dreemurrs-embedded/Pine64-Arch/releases">mobile friendly
version</a> that <em>just works</em>, but I installed it the Arch way instead;</li>
<li>Window Manager: dwm. Yep, the suckless one. On a phone. To be honest I didn't
think it would be usable at first, but a project called <a href="https://sxmo.org/">sxmo</a> went and did
it before me, and I took some inspiration and made my own build;</li>
<li>Phone stuff: for calls and SMS, I wrote my own scripts that interface with
<a href="https://www.freedesktop.org/wiki/Software/ModemManager/">ModemManager</a>.</li>
</ul>
<p>That's basically it, I use it as if it were a desktop and it <em>works</em>. Of course
it is <em>very</em> limited by the form factor, but it's the path I took. I just do
most stuff on the terminal and launch graphical programs with dmenu. To make it
suck a little less, I make use of many single-letter aliases and scripts.</p>
<p>The point here is, although I wouldn't really recommend it, you are free to do
crazy stuff like this.</p>
<h2>Signal</h2>
<p>There is a lot I dislike about the <a href="https://signal.org">Signal</a> messenger, but
that's not for this post.</p>
<p>There are several ways to use Signal on a Linux phone. There are third party
clients such as <a href="https://github.com/axolotl-chat/axolotl">Axolotl</a>, which support registering a new account, but using
a phone as the master device is dumb (I still hate phones, remember?). I use
<a href="https://github.com/AsamK/signal-cli">signal-cli</a> on the desktop for that, and on the PinePhone I just link the
desktop app to it. Once again, yep, the Signal-Desktop app, on a phone. It's...
usable.</p>
<p>Unfortunately there is no official Linux ARM support from Signal, so I use <a href="https://github.com/dennisameling/Signal-Desktop">this
unofficial build</a> instead. Use it at your own risk.</p>
<h2>Waydroid</h2>
<p>It is possible to run Android apps on the PinePhone through <a href="https://waydro.id/">Waydroid</a>. But
should you? I think that defeats the purpose of using a Linux phone. If you are
going to run Android on top of it, you might as well run it natively without the
extra complexity and overhead. Also the main use case for it would be supporting
proprietary spyware that you shouldn't be running anyway.</p>
<p>Once again, <em>this is not a review</em>. I personally don't use Waydroid. Some people
say it works great. Good for them. That's not the point.</p>
<h2>The Paradox</h2>
<p>This is basically how being a phone-hating-FOSS-enthusiast-PinePhone-owner
feels:</p>
<ul>
<li>I want to build stuff from scratch and be in control of my device;</li>
<li>If you've been paying attention, you know I don't particularly like phones;</li>
<li>Therefore I don't feel like wasting time improving my phone;</li>
<li>I end up with a worse experience than I would have if I put a little more
effort into it;</li>
<li>Even considering switching to a &quot;just works&quot; distro is too much of a chore
since I really don't care enough, and also because this list wraps around.</li>
</ul>
<h2>And Now Some Negative Notes</h2>
<p>Before that let me emphasize that some of the bad stuff here might be specific
to my setup, and not to the PinePhone in general. Also I can't comment on other
Linux phones.</p>
<ul>
<li>The camera sucks: I haven't been keeping up with the latest development, but
last I checked there was only <a href="https://gitlab.com/megapixels-org/Megapixels">one specific app</a> that knows how to access
it. It is not usable in web browsers for example. The supported camera app
always crashes for me when I try to take a picture. I think it only crashes on
X11 (biggest mobile-friendly UIs use Wayland). Also the image quality isn't
great;</li>
<li>The modem can be unstable sometimes: The thing that makes the Pine<em>Phone</em> a
<em>phone</em> is not very reliable. There have been improvements but I'm still not
confident in its ability to do a bunch of tasks without a reboot;</li>
<li>Call audio sucks: It's too quiet on my end, and sounds like shit on the other
end;</li>
<li>Battery life isn't great. That's not much of a problem for me because I keep
it powered off all the time.</li>
</ul>
<p>That being said, I do appreciate all the work that volunteers are doing to
improve things.</p>
<p>And now back to <em>why all phones will always suck</em>.</p>
<h2>Crippled Pocket Computers</h2>
<p>Let me elaborate on this quote from <a href="/posts/20250209-phones.html">Part 1</a>:</p>
<blockquote>
<p>phones will always be crippled pocket computers, either because there <em>is</em> too
much abstraction in order to fit the form factor, or because there <em>isn't</em>.</p>
</blockquote>
<p>You can absolutely do <em>computer stuff</em> on the PinePhone (or other Linux phones,
or even Android to some extent), but <em>computer stuff</em> usually requires a larger
screen and a keyboard. So if you treat a phone like a computer, it's powerful
but hard to use, and if you abstract away all the mobile-unfriendliness, you end
up with a frustratingly limited toy with a cute shiny UI.</p>
<h3>Virtual Keyboards</h3>
<p>They always suck:</p>
<ul>
<li>They cover a significant portion of the screen, giving you even less space to
work with;</li>
<li>You can't feel the tiny keys on your fingertips going down and up as you press
them. The best you'll get is an artificial vibration;</li>
<li>Typing should be done with 10 fingers, not just 2 thumbs;</li>
<li>Layouts suck, either because they only show a small subset of the keys on the
main layer or because they show too many and it gets all cluttered.</li>
</ul>
<p>Keyboard-driven workflows are just <em>superior</em> on the desktop, but painful on
mobile. So you can either get used to it or give in to the high level toy
interfaces.</p>
<h2>Conclusion</h2>
<p>I still hate phones. Linux phones are great for what they are, but they are
still <em>phones</em>. You get a whole lot more freedom from them compared to Android
and iOS phones, but freedom always comes at the cost of not being spoon-fed
stuff that <em>just works</em> for the average blue pill junky, and instead having to
make choices and set things up yourself.</p>
<p>If you are a FOSS enjoyer, and you think my opinions on phones are too extreme,
then by all means give Linux phones a try. If instead you totally agree, then
take a step further and go phoneless.</p>
<p>Now I can hopefully quit yapping about hating stuff and write about some more
joyful subjects. Hopefully.</p>
]]></description>
</item>
<item>
<title>I Hate Phones</title>
<link>https://jlucas.codeberg.page/posts/20250209-phones.html</link>
<pubDate>Sun, 09 Feb 2025 00:00:00 +0000</pubDate>
<guid>https://jlucas.codeberg.page/posts/20250209-phones.html</guid>
<description><![CDATA[
<p>They <em>can</em> be quite convenient if you are willing to put up with all the
surveillance and ads and arbitrary restrictions to user freedom and that
inconsiderate shithead sitting next to you on the bus scrolling through loud
TikToks or talking on speakerphone.</p>
<p>In this post I'll be bitching some more about why phones generally suck and you
shouldn't use one most of the times, so keep on reading if that's something that
interests you.</p>
<p>Note: this is mostly aimed towards <em>smartphones</em>, but many things also apply to
the dumb ones.</p>
<p>UPDATE 2025-07-13:
Just came across <a href="https://videos.lukesmith.xyz/w/rdKQ7MVTBrMpqxgzXRa5n9">this video</a>
by <a href="https://lukesmith.xyz">Luke Smith</a> on the same subject. Totally agree.
Go watch it.</p>
<h2>Surveillance Gold Mine</h2>
<p>Most phones ship spyware out of the box. And most users go out of their way to
install some more spyware on top. Phones are just the perfect surveillance gold
mine: with all the cameras and microphones and sensors and the fact that most
people voluntarily carry them around everywhere, always powered on and connected
to the internet, their potential for data collection is far beyond what you
would get from your traditional desktop spyware.</p>
<p>Even if you are smart enough not to install random spyware into your phone, it's
likely that <em>everyone around you</em> is not, and is unknowingly snitching on you
with their pocket surveillance devices.</p>
<p>Also if your device gets pwned through some zero-day exploit, despite your best
efforts to keep it secured, the cyber bad guys who don't wear suits will also
get to enjoy all the juicy data your phone can provide.</p>
<h2>Too Much Abstraction</h2>
<p>That thing in your pocket is actually a <em>computer</em>. But likely has an OS that
tries to pretend otherwise, by abstracting away a bunch of stuff. <em>Pictures? Oh
yeah, those are stored in the gallery app. Absolute paths? File systems? You're
talking nonsense, hackerman! This is a phone!</em></p>
<p>There might be valid reasons for this to some extent: You don't usually have a
keyboard hooked up to a phone, just like you don't usually have a touchscreen on
your desktop computer. It has to take some shortcuts to be <em>convenient</em> to
use. However it ends up getting in the way of advanced users. The concept of
&quot;program&quot; is abstracted away by these &quot;mobile apps&quot;, with mandatory complexity
to look and behave consistently. It's nice to have a consistent user experience,
but if the best tool for the job looks ugly and out of place, then so be it.</p>
<p>Not only is this annoying for people who want to do <em>computer stuff</em>, it also
gives a false sense of security. After all, <em>what harm could this random app
that I don't really need possibly do? Look how cute its shiny little icon looks
sitting on my home screen</em>. Most people are to some degree aware of the dangers
of installing random software on computers, but phones? <em>The trusted <del>ad</del> app
store says all the cool kids are using this app, so I must have it</em>.</p>
<p>This makes people more willing to install random apps on a phone than they would
be on a desktop computer. And don't get me started on the dumb low effort mobile
games whose sole purpose is rewarding you with fake points for microtransactions
or voluntarily watching ads, aside from the ones they were gonna shove down your
throat anyway.</p>
<p>To summarize, the main point here is that <em>mobile apps</em> are viewed as something
entirely different from <em>desktop computer programs</em> for artificial reasons,
resulting in harm to the freedom of users and developers.</p>
<h2>Fake Restrictions</h2>
<p>Certain platforms restrict functionalities for made up reasons. Wanna play music
from YouTube in the background (don't do that, own your audio files)? That'll be
$13.99/month, fuck you. Wanna view certain message formats on Instagram? Sorry
buddy, the web version won't do, you'll need the app for that!</p>
<p>Then there are all those mobile apps that serve as the only authorized way to
access some online service. Most of the times that is either <em>stupid</em> or <em>evil</em>.
Your dumb mobile app could very well be a <em>web app</em>. But you just can't fit
enough spyware in a web app, can you? If you are going to milk your users for
data, might as well force them into more spyware-friendly platforms.</p>
<p>Take WhatsApp for example. It has valid reasons to require a native app, such as
the alleged end-to-end encryption. But the only way to use it is to have that
one spyware app running on your spyware device. Signal is far from perfect, but
at least there are non-official ways to use it without ever installing the
mobile app, such as <a href="https://github.com/AsamK/signal-cli">signal-cli</a>. And
that's the beauty of FOSS.</p>
<h2>Not Much Choice</h2>
<p>So far a lot of what I said was about shitty software running on phones, and
not about <em>phones</em> themselves being shitty. So you could just use non-shitty
software, right? If we're talking about the operating system, sure, as long as
you are able to replace it without bricking the phone. But of course the better
alternatives won't magically support the shitty mobile apps you might need,
because the term &quot;mobile app&quot; usually just means &quot;Android/iOS app&quot;, and has
nothing to do with the hardware it runs on. Compatibility layers and emulation
defeat the purpose and are overly complex solutions for a fabricated problem.</p>
<p>That being said, let's take a look at the 2 only options that non-hackers use.
I'll leave <em>real</em> mobile Linux for the upcoming PinePhone article.
UPDATE 2025-03-29: <a href="/posts/20250329-pinephone.html">Here it is</a>.</p>
<h3>iOS</h3>
<p>This heading is only here for completeness. Nothing to say here other than
<em>just don't use Apple's proprietary trash</em>.</p>
<h3>Android</h3>
<p>A lot of what I said so far was with Android in mind, so I won't be repeating
it. Here are some other things that piss me off about this compromised OS:</p>
<h4>No Root Access</h4>
<p>You should <em>avoid</em> running things as root for security reasons. That was never a
phone specific concept, and should not have had the phone specific solution of
taking away the ability to do so. You <em>need</em> root access if you want to <em>own</em>
your device. If you think it's okay to take freedom away from users for their
own safety, you might as well put on a straitjacket and go live in a padded cell
to protect yourself from the dangers of paper cuts and shark attacks.</p>
<h4>Can't Remove Stock Spyware</h4>
<p>The vast majority of Android phones you can buy ship with a lot of proprietary
spyware sprinkled on top, that cannot be uninstalled by regular users. Some of
the built in spyware trash only provides the option to uninstall updates, i.e.
downgrade the spyware. Some can also be &quot;disabled&quot;, which is like uninstalling
it except it's still there. I can't think of any good (as opposed to evil)
reason for this, considering that uninstalling these apps would not break the
system in any way if you weren't using them. But you could just use the
open-source part of Android without all the shit smeared on top, right?</p>
<h4>Kinda Open-Source-<em>ish</em> But Not Really</h4>
<p>Most people just use stock spyware. Maybe you can get some freedom with Android
if you jump through enough hoops. But let me tell you the story of when I tried
building a simple &quot;hello world&quot; app for Android using FOSS only. Spoiler: I gave
up.</p>
<p>I was trying to install the Android SDK, which is supposedly open-source, when I
was asked to accept a EULA. <em>Something was very clearly wrong</em>. So apparently
the official binary distribution is nonfree. Very well, let's build it from
source. After all I'm a Gentoo user, that's what I do. The problem is there
don't seem to be any <em>good</em> instructions on how to do so: you either run a
script that ends up pulling the proprietary binary sources from somewhere else,
or you use Google's git-based repo management thingy that pulls from a bunch of
repos and then you supposedly get to build the thing. Well that sounds <em>sketchy
as hell</em>! It's almost like they don't want you using the free version (who
would've guessed?). Screw this, I'm outta here!</p>
<p>After writing the previous paragraph I went and gave it another shot to make
sure I wasn't being unfair. So I installed the git wrapper thingy and tried to
clone the sources as instructed <a href="https://android.googlesource.com/platform/sdk/+/master/docs/howto_build_SDK.txt">here</a>. Result: after 20 minutes and 7%
progress, it had fetched 17 GB. Also it logged a bunch of errors to the console
about not being able to fetch some repos. I remind you I only wanted to build a
&quot;hello world&quot;. For reference, the LibreWolf browser's unpacked sources take up
4.4 GB and my kernel directory (including compiled objects) takes up only 2.5
GB. It turns out it was fetching the whole Android sources, not just the SDK,
but I couldn't find any docs on how to fetch only the SDK components (assuming
it doesn't actually require the whole thing), and if the instructions are not in
the file called &quot;howto_build_SDK.txt&quot;, then I don't know where else they would
be.</p>
<p>I'd love to be proven wrong on this one, so if you know of an officially
documented way to get a free SDK that doesn't try to roofie you into using the
nonfree release every step of the way, and can be built/installed completely
offline after getting the sources, then please <a href="/contact.html">let me know</a>.</p>
<h2>You Don't Own Your Phone Number</h2>
<p>This starts the &quot;<em>phones</em> themselves being shitty&quot; section.</p>
<p>It is ridiculous that so many services use phone numbers for verifications and
SMS-based 2FA. Securing your number from SIM-swapping attacks is outside of your
control. All you can do is hope your carrier does not fuck it up.</p>
<h2>Pinging Cell Towers</h2>
<p>Your phone does this all the time regardless of how &quot;smart&quot; it is marketed as.
Whoever controls the towers can track your location.</p>
<h2>Conclusion</h2>
<p>I've been writing this post for over 3 weeks now, let's wrap it up, we've all
got better stuff to do.</p>
<p>Phones are everywhere and society expects you to carry one. That's harmful for
freedom and privacy, since it's impossible to have a phone that does not milk
you for your data and at the same time meets society's expectations of being
able to run all the fancy exclusive &quot;mobile apps&quot; made for Android/iOS. So the
main problem is this proprietary &quot;mobile app&quot; trend.</p>
<p>It's nice that there are projects aiming to bring user freedom to mobile
platforms, and I don't see much of a problem with using them <em>responsibly</em>, but
phones will always be crippled pocket computers, either because there <em>is</em> too
much abstraction in order to fit the form factor, or because there <em>isn't</em>. And
like with desktop computers, there is a time to use them, and there is a time to
<em>touch grass</em>. So <em>get off that damn phone!</em></p>
]]></description>
</item>
<item>
<title>Speeding up OpenRC Startup</title>
<link>https://jlucas.codeberg.page/posts/20250121-openrc-async.html</link>
<pubDate>Tue, 21 Jan 2025 00:00:00 +0000</pubDate>
<guid>https://jlucas.codeberg.page/posts/20250121-openrc-async.html</guid>
<description><![CDATA[
<p>This guide contains some tips and tricks to get faster boots with <a href="https://wiki.gentoo.org/wiki/Project:OpenRC">OpenRC</a>.</p>
<h2>TL;DR</h2>
<ul>
<li>Move non-critical services to a separate custom runlevel;</li>
<li>Write a boot script that switches to that runlevel in the background, allowing
you to log in while some services finish starting up;</li>
<li>Set rc_parallel to &quot;YES&quot; in /etc/rc.conf.</li>
</ul>
<h2>The Problem</h2>
<p>By default OpenRC will start all services in the runlevel before it presents you
a login prompt (specifically referring to the <code>getty</code>, I don't use a display
manager so I cannot comment on those). This takes up a lot of boot time for no
good reason. Do you really need your network related services to be running
<em>before</em> you log into your desktop? Couldn't they just start in the background,
and be done by the time your bloated web browser finally opens?</p>
<p>I couldn't find any properly documented solution for this problem, so I had to
come up with a little hack. The best I could find was this <a href="https://redlib.northboot.xyz/r/Gentoo/comments/m6a28y/openrc_can_we_start_services_later/">Reddit thread</a>,
which describes the exact problem but does not provide a direct solution. It
does however mention the use of custom runlevels, which I did end up using.</p>
<h2>The Solution</h2>
<p>Here's an overview of the hacky solution I came up
with:</p>
<ul>
<li>First of all, we create a custom runlevel;</li>
<li>Secondly, we move all services from the <code>default</code> runlevel that we wish to
start in the background into it;</li>
<li>Then we stack the <code>default</code> runlevel onto it;</li>
<li>Finally we write a little boot script that switches to our custom runlevel
asynchronously, starting all required services in the background.</li>
</ul>
<p>Let's break it down.</p>
<h3>Creating a Custom Runlevel</h3>
<p>I called mine <code>async</code>. Name it whatever you want. Run the following as root:</p>
<pre><code>mkdir /etc/runlevels/async
</code></pre>
<p>As easy as that.</p>
<h3>Moving Stuff Over</h3>
<p>Now we will move most of the services from the <code>default</code> runlevel into <code>async</code>.
In my case it was safe to move all except for the <code>getty</code>s and <code>local</code> (you may
not have <code>getty</code>s there if booting with <code>sysvinit</code>). The easiest approach here
is probably to directly move the symlinks from <code>/etc/runlevels/default/</code> into
<code>/etc/runlevels/async/</code>, although you can also use utilities such as <code>rc-update</code>
as you normally would. Here's an example (run as root, add or remove services as
needed):</p>
<pre><code>cd /etc/runlevels/default
mv net.* sshd chronyd distccd tor libvirtd ../async
</code></pre>
<h3>Stacking Runlevels</h3>
<p>Runlevel stacking is what they call adding a runlevel to another runlevel, i.e.
inheriting services from another runlevel. We need this because otherwise the
services from the <code>default</code> runlevel would be stopped upon switching to the
<code>async</code> runlevel (unless they were also added to it). Run the following as root:</p>
<pre><code>rc-update add -s default async
</code></pre>
<h3>Starting It Asynchronously on Boot</h3>
<p><strong>Note</strong>: If you use OpenRC to start the <code>getty</code>s, e.g. you use <code>openrc-init</code>,
then it would suffice to boot into the <code>async</code> runlevel by appending
<code>softlevel=async</code> to the kernel cmdline. But the <code>getty</code> will get buried by the
console logs of the services in <code>async</code>, so you might prefer these next methods
that also don't involve messing with the kernel parameters.</p>
<p>I find that the simplest way to achieve this is through a custom script in
<code>/etc/local.d/</code>, but for some more complex setups it might be better to write an
init script instead (i.e. in <code>/etc/init.d/</code>) and add it to the <code>default</code>
runlevel. The local.d approach will work as long as you have the <code>local</code> service
enabled in the <code>default</code> runlevel, which at least in Gentoo should be there if
you haven't removed it.</p>
<p>Simply create the file <code>/etc/local.d/async.start</code> (or anything in that directory
ending in .start) with the following contents:</p>
<pre><code>#!/bin/sh
/sbin/openrc async &amp;
</code></pre>
<p>And make it executable: (as root)</p>
<pre><code>chmod +x /etc/local.d/async.start
</code></pre>
<p>And you're all set! All the script is doing is switching to the <code>async</code>
runlevel, using the '&amp;' character to background the process.</p>
<h2>Bonus: Parallel Service Startup</h2>
<p>You might already know this, but here is a general tip for faster OpenRC boots:
Enable parallel service startup by editing <code>/etc/rc.conf</code> and ensuring that you
have an uncommented line with <code>rc_parallel=&quot;YES&quot;</code>. This will considerably speed
up the boot process but won't fix the main issue I've described. However it is
safe to use together with the solution that I've demonstrated here.</p>
<h2>Conclusion</h2>
<p>And just as I was about to post this, I checked again for existing solutions,
and guess what I found: <a href="https://ptrcnull.me/posts/openrc-async-services/">NEARLY THE EXACT SAME THING POSTED BY SOMEONE ELSE 3
YEARS AGO</a>! With the same runlevel name and all. I swear on my life it's the
first time I'm looking at it.</p>
<p>Seriously though, go check it out if you're using <code>sysvinit</code>, it seems cleaner
than my approach. It will not work for <code>openrc-init</code> however, which means my
article is still <em>kinda</em> relevant and I'm <em>slightly</em> less mad.</p>
]]></description>
</item>
<item>
<title>Making a simple Static Site Generator</title>
<link>https://jlucas.codeberg.page/posts/20250117-ssg.html</link>
<pubDate>Fri, 17 Jan 2025 00:00:00 +0000</pubDate>
<guid>https://jlucas.codeberg.page/posts/20250117-ssg.html</guid>
<description><![CDATA[
<p>Static Site Generators can be very simple. You take your markdown files, you
convert them to HTML and you stick them in a template. Sure, there are some
extras that are nice to have, and some of them can also be easily scripted. In
this article I will explain how I generate this site using a Makefile and a
shell script.</p>
<h2>TL;DR</h2>
<p>I show you how I build this site using a script called from a Makefile, that
converts markdown files to HTML with some additional processing.</p>
<h2>Converting Markdown to HTML</h2>
<p>If you're feeling adventurous, implement it from scratch. That's not what I did
here, I felt this was a wheel not worth reinventing. There are several tools you
can use for this. I chose <a href="https://github.com/github/cmark-gfm">cmark-gfm</a> (GitHub's fork of <a href="https://github.com/commonmark/cmark">cmark</a>).</p>
<p>The syntax is as simple as:</p>
<pre><code>cmark-gfm file.md &gt; file.html
</code></pre>
<p>It can also read from stdin in case you need to do some preprocessing:</p>
<pre><code>my-preprocessing-function | cmark-gfm &gt; file.html
</code></pre>
<h2>Putting it in a Makefile</h2>
<p>Makefiles are great since they only rebuild files if their source was updated.
Check out the great tutorial in the references<sup class="footnote-ref"><a href="#fn-1" id="fnref-1" data-footnote-ref>1</a></sup>  to learn more. Let's start
putting one together with what we've learned so far. What we want is to take all
the markdown files in the current directory and convert them to HTML files with
the same name and different extension. We start by listing the files that we
will be working with:</p>
<pre><code>MD   := $(wildcard *.md)
HTML := $(MD:%.md=%.html)
</code></pre>
<p>Here we're first using a wildcard to match all .md files in the current
directory and put them in the <code>$(MD)</code> variable. Then we use a <em>Substitution
Reference</em><sup class="footnote-ref"><a href="#fn-2" id="fnref-2" data-footnote-ref>2</a></sup> to replace all the .md extensions in <code>$(MD)</code> with .html, and save
it in the <code>$(HTML)</code> variable, which now contains all our targets.</p>
<p>Now we need a rule to build our targets:</p>
<pre><code>all: $(HTML)

%.html: %.md
	cmark-gfm $&lt; &gt; $@
</code></pre>
<p>Here we're saying that the .html files should be built from the corresponding
.md files using <code>cmark-gfm</code>. The special variables <code>$&lt;</code> and <code>$@</code> mean the name
of the first prerequisite (.md file) and the name of the target (.html file),
respectively. <sup class="footnote-ref"><a href="#fn-3" id="fnref-3" data-footnote-ref>3</a></sup></p>
<p>The &quot;all&quot; target (or whatever you decide to name it) must be specified and
depend on the files in the <code>$(HTML)</code> variable.</p>
<h2>Preprocessing</h2>
<p>That's cool and all, but our HTML files are still lacking some important stuff,
like hmm... I don't know, <em>a &lt;html&gt; tag maybe</em>? I suppose you could do
something as ugly as <code>echo</code>ing/<code>cat</code>ing some template HTML to prepend/append to
the generated one, with all your headers and stuff, <del>but let's do something
<em>slightly less ugly</em></del>. Oh wait, never mind, that's almost exactly what I did.
Let's put it in a shell script, we'll get back to the Makefile soon. Here is the
main thing (we'll get to the functions later):</p>
<pre><code>cat &lt;&lt;-EOF
&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
	&lt;meta charset=&quot;utf-8&quot;&gt;
	&lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;/css/style.css&quot;/&gt;
	&lt;title&gt;~jlucas/$(gettitle &quot;$input&quot;)&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
	&lt;header&gt;
		$(cat &quot;${srcdir}/templates/header.html&quot;)
	&lt;/header&gt;
&lt;main&gt;
	$(preprocess &quot;$input&quot; | cmark-gfm --unsafe -e strikethrough -e table -e footnotes)
&lt;/main&gt;
&lt;footer&gt;
	$(cat &quot;${srcdir}/templates/footer.html&quot;)
&lt;/footer&gt;
&lt;/body&gt;
EOF
</code></pre>
<p>Yes, a lot of it is hardcoded. Yes, I could've used a <em>real</em> template engine.
But this is specific for this site and I don't plan on changing this structure
for specific files, so <em>who cares?</em></p>
<p>With that out of the way, let's break it down. In the middle of this hardcoded
mess, you can find some <code>$(shell substitutions)</code>. They are used to insert the
page title in the &lt;head&gt;, the navigation bar in the &lt;header&gt;, the converted
HTML in &lt;main&gt; and the footer in the &lt;footer&gt;.</p>
<p>The header and footer are pretty simple. I just do a little <code>cat</code>ception to
include them from static manually written files.</p>
<p>As for the title, I'm taking it from the heading on the first line of the
markdown file. Here's the function:</p>
<pre><code>gettitle() {
  sed -n '1s/^#* *//p' &quot;$1&quot;
}
</code></pre>
<p>The regex is just removing any '#' symbols that might be prepended.</p>
<p>The final piece we're missing here is the <code>preprocess</code> function. This will
depend on the use case and might not always be needed. All I use it for at the
time of writing is to auto generate a list with all posts. For that I make it
search each line of the input file for the expression &quot;@POSTS@&quot; and replace it
with the actual list. Here it is:</p>
<pre><code>preprocess() {
  while IFS= read -r line; do
    case &quot;$line&quot; in
      '@POSTS@') listposts ;;
              *) printf '%s\n' &quot;$line&quot; ;;
    esac
  done &lt; &quot;$1&quot;
}
</code></pre>
<p>Basically it reads each line of the markdown file, and either prints it as is or
lists the posts. The &quot;IFS=&quot; is needed to prevent it from removing leading
whitespace. <em>And what's that <code>listposts</code> thing?</em> Oh, right, it's yet another
function. I promise it's the last one. Here it is:</p>
<pre><code>listposts() {
  find &quot;${srcdir}/posts&quot; -type f -regextype posix-extended \
                         -regex '.*/[0-9]{8}-.*\.md' -printf '%f\n' |
  sort -r |
  while read -r file; do
    filedate=&quot;$(date -d &quot;${file%%-*}&quot; +%F)&quot;
    printf '+ [%s] [%s](%s)\n' \
      &quot;$filedate&quot; \
      &quot;$(gettitle &quot;${srcdir}/posts/${file}&quot;)&quot; \
      &quot;/posts/${file%md}html&quot;
  done
}
</code></pre>
<p>I decided to name my posts with a prepended date, in the format
<em>YYYYMMDD</em>-<em>title-goes-here</em>.md. That way I avoid having to store some metadata
elsewhere. Then I just list all the files in the posts dir, sort them as latest
first, and write the date, title and link in a markdown list.</p>
<p>Then this preprocessed markdown gets fed to <code>cmark-gfm</code> with some additional
options to enable some nice extensions and allow raw HTML.</p>
<h2>Putting it all together</h2>
<p>So far we have an outdated Makefile and a shell script that takes a markdown
file, does some preprocessing to it and spits out the resulting HTML to stdout.
We'll name the shell script <code>src/generate.sh</code>. Let's update that Makefile.</p>
<p>Previously we were calling <code>cmark-gfm</code> directly in the Makefile. That's now
handled by the script, so just replace <code>cmark-gfm</code> with <code>src/generate.sh</code>. And
that's all there is to it, unless of course you want to go a little bit further
with organizing the directory structure. You do? Cool, let's do that.</p>
<p>Since I wanted to have the HTML and markdown in separate directories while
mirroring the directory structure, I had to make some changes to the way I get
the list of targets, as well as the rule to build them. Instead of a wildcard
like we had before, we can invoke a shell with the <code>find</code> command to list
markdown files recursively. Also I keep the markdown files in the src directory,
so that also needs to be handled when we do the substitution for the HTML
targets:</p>
<pre><code>MD   := $(shell find src -type f -name '*.md')
HTML := $(MD:src/%.md=%.html)
</code></pre>
<p>And finally the rule to build our HTML should look like this:</p>
<pre><code>$(HTML): %.html: src/%.md
	@mkdir -p $(@D)
	src/generate.sh $&lt; &gt; $@
</code></pre>
<p>Notice I added a <code>mkdir</code> command to create directories if needed. The <code>$(@D)</code>
variable evaluates to the directory containing <code>$@</code>. Also I had to use <em>Static
Pattern Rules</em><sup class="footnote-ref"><a href="#fn-4" id="fnref-4" data-footnote-ref>4</a></sup> to allow the pattern matching to work properly with
subdirectories.</p>
<p>Now add some more dependencies to have certain files rebuild when scripts or
templates change, and we're all set (check out the complete Makefile below).</p>
<h2>Wrapping up</h2>
<p>Wow, that came out a bit longer than I was expecting. Still, the point is that
if you don't need all the complexity of modern SSGs, you might be better off
making your own thing. All we did here convert markdown to HTML with a little
{pre,post}processing and the help of a Makefile. Oh, and you might want to
sprinkle some nice CSS on top. But not JS. Don't do that. <em>That's bad, mkay?</em></p>
<p>I'm leaving the complete Makefile and script here for convenience, though you
can also view the source on Codeberg (or even <a href="/Makefile">here</a> and <a href="/src/generate.sh">here</a> since I
don't keep the source in a separate repo).</p>
<details>
<summary>Makefile</summary>
<pre><code>MD        := $(shell find src -type f -name '*.md')
HTML      := $(MD:src/%.md=%.html)
TEMPLATES := $(wildcard src/templates/*.html)
SCRIPTS   := $(wildcard src/*.sh)

.PHONY: all clean

all: $(HTML)

# Rebuild post lists when posts are updated
index.html posts/index.html: $(wildcard src/posts/*.md)

$(HTML): %.html: src/%.md $(TEMPLATES) $(SCRIPTS)
	@mkdir -p $(@D)
	src/generate.sh $&lt; &gt; $@

clean:
	rm -f $(HTML)
</code></pre>
</details>
<details>
<summary>src/generate.sh</summary>
<pre><code>#!/bin/sh
srcdir=&quot;$(dirname $0)&quot;

die() {
  printf '%s\n' &quot;$1&quot; &gt;&amp;2
  exit &quot;${2:-1}&quot;
}

gettitle() {
  sed -n '1s/^#* *//p' &quot;$1&quot;
}

listposts() {
  find &quot;${srcdir}/posts&quot; -type f -regextype posix-extended \
                         -regex '.*/[0-9]{8}-.*\.md' -printf '%f\n' |
  sort -r |
  while read -r file; do
    filedate=&quot;$(date -d &quot;${file%%-*}&quot; +%F)&quot;
    printf '+ [%s] [%s](%s)\n' \
      &quot;$filedate&quot; \
      &quot;$(gettitle &quot;${srcdir}/posts/${file}&quot;)&quot; \
      &quot;/posts/${file%md}html&quot;
  done
}

preprocess() {
  while IFS= read -r line; do
    case &quot;$line&quot; in
      '@POSTS@') listposts ;;
              *) printf '%s\n' &quot;$line&quot; ;;
    esac
  done &lt; &quot;$1&quot;
}

input=&quot;$1&quot;
echo &quot;$input&quot; | grep -sq '\.md$' || die &quot;Usage: $0 INPUT_FILE.md&quot;

cat &lt;&lt;-EOF
&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
	&lt;meta charset=&quot;utf-8&quot;&gt;
	&lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;/css/style.css&quot;/&gt;
	&lt;title&gt;~jlucas/$(gettitle &quot;$input&quot;)&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
	&lt;header&gt;
		$(cat &quot;${srcdir}/templates/header.html&quot;)
	&lt;/header&gt;
&lt;main&gt;
	$(preprocess &quot;$input&quot; | cmark-gfm --unsafe -e strikethrough -e table -e footnotes)
&lt;/main&gt;
&lt;footer&gt;
	$(cat &quot;${srcdir}/templates/footer.html&quot;)
&lt;/footer&gt;
&lt;/body&gt;
EOF
</code></pre>
</details>
<h2>References</h2>
<section class="footnotes" data-footnotes>
<ol>
<li id="fn-1">
<p><a href="https://makefiletutorial.com">https://makefiletutorial.com</a> <a href="#fnref-1" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a></p>
</li>
<li id="fn-2">
<p><a href="https://www.gnu.org/software/make/manual/html_node/Substitution-Refs.html">https://www.gnu.org/software/make/manual/html_node/Substitution-Refs.html</a> <a href="#fnref-2" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="2" aria-label="Back to reference 2">↩</a></p>
</li>
<li id="fn-3">
<p><a href="https://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html">https://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html</a> <a href="#fnref-3" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="3" aria-label="Back to reference 3">↩</a></p>
</li>
<li id="fn-4">
<p><a href="https://www.gnu.org/software/make/manual/html_node/Static-Usage.html">https://www.gnu.org/software/make/manual/html_node/Static-Usage.html</a> <a href="#fnref-4" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="4" aria-label="Back to reference 4">↩</a></p>
</li>
</ol>
</section>
]]></description>
</item>
<item>
<title>Hello, World!</title>
<link>https://jlucas.codeberg.page/posts/20250116-hello-world.html</link>
<pubDate>Thu, 16 Jan 2025 00:00:00 +0000</pubDate>
<guid>https://jlucas.codeberg.page/posts/20250116-hello-world.html</guid>
<description><![CDATA[
<p>So I actually went and made a blog. Hi, I'm <strong>jlucas</strong>.</p>
<h2>Who?</h2>
<p>Check out the <a href="/about.html">about</a> page.</p>
<h2>Why?</h2>
<p>Because I wanted to write stuff as an exercise and for future reference, and
social media is dumb. Also I was playing around with <a href="https://codeberg.page">Codeberg Pages</a>
and <a href="/posts/20250117-ssg.html">building my own simple static site generator</a>,
and decided I might as well keep it.</p>
]]></description>
</item>
</channel>
</rss>
