<?xml version='1.0' encoding='UTF-8'?>
<feed xmlns='http://www.w3.org/2005/Atom'>
<link href='https://loang.net/~cnx/index.xml' rel='self'/>
<link href='https://loang.net/~cnx/' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/</id>
<title>cnx log</title>
<icon>https://loang.net/~cnx/favicon.svg</icon>
<author>
<name>Nguyễn Gia Phong</name>
<email>cnx@loang.net</email>
</author>
<rights>CC BY-SA 4.0</rights>
<updated>2026-08-03T08:14:53Z</updated>
<generator uri='https://chim.loan/xoa' version='0.5.0'>Xoa</generator>
<entry>
<link href='https://loang.net/~cnx/blog/2020/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/2020/index.html</id>
<title>The 2020 experience</title>
<summary>My life in 2020</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/2020/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<section id="The-2020-Experience">
<h1>The 2020 Experience</h1>
<div class="note">
<p>Not to be confused with <em>The 20/20 Experience</em>.</p>
</div>
<ol>
<li>
<a href="#The-Germination">The Germination</a></li>
<li>
<a href="#The-Fruition">The Fruition</a></li>
<li>
<a href="#The-Disease">The Disease</a></li>
<li>
<a href="#The-Profit">The Profit</a></li>
<li>
<a href="#The-Migrations">The Migrations</a></li>
<li>
<a href="#The-Moral">The Moral</a></li>
</ol>
<section id="The-Germination">
<h2>The Germination</h2>
<p>To understand my 2020, we have to travel back a few months,
when it all started.  No, not <em>that thing</em> beginning at the end of &#x2019;19.
I am talking about <em>my</em> 2020 experience, remember?</p>
<p>The story started in October 1810 in the not-so-little city of Munich, Germany.
Alright, it sounds like I lied about the 2019 and my story part,
but bear with me, it&#x2019;s all connected.  Anyhow, some Bavarian couple
got married and threw a big party.  People like parties, so naturally
they celebrated the anniversaries, year after year until it became
a tradition known to in English as the <a href="https://en.wikipedia.org/wiki/Oktoberfest">Oktoberfest</a>.</p>
<p>Over two centuries years later, on the wedding day of another
Bavarian couple,<a id="fnref1" href="#fn1" role="doc-noteref"><sup>1</sup></a> DigitalOcean began to an annual PR campaign
on the same month called Hacktoberfest.  I know, to many of you maintaining
projects on GitHub (and more recently GitLab.com), the name might not
remind you of something festive, but it really opened a new chapter in my life.</p>
<p><a href="https://www.whoismrrobot.com">Back to the future</a> in 2019, it was my first year taking part in the event.
The premise was that one would receive a t-shirt after having filed at least
four GitHub Pull Requests™.<a id="fnref2" href="#fn2" role="doc-noteref"><sup>2</sup></a>  Unlike <em>plethora</em>, this does not sound like
it was a lot, yet more than I ever had done.  Getting out of my comfort zone
was the first baby step, opening various opportunities in the upcoming months
and perhaps, years.</p>
<figure>
  <img src="codersrank.png"
       alt="Graph showing steeper growth from October 2019"/>
  <figcaption>My activities on GitHub over the years</figcaption>
</figure>
</section>
<section id="The-Fruition">
<h2>The Fruition</h2>
<p>Probably what I benefited the most from participating in Hacktoberfest
was learning to not be afraid of communicating with complete strangers
maintaining the software I use.  Stepping into 2020, I started to do
a larger variety of stuff in Python, which made installing libraries
happen on a regular basis.  The international Internet connection from home
at the time was unstable and usually downloads from the package warehouse
was a few kBps and that definitely did not help.  A few moments later,
I found myself on <a href="https://pypa.io">PyPA</a>&#x2019;s IRC channel discussing strategies to speed up
pip downloading.</p>
<p>After several days of on-off conversations (mostly I was asking questions
to fill in the blanks), a proposal was under draft: I was an undergrad
sophomore and had been eyeing on Google Summer of Code (GSoC) for quite a while.
Applying for pip wasn&#x2019;t the plan, but rather <a href="https://octave.org">Octave</a>, the first big project
I have contributed code to.<a id="fnref3" href="#fn3" role="doc-noteref"><sup>3</sup></a>  Now thinking about it, it was
a better choice since I was more comfortable with pip&#x2019;s tech stack.
The <a href="/blog/2020/gsoc">rest of the story</a> was already noted down so I won&#x2019;t be
retelling it here.</p>
</section>
<section id="The-Disease">
<h2>The Disease</h2>
<p>When the world had been battling SARS-CoV-2 for a few months, Việt Nam
was barely affected.  By refusing inbound travelers and temporary switching
to work/study-from-home, the number of cases and deaths was neglible
and by the end of summer we were virtually back to normal.  I hated
that most organizations, my university included, straight up offered
big techs our data without a second thought, and was thankful online learning
did not last.</p>
<p>Like many others, I spent that summer rarely leaving the house.  I was grateful
of GSoC for keeping me busy and giving me the opportunity to socialize
with new cool people.  It was impossible for me to catch <em>the</em> virus, I thought.
I was not wrong though, but I got something else: <a href="https://en.wikipedia.org/wiki/Dengue_fever">dengue fever</a>.  The fever
wasn&#x2019;t too bad, I was high as a kite for half a week, but never critical.
The aftermath, however, was much less pleasant.</p>
<p>For the next week, I was in a living hell because of a throat infection.
I&#x2019;d had sore throats before, quite regularly in fact, often at least
once every few months, but they had been a mere inconvenience.  Usually,
all I&#x2019;d gotta do had been to <a href="https://en.wiktionary.org/wiki/person_up">person up</a>, swallow a few times and get on
with my day.  This was different.  Everything hurt like a bitch.  The slightest
texture or flavor could cause minutes of pain.  For the first time,
I experience throat lozenges being the opposite of soothing.</p>
<p>For the entire week, I survived on undercooked scrambled eggs
and mushy porridge.  I had to take α-chymotrypsin<a id="fnref4" href="#fn4" role="doc-noteref"><sup>4</sup></a> before every meal
and was practically microdosing it throughout the day to be able
to drink water.  You can&#x2019;t imagine how happy I was when I could finally
eat rice again.  While the infection was not directly caused by dengue
(it only weakened my immune system), the trauma was enough to make me finally
care about home mosquito eradication.  Guess who learnt it the hard way!</p>
</section>
<section id="The-Profit">
<h2>The Profit</h2>
<p>GSoC gave me in stipend 3000 USD, minus Payoneer fees and shitty currency
exchange &#x201c;tax&#x201d;.  That was the largest sum I&#x2019;d ever had in my hands.
Because of the low cost of living in Việt Nam,<a id="fnref5" href="#fn5" role="doc-noteref"><sup>5</sup></a> I no longer completely
financially dependent on my parents.  I could pay my own school fees
(scholarship would give back the money <em>months</em> after paying), hang out
more with friends (we had zero-COVID for a while, remember?), tip free software
projects and services I had (and have) been using for years.</p>
<p>More importantly, I could buy myself <em>future</em> e-waste.  I got a <a href="https://www.pckeyboard.com">Model M</a>
so that I no longer need to change keyboard every year, a <a href="https://video.hardlimit.com/w/uucN1eWVurTSzY325PLS2s">lefty</a> <a href="https://ploopy.co">Ploopy</a>
to ease my traffic-accident-injured right wrist that&#x2019;s prolly never gonna
fully heal, a <a href="https://nixnet.social/notice/AI9eETauDunmiiIfHE">new DAP to replace my dead walk man</a>, my <a href="/blog/2020/gsoc/article/4/#snap_back_to_reality">first phone</a>
and perhaps some other things.  <a href="https://www.youtube.com/watch?v=5z25pGEGBM4">No worries</a>, I&#x2019;m still daily driving them
today, they ain&#x2019;t ended up in the landfill (yet).</p>
</section>
<section id="The-Migrations">
<h2>The Migrations</h2>
<p>Admittedly, the first <em><a href="https://freedesktop.org">freedesktop.org</a> smartphone</em> caught my eye was actually
the Librem 5, which I could afford neither the time nor the money for.
I know, the terminology sounds ridiculous, but <em>Linux</em> would include Android
and <em>GNU</em>&#x2019;d exclude <a href="https://postmarketos.org">postmarketOS</a>.  Anyway, <a href="https://puri.sm">Purism</a>, the company behind
the Librems, has seriously invested in adaptive GUI and federated services.
My first <a href="https://activitypub.rocks">ActivityPub</a> account was provided by <a href="https://librem.one">Librem One</a>.</p>
<p>It was not the first time I use a federated service.  I&#x2019;ve used email
for as long as I can remember and begun to use <a href="https://matrix.org">Matrix</a> intensively
since I entered university.  So what (were there to be) changed?
At the time, my online presence<a id="fnref6" href="#fn6" role="doc-noteref"><sup>6</sup></a> was primarily inside
<a href="https://github.com/McSinyx/mcsinyx.github.io/commit/af8e02ec3989.patch">surveillance capitalist walled gardens</a>.  I was mostly active(ly
posting) on bird site socializing with people I acquainted during my GSoC
and publishing my development/shitpost<a id="fnref7" href="#fn7" role="doc-noteref"><sup>7</sup></a> videos to YouTube.</p>
<p>Nothing on fedi really caught my eyes, until I got (hyped up for getting)
my PinePhone.  Its software landscape was incredibly fast moving back then.
Most peripherals were barely working.  Desktop programs were being ported
for narrower screens using brand new convergent libraries.  Many developers
were contracted by Purism or sponsored by Pine64, a large fraction of whom
are free software purists, rejecting spyware disguised as social media.
Never before, hanging out in chat rooms<a id="fnref8" href="#fn8" role="doc-noteref"><sup>8</sup></a> and the Fediverse
were the absolutely best ways to keep up with life-quality-changing updates.</p>
<p>Like with desktop-handheld convergence, I was impressed with Fediverse&#x2019;s
interoperability between multiple media formats, from (micro)blogs
to picture albums to videos.  Imagine being able to share and comment
on a YouTube directly from Twitter!  Shortly, I registered for a <a href="https://joinpeertube.org">PeerTube</a>
account and migrate all my videos there.  The longer I stayed on fedi,
the more cool stuff I found and the more satisfied I was.  Fast forward
over two years, I have deleted or permanently logged out of most;
only quiddit<a id="fnref9" href="#fn9" role="doc-noteref"><sup>9</sup></a> is left.</p>
<p>One thing led to another, <a href="https://blog.brixit.nl/apps">Martijn Braam&#x2019;s apps</a> introduced me
to <a href="https://sourcehut.org">SourceHut</a>, which embraces email for federation and focuses
on useful stuff like <a href="https://man.sr.ht/builds.sr.ht/build-ssh.md">SSH for CI</a>, instead of trying to be
a <a href="https://arxiv.org/abs/2006.02371">social media</a> or <a href="https://githubcopilotlitigation.com">relicense the projects it hosts</a>.
I have moved most of the software I maintain <a href="https://GiveUpGitHub.org">from GitHub</a> to sr.ht,
but the network effect is too strong: I still have to stick around
with the former to contribute to software I regularly use.</p>
<p>However, it&#x2019;s unlikely that most of those growing up with GitHub,
especially inexperienced contributors, will be <a href="https://adol.pw/2022/05/09/maintaining-first-project-part-iv-end">willing to adapt to
a workflow revolving around mailing lists</a> for such kind of forge
to become mainstream again.  On the bright side, I start to seeing more
larger projects hosting their development platform, and I am watching
<a href="">forge federation</a> with great interest.</p>
</section>
<section id="The-Moral">
<h2>The Moral</h2>
<p>At this point, you probably wonder, what I am trying to tell from all these
random rambling.  Welp, nothing.  My life is <a>not like the movies</a>,
there ain&#x2019;t no plot, no meaning.  The whole point of this log is to bridge
the gap between <a href="https://loang.net/~cnx/blog">/blog</a> and <a href="/blog/2020/gsoc">/blog/2020/gsoc</a>.  2020 was indeed
positively life-changing for me, tho/so I can&#x2019;t expect most of y&#x2019;all&#x2019;ll
be able to relate.  2023 is already underway, and I hope we will all
have a year we can look back to the same way I did in this post.</p>
<p>https://forgefriends.org/blog/2022/06/30/2022-06-state-forge-federation
[not like the movies]: https://www.youtube.com/watch?v=9ewTkrfaWtA
[git-request-pull]: https://git-scm.com/docs/git-request-pull
[keymap]: /works/#simplified<em>vietnamese</em>keymaps</p>
</section>
</section>
<section role="doc-endnotes">
<hr/>
<ol>
<li id="fn1">
<p>There must be at least one wedding everyday in Bavaria, I think.<a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn2">
<p>It is a vendor locked-in version of <a>git-request-pull</a>.<a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn3">
<p>Not counting Vim because it was a <a>keymap</a> contribution.<a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn4">
<p>Proteolytic enzyme; taken orally for inflammation.  Shit&#x2019;s magic.<a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn5">
<p>A meal at a diner costed around 1 USD at the time.<a href="#fnref5" role="doc-backlink">↩︎︎</a><a href="#fnref5" role="doc-backlink">↩︎︎</a><a href="#fnref5" role="doc-backlink">↩︎︎</a><a href="#fnref5" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn6">
<p>Gah, I hate this term!<a href="#fnref6" role="doc-backlink">↩︎︎</a><a href="#fnref6" role="doc-backlink">↩︎︎</a><a href="#fnref6" role="doc-backlink">↩︎︎</a><a href="#fnref6" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn7">
<p>I don&#x2019;t like keeping too serious logs.<a href="#fnref7" role="doc-backlink">↩︎︎</a><a href="#fnref7" role="doc-backlink">↩︎︎</a><a href="#fnref7" role="doc-backlink">↩︎︎</a><a href="#fnref7" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn8">
<p>A room was bridged between 5 protocols, fun but also an eye sore.<a href="#fnref8" role="doc-backlink">↩︎︎</a><a href="#fnref8" role="doc-backlink">↩︎︎</a><a href="#fnref8" role="doc-backlink">↩︎︎</a><a href="#fnref8" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn9">
<p>Hey, the site name was a pun on <em>read it</em> in the first place!<a href="#fnref9" role="doc-backlink">↩︎︎</a><a href="#fnref9" role="doc-backlink">↩︎︎</a><a href="#fnref9" role="doc-backlink">↩︎︎</a><a href="#fnref9" role="doc-backlink">↩︎︎</a></p>
</li>
</ol>
</section>
</div>
</content>
<category term='lyf'/>
<category term='exp'/>
<published>2023-01-07T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/2020/gsoc/article/2/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/2020/gsoc/article/2/index.html</id>
<title>The Wonderful Wizard of O'zip</title>
<summary>GSoC 2020: The Wonderful Wizard of O'zip</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/2020/gsoc/article/2/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<section id="The-Wonderful-Wizard-of-O'zip">
<h1>The Wonderful Wizard of O&#x2019;zip</h1>
<blockquote>
<p>Never give up&#x2026; No one knows what&#x2019;s going to happen next.</p>
</blockquote>
<section id="Preface">
<h2>Preface</h2>
<p>Greetings and best wishes!  I had a lot of fun during the last week,
although admittedly nothing was really finished.  In summary,
these are the works I carried out in the last seven days:</p>
<ul>
<li>
Finilizing <a href="https://github.com/pypa/pip/issues/8320">utilities for parallelization</a>
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8467">Continuing experimenting</a>
on <a href="https://github.com/pypa/pip/issues/8442">using lazy wheels or dependency resolution</a>
</li>
<li>
Polishing up <a href="https://github.com/pypa/pip/issues/8411">the patch</a> refactoring
<code>operations.prepare.prepare_linked_requirement</code>
</li>
<li>
Adding <code>flake8-logging-format</code>
<a href="https://github.com/pypa/pip/issues/8423#issuecomment-645418725">to the linter</a>
</li>
<li>
Splitting <a href="https://github.com/pypa/pip/issues/8456">the linting patch</a> from <a href="https://github.com/pypa/pip/issues/8332">the PR adding the license requirement to vendor README</a>
</li>
</ul>
</section>
<section id="The-multiprocessingdummy-wrapper">
<h2>The <code>multiprocessing[.dummy]</code> wrapper</h2>
<p>Yes, you read it right, this is the same section as last fortnight&#x2019;s blog.
My mentor Pradyun Gedam gave me a green light to have <a href="https://github.com/pypa/pip/issues/8411">GH-8411</a> merged
without support for Python 2 and the non-lazy map variant, which turns out
to be troublesome for multithreading.</p>
<p>The tests still needs to pass of course and the flaky tests (see failing tests
over Azure Pipeline in the past) really gave me a panic attack earlier today.
We probably need to mark them as xfail or investigate why they are
undeterministic specifically on Azure, but the real reason I was <em>all caught up
and confused</em> was that the unit tests I added mess with the cached imports
and as <code>pip</code>&#x2019;s tests are run in parallel, who knows what it might affect.
I was so relieved to not discover any new set of tests made flaky by ones
I&#x2019;m trying to add!</p>
</section>
<section id="The-file-like-object-mapping-ZIP-over-HTTP">
<h2>The file-like object mapping ZIP over HTTP</h2>
<p>This is where the fun starts.  Before we dive in, let&#x2019;s recall some
background information on this.  As discovered by Danny McClanahan
in <a href="https://github.com/pypa/pip/issues/7819">GH-7819</a>, it is possible to only download a potion of a wheel
and it&#x2019;s still valid for <code>pip</code> to get the distribution&#x2019;s metadata.
In the same thread, Daniel Holth suggested that one may use
HTTP range requests to specifically ask for the tail of the wheel,
where the ZIP&#x2019;s central directory record as well as where usually
<code>dist-info</code> (the directory containing <code>METADATA</code>) can be found.</p>
<p>Well, <em>usually</em>.  While <a href="https://peps.python.org/pep-427">PEP 427</a> does indeed recommend</p>
<blockquote>
<p>Archivers are encouraged to place the <code>.dist-info</code> files physically
at the end of the archive.  This enables some potentially interesting
ZIP tricks including the ability to amend the metadata without
rewriting the entire archive.</p>
</blockquote>
<p>one of the mentioned <em>tricks</em> is adding shared libraries to wheels
of extension modules (using e.g. <code>auditwheel</code> or <code>delocate</code>).
Thus for non-pure Python wheels, it is unlikely that the metadata
lie in the last few megabytes.  Ignoring source distributions is bad enough,
we can&#x2019;t afford making an optimization that doesn&#x2019;t work for extension modules,
which are still an integral part of the Python ecosystem )-:</p>
<p>But hey, the ZIP&#x2019;s directory record is warrantied to be at the end of the file!
Couldn&#x2019;t we do something about that?  The short answer is yes.  The long answer
is, well, yessssssss! That, plus magic provided by most operating systems,
this is what we figured out:</p>
<ol>
<li>
<p>We can download a realatively small chunk at the end of the wheel
until it is recognizable as a valid ZIP file.</p>
</li>
<li>
<p>In order for the end of the archive to actually appear as the end to
<code>zipfile</code>, we feed to it an object with <code>seek</code> and <code>read</code> defined.
As navigating to the rear of the file is performed by calling <code>seek</code>
with relative offset and <code>whence=SEEK_END</code> (see <code>man 3 fseek</code>
for more details), we are completely able to make the wheels in the cloud
to behave as if it were available locally.</p>
<p><img alt="Wheel in the cloud" src="cloud.gif"/></p>
</li>
<li>
<p>For large wheels, it is better to store them in hard disks instead of memory.
For smaller ones, it is also preferable to store it as a file to avoid
(error-prony and often not really efficient) manual tracking and joining
of downloaded segments.  We only use a small potion of the wheel, however
just in case one is wonderring, we have very little control over
when <code>tempfile.SpooledTemporaryFile</code> rolls over, so the memory-disk hybrid
is not exactly working as expected.</p>
</li>
<li>
<p>With all these in mind, all we have to do is to define an intermediate object
check for local availability and download if needed on calls to <code>read</code>,
to lazily provide the data over HTTP and reduce execution time.</p>
</li>
</ol>
<p>The only theoretical challenge left is to keep track of downloaded intervals,
which I finally figured out after a few trials and errors.  The code
was submitted as a pull request to <code>pip</code> at <a href="https://github.com/pypa/pip/issues/8467">GH-8467</a>.  A more modern
(read: Python 3-only) variant was packaged and uploaded to PyPI under
the name of <a href="https://pypi.org/project/lazip/">lazip</a>.  I am unaware of any use case for it outside of <code>pip</code>,
but it&#x2019;s certainly fun to play with d-:</p>
</section>
<section id="What's-next">
<h2>What&#x2019;s next?</h2>
<p>I have been falling short of getting the PRs mention above merged for
quite a while.  With <code>pip</code>&#x2019;s next beta coming really soon, I have to somehow
make the patches reach a certain standard and enough attention to be part of
the pre-release—beta-testing would greatly help the success of the GSoC project.
To other GSoC students and mentors reading this, I also hope your projects
to turn out successful!</p>
</section>
</section>
</div>
</content>
<category term='gsoc'/>
<category term='python'/>
<category term='net'/>
<published>2020-06-22T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/2020/gsoc/checkin/4/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/2020/gsoc/checkin/4/index.html</id>
<title>Fourth Check-In</title>
<summary>GSoC 2020: Fourth Check-In</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/2020/gsoc/checkin/4/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<p>+++
date = Date(2020, 7, 13)
tags = [&#x201c;gsoc&#x201d;, &#x201c;pip&#x201d;]
+++</p>
<section id="Fourth-Check-In">
<h1>Fourth Check-In</h1>
<p>Hello there! I&#x2019;m having my second year&#x2019;s last exam tomorrow,
but it [feels like summer] already!  I&#x2019;ve been finalizing quite a few things
to get them ready for pip 20.2b2.</p>
<section id="What-did-I-do-last-week">
<h2>What did I do last week?</h2>
<p>I&#x2019;ve spent most of the time on getting {{pip 8532 &#x201c;the opt-in&#x201d;} for obtaining
dependency information via lazy wheels ready.  It will be available as
<code>--use-feature=fast-deps</code> and only has effect when
<code>--use-feature=2020-resolver</code> also presents.</p>
<p>While waiting for reviews and suggestions, I made some patches for
internal cleansing, namely {{pip 8568}}, {{pip 8571}} and {{pip 8578}}.
Some of the similar patches I made earlier were also merged last week:
{{pip 8456}} and {{pip 8538}}.</p>
</section>
<section id="Did-I-get-stuck-anywhere">
<h2>Did I get stuck anywhere?</h2>
<p>Not really, everything was going as expected for me.</p>
</section>
<section id="What-is-coming-up-next">
<h2>What is coming up next?</h2>
<p>After {{pip 8532}}, I&#x2019;ll work on the parallel download of the postponed wheels.
My main current concern is with how the download progress will be reported
to the users, but I think I&#x2019;ll figure it out soon.</p>
</section>
</section>
</div>
</content>
<category term='gsoc'/>
<published>2020-07-13T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/gotham/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/gotham/index.html</id>
<title>Generic Homemade Ham</title>
<summary>An easy template for making uncured ham or similar brined pork</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/gotham/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<section id="Generic-Homemade-Ham">
<h1>Generic Homemade Ham</h1>
<p>Where I&#x2019;m from, hams are stupid expensive due to the lack of demand.
This is unacceptable because I <em>love</em> hams!<a id="fnref1" href="#fn1" role="doc-noteref"><sup>1</sup></a>  After years of not tasting
even a single slice, I decided for myself to make some, and noting down
what works and what doesn&#x2019;t.</p>
<p>Unlike other stuff you usually find on the interweb, the following recipe
will not require any fancy equipment,<a id="fnref2" href="#fn2" role="doc-noteref"><sup>2</sup></a> chemical<a id="fnref3" href="#fn3" role="doc-noteref"><sup>3</sup></a>
or quantities that (should) only appear in a math textbook.  It will also
try to be flexible, so that you can be free to experiment with whatever
you feel like that day, while knowing for sure you&#x2019;ll still end up
with something at least remotely resemble a piece of ham.</p>
<section id="Brining">
<h2>Brining</h2>
<p>Making ham, like any other food, comprises of only two steps: preparing
and cooking.  Brining not only makes the meat salty<a id="fnref4" href="#fn4" role="doc-noteref"><sup>4</sup></a> but also enhances
its tenderness by braking down the proteins.</p>
<p>The most important ingredients for this process are meat, salt and sugar.
As for the meat, it&#x2019;s preferably from a pig&#x2019;s thigh, but anything
with a similar texture will do.  You do want a cut with parallel muscles
to minimize the amount of silver skin and tendon though, plus it will have
better presentation.  As always, intramuscular fat is a delicious cherry
on top, but not too crucial in this case.  On the other hand, any kind
of salt and sugar would do.  Personally I use sea salt and brown sugar
because they are the cheapest to be found locally, whilst they add some extra
flavors and minerals.</p>
<section id="Dry">
<h3>Dry</h3>
<p>Dry brining is only suitable for (family-)serving-size cuts of meat,
somewhere from 200 to 500 grams.  Anything larger would have troubles
absorbing the seasoning.  Otherwise, cover the meat in coarse salt and sugar
and leave it in the fridge from a few hours to overnight, depending
on its mass.</p>
<p>How much seasoning?  Be generous, but you&#x2019;d want to still be able to see
the meat underneath.  I don&#x2019;t think you can&#x2019;t overseason it, just remember
to rinse off the remaining rub before cooking.  As for the ratio, I like
to twice as much salt as sugar, but I&#x2019;ve seen people doing 1:1 or even 1:2.</p>
</section>
<section id="Wet">
<h3>Wet</h3>
<p>The brine formula I&#x2019;m about to describe is heavily influenced from <a href="https://www.youtube.com/watch?v=5fm3lNM5vV4">Mike G&#x2019;s
recipe</a>, which is also uncured ham.  First, pour enough water to submerge
the meat in a pot (no, don&#x2019;t put the meat in the pot) and heat it up.  If you
have a fairly fitting container, the amount is close to the mass of the meat
itself.</p>
<p>Then, add 5% salt, 3% sugar, and whatever spices can go well
with your future ham.  I usually use a few bay leaves, some thyme
and crushed peppercorn, but any aromatic, fresh or dry, should work.
You don&#x2019;t have to be exact with the amount of seasoning either: if you
don&#x2019;t have a scale, measure with a spoon and be generous.  Due to the lack
of nitrate, the brining shouldn&#x2019;t occur for more than a few days
and the more concentrated the solution, the faster the absorption.</p>
<p>Let the brine cool down, pour it in a container,
drown the meat<a id="fnref5" href="#fn5" role="doc-noteref"><sup>5</sup></a> (use a weight if necessary) and put it
in the fridge.  A cut of a few hundred grams should take around 24 hours.</p>
</section>
</section>
<section id="Cooking">
<h2>Cooking</h2>
<p>After taking the meat out of the fridge and wash it lightly, wait around
an hour for it to reach room temperature.  If you don&#x2019;t have paper towel,
place it on a rack or an elevated plane to dry off the surface.</p>
<p>Before cooking, I like to rub a few other extra spices on my meat.
My favorite are smoked paprika (for the smoky flavor), garlic powder,
freshly grounded black pepper and perhaps some nutmeg.</p>
<p>From here, it&#x2019;s similar to cooking a steak: you&#x2019;d want it in an environment
close to the target temperature, which is around 68°C, or 63°C if pork
in your area is heavily regulated.  The closer it is, the smaller the difference
between the center and the outer layers may be, i.e. you&#x2019;ll less likely
to overcook the latter.  There are three ways<a id="fnref6" href="#fn6" role="doc-noteref"><sup>6</sup></a> to do this indoor:
sous vide, pan-frying and oven-roasting.  If you have a sous vide machine,
I&#x2019;d assume you wouldn&#x2019;t need my instructions, so I will focus on the other
two methods.</p>
<section id="Pan-frying">
<h3>Pan-frying</h3>
<p>First, rub a touch of cooking oil<a id="fnref7" href="#fn7" role="doc-noteref"><sup>7</sup></a> all over your meat, then turn on
the stove to the lowest-possible heat and place the pan and the meat
on it.  It should take 30 to 40 minutes to reach to desired temperature,
depending on your stove.  You can use your finger or a chopstick to poke
on the meat: if it feels raw it&#x2019;s probably raw, if it&#x2019;s solid it&#x2019;s overcooked;
you&#x2019;d want it bouncy, right before it stops being so.  Yes, it&#x2019;s a lot of trial
and error and unnecessarily stressful, just get a thermometer, especially
the one you can stick in for the entire process.</p>
<p>It is not compulsory to sear a ham, but I&#x2019;m addicted to the <a href="https://en.wikipedia.org/wiki/Maillard_reaction">Maillard
reaction</a> so Imma do it anyway.  You can sear before or after cooking,
I usually do the latter (reverse searing) because it seems to make more sense.
Move the meat to a temporary plate and wipe the pan clean.  Turn the stove
up to medium-high and wait for it to get hot.</p>
<p>If your meat does not look like it can fit it a body building contest,
coat it with little more oil, then drop it on the pan.  Rotate it every 30
seconds until the whole surface area is golden brown, then transport it
back to the plate for resting until you can comfortably touch it before slicing.
Serve with yellow mustard.</p>
</section>
<section id="Roasting">
<h3>Roasting</h3>
<p>If you have an oven, place the meat on its rack and turn it down to lowest heat
(mine is 100°C).  In this method, a thermometer is also compulsory to monitor
the meat inner temperature, which should take around 80 minutes to raise
to the target one.  I suggest bisecting the checking intervals, e.g. check after
40 minutes, then 20, and so on.</p>
<p>If you&#x2019;re worried about the wasted energy, you can cut some carrots, potatoes,
tomatoes and/or onions (anything high in carbs, really) in half and throw them
in the oven.  After taking the meat out, turn the oven up to highest and you&#x2019;ll
have some beautifully caramelized side dishes.</p>
<p>The oven I have at home is not powerful enough for searing the meat (quickly)
so I usually turn to the pan instead.</p>
</section>
<section id="Slow-cooking-bonus">
<h3>Slow cooking (bonus)</h3>
<p>This is a bonus because I could never make a ham out of it, but pulled pork.
On the other hand, it&#x2019;s so tender that you won&#x2019;t be able to slice and needs
much less attention.  Since we won&#x2019;t sear the meat, it&#x2019;s a good idea to use
a binding like mustard to stick even more rubbing spices on the surface.</p>
<p>After rubbing, touch the bottom of the slow cooker with a bit a oil
to avoid sticking, drop the bay leafs from the brine on it and place the meat
on top.  Cook on <em>low</em> from six to eight hours, then using forks or chopsticks
separate the muscles from each other.  You can serve immediately or let it
cook a bit more after pulling.</p>
</section>
</section>
</section>
<section role="doc-endnotes">
<hr/>
<ol>
<li id="fn1">
<p>Especially <a href="https://www.youtube.com/watch?v=IiLJsOsRKUI">Jon Hamm&#x2019;s John Ham</a>.<a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn2">
<p>Ain&#x2019;t nobody got at smoker at home.<a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn3">
<p>Where can I get nitrates?  A chemistry lab?<a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn4">
<p>Like yours truly.<a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn5">
<p>Or the other way around, it&#x2019;s not cereal.<a href="#fnref5" role="doc-backlink">↩︎︎</a><a href="#fnref5" role="doc-backlink">↩︎︎</a><a href="#fnref5" role="doc-backlink">↩︎︎</a><a href="#fnref5" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn6">
<p>Nice!<a href="#fnref6" role="doc-backlink">↩︎︎</a><a href="#fnref6" role="doc-backlink">↩︎︎</a><a href="#fnref6" role="doc-backlink">↩︎︎</a><a href="#fnref6" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn7">
<p>One with smoking point above 170°C.<a href="#fnref7" role="doc-backlink">↩︎︎</a><a href="#fnref7" role="doc-backlink">↩︎︎</a><a href="#fnref7" role="doc-backlink">↩︎︎</a><a href="#fnref7" role="doc-backlink">↩︎︎</a></p>
</li>
</ol>
</section>
</div>
</content>
<category term='lyf'/>
<category term='recipe'/>
<published>2021-11-19T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/kholow/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/kholow/index.html</id>
<title>Slowed Cooked Pork and Eggs</title>
<summary>Sino-Vietnamese caramelized pork and eggs, but by a slow cooker</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/kholow/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<section id="Slow-Cooked-Pork-and-Eggs">
<h1>Slow Cooked Pork and Eggs</h1>
<p><a href="https://en.wikipedia.org/wiki/Caramelized_pork_and_eggs">Thịt kho tàu</a>, literally <em>Chinese braised pork</em>, is one of the most common
Vietnamese dish, to be found anywhere from military camps<a id="fnref1" href="#fn1" role="doc-noteref"><sup>1</sup></a>
to fancy restaurants, anytime from family diners to new year holidays.
While originated from southeastern China, over the years it adopted
local ingredients such as <a href="https://en.wikipedia.org/wiki/Fish_sauce">fish sauce</a> and coconut flesh and probably
does not taste the same.</p>
<p>Due to time constraints, home cooks usually relies on fattier cuts
such as the belly to maintain the juiciness.  The downside is that
the excess fat can quickly tell the liver to tune down the appetite
after a few meals.<a id="fnref2" href="#fn2" role="doc-noteref"><sup>2</sup></a>  This put me in an awkward position,
since I was conditioned to feel wrong about braising a serving size
of anything (I was living alone when typing this).</p>
<p>Though, as said three sentences ago, leaner cuts can be as tender
when cooked longer.  This is where slow cookers come to the rescue:
they maintain temperature between 80 and 100°C and after eight hours
even the toughest cuts will just fall apart.  The best part?
No supervision needed.  Water doesn&#x2019;t even boil at that temperature,
so accidentally burning food is never a worry.</p>
<p>For the ease of maintenance, I&#x2019;d recommend slow cookers whose pot and lid
can be taken out for cleaning.  The pot should also be relatively large
(3L<a id="fnref3" href="#fn3" role="doc-noteref"><sup>3</sup></a> or more) if you want to make other vegetable-rich stews.</p>
<section id="Ingredients">
<h2>Ingredients</h2>
<p>As a <a href="https://commons.wikimedia.org/wiki/File:2013-06-08_mechanical_fan_for_hot_air_ballon.jpg">big fan</a> of <a href="https://chefjeanpierre.com">Chef Jean-Pierre</a>, I eyeball the amount of pretty much
all ingredients here.  The amount of pork and eggs should be enough
to at least fill the bottom of the pot.  I prefer quail eggs for their
bite size and leaner cuts of pork but with some intramuscular fat
and tendons.  Hocks, hams and shoulders are all good and cheap candidates.
Leave the skin on, the gelatine helps thicken the sauce.  I like equal amount
of eggs and meat.</p>
<p>For seasoning, you&#x2019;ll need fish sauce, sugar, whole black pepper,
and optionally shallot, garlic and hard coconut meat.</p>
</section>
<section id="Preparation">
<h2>Preparation</h2>
<p>Boil the eggs and peel them.  Layer them in the pot.  Peel and slice one
or two cloves of garlic and sprinkle them in there.  If you have coconut meat,
julienne<a id="fnref4" href="#fn4" role="doc-noteref"><sup>4</sup></a> and throw it in as well.</p>
<p>Cut the pork into bite-size dice.  Place the skin facing up or the side
of the pot.  You want (some of it) to be drier for texture variety.
Peel a few cloves of shallot and embed them between the dice of pork.</p>
</section>
<section id="Cooking">
<h2>Cooking</h2>
<p>Pour a very thin layer of sugar on a sauce pan and heat it up at medium low
to make some dark caramel (too low you&#x2019;ll just get liquid sugar and too high
you&#x2019;ll burn it faster than the <a href="https://nixnet.social/notice/AL2XqGNF2VwKgmbLfc">blue hedgehog</a>).  Soon as it&#x2019;s bubbling,
carefully pour in some water.  The amount should be able to almost cover
the meat and eggs in the pot.</p>
<p>While waiting the caramel to dissolve, add fish sauce to taste, and throw in
a generous number of peppercorns.  Transfer the sauce to the pot, making sure
the eggs are fully covered (they can be really chewy when dry: another reason
to favor the quail ones).</p>
<p>Turn the slow cooker on low and cook for around eight hours.  Tastes amazing
either hot or cold, best served with boiled or pickled vegetables and any kind
of starch, commonly rice or sweet potatoes but you can try bread, potatoes,
or even short pasta if you&#x2019;re feeling adventurous.</p>
</section>
</section>
<section role="doc-endnotes">
<hr/>
<ol>
<li id="fn1">
<p>My ole frens from <a href="https://usth.edu.vn">USTH</a> absolutely dug it during military training!<a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn2">
<p>Okay, maybe I lied about the digging part.<a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn3">
<p>Or 0.12245589 diesel tank in freedom units.<a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn4">
<p>Anywhere between the size of a matchstick and a chopstick is good.<a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a></p>
</li>
</ol>
</section>
</div>
</content>
<category term='lyf'/>
<category term='recipe'/>
<published>2023-06-03T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/conseq/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/conseq/index.html</id>
<title>Infinite Sequences: A Case Study in Functional Python</title>
<summary>SICP subsection 3.5.2 in Python</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/conseq/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<section id="Infinite-Sequences:-A-Case-Study-in-Functional-Python">
<h1>Infinite Sequences: A Case Study in Functional Python</h1>
<p>In this article, we will only consider sequences defined by a function
whose domain is a subset of the set of all integers.  Such sequences will be
<em>visualized</em>, i.e. we will try to evaluate the first few (thousand) elements,
using functional programming paradigm, where functions are more similar
to the ones in math (in contrast to imperative style with side effects
confusing to inexperenced coders).  The idea is taken from <a href="https://mitpress.mit.edu/sites/default/files/sicp/full-text/book/book-Z-H-24.html#%_sec_3.5.2">subsection 3.5.2
of SICP</a> and adapted to Python, which, compare to Scheme, is significantly
more popular: Python is pre-installed on almost every modern Unix-like system,
namely macOS, GNU/Linux and the *BSDs; and even at MIT, the new 6.01 in Python
has recently replaced the legendary 6.001 (SICP).</p>
<p>One notable advantage of using Python is its huge <em>standard</em> library.
For example the <em>identity sequence</em> (sequence defined by the identity function)
can be imported directly from <code>itertools</code>:</p>
<pre><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="keyword">from</code><code class="whitespace python"> </code><code class="identifier">itertools</code><code class="whitespace python"> </code><code class="keyword">import</code><code class="whitespace python"> </code><code class="identifier">count</code><code class="whitespace python">
</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="identifier">positive_integers</code><code class="whitespace python"> </code><code class="operator">=</code><code class="whitespace python"> </code><code class="function">count</code><code class="operator">(</code><code class="identifier">start</code><code class="operator">=</code><code class="number">1</code><code class="operator">)</code><code class="whitespace python">
</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="function builtin">next</code><code class="operator">(</code><code class="identifier">positive_integers</code><code class="operator">)</code><code class="whitespace python">
</code><code class="number">1</code><code class="whitespace python">
</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="function builtin">next</code><code class="operator">(</code><code class="identifier">positive_integers</code><code class="operator">)</code><code class="whitespace python">
</code><code class="number">2</code><code class="whitespace python">
</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="keyword">for</code><code class="whitespace python"> </code><code class="keyword soft">_</code><code class="whitespace python"> </code><code class="keyword">in</code><code class="whitespace python"> </code><code class="function builtin">range</code><code class="operator">(</code><code class="number">4</code><code class="operator">)</code><code class="operator">:</code><code class="whitespace python"> </code><code class="function builtin">next</code><code class="operator">(</code><code class="identifier">positive_integers</code><code class="operator">)</code><code class="whitespace python">
</code><code class="operator">.</code><code class="operator">.</code><code class="operator">.</code><code class="whitespace python"> 
</code><code class="number">3</code><code class="whitespace python">
</code><code class="number">4</code><code class="whitespace python">
</code><code class="number">5</code><code class="whitespace python">
</code><code class="number">6</code><code class="whitespace python">
</code></pre><p>To open a Python emulator, simply launch your terminal and run <code>python</code>.
If that is somehow still too struggling, navigate to <a href="https://www.python.org/shell">the interactive shell</a>
on Python.org.</p>
<p><em>Let&#x2019;s get it started</em> with somethings everyone hates: recursively defined
sequences, e.g. the famous Fibonacci <math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mrow><mo>(</mo><msub><mi>F</mi><mi>n</mi></msub><mo>&#x003D;</mo><msub><mi>F</mi><mrow><mi>n</mi><mo>&#x2212;</mo><mn>1</mn></mrow></msub><mo>&#x002B;</mo><msub><mi>F</mi><mrow><mi>n</mi><mo>&#x2212;</mo><mn>2</mn></mrow></msub></mrow></math>,
<math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mrow><msub><mi>F</mi><mn>1</mn></msub><mo>&#x003D;</mo><mn>1</mn></mrow></math> and <math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mrow><msub><mi>F</mi><mn>0</mn></msub><mo>&#x003D;</mo><mn>0</mn><mo>)</mo></mrow></math>.  Since <a href="https://neopythonic.blogspot.com/2009/04/final-words-on-tail-calls.html">Python does not support</a>
<a href="https://mitpress.mit.edu/sites/default/files/sicp/full-text/book/book-Z-H-11.html#call_footnote_Temp_48">tail recursion</a>, it&#x2019;s generally <strong>not</strong> a good idea to define anything
recursively (which is, ironically, the only trivial <em>functional</em> solution
in this case) but as we will only evaluate the first few terms
(use the <strong>Tab</strong> key to indent the line when needed):</p>
<pre><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="keyword">def</code><code class="whitespace python"> </code><code class="function">fibonacci</code><code class="operator">(</code><code class="identifier">n</code><code class="operator">,</code><code class="whitespace python"> </code><code class="identifier">a</code><code class="operator">=</code><code class="number">0</code><code class="operator">,</code><code class="whitespace python"> </code><code class="identifier">b</code><code class="operator">=</code><code class="number">1</code><code class="operator">)</code><code class="operator">:</code><code class="whitespace python">
</code><code class="operator">.</code><code class="operator">.</code><code class="operator">.</code><code class="whitespace python">     </code><code class="comment"># To avoid making the code look complicated,</code><code class="whitespace python">
</code><code class="operator">.</code><code class="operator">.</code><code class="operator">.</code><code class="whitespace python">     </code><code class="comment"># n &lt; 0 is not handled here.</code><code class="whitespace python">
</code><code class="operator">.</code><code class="operator">.</code><code class="operator">.</code><code class="whitespace python">     </code><code class="keyword">return</code><code class="whitespace python"> </code><code class="identifier">a</code><code class="whitespace python"> </code><code class="keyword">if</code><code class="whitespace python"> </code><code class="identifier">n</code><code class="whitespace python"> </code><code class="operator">=</code><code class="operator">=</code><code class="whitespace python"> </code><code class="number">0</code><code class="whitespace python"> </code><code class="keyword">else</code><code class="whitespace python"> </code><code class="function">fibonacci</code><code class="operator">(</code><code class="identifier">n</code><code class="whitespace python"> </code><code class="operator">-</code><code class="whitespace python"> </code><code class="number">1</code><code class="operator">,</code><code class="whitespace python"> </code><code class="identifier">b</code><code class="operator">,</code><code class="whitespace python"> </code><code class="identifier">a</code><code class="whitespace python"> </code><code class="operator">+</code><code class="whitespace python"> </code><code class="identifier">b</code><code class="operator">)</code><code class="whitespace python">
</code><code class="operator">.</code><code class="operator">.</code><code class="operator">.</code><code class="whitespace python"> 
</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="identifier">fibo_seq</code><code class="whitespace python"> </code><code class="operator">=</code><code class="whitespace python"> </code><code class="operator">(</code><code class="function">fibonacci</code><code class="operator">(</code><code class="identifier">n</code><code class="operator">)</code><code class="whitespace python"> </code><code class="keyword">for</code><code class="whitespace python"> </code><code class="identifier">n</code><code class="whitespace python"> </code><code class="keyword">in</code><code class="whitespace python"> </code><code class="function">count</code><code class="operator">(</code><code class="identifier">start</code><code class="operator">=</code><code class="number">0</code><code class="operator">)</code><code class="operator">)</code><code class="whitespace python">
</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="keyword">for</code><code class="whitespace python"> </code><code class="keyword soft">_</code><code class="whitespace python"> </code><code class="keyword">in</code><code class="whitespace python"> </code><code class="function builtin">range</code><code class="operator">(</code><code class="number">7</code><code class="operator">)</code><code class="operator">:</code><code class="whitespace python"> </code><code class="function builtin">next</code><code class="operator">(</code><code class="identifier">fibo_seq</code><code class="operator">)</code><code class="whitespace python">
</code><code class="operator">.</code><code class="operator">.</code><code class="operator">.</code><code class="whitespace python"> 
</code><code class="number">0</code><code class="whitespace python">
</code><code class="number">1</code><code class="whitespace python">
</code><code class="number">1</code><code class="whitespace python">
</code><code class="number">2</code><code class="whitespace python">
</code><code class="number">3</code><code class="whitespace python">
</code><code class="number">5</code><code class="whitespace python">
</code><code class="number">8</code><code class="whitespace python">
</code></pre><div class="note">
<h3 id="Note">Note</h3>
<p>The <code>fibo_seq</code> above is just to demonstrate how <code>itertools.count</code>
can be use to create an infinite sequence defined by a function.
For better performance, the following should be used instead:</p>
<pre><code class="keyword">def</code><code class="whitespace python"> </code><code class="function">fibonacci_sequence</code><code class="operator">(</code><code class="identifier">a</code><code class="operator">=</code><code class="number">0</code><code class="operator">,</code><code class="whitespace python"> </code><code class="identifier">b</code><code class="operator">=</code><code class="number">1</code><code class="operator">)</code><code class="operator">:</code><code class="whitespace python">
    </code><code class="keyword">yield</code><code class="whitespace python"> </code><code class="identifier">a</code><code class="whitespace python">
    </code><code class="keyword">yield</code><code class="whitespace python"> </code><code class="keyword">from</code><code class="whitespace python"> </code><code class="function">fibonacci_sequence</code><code class="operator">(</code><code class="identifier">b</code><code class="operator">,</code><code class="whitespace python"> </code><code class="identifier">a</code><code class="operator">+</code><code class="identifier">b</code><code class="operator">)</code><code class="whitespace python">
</code></pre></div>
<p>It is noticable that the elements having been iterated through (using <code>next</code>)
will disappear forever in the void (oh no!), but that is the cost we are
willing to pay to save some memory, especially when we need to evaluate a
member of (arbitrarily) large index to estimate the sequence&#x2019;s limit.
One case in point is estimating a definite integral using <a href="https://en.wikipedia.org/wiki/Riemann_sum#Left_Riemann_sum">left Riemann sum</a>.</p>
<pre><code class="keyword">def</code><code class="whitespace python"> </code><code class="function">integral</code><code class="operator">(</code><code class="identifier">f</code><code class="operator">,</code><code class="whitespace python"> </code><code class="identifier">a</code><code class="operator">,</code><code class="whitespace python"> </code><code class="identifier">b</code><code class="operator">)</code><code class="operator">:</code><code class="whitespace python">
    </code><code class="keyword">def</code><code class="whitespace python"> </code><code class="function">left_riemann_sum</code><code class="operator">(</code><code class="identifier">n</code><code class="operator">)</code><code class="operator">:</code><code class="whitespace python">
        </code><code class="identifier">dx</code><code class="whitespace python"> </code><code class="operator">=</code><code class="whitespace python"> </code><code class="operator">(</code><code class="identifier">b</code><code class="operator">-</code><code class="identifier">a</code><code class="operator">)</code><code class="whitespace python"> </code><code class="operator">/</code><code class="whitespace python"> </code><code class="identifier">n</code><code class="whitespace python">
        </code><code class="keyword">def</code><code class="whitespace python"> </code><code class="function">x</code><code class="operator">(</code><code class="identifier">i</code><code class="operator">)</code><code class="operator">:</code><code class="whitespace python"> </code><code class="keyword">return</code><code class="whitespace python"> </code><code class="identifier">a</code><code class="whitespace python"> </code><code class="operator">+</code><code class="whitespace python"> </code><code class="identifier">i</code><code class="operator">*</code><code class="identifier">dx</code><code class="whitespace python">
        </code><code class="keyword">return</code><code class="whitespace python"> </code><code class="function builtin">sum</code><code class="operator">(</code><code class="function">f</code><code class="operator">(</code><code class="function">x</code><code class="operator">(</code><code class="identifier">i</code><code class="operator">)</code><code class="operator">)</code><code class="whitespace python"> </code><code class="keyword">for</code><code class="whitespace python"> </code><code class="identifier">i</code><code class="whitespace python"> </code><code class="keyword">in</code><code class="whitespace python"> </code><code class="function builtin">range</code><code class="operator">(</code><code class="identifier">n</code><code class="operator">)</code><code class="operator">)</code><code class="whitespace python"> </code><code class="operator">*</code><code class="whitespace python"> </code><code class="identifier">dx</code><code class="whitespace python">
    </code><code class="keyword">return</code><code class="whitespace python"> </code><code class="identifier">left_riemann_sum</code><code class="whitespace python">
</code></pre><p>The function <code>integral(f, a, b)</code> as defined above returns a function taking
<math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mi>n</mi></math> as an argument.  As <math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mrow><mi>n</mi><mo>&#x2192;</mo><mo>&#x221E;</mo></mrow></math>, its result approaches
<math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mrow><munderover><mstyle mathsize='big'><mo>&#x222B;</mo></mstyle><mi>a</mi><mi>b</mi></munderover><mi>f</mi><mo>(</mo><mi>x</mi><mo>)</mo><mi>d</mi><mi>x</mi></mrow></math>.  For example, we are going to estimate
<math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mo>&#x03C0;</mo></math> as the area of a semicircle whose radius is <math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><msqrt><mn>2</mn></msqrt></math>:</p>
<pre><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="keyword">from</code><code class="whitespace python"> </code><code class="identifier">math</code><code class="whitespace python"> </code><code class="keyword">import</code><code class="whitespace python"> </code><code class="identifier">sqrt</code><code class="whitespace python">
</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="keyword">def</code><code class="whitespace python"> </code><code class="function">semicircle</code><code class="operator">(</code><code class="identifier">x</code><code class="operator">)</code><code class="operator">:</code><code class="whitespace python"> </code><code class="keyword">return</code><code class="whitespace python"> </code><code class="function">sqrt</code><code class="operator">(</code><code class="function builtin">abs</code><code class="operator">(</code><code class="number">2</code><code class="whitespace python"> </code><code class="operator">-</code><code class="whitespace python"> </code><code class="identifier">x</code><code class="operator">*</code><code class="identifier">x</code><code class="operator">)</code><code class="operator">)</code><code class="whitespace python">
</code><code class="operator">.</code><code class="operator">.</code><code class="operator">.</code><code class="whitespace python"> 
</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="identifier">pi</code><code class="whitespace python"> </code><code class="operator">=</code><code class="whitespace python"> </code><code class="function">integral</code><code class="operator">(</code><code class="identifier">semicircle</code><code class="operator">,</code><code class="whitespace python"> </code><code class="operator">-</code><code class="function">sqrt</code><code class="operator">(</code><code class="number">2</code><code class="operator">)</code><code class="operator">,</code><code class="whitespace python"> </code><code class="function">sqrt</code><code class="operator">(</code><code class="number">2</code><code class="operator">)</code><code class="operator">)</code><code class="whitespace python">
</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="identifier">pi_seq</code><code class="whitespace python"> </code><code class="operator">=</code><code class="whitespace python"> </code><code class="operator">(</code><code class="function">pi</code><code class="operator">(</code><code class="identifier">n</code><code class="operator">)</code><code class="whitespace python"> </code><code class="keyword">for</code><code class="whitespace python"> </code><code class="identifier">n</code><code class="whitespace python"> </code><code class="keyword">in</code><code class="whitespace python"> </code><code class="function">count</code><code class="operator">(</code><code class="identifier">start</code><code class="operator">=</code><code class="number">2</code><code class="operator">)</code><code class="operator">)</code><code class="whitespace python">
</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="keyword">for</code><code class="whitespace python"> </code><code class="keyword soft">_</code><code class="whitespace python"> </code><code class="keyword">in</code><code class="whitespace python"> </code><code class="function builtin">range</code><code class="operator">(</code><code class="number">3</code><code class="operator">)</code><code class="operator">:</code><code class="whitespace python"> </code><code class="function builtin">next</code><code class="operator">(</code><code class="identifier">pi_seq</code><code class="operator">)</code><code class="whitespace python">
</code><code class="operator">.</code><code class="operator">.</code><code class="operator">.</code><code class="whitespace python"> 
</code><code class="number">2.000000029802323</code><code class="whitespace python">
</code><code class="number">2.514157464087051</code><code class="whitespace python">
</code><code class="number">2.7320508224700384</code><code class="whitespace python">
</code></pre><p>Whilst the first few aren&#x2019;t quite close, at index around 1000,
the result is somewhat acceptable:</p>
<pre><code>3.1414873191059525
3.1414874770617427
3.1414876346231577
</code></pre><p>Since we are comfortable with sequence of sums, let&#x2019;s move on to sums of
a sequence, which are called series.  For estimation, again, we are going to
make use of infinite sequences of partial sums, which are implemented as
<code>itertools.accumulate</code> by thoughtful Python developers.  <a href="https://en.wikipedia.org/wiki/Geometric_series">Geometric</a> and
<a href="https://math.oregonstate.edu/home/programs/undergrad/CalculusQuestStudyGuides/SandS/SeriesTests/p-series.html">p-series</a> can be defined as follow:</p>
<pre><code class="keyword">from</code><code class="whitespace python"> </code><code class="identifier">itertools</code><code class="whitespace python"> </code><code class="keyword">import</code><code class="whitespace python"> </code><code class="identifier">accumulate</code><code class="whitespace python"> </code><code class="keyword">as</code><code class="whitespace python"> </code><code class="identifier">partial_sums</code><code class="whitespace python">

</code><code class="keyword">def</code><code class="whitespace python"> </code><code class="function">geometric_series</code><code class="operator">(</code><code class="identifier">r</code><code class="operator">,</code><code class="whitespace python"> </code><code class="identifier">a</code><code class="operator">=</code><code class="number">1</code><code class="operator">)</code><code class="operator">:</code><code class="whitespace python">
    </code><code class="keyword">return</code><code class="whitespace python"> </code><code class="function">partial_sums</code><code class="operator">(</code><code class="identifier">a</code><code class="operator">*</code><code class="identifier">r</code><code class="operator">*</code><code class="operator">*</code><code class="identifier">n</code><code class="whitespace python"> </code><code class="keyword">for</code><code class="whitespace python"> </code><code class="identifier">n</code><code class="whitespace python"> </code><code class="keyword">in</code><code class="whitespace python"> </code><code class="function">count</code><code class="operator">(</code><code class="number">0</code><code class="operator">)</code><code class="operator">)</code><code class="whitespace python">

</code><code class="keyword">def</code><code class="whitespace python"> </code><code class="function">p_series</code><code class="operator">(</code><code class="identifier">p</code><code class="operator">)</code><code class="operator">:</code><code class="whitespace python">
    </code><code class="keyword">return</code><code class="whitespace python"> </code><code class="function">partial_sums</code><code class="operator">(</code><code class="number">1</code><code class="whitespace python"> </code><code class="operator">/</code><code class="whitespace python"> </code><code class="identifier">n</code><code class="operator">*</code><code class="operator">*</code><code class="identifier">p</code><code class="whitespace python"> </code><code class="keyword">for</code><code class="whitespace python"> </code><code class="identifier">n</code><code class="whitespace python"> </code><code class="keyword">in</code><code class="whitespace python"> </code><code class="function">count</code><code class="operator">(</code><code class="number">1</code><code class="operator">)</code><code class="operator">)</code><code class="whitespace python">
</code></pre><p>We can then use these to determine whether a series is convergent or divergent.
For instance, one can easily verify that the <math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mi>p</mi></math>-series with <math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mrow><mi>p</mi><mo>&#x003D;</mo><mn>2</mn></mrow></math>
converges to <math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mrow><mfrac><msup><mo>&#x03C0;</mo><mn>2</mn></msup><mn>6</mn></mfrac><mo>&#x2243;</mo><mn>1</mn><mo>.</mo><mn>6</mn><mn>4</mn><mn>4</mn><mn>9</mn><mn>3</mn><mn>4</mn><mn>0</mn><mn>6</mn><mn>6</mn><mn>8</mn><mn>4</mn><mn>8</mn><mn>2</mn><mn>2</mn><mn>6</mn><mn>4</mn></mrow></math> via</p>
<pre><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="identifier">s</code><code class="whitespace python"> </code><code class="operator">=</code><code class="whitespace python"> </code><code class="function">p_series</code><code class="operator">(</code><code class="identifier">p</code><code class="operator">=</code><code class="number">2</code><code class="operator">)</code><code class="whitespace python">
</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="keyword">for</code><code class="whitespace python"> </code><code class="keyword soft">_</code><code class="whitespace python"> </code><code class="keyword">in</code><code class="whitespace python"> </code><code class="function builtin">range</code><code class="operator">(</code><code class="number">11</code><code class="operator">)</code><code class="operator">:</code><code class="whitespace python"> </code><code class="function builtin">next</code><code class="operator">(</code><code class="identifier">s</code><code class="operator">)</code><code class="whitespace python">
</code><code class="operator">.</code><code class="operator">.</code><code class="operator">.</code><code class="whitespace python"> 
</code><code class="number">1.0</code><code class="whitespace python">
</code><code class="number">1.25</code><code class="whitespace python">
</code><code class="number">1.3611111111111112</code><code class="whitespace python">
</code><code class="number">1.4236111111111112</code><code class="whitespace python">
</code><code class="number">1.4636111111111112</code><code class="whitespace python">
</code><code class="number">1.4913888888888889</code><code class="whitespace python">
</code><code class="number">1.511797052154195</code><code class="whitespace python">
</code><code class="number">1.527422052154195</code><code class="whitespace python">
</code><code class="number">1.5397677311665408</code><code class="whitespace python">
</code><code class="number">1.5497677311665408</code><code class="whitespace python">
</code><code class="number">1.558032193976458</code><code class="whitespace python">
</code></pre><p>We can observe that it takes quite a lot of steps to get the precision we would
generally expect <math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mrow><mo>(</mo><msub><mi>s</mi><mrow><mn>1</mn><mn>1</mn></mrow></msub></mrow></math> is only precise to the first decimal place;
second decimal places: <math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><msub><mi>s</mi><mrow><mn>1</mn><mn>0</mn><mn>1</mn></mrow></msub></math>; third: <math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mrow><msub><mi>s</mi><mrow><mn>2</mn><mn>3</mn><mn>0</mn><mn>4</mn></mrow></msub><mo>)</mo></mrow></math>.
Luckily, many techniques for series acceleration are available.
<a href="https://en.wikipedia.org/wiki/Shanks_transformation">Shanks transformation</a> for instance, can be implemented as follow:</p>
<pre><code class="keyword">from</code><code class="whitespace python"> </code><code class="identifier">itertools</code><code class="whitespace python"> </code><code class="keyword">import</code><code class="whitespace python"> </code><code class="identifier">islice</code><code class="operator">,</code><code class="whitespace python"> </code><code class="identifier">tee</code><code class="whitespace python">

</code><code class="keyword">def</code><code class="whitespace python"> </code><code class="function">shanks</code><code class="operator">(</code><code class="identifier">seq</code><code class="operator">)</code><code class="operator">:</code><code class="whitespace python">
    </code><code class="keyword">return</code><code class="whitespace python"> </code><code class="function builtin">map</code><code class="operator">(</code><code class="keyword">lambda</code><code class="whitespace python"> </code><code class="identifier">x</code><code class="operator">,</code><code class="whitespace python"> </code><code class="identifier">y</code><code class="operator">,</code><code class="whitespace python"> </code><code class="identifier">z</code><code class="operator">:</code><code class="whitespace python"> </code><code class="operator">(</code><code class="identifier">x</code><code class="operator">*</code><code class="identifier">z</code><code class="whitespace python"> </code><code class="operator">-</code><code class="whitespace python"> </code><code class="identifier">y</code><code class="operator">*</code><code class="identifier">y</code><code class="operator">)</code><code class="whitespace python"> </code><code class="operator">/</code><code class="whitespace python"> </code><code class="operator">(</code><code class="identifier">x</code><code class="whitespace python"> </code><code class="operator">+</code><code class="whitespace python"> </code><code class="identifier">z</code><code class="whitespace python"> </code><code class="operator">-</code><code class="whitespace python"> </code><code class="identifier">y</code><code class="operator">*</code><code class="number">2</code><code class="operator">)</code><code class="operator">,</code><code class="whitespace python">
               </code><code class="operator">*</code><code class="operator">(</code><code class="function">islice</code><code class="operator">(</code><code class="identifier">t</code><code class="operator">,</code><code class="whitespace python"> </code><code class="identifier">i</code><code class="operator">,</code><code class="whitespace python"> </code><code class="keyword">None</code><code class="operator">)</code><code class="whitespace python"> </code><code class="keyword">for</code><code class="whitespace python"> </code><code class="identifier">i</code><code class="operator">,</code><code class="whitespace python"> </code><code class="identifier">t</code><code class="whitespace python"> </code><code class="keyword">in</code><code class="whitespace python"> </code><code class="function builtin">enumerate</code><code class="operator">(</code><code class="function">tee</code><code class="operator">(</code><code class="identifier">seq</code><code class="operator">,</code><code class="whitespace python"> </code><code class="number">3</code><code class="operator">)</code><code class="operator">)</code><code class="operator">)</code><code class="operator">)</code><code class="whitespace python">
</code></pre><p>In the code above, <code>lambda x, y, z: (x*z - y*y) / (x + z - y*2)</code> denotes
the anonymous function <math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mrow><mo>&#x03BB;</mo><mi>x</mi><mo>.</mo><mo>&#x03BB;</mo><mi>y</mi><mo>.</mo><mo>&#x03BB;</mo><mi>z</mi><mo>.</mo><mfrac><mrow><mi>x</mi><mi>z</mi><mo>&#x2212;</mo><msup><mi>y</mi><mn>2</mn></msup></mrow><mrow><mi>x</mi><mo>&#x002B;</mo><mi>z</mi><mo>&#x2212;</mo><mn>2</mn><mi>y</mi></mrow></mfrac></mrow></math> and <code>map</code> is a higher order function
applying that function to respective elements of subsequences
starting from index 1, 2 and 3 of <code>seq</code>.  On Python 2, one should
import <code>imap</code> from <code>itertools</code> to get the same <a href="https://en.wikipedia.org/wiki/Lazy_evaluation">lazy</a> behavior
of <code>map</code> on Python 3.</p>
<pre><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="identifier">s</code><code class="whitespace python"> </code><code class="operator">=</code><code class="whitespace python"> </code><code class="function">shanks</code><code class="operator">(</code><code class="function">p_series</code><code class="operator">(</code><code class="number">2</code><code class="operator">)</code><code class="operator">)</code><code class="whitespace python">
</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="keyword">for</code><code class="whitespace python"> </code><code class="keyword soft">_</code><code class="whitespace python"> </code><code class="keyword">in</code><code class="whitespace python"> </code><code class="function builtin">range</code><code class="operator">(</code><code class="number">10</code><code class="operator">)</code><code class="operator">:</code><code class="whitespace python"> </code><code class="function builtin">next</code><code class="operator">(</code><code class="identifier">s</code><code class="operator">)</code><code class="whitespace python">
</code><code class="operator">.</code><code class="operator">.</code><code class="operator">.</code><code class="whitespace python"> 
</code><code class="number">1.4500000000000002</code><code class="whitespace python">
</code><code class="number">1.503968253968257</code><code class="whitespace python">
</code><code class="number">1.53472222222223</code><code class="whitespace python">
</code><code class="number">1.5545202020202133</code><code class="whitespace python">
</code><code class="number">1.5683119658120213</code><code class="whitespace python">
</code><code class="number">1.57846371882088</code><code class="whitespace python">
</code><code class="number">1.5862455815659202</code><code class="whitespace python">
</code><code class="number">1.5923993101138652</code><code class="whitespace python">
</code><code class="number">1.5973867787856946</code><code class="whitespace python">
</code><code class="number">1.6015104548459742</code><code class="whitespace python">
</code></pre><p>The result was quite satisfying, yet we can do one step futher
by continuously applying the transformation to the sequence:</p>
<pre><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="keyword">def</code><code class="whitespace python"> </code><code class="function">compose</code><code class="operator">(</code><code class="identifier">transform</code><code class="operator">,</code><code class="whitespace python"> </code><code class="identifier">seq</code><code class="operator">)</code><code class="operator">:</code><code class="whitespace python">
</code><code class="operator">.</code><code class="operator">.</code><code class="operator">.</code><code class="whitespace python"> 	</code><code class="keyword">yield</code><code class="whitespace python"> </code><code class="function builtin">next</code><code class="operator">(</code><code class="identifier">seq</code><code class="operator">)</code><code class="whitespace python">
</code><code class="operator">.</code><code class="operator">.</code><code class="operator">.</code><code class="whitespace python"> 	</code><code class="keyword">yield</code><code class="whitespace python"> </code><code class="keyword">from</code><code class="whitespace python"> </code><code class="function">compose</code><code class="operator">(</code><code class="identifier">transform</code><code class="operator">,</code><code class="whitespace python"> </code><code class="function">transform</code><code class="operator">(</code><code class="identifier">seq</code><code class="operator">)</code><code class="operator">)</code><code class="whitespace python">
</code><code class="operator">.</code><code class="operator">.</code><code class="operator">.</code><code class="whitespace python"> 
</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="identifier">s</code><code class="whitespace python"> </code><code class="operator">=</code><code class="whitespace python"> </code><code class="function">compose</code><code class="operator">(</code><code class="identifier">shanks</code><code class="operator">,</code><code class="whitespace python"> </code><code class="function">p_series</code><code class="operator">(</code><code class="number">2</code><code class="operator">)</code><code class="operator">)</code><code class="whitespace python">
</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="keyword">for</code><code class="whitespace python"> </code><code class="keyword soft">_</code><code class="whitespace python"> </code><code class="keyword">in</code><code class="whitespace python"> </code><code class="function builtin">range</code><code class="operator">(</code><code class="number">10</code><code class="operator">)</code><code class="operator">:</code><code class="whitespace python"> </code><code class="function builtin">next</code><code class="operator">(</code><code class="identifier">s</code><code class="operator">)</code><code class="whitespace python">
</code><code class="operator">.</code><code class="operator">.</code><code class="operator">.</code><code class="whitespace python"> 
</code><code class="number">1.0</code><code class="whitespace python">
</code><code class="number">1.503968253968257</code><code class="whitespace python">
</code><code class="number">1.5999812811165188</code><code class="whitespace python">
</code><code class="number">1.6284732442271674</code><code class="whitespace python">
</code><code class="number">1.6384666832276524</code><code class="whitespace python">
</code><code class="number">1.642311342667821</code><code class="whitespace python">
</code><code class="number">1.6425249569252578</code><code class="whitespace python">
</code><code class="number">1.640277484549416</code><code class="whitespace python">
</code><code class="number">1.6415443295058203</code><code class="whitespace python">
</code><code class="number">1.642038043478661</code><code class="whitespace python">
</code></pre><p>Shanks transformation works on every sequence (not just sequences of
partial sums).  Back to previous example of using left Riemann sum
to compute definite integral:</p>
<pre><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="identifier">pi_seq</code><code class="whitespace python"> </code><code class="operator">=</code><code class="whitespace python"> </code><code class="function">compose</code><code class="operator">(</code><code class="identifier">shanks</code><code class="operator">,</code><code class="whitespace python"> </code><code class="function builtin">map</code><code class="operator">(</code><code class="identifier">pi</code><code class="operator">,</code><code class="whitespace python"> </code><code class="function">count</code><code class="operator">(</code><code class="number">2</code><code class="operator">)</code><code class="operator">)</code><code class="operator">)</code><code class="whitespace python">
</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="keyword">for</code><code class="whitespace python"> </code><code class="keyword soft">_</code><code class="whitespace python"> </code><code class="keyword">in</code><code class="whitespace python"> </code><code class="function builtin">range</code><code class="operator">(</code><code class="number">10</code><code class="operator">)</code><code class="operator">:</code><code class="whitespace python"> </code><code class="function builtin">next</code><code class="operator">(</code><code class="identifier">pi_seq</code><code class="operator">)</code><code class="whitespace python">
</code><code class="operator">.</code><code class="operator">.</code><code class="operator">.</code><code class="whitespace python"> 
</code><code class="number">2.000000029802323</code><code class="whitespace python">
</code><code class="number">2.978391111182236</code><code class="whitespace python">
</code><code class="number">3.105916845397819</code><code class="whitespace python">
</code><code class="number">3.1323116570377185</code><code class="whitespace python">
</code><code class="number">3.1389379264270736</code><code class="whitespace python">
</code><code class="number">3.140788413965646</code><code class="whitespace python">
</code><code class="number">3.140921512857936</code><code class="whitespace python">
</code><code class="number">3.1400282163913436</code><code class="whitespace python">
</code><code class="number">3.1400874774021816</code><code class="whitespace python">
</code><code class="number">3.1407097229603256</code><code class="whitespace python">
</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="function builtin">next</code><code class="operator">(</code><code class="function">islice</code><code class="operator">(</code><code class="identifier">pi_seq</code><code class="operator">,</code><code class="whitespace python"> </code><code class="number">300</code><code class="operator">,</code><code class="whitespace python"> </code><code class="keyword">None</code><code class="operator">)</code><code class="operator">)</code><code class="whitespace python">
</code><code class="number">3.1415061302492413</code><code class="whitespace python">
</code></pre><p>Now having series defined, let&#x2019;s see if we can learn anything
about power series. Sequence of partial sums of power series
<math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mrow><munder><mstyle mathsize='big'><mo>&#x03A3;</mo></mstyle><msub><mi>c</mi><mi>n</mi></msub></munder><mo>(</mo><mi>x</mi><mo>&#x2212;</mo><mi>a</mi><msup><mo>)</mo><mi>n</mi></msup></mrow></math> can be defined as</p>
<pre><code class="keyword">from</code><code class="whitespace python"> </code><code class="identifier">operator</code><code class="whitespace python"> </code><code class="keyword">import</code><code class="whitespace python"> </code><code class="identifier">mul</code><code class="whitespace python">

</code><code class="keyword">def</code><code class="whitespace python"> </code><code class="function">power_series</code><code class="operator">(</code><code class="identifier">c</code><code class="operator">,</code><code class="whitespace python"> </code><code class="identifier">start</code><code class="operator">=</code><code class="number">0</code><code class="operator">,</code><code class="whitespace python"> </code><code class="identifier">a</code><code class="operator">=</code><code class="number">0</code><code class="operator">)</code><code class="operator">:</code><code class="whitespace python">
    </code><code class="keyword">return</code><code class="whitespace python"> </code><code class="keyword">lambda</code><code class="whitespace python"> </code><code class="identifier">x</code><code class="operator">:</code><code class="whitespace python"> </code><code class="function">partial_sums</code><code class="operator">(</code><code class="function builtin">map</code><code class="operator">(</code><code class="identifier">mul</code><code class="operator">,</code><code class="whitespace python"> </code><code class="identifier">c</code><code class="operator">,</code><code class="whitespace python"> </code><code class="operator">(</code><code class="identifier">x</code><code class="operator">*</code><code class="operator">*</code><code class="identifier">n</code><code class="whitespace python"> </code><code class="keyword">for</code><code class="whitespace python"> </code><code class="identifier">n</code><code class="whitespace python"> </code><code class="keyword">in</code><code class="whitespace python"> </code><code class="function">count</code><code class="operator">(</code><code class="identifier">start</code><code class="operator">)</code><code class="operator">)</code><code class="operator">)</code><code class="operator">)</code><code class="whitespace python">
</code></pre><p>We can use this to compute functions that can be written as
<a href="https://en.wikipedia.org/wiki/Taylor_series">Taylor series</a>:</p>
<pre><code class="keyword">from</code><code class="whitespace python"> </code><code class="identifier">math</code><code class="whitespace python"> </code><code class="keyword">import</code><code class="whitespace python"> </code><code class="identifier">factorial</code><code class="whitespace python">
</code><code class="keyword">def</code><code class="whitespace python"> </code><code class="function">exp</code><code class="operator">(</code><code class="identifier">x</code><code class="operator">)</code><code class="operator">:</code><code class="whitespace python">
    </code><code class="keyword">return</code><code class="whitespace python"> </code><code class="function">power_series</code><code class="operator">(</code><code class="number">1</code><code class="operator">/</code><code class="function">factorial</code><code class="operator">(</code><code class="identifier">n</code><code class="operator">)</code><code class="whitespace python"> </code><code class="keyword">for</code><code class="whitespace python"> </code><code class="identifier">n</code><code class="whitespace python"> </code><code class="keyword">in</code><code class="whitespace python"> </code><code class="function">count</code><code class="operator">(</code><code class="number">0</code><code class="operator">)</code><code class="operator">)</code><code class="operator">(</code><code class="identifier">x</code><code class="operator">)</code><code class="whitespace python">

</code><code class="keyword">def</code><code class="whitespace python"> </code><code class="function">cos</code><code class="operator">(</code><code class="identifier">x</code><code class="operator">)</code><code class="operator">:</code><code class="whitespace python">
    </code><code class="identifier">c</code><code class="whitespace python"> </code><code class="operator">=</code><code class="whitespace python"> </code><code class="operator">(</code><code class="operator">(</code><code class="number">1</code><code class="whitespace python"> </code><code class="operator">-</code><code class="whitespace python"> </code><code class="identifier">n</code><code class="operator">%</code><code class="number">2</code><code class="operator">)</code><code class="whitespace python"> </code><code class="operator">*</code><code class="whitespace python"> </code><code class="operator">(</code><code class="number">1</code><code class="whitespace python"> </code><code class="operator">-</code><code class="whitespace python"> </code><code class="identifier">n</code><code class="operator">%</code><code class="number">4</code><code class="operator">)</code><code class="whitespace python"> </code><code class="operator">/</code><code class="whitespace python"> </code><code class="function">factorial</code><code class="operator">(</code><code class="identifier">n</code><code class="operator">)</code><code class="whitespace python"> </code><code class="keyword">for</code><code class="whitespace python"> </code><code class="identifier">n</code><code class="whitespace python"> </code><code class="keyword">in</code><code class="whitespace python"> </code><code class="function">count</code><code class="operator">(</code><code class="number">0</code><code class="operator">)</code><code class="operator">)</code><code class="whitespace python">
    </code><code class="keyword">return</code><code class="whitespace python"> </code><code class="function">power_series</code><code class="operator">(</code><code class="identifier">c</code><code class="operator">)</code><code class="operator">(</code><code class="identifier">x</code><code class="operator">)</code><code class="whitespace python">

</code><code class="keyword">def</code><code class="whitespace python"> </code><code class="function">sin</code><code class="operator">(</code><code class="identifier">x</code><code class="operator">)</code><code class="operator">:</code><code class="whitespace python">
    </code><code class="identifier">c</code><code class="whitespace python"> </code><code class="operator">=</code><code class="whitespace python"> </code><code class="operator">(</code><code class="identifier">n</code><code class="operator">%</code><code class="number">2</code><code class="whitespace python"> </code><code class="operator">*</code><code class="whitespace python"> </code><code class="operator">(</code><code class="number">2</code><code class="whitespace python"> </code><code class="operator">-</code><code class="whitespace python"> </code><code class="identifier">n</code><code class="operator">%</code><code class="number">4</code><code class="operator">)</code><code class="whitespace python"> </code><code class="operator">/</code><code class="whitespace python"> </code><code class="function">factorial</code><code class="operator">(</code><code class="identifier">n</code><code class="operator">)</code><code class="whitespace python"> </code><code class="keyword">for</code><code class="whitespace python"> </code><code class="identifier">n</code><code class="whitespace python"> </code><code class="keyword">in</code><code class="whitespace python"> </code><code class="function">count</code><code class="operator">(</code><code class="number">1</code><code class="operator">)</code><code class="operator">)</code><code class="whitespace python">
    </code><code class="keyword">return</code><code class="whitespace python"> </code><code class="function">power_series</code><code class="operator">(</code><code class="identifier">c</code><code class="operator">,</code><code class="whitespace python"> </code><code class="identifier">start</code><code class="operator">=</code><code class="number">1</code><code class="operator">)</code><code class="operator">(</code><code class="identifier">x</code><code class="operator">)</code><code class="whitespace python">
</code></pre><p>Amazing!  Let&#x2019;s test &#x2019;em!</p>
<pre><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="identifier">e</code><code class="whitespace python"> </code><code class="operator">=</code><code class="whitespace python"> </code><code class="function">compose</code><code class="operator">(</code><code class="identifier">shanks</code><code class="operator">,</code><code class="whitespace python"> </code><code class="function">exp</code><code class="operator">(</code><code class="number">1</code><code class="operator">)</code><code class="operator">)</code><code class="whitespace python"> </code><code class="comment"># this should converges to 2.718281828459045</code><code class="whitespace python">
</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="keyword">for</code><code class="whitespace python"> </code><code class="keyword soft">_</code><code class="whitespace python"> </code><code class="keyword">in</code><code class="whitespace python"> </code><code class="function builtin">range</code><code class="operator">(</code><code class="number">4</code><code class="operator">)</code><code class="operator">:</code><code class="whitespace python"> </code><code class="function builtin">next</code><code class="operator">(</code><code class="identifier">e</code><code class="operator">)</code><code class="whitespace python">
</code><code class="operator">.</code><code class="operator">.</code><code class="operator">.</code><code class="whitespace python"> 
</code><code class="number">1.0</code><code class="whitespace python">
</code><code class="number">2.749999999999996</code><code class="whitespace python">
</code><code class="number">2.718276515152136</code><code class="whitespace python">
</code><code class="number">2.718281825486623</code><code class="whitespace python">
</code></pre><p>Impressive, huh? For sine and cosine, series acceleration is not even necessary:</p>
<pre><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="keyword">from</code><code class="whitespace python"> </code><code class="identifier">math</code><code class="whitespace python"> </code><code class="keyword">import</code><code class="whitespace python"> </code><code class="identifier">pi</code><code class="whitespace python"> </code><code class="keyword">as</code><code class="whitespace python"> </code><code class="identifier">PI</code><code class="whitespace python">
</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="identifier">s</code><code class="whitespace python"> </code><code class="operator">=</code><code class="whitespace python"> </code><code class="function">sin</code><code class="operator">(</code><code class="identifier">PI</code><code class="operator">/</code><code class="number">6</code><code class="operator">)</code><code class="whitespace python">
</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="keyword">for</code><code class="whitespace python"> </code><code class="keyword soft">_</code><code class="whitespace python"> </code><code class="keyword">in</code><code class="whitespace python"> </code><code class="function builtin">range</code><code class="operator">(</code><code class="number">5</code><code class="operator">)</code><code class="operator">:</code><code class="whitespace python"> </code><code class="function builtin">next</code><code class="operator">(</code><code class="identifier">s</code><code class="operator">)</code><code class="whitespace python">
</code><code class="operator">.</code><code class="operator">.</code><code class="operator">.</code><code class="whitespace python"> 
</code><code class="number">0.5235987755982988</code><code class="whitespace python">
</code><code class="number">0.5235987755982988</code><code class="whitespace python">
</code><code class="number">0.49967417939436376</code><code class="whitespace python">
</code><code class="number">0.49967417939436376</code><code class="whitespace python">
</code><code class="number">0.5000021325887924</code><code class="whitespace python">
</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="operator">&gt;</code><code class="whitespace python"> </code><code class="function builtin">next</code><code class="operator">(</code><code class="function">islice</code><code class="operator">(</code><code class="function">cos</code><code class="operator">(</code><code class="identifier">PI</code><code class="operator">/</code><code class="number">3</code><code class="operator">)</code><code class="operator">,</code><code class="whitespace python"> </code><code class="number">8</code><code class="operator">,</code><code class="whitespace python"> </code><code class="keyword">None</code><code class="operator">)</code><code class="operator">)</code><code class="whitespace python">
</code><code class="number">0.500000433432915</code><code class="whitespace python">
</code></pre></section>
</div>
</content>
<category term='fun'/>
<category term='math'/>
<category term='python'/>
<published>2019-02-28T12:00:00Z</published>
<updated>2026-08-03T06:34:51Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/gifrip/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/gifrip/index.html</id>
<title>Artisanal GIF Ripping</title>
<summary>How to make GIF files from videos</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/gifrip/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<section id="Artisanal-GIF-Ripping">
<h1>Artisanal GIF Ripping</h1>
<div class="note">
<h3 id="Pronounciation">Pronounciation</h3>
<p>/dʒɪf/ is the format, /ɡɪf/ is the handball club in Eskilstuna, Sweden.</p>
</div>
<p>GIF, <em>the</em> graphics interchange format, is probably the most <a href="https://telegram.org/blog/gif-revolution">inefficient</a>
representation for animated images in quality/size that is widespread today.
However, it does live up to its name, being also the most portable format
for animated graphics exchange.  If a device has a color display
and is connected to the Internet, tis likely to support GIF out of the box.</p>
<p>Like with incandescent light bulbs, it&#x2019;d be wasteful to have not switched
to more efficient alternatives, though GIF still has its charms.  Not having
to worry about codec compatibility is one thing, the nostalgia induced
by the <code>.gif</code> file extension is another.  For years, I got an <em>Internet</em> folder
full of those (along with still images and short videos) for offline viewing,
shitposting and reaction.</p>
<p>More recently, I <a href="https://nixnet.social/notice/A8VniqEBKfJvMc2dTE">began</a> <a href="https://nixnet.social/notice/A9gMc47yxgoTNDIa7U">to</a> <a href="https://fe.disroot.org/notice/ANK2GqGxIdcDGBRAFU">make</a>
<a href="https://nixnet.social/notice/AOhGjOUwKJmiEFNLkG">my</a> <a href="https://fe.disroot.org/notice/APhUh2H8radlKKIZGa">own</a> <a href="https://fe.disroot.org/notice/APhUh2H8radlKKIZGa">animated</a> <a href="https://larkspur.one/notice/ATe4i9UvhxjiFvXprs">images</a>
and take pride in them.  This tutorial will step-by-step
lay out my process in cutting out a high-quality GIF
from a video, so <a href="https://www.youtube.com/watch?v=TVMK2gQig4A">you can be just like me</a>!</p>
<ol>
<li>
<a href="#Decide-on-the-Format">Decide on the Format</a></li>
<li>
<a href="#Extract-Frames">Extract Frames</a></li>
<li>
<a href="#Crop-and-Resize">Crop and Resize</a></li>
<li>
<a href="#Combine">Combine</a></li>
</ol>
<section id="Decide-on-the-Format">
<h2>Decide on the Format</h2>
<blockquote>
<p>Just because you can doesn&#x2019;t mean that you should.</p>
</blockquote>
<p>Good things often don&#x2019;t come out of desire, but necessity.
GIF is cool because tis portable, but plain text is even more portable.
Multimedia weigh hundreds of kilobytes each, so they better best convey
whatever information they&#x2019;re meant to communicate.  Don&#x2019;t replace
the audio with a subtitle just so it can be an animated image.
Don&#x2019;t loop anything longer than a few seconds.</p>
<p>There is an old saying: <em>if tis doing fine being a video, let it be a video</em>.
Videos don&#x2019;t need each frame to be perfect, and thus all the following steps
can be done in a single ffmpeg command.  Work smart, even just for shitposting.</p>
</section>
<section id="Extract-Frames">
<h2>Extract Frames</h2>
<p>Open the source video in <a href="https://mpv.io">mpv</a>, seek then spam <code>s.</code> repeatedly.
That&#x2019;s all, y waste time write lot word wen few word do trick?</p>
<p>Aight, maybe there&#x2019;s a bit more to it.  Operating on the level of frames
will allow you to skip redundant ones in case of misencoded sources
or duplicate and reverse a subsequence for a closer-to-perfect loop,
but really, there&#x2019;s not much to talk about.</p>
</section>
<section id="Crop-and-Resize">
<h2>Crop and Resize</h2>
<p>Ripping a GIF from a video is taking a sequence of frames out of context.
Framing and some objects in the scene might not make sense in the target
animated image.  Some also like to export videos with giant black bars
just to fuck with us.  That&#x2019;s where cropping comes into play.  For measuring
I use [GIMP], which doesn&#x2019;t seem to be the right tool for the job,
so please let me know of anything lighter that has a ruler.</p>
<p>In addition, videos from social media (and space-efficient movies)
are heavily compressed and look pretty bad for their resolution.  Usually
I have to shrink them down to two third or a half of their original width
for them to look decently sharp.  When you have the geometry in mind,
summon the <a href="https://imagemagick.org">image wizard</a>:</p>
<pre><code class="variable">crop</code><code class="operator">=</code><code class="string">"-crop ${w}x${h}+${dx}+${dy}"</code><code class="whitespace bash">
</code><code class="variable">resize</code><code class="operator">=</code><code class="string">"-resize ${width}x${height}"</code><code class="whitespace bash">
</code><code class="identifier">parallel</code><code class="whitespace bash"> </code><code class="default">-i</code><code class="whitespace bash"> </code><code class="identifier">sh</code><code class="whitespace bash"> </code><code class="default">-c</code><code class="whitespace bash"> </code><code class="string">"convert $crop $resize"</code><code class="default">\</code><code class="whitespace bash">
</code><code class="string">' {} $(basename {} .jpg).png'</code><code class="whitespace bash"> </code><code class="default">-</code><code class="default">-</code><code class="whitespace bash"> </code><code class="identifier">mpv</code><code class="default">-shot</code><code class="default">*</code><code class="default">.</code><code class="identifier">jpg</code><code class="whitespace bash">
</code></pre><p>In case you don&#x2019;t have [moreutils]/[GNU parallel],
or if you are rocking a single-CPU machine, run:</p>
<pre><code class="identifier">mogrify</code><code class="whitespace bash"> </code><code class="operator">$</code><code class="variable">crop</code><code class="whitespace bash"> </code><code class="operator">$</code><code class="variable">resize</code><code class="whitespace bash"> </code><code class="default">-format</code><code class="whitespace bash"> </code><code class="identifier">png</code><code class="whitespace bash"> </code><code class="identifier">mpv</code><code class="default">-shot</code><code class="default">*</code><code class="default">.</code><code class="identifier">jpg</code><code class="whitespace bash">
</code></pre><p>For resizing, image processing expert Nicolas Robidoux wrote
a long article on <a href="https://imagemagick.org/Usage/filter/nicolas">resampling filters</a>.  Although I can&#x2019;t spot
any distinction for this use case, it doesn&#x2019;t hurt to give it a read
and play around with different options.</p>
<p><img alt="Pam stating that they are the same picture" src="same-picture.png"/></p>
</section>
<section id="Combine">
<h2>Combine</h2>
<p>We are going to encode in best quality possible using <a href="https://gif.ski">gifski</a>.
The reason we converted from JPEG to PNG earlier is that our precious encoder
refuses to touch any other <em>image</em> format, and that mpv take screenshots
in JPEG by default (you can <a href="https://mpv.io/manual/master/#screenshot">configure</a> it to write in
lossless format but in my experience, the source videos are often too heavily
compressed for it to make any difference).  Anyhow, invoking gifski
is rather straightforward:</p>
<pre><code class="identifier">gifski</code><code class="whitespace bash"> </code><code class="default">-r</code><code class="whitespace bash"> </code><code class="operator">$</code><code class="variable">fps</code><code class="whitespace bash"> </code><code class="default">-Q</code><code class="whitespace bash"> </code><code class="number">100</code><code class="whitespace bash"> </code><code class="default">-o</code><code class="whitespace bash"> </code><code class="operator">$</code><code class="variable">name</code><code class="default">.</code><code class="identifier">gif</code><code class="whitespace bash"> </code><code class="identifier">mpv</code><code class="default">-shot</code><code class="default">*</code><code class="default">.</code><code class="identifier">png</code><code class="whitespace bash">
</code></pre><p>Note that it optionally takes width and height as an argument
and yield better quality than with pre-shrunken images,
but at the cost of a significantly larger file size.  Choose wisely.</p>
</section>
</section>
</div>
</content>
<category term='fun'/>
<category term='recipe'/>
<published>2023-03-16T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/2020/gsoc/checkin/2/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/2020/gsoc/checkin/2/index.html</id>
<title>Second Check-In</title>
<summary>GSoC 2020: Second Check-In</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/2020/gsoc/checkin/2/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<p>+++
date = Date(2020, 6, 15)
tags = [&#x201c;gsoc&#x201d;, &#x201c;pip&#x201d;]
+++</p>
<section id="Second-Check-In">
<h1>Second Check-In</h1>
<p>Hi everyone and may the odds ever in your favor, especially during this
tough time!</p>
<section id="What-did-I-do-last-week">
<h2>What did I do last week?</h2>
<p>Not as much I wished, apparently (-:</p>
<ul>
<li>
Finalizing {{pip 8411 &#x201d;the refactoring patch&#x201d;}
of <code>operations.prepare.prepare_linked_requirement</code>
</li>
<li>
{{pip 8423 &#x201d;Nitpicking some logging calls&#x201d;}.  This (as well as the next one)
was to fill up the time my brain not being as productive as I want it to XD
</li>
<li>
{{pip 8423 &#x201d;Beginning to migrate&#x201d;} from <code>%</code>- to <code>{}</code>-style logging.
The amount of tests failing due to this was way beyond my imagination,
but I got functional tests for <code>pip install</code> and unit tests passing now!
</li>
<li>
{{pip 8442 &#x201d;Mocking up a working partial wheel download during
dependency resolution&#x201d;} for [the new resolver].
</li>
</ul>
</section>
<section id="Did-I-get-stuck-anywhere">
<h2>Did I get stuck anywhere?</h2>
<p>Yes, of course!  {{pip 8320 &#x201d;Parallel maps&#x201d;} are still stalling
as well as other small PRs listed above.  The failure related to
<code>logging</code> are still making me pulling my hair out and the proof of
concept for partial wheel downloading is too ugly even for a PoC.
I imagine that I will have a lot of clean up to do this week (yay!).</p>
</section>
<section id="What-is-coming-up-next">
<h2>What is coming up next?</h2>
<p>I&#x2019;m trying get the multi-{threading,processing} facilities merged ASAP
to start rolling it out in practice.  The first thing popping out of my
head is to get back {{pip 7962 &#x201d;the multi-threaded&#x201d;} <code>pip list -o</code>.</p>
<p>The other experimental improvement (this phrase does not sound right!)
I would like to get done is the partial wheel download.  It would be
really nice if I can get both included as <code>unstable-feature</code>&#x2019;s
in {{pip 7628#issuecomment-636319539 &#x201d;the upcoming beta release of pip 20.2&#x201d;}.</p>
</section>
</section>
</div>
</content>
<category term='gsoc'/>
<published>2020-06-15T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/2020/gsoc/checkin/7/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/2020/gsoc/checkin/7/index.html</id>
<title>Final Check-In</title>
<summary>GSoC 2020: Final Check-In</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/2020/gsoc/checkin/7/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<p>+++
date = Date(2020, 8, 24)
tags = [&#x201c;gsoc&#x201d;, &#x201c;pip&#x201d;]
+++</p>
<section id="Final-Check-In">
<h1>Final Check-In</h1>
<p>Hello there!</p>
<section id="What-did-I-do-last-week">
<h2>What did I do last week?</h2>
<p>Not much, but seemingly implementation-wise I have finished my GSoC project:</p>
<ul>
<li>
Finish the implementation of wheels&#x2019; parallel download ({{pip 8771}})
</li>
<li>
Help make <code>pip</code>&#x2019;s CI green again ({{pip 8790}})
</li>
<li>
Reformat a few spots in user guide ({{pip 8795}})
</li>
</ul>
</section>
<section id="Did-I-get-stuck-anywhere">
<h2>Did I get stuck anywhere?</h2>
<p>I got sick, but I am recovering now!</p>
</section>
<section id="What-is-coming-up-next">
<h2>What is coming up next?</h2>
<p>I will try to spend the time I got left within the scope of GSoC
to {{pip 8720 &#x201d;improve cache usage of the fast-deps feature&#x201d;}.</p>
</section>
</section>
</div>
</content>
<category term='gsoc'/>
<published>2020-08-24T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/2020/gsoc/checkin/6/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/2020/gsoc/checkin/6/index.html</id>
<title>Sixth Check-In</title>
<summary>GSoC 2020: Sixth Check-In</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/2020/gsoc/checkin/6/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<p>+++
date = Date(2020, 8, 10)
tags = [&#x201c;gsoc&#x201d;, &#x201c;pip&#x201d;]
+++</p>
<section id="Sixth-Check-In">
<h1>Sixth Check-In</h1>
<p>Hello there!</p>
<section id="What-did-I-do-last-week">
<h2>What did I do last week?</h2>
<p>It has been a quite fun week for me, given the current state of
development and the newly dicovered bugs thanks to pip 20.2 release:</p>
<ul>
<li>
Initiate discussion with the maintainers of pip on isolating
networking code for late download in parallel ({{pip 8697}})
</li>
<li>
Discuss the UI of parallel download ({{pip 8698}})
</li>
<li>
Log debug information relating lazy wheel decision ({{pip 8710}})
</li>
<li>
Disable caching for range requests ({{pip 8716}})
</li>
<li>
Dedent late download logs ({{pip 8722}})
</li>
<li>
Add a hook for batch downloading (third attempt I think) ({{pip 8737}})
</li>
<li>
Test hash checking for fast-deps ({{pip 8743}})
</li>
</ul>
</section>
<section id="Did-I-get-stuck-anywhere">
<h2>Did I get stuck anywhere?</h2>
<p>Not exactly, everything is going smoothly and I&#x2019;m feeling awesome!</p>
</section>
<section id="What-is-coming-up-next">
<h2>What is coming up next?</h2>
<p>I&#x2019;ll try to solve {{pip 8697}} and {{pip 8698}} within the next few days.
I am optimistic that the parallel download prototype will be done
within this week.</p>
</section>
</section>
</div>
</content>
<category term='gsoc'/>
<published>2020-08-10T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/threa/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/threa/index.html</id>
<title>Writing a Clipboard Manager</title>
<summary>Raku's concision demonstrated in form of a tutorial</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/threa/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<section id="Writing-a-Clipboard-Manager">
<h1>Writing a Clipboard Manager</h1>
<div class="note">
<h3 id="A-word-of-protest">A word of protest</h3>
<p>This was intended to be presented in <a href="https://conf.raku.org">The Raku Conference</a>,
however the organizers insisted on using <a href="https://stallman.org/zoom.html">Zoom</a> and <a href="https://stallman.org/skype.html">Skype</a>,
which are privacy invasive platforms running on proprietary software
and shadily managed.</p>
</div>
<section id="Motivation">
<h2>Motivation</h2>
<p>Clipboard management is very important to my workflow.  To me,
a clipboard manager is useful in two ways:</p>
<ol>
<li>
It extends my (rather poor) temporary mundane memory by caching
a few dozens of most recent selections.
</li>
<li>
It synchronizes clipboard and primary selections.
Since some programs only support one kind of selection,
this is particularly useful.
</li>
</ol>
<p>For the first point, I have to be able to choose from the history
by pressing a few keystrokes.  Having to touch the mouse during
writing sessions is unacceptable.  The menu dropping down from the systray
is also undesirable because I have a multi-monitor setup.  This narrows down
to only one plausible option: <a href="https://launchpad.net/diodon">Diodon</a>, which I having been using on Debian
for at least two years.  However, as I was migrating to NixOS earlier
last month, <a href="https://github.com/NixOS/nixpkgs/pull/126190">I was unable to package it for Nix</a>.</p>
<p>Naturally, I went looking for <a href="https://search.nixos.org/packages?query=clip">alternatives</a>, most of which I had
tried before and did not satisfy my requirements.  <a href="https://github.com/cdown/clipmenu">clipmenu</a> got
my attention however: it was made to work with dmenu(-compliant launchers),
which I had a rather nice experience with in <a href="https://sxmo.org">Sxmo</a> on my <a href="https://www.pine64.org/pinephone">PinePhone</a>.
However, I use <a href="https://awesomewm.org">awesome</a> on my workstation and its widget toolkit covers
my launcher and menu need perfectly.  I don&#x2019;t need <a href="https://tools.suckless.org/dmenu">dmenu</a> and do not
wish to spend time configuring and theming it.  Plus, the architecture
of dmenu scripts and awesome widgets vastly differs: while awesome
executes the input programs, dmenu is called from the scripts.</p>
</section>
<section id="Inspirations-and-Design">
<h2>Inspirations and Design</h2>
<p>As even the most plausible candidate is not a suitable replacement,
I would need to write my own clipboard manager.  clipmenu is not really
a good base though because it&#x2019;s written in shell script, something I ain&#x2019;t
fluent in.<a id="fnref1" href="#fn1" role="doc-noteref"><sup>1</sup></a>  Its idea is brilliant however:</p>
<blockquote>
<ol>
<li>
<code>clipmenud</code> uses <code>clipnotify</code> to wait for new clipboard events.
</li>
<li>
If <code>clipmenud</code> detects changes to the clipboard contents,
it writes them out to the cache directory
and an index using a hash as the filename.
</li>
</ol>
</blockquote>
<p>I later translated <a href="https://github.com/cdown/clipnotify">clipnotify</a> to <a>Zig</a> and called it <a href="https://trong.loang.net/~cnx/clipbuzz">clipbuzz</a>.<a id="fnref2" href="#fn2" role="doc-noteref"><sup>2</sup></a>
From clipbuzz&#x2019;s usage,</p>
<pre><code class="keyword">while</code><code class="whitespace bash"> </code><code class="identifier">clipbuzz</code><code class="whitespace bash">
</code><code class="keyword">do</code><code class="whitespace bash"> </code><code class="comment"># something with xclip or xsel</code><code class="whitespace bash">
</code><code class="keyword">done</code><code class="whitespace bash">
</code></pre><p>and this is exactly how yet another clipboard manager was written,
but before we get there, let&#x2019;s talk about this article&#x2019;s sponsor!</p>
<p>I&#x2019;m kidding d-; though we cannot jump into the implementation just yet:
we only resolved the first point out of two.  How about the data structure?
Hashing sounds like overengineering in this case: nobody needs more than
a few dozen entries<a id="fnref3" href="#fn3" role="doc-noteref"><sup>3</sup></a> and hashes are not very memorable.  Printable characters
can serve much better as indices.</p>
<p>What?  What happens when we run out of them?  We reuse/recycle them!<a id="fnref4" href="#fn4" role="doc-noteref"><sup>4</sup></a>
They would also fit within one single line, heck, we just store all of them
in order inside a file and rotate each time there&#x2019;s a new selection.
Picking would just be moving a char to the beginning.  The entire
cache directory can just look something like this:</p>
<pre><code>$ ls $XDG_CACHE_HOME/$project
order
R
A
K
U
</code></pre><p>Wait, is that a sign?  We must use <a href="https://raku.org">Raku</a> to implement <code>$project</code> then…
Speaking of <code>$project</code>, I planned to use it with awesome and <a href="https://vicious.rtfd.io">vicious</a>
so let&#x2019;s call it something brutal, like a <em>cutting board</em>, which is <em>thớt</em>
in Vietnamese, an Internet slang for <em>thread</em>.  Cool, now we have
the daemon name, and conventionally the client shall be <em>threac</em>,
or <em>threa client</em>.</p>
</section>
<section id="Daemon-Implementation">
<h2>Daemon Implementation</h2>
<section id="Reading-Inputs">
<h3>Reading Inputs</h3>
<p>Raku was chosen<a id="fnref5" href="#fn5" role="doc-noteref"><sup>5</sup></a> for the ease of text manipulation and seamless interfacing
with external programs.  I learned it quite a while ago and has always been
waiting for a chance to do something more practical with it, other than
competitive programming which isn&#x2019;t a good fit due to Rakudo&#x2019;s poor performance.
In Raku, the snippet from clipbuzz&#x2019;s README becomes:</p>
<pre><code class="keyword">while</code><code class="whitespace perl"> </code><code class="identifier">run</code><code class="whitespace perl"> </code><code class="string">'clipbuzz'</code><code class="whitespace perl"> </code><code class="operator">{</code><code class="whitespace perl">
    </code><code class="comment"># do something with xclip or xsel</code><code class="whitespace perl">
</code><code class="operator">}</code><code class="whitespace perl">
</code></pre><p>Out of all languages I know, this is by far the simplest way to <a href="https://docs.raku.org/routine/run">run</a>
an external program.  Most would require one to import something
or do something with the call&#x2019;s return value, and don&#x2019;t even get me start
on POSIX <code>fork</code> and <code>exec</code> model.</p>
<p>OK, now what are we gonna do with <code>xclip</code>?  One obvious thing would be
to read the current selection.  Raku got you covered, fam:</p>
<pre><code class="keyword">my</code><code class="whitespace perl"> </code><code class="variable">$selection</code><code class="whitespace perl"> </code><code class="operator">=</code><code class="whitespace perl"> </code><code class="string">qx/xclip -out/</code><code class="operator">;</code><code class="whitespace perl">
</code></pre><p>Remember when I said Raku can seamlessly interact with external programs?
<a href="https://docs.raku.org/syntax/qx">qx</a> is how you capture their standard output, it is really that simple.
But wait, which selection is that?  No worries, <code>xclip</code> supports both
primary and clipboard:</p>
<pre><code class="keyword">my</code><code class="whitespace perl"> </code><code class="variable">$primary</code><code class="whitespace perl"> </code><code class="operator">=</code><code class="whitespace perl"> </code><code class="string">qx/xclip -out -selection primary/</code><code class="operator">;</code><code class="whitespace perl">
</code><code class="keyword">my</code><code class="whitespace perl"> </code><code class="variable">$clipbroad</code><code class="whitespace perl"> </code><code class="operator">=</code><code class="whitespace perl"> </code><code class="string">qx/xclip -out -selection clipboard/</code><code class="operator">;</code><code class="whitespace perl">
</code></pre></section>
<section id="Cache-Directory-Setup">
<h3>Cache Directory Setup</h3>
<p>This is when we write those selection down for later use, right?
Well, we need to figure out where to save them first.  According to
<a href="https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html">XDG Base Directory Specification</a>, <code>$XDG_CACHE_HOME</code> shall
falls back to <code>$HOME/.cache</code>:</p>
<pre><code class="keyword">my</code><code class="whitespace perl"> </code><code class="variable">$XDG_CACHE_HOME</code><code class="whitespace perl"> </code><code class="operator">=</code><code class="whitespace perl"> </code><code class="operator">%</code><code class="operator">*</code><code class="constant builtin">ENV</code><code class="operator">&lt;</code><code class="identifier">XDG_CACHE_HOME</code><code class="operator">&gt;</code><code class="whitespace perl"> </code><code class="regex">//</code><code class="whitespace perl"> </code><code class="identifier">path</code><code class="whitespace perl"> </code><code class="variable">$*</code><code class="identifier">HOME</code><code class="whitespace perl"> </code><code class="operator">/</code><code class="whitespace perl"> </code><code class="string">'.cache'</code><code class="operator">:</code><code class="operator">;</code><code class="whitespace perl">
</code></pre><p>For convenience purposes, I defined the <code>/</code> operator as an alias
for path concatination:</p>
<pre><code class="identifier">multi</code><code class="whitespace perl"> </code><code class="keyword">sub</code><code class="whitespace perl"> </code><code class="identifier">infix</code><code class="operator">:</code><code class="operator">&lt;</code><code class="regex">/&gt;($parent, $child) { add $parent: $child }</code><code class="whitespace perl">
</code></pre><p>With <code>$XDG_CACHE_HOME</code> defined, we can prepare the base directory as follows:</p>
<pre><code class="keyword">my</code><code class="whitespace perl"> </code><code class="variable">$base</code><code class="whitespace perl"> </code><code class="operator">=</code><code class="whitespace perl"> </code><code class="variable">$XDG_CACHE_HOME</code><code class="operator">.</code><code class="identifier">IO</code><code class="whitespace perl"> </code><code class="operator">/</code><code class="whitespace perl"> </code><code class="string">'threa'</code><code class="operator">;</code><code class="whitespace perl">
</code><code class="function builtin">mkdir</code><code class="whitespace perl"> </code><code class="variable">$base</code><code class="operator">:</code><code class="whitespace perl"> </code><code class="keyword">unless</code><code class="whitespace perl"> </code><code class="variable">$base</code><code class="operator">.</code><code class="identifier">e</code><code class="operator">;</code><code class="whitespace perl">
</code><code class="function builtin">die</code><code class="whitespace perl"> </code><code class="string">"thread: $base: File exists"</code><code class="whitespace perl"> </code><code class="identifier">when</code><code class="whitespace perl"> </code><code class="variable">$base</code><code class="operator">.</code><code class="identifier">f</code><code class="operator">;</code><code class="whitespace perl">
</code></pre><p>As <a href="https://vrurg.github.io/2021/06/16/article-on-roles">a wise man once said</a>,</p>
<blockquote>
<p>As it often happens, writing an article ends up with a bug found in Rakudo.</p>
</blockquote>
<p>In this case, there&#x2019;s a <a href="https://github.com/MoarVM/MoarVM/pull/1507">bug in mkdir</a> that makes it happily returns
even if the target path is a file.  I&#x2019;m trying to fix it at the moment
but <a href="https://github.com/rakudo/rakudo/pull/4408">a test</a> is still failing.  <em>Update: it passed after a maintainer
bumped the dependencies to the patched version.</em></p>
<p>Anyway, back to our clipboard manager.  Here we are using
<a href="https://docs.raku.org/language/control">flow controllers</a> such as <code>unless</code> and <code>when</code> in the form
of <em>statement modifiers</em>, which can sometimes be easier on eyes
keeping the code flat.  Existence checks like <code>e</code> (exists) and <code>f</code> (file)
are also really handy.  Next, we check on the <code>order</code>:</p>
<pre><code class="identifier">constant</code><code class="whitespace perl"> </code><code class="variable">$ALNUM</code><code class="whitespace perl"> </code><code class="operator">=</code><code class="whitespace perl"> </code><code class="string">'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'</code><code class="operator">;</code><code class="whitespace perl">

</code><code class="keyword">sub</code><code class="whitespace perl"> </code><code class="function">valid</code><code class="operator">(</code><code class="variable">$path</code><code class="operator">)</code><code class="whitespace perl"> </code><code class="operator">{</code><code class="whitespace perl">
    </code><code class="keyword">return</code><code class="whitespace perl"> </code><code class="identifier">False</code><code class="whitespace perl"> </code><code class="keyword">unless</code><code class="whitespace perl"> </code><code class="variable">$path</code><code class="operator">.</code><code class="identifier">f</code><code class="operator">;</code><code class="whitespace perl">
    </code><code class="identifier">so</code><code class="whitespace perl"> </code><code class="operator">/</code><code class="operator">^</code><code class="operator">\</code><code class="identifier">w</code><code class="operator">\</code><code class="identifier">w</code><code class="operator">+</code><code class="variable builtin">$/</code><code class="whitespace perl"> </code><code class="operator">&amp;</code><code class="operator">&amp;</code><code class="whitespace perl"> </code><code class="operator">.</code><code class="identifier">chars</code><code class="whitespace perl"> </code><code class="operator">=</code><code class="operator">=</code><code class="whitespace perl"> </code><code class="operator">.</code><code class="identifier">comb</code><code class="operator">.</code><code class="identifier">unique</code><code class="whitespace perl"> </code><code class="identifier">given</code><code class="whitespace perl"> </code><code class="identifier">trim</code><code class="whitespace perl"> </code><code class="identifier">slurp</code><code class="whitespace perl"> </code><code class="variable">$path</code><code class="whitespace perl">
</code><code class="operator">}</code><code class="whitespace perl">

</code><code class="keyword">my</code><code class="whitespace perl"> </code><code class="variable">$order</code><code class="whitespace perl"> </code><code class="operator">=</code><code class="whitespace perl"> </code><code class="variable">$base</code><code class="whitespace perl"> </code><code class="operator">/</code><code class="whitespace perl"> </code><code class="string">'order'</code><code class="operator">;</code><code class="whitespace perl">
</code><code class="identifier">spurt</code><code class="whitespace perl"> </code><code class="variable">$order</code><code class="operator">,</code><code class="whitespace perl"> </code><code class="variable">$ALNUM</code><code class="whitespace perl"> </code><code class="keyword">unless</code><code class="whitespace perl"> </code><code class="identifier">valid</code><code class="whitespace perl"> </code><code class="variable">$order</code><code class="operator">;</code><code class="whitespace perl">
</code></pre><p>Instead of printable, we only allow alphanumerics and fallback to
the uppercase ones (mainly because my screen can only fit as much vertically),
unless <code>$XDG_CACHE_HOME/threa/order</code> is a file, contains at least
two unique alphanumerics (exclusively).  Reading and writing files in Raku
is incredibly trivial, just <code>slurp</code> and <code>spurt</code> the path.  Since we are not
interested in whitespaces, they are <code>trim</code>&#x2019;ed from <code>order</code>.  Notice that
Raku allows subroutines to be called without any parentheses—I love Lisp,
but opening parenthesis <em>after</em> the function name always confuses me,
especially when nested.</p>
<p>As you might have guessed, <code>given</code> is another statement modifier
setting the <a href="https://docs.raku.org/syntax/$_">topic variable</a> that is particularly useful
in <a href="https://raku-advent.blog/2020/12/22/draft-whats-the-point-of-point-free-programming">pointfree programming</a>, where regular expressions (e.g. <code>/^\w\w+$/</code>)
are matched against directly and methods are called without specifying
the object.  Raku is also a weakly-typed language: <code>.comb.unique</code> (a list
of unique characters) is coerced into an integer when compared to one
(number of <code>.chars</code>).</p>
</section>
<section id="Comparing-and-Saving-Selections">
<h3>Comparing and Saving Selections</h3>
<p>What do we do with the order then?  First we can determine the latest
selection and compare it to the ones we got from <code>xclip</code> earlier
to see which one is really new.  We&#x2019;ll also need to rotate the order,
i.e. write the new selection to the <code>$last</code> file and move it in front
of the others that we <code>$keep</code> as-is:</p>
<pre><code class="keyword">my</code><code class="whitespace perl"> </code><code class="operator">(</code><code class="variable">$first</code><code class="operator">,</code><code class="whitespace perl"> </code><code class="variable">$keep</code><code class="operator">,</code><code class="whitespace perl"> </code><code class="variable">$last</code><code class="operator">)</code><code class="whitespace perl"> </code><code class="operator">=</code><code class="whitespace perl"> </code><code class="keyword">do</code><code class="whitespace perl">
    </code><code class="identifier">given</code><code class="whitespace perl"> </code><code class="identifier">trim</code><code class="whitespace perl"> </code><code class="identifier">slurp</code><code class="whitespace perl"> </code><code class="variable">$order</code><code class="whitespace perl"> </code><code class="operator">{</code><code class="whitespace perl"> </code><code class="operator">.</code><code class="identifier">comb</code><code class="operator">.</code><code class="identifier">first</code><code class="operator">,</code><code class="whitespace perl"> </code><code class="operator">.</code><code class="function builtin">chop</code><code class="operator">,</code><code class="whitespace perl"> </code><code class="operator">.</code><code class="function builtin">substr</code><code class="operator">:</code><code class="whitespace perl"> </code><code class="operator">*</code><code class="number">-1</code><code class="whitespace perl"> </code><code class="operator">}</code><code class="whitespace perl">
</code><code class="keyword">my</code><code class="whitespace perl"> </code><code class="operator">(</code><code class="variable">$other</code><code class="operator">,</code><code class="whitespace perl"> </code><code class="variable">$content</code><code class="operator">)</code><code class="whitespace perl"> </code><code class="operator">=</code><code class="whitespace perl"> </code><code class="keyword">do</code><code class="whitespace perl"> </code><code class="identifier">given</code><code class="whitespace perl"> </code><code class="identifier">try</code><code class="whitespace perl"> </code><code class="identifier">slurp</code><code class="whitespace perl"> </code><code class="variable">$base</code><code class="whitespace perl"> </code><code class="operator">/</code><code class="whitespace perl"> </code><code class="variable">$first</code><code class="whitespace perl"> </code><code class="keyword">or</code><code class="whitespace perl"> </code><code class="string">''</code><code class="whitespace perl"> </code><code class="operator">{</code><code class="whitespace perl">
    </code><code class="identifier">when</code><code class="whitespace perl"> </code><code class="operator">*</code><code class="whitespace perl"> </code><code class="keyword">ne</code><code class="whitespace perl"> </code><code class="variable">$primary</code><code class="whitespace perl"> </code><code class="operator">{</code><code class="whitespace perl"> </code><code class="string">'clipboard'</code><code class="operator">,</code><code class="whitespace perl"> </code><code class="variable">$primary</code><code class="whitespace perl"> </code><code class="operator">}</code><code class="whitespace perl">
    </code><code class="identifier">when</code><code class="whitespace perl"> </code><code class="operator">*</code><code class="whitespace perl"> </code><code class="keyword">ne</code><code class="whitespace perl"> </code><code class="variable">$clipboard</code><code class="whitespace perl"> </code><code class="operator">{</code><code class="whitespace perl"> </code><code class="string">'primary'</code><code class="operator">,</code><code class="whitespace perl"> </code><code class="variable">$clipboard</code><code class="whitespace perl"> </code><code class="operator">}</code><code class="whitespace perl">
</code><code class="operator">}</code><code class="whitespace perl">
</code></pre><p>On the first few run, probably the cache files don&#x2019;t exist just yet,
so we fall them back to empty ones using <code>try ... or ...</code>.  We need
to know the <code>$other</code> selection (outdated one) to later synchronize them both.
In case of reselection, neither is updated and we simply skip this iteration:</p>
<pre><code class="keyword">next</code><code class="whitespace perl"> </code><code class="keyword">unless</code><code class="whitespace perl"> </code><code class="variable">$other</code><code class="operator">;</code><code class="whitespace perl">
</code></pre><p>Otherwise, let&#x2019;s go ahead, write down the <code>$content</code>, rotate <code>$order</code>
and synchronize with the <code>$other</code> selection:</p>
<pre><code class="keyword">my</code><code class="whitespace perl"> </code><code class="variable">$path</code><code class="whitespace perl"> </code><code class="operator">=</code><code class="whitespace perl"> </code><code class="variable">$base</code><code class="whitespace perl"> </code><code class="operator">/</code><code class="whitespace perl"> </code><code class="variable">$last</code><code class="operator">;</code><code class="whitespace perl">
</code><code class="identifier">spurt</code><code class="whitespace perl"> </code><code class="variable">$path</code><code class="operator">,</code><code class="whitespace perl"> </code><code class="variable">$content</code><code class="operator">;</code><code class="whitespace perl">
</code><code class="identifier">spurt</code><code class="whitespace perl"> </code><code class="variable">$order</code><code class="operator">,</code><code class="whitespace perl"> </code><code class="variable">$last</code><code class="whitespace perl"> </code><code class="operator">~</code><code class="whitespace perl"> </code><code class="variable">$keep</code><code class="operator">;</code><code class="whitespace perl">
</code><code class="identifier">run</code><code class="whitespace perl"> </code><code class="operator">&lt;</code><code class="identifier">xclip</code><code class="whitespace perl"> </code><code class="operator">-</code><code class="identifier">in</code><code class="whitespace perl"> </code><code class="operator">-</code><code class="identifier">selection</code><code class="operator">&gt;</code><code class="operator">,</code><code class="whitespace perl"> </code><code class="variable">$other</code><code class="operator">,</code><code class="whitespace perl"> </code><code class="variable">$path</code><code class="whitespace perl">
</code></pre><p>That&#x2019;s it, now put the daemon in <code>$PATH</code> and run it in <code>~/.xinitrc</code>
or something IDK.  If you&#x2019;re worried that some selection might be
too big to read that you&#x2019;ll the next event, asynchronize the <code>qx</code> calls
by prefixing them with <code>start</code>, and <code>await</code> the results later on.
It is <em>that</em> easy.</p>
</section>
<section id="Command-Line-Interface">
<h3>Command-Line Interface</h3>
<p>Hol up, what if I want to store the cache elsewhere or use another set
of characters?  <em>&#x201c;Then you can go right ahead and have an intercourse
with yourself, you ungrateful little piece of [redacted].&#x201d;</em>  I would have said
this were I to implement this in other languages, but luckily I got Raku,
and Raku got <code>sub MAIN</code>:</p>
<pre><code class="keyword">sub</code><code class="whitespace perl"> </code><code class="function">MAIN</code><code class="operator">(</code><code class="whitespace perl">
  </code><code class="operator">:</code><code class="variable">$children</code><code class="whitespace perl"> </code><code class="identifier">where</code><code class="whitespace perl"> </code><code class="operator">/</code><code class="operator">^</code><code class="operator">\</code><code class="identifier">w</code><code class="operator">\</code><code class="identifier">w</code><code class="operator">+</code><code class="variable builtin">$/</code><code class="whitespace perl"> </code><code class="operator">=</code><code class="whitespace perl"> </code><code class="variable">$ALNUM</code><code class="operator">,</code><code class="whitespace perl"> </code><code class="comment">#= alphanumerics</code><code class="whitespace perl">
  </code><code class="operator">:</code><code class="variable">$parent</code><code class="whitespace perl"> </code><code class="operator">=</code><code class="whitespace perl"> </code><code class="variable">$XDG_CACHE_HOME</code><code class="whitespace perl">           </code><code class="comment">#= cache path</code><code class="whitespace perl">
</code><code class="operator">)</code><code class="whitespace perl"> </code><code class="operator">{</code><code class="whitespace perl">
    </code><code class="keyword">my</code><code class="whitespace perl"> </code><code class="variable">$snowflakes</code><code class="whitespace perl"> </code><code class="operator">=</code><code class="whitespace perl"> </code><code class="variable">$children</code><code class="operator">.</code><code class="identifier">comb</code><code class="operator">.</code><code class="identifier">unique</code><code class="operator">.</code><code class="function builtin">join</code><code class="operator">;</code><code class="whitespace perl">
    </code><code class="keyword">my</code><code class="whitespace perl"> </code><code class="variable">$base</code><code class="whitespace perl"> </code><code class="operator">=</code><code class="whitespace perl"> </code><code class="variable">$parent</code><code class="operator">.</code><code class="identifier">IO</code><code class="whitespace perl"> </code><code class="operator">/</code><code class="whitespace perl"> </code><code class="string">'threa'</code><code class="operator">;</code><code class="whitespace perl">
    </code><code class="keyword">my</code><code class="whitespace perl"> </code><code class="variable">$order</code><code class="whitespace perl"> </code><code class="operator">=</code><code class="whitespace perl"> </code><code class="variable">$base</code><code class="whitespace perl"> </code><code class="operator">/</code><code class="whitespace perl"> </code><code class="string">'order'</code><code class="operator">;</code><code class="whitespace perl">

    </code><code class="keyword">while</code><code class="whitespace perl"> </code><code class="identifier">run</code><code class="whitespace perl"> </code><code class="string">'clipbuzz'</code><code class="whitespace perl"> </code><code class="operator">{</code><code class="whitespace perl">
        </code><code class="operator">..</code><code class="operator">.</code><code class="whitespace perl">
        </code><code class="identifier">spurt</code><code class="whitespace perl"> </code><code class="variable">$order</code><code class="operator">,</code><code class="whitespace perl"> </code><code class="variable">$snowflakes</code><code class="whitespace perl"> </code><code class="keyword">unless</code><code class="whitespace perl"> </code><code class="identifier">valid</code><code class="whitespace perl"> </code><code class="variable">$order</code><code class="operator">;</code><code class="whitespace perl">
        </code><code class="operator">..</code><code class="operator">.</code><code class="whitespace perl">
    </code><code class="operator">}</code><code class="whitespace perl">
</code><code class="operator">}</code><code class="whitespace perl">
</code></pre><p>No matter how cool you think this is, it is cooler, I mean, look:</p>
<pre><code>$ thread --help
Usage:
  thread [--children[=Str where { ... }]] [--parent=&lt;Str&gt;]
  
    --children[=Str where { ... }]    alphanumerics
    --parent=&lt;Str&gt;                    cache path
</code></pre></section>
</section>
<section id="Client-Implementation">
<h2>Client Implementation</h2>
<section id="Back-End">
<h3>Back-End</h3>
<p>Following the Unix™ philosophy, <code>threac</code> will do only one thing and do it well:
it shall take the chosen selection and <em>schedule</em> it to move to the beginning:</p>
<pre><code class="keyword">my</code><code class="whitespace perl"> </code><code class="variable">$XDG_CACHE_HOME</code><code class="whitespace perl"> </code><code class="operator">=</code><code class="whitespace perl"> </code><code class="operator">%</code><code class="operator">*</code><code class="constant builtin">ENV</code><code class="operator">&lt;</code><code class="identifier">XDG_CACHE_HOME</code><code class="operator">&gt;</code><code class="whitespace perl"> </code><code class="regex">//</code><code class="whitespace perl"> </code><code class="identifier">path</code><code class="whitespace perl"> </code><code class="identifier">add</code><code class="whitespace perl"> </code><code class="variable">$*</code><code class="identifier">HOME</code><code class="operator">:</code><code class="whitespace perl"> </code><code class="string">'.cache'</code><code class="operator">:</code><code class="operator">;</code><code class="whitespace perl">

</code><code class="keyword">sub</code><code class="whitespace perl"> </code><code class="function">MAIN</code><code class="operator">(</code><code class="whitespace perl">
   </code><code class="variable">$choice</code><code class="whitespace perl"> </code><code class="identifier">where</code><code class="whitespace perl"> </code><code class="operator">/</code><code class="operator">^</code><code class="operator">\</code><code class="identifier">w</code><code class="operator">?</code><code class="variable builtin">$/</code><code class="operator">,</code><code class="whitespace perl">    </code><code class="comment">#= alphanumeric</code><code class="whitespace perl">
  </code><code class="operator">:</code><code class="variable">$parent</code><code class="whitespace perl"> </code><code class="operator">=</code><code class="whitespace perl"> </code><code class="variable">$XDG_CACHE_HOME</code><code class="whitespace perl"> </code><code class="comment">#= cache path</code><code class="whitespace perl">
</code><code class="operator">)</code><code class="whitespace perl"> </code><code class="operator">{</code><code class="whitespace perl">
    </code><code class="keyword">my</code><code class="whitespace perl"> </code><code class="variable">$base</code><code class="whitespace perl"> </code><code class="operator">=</code><code class="whitespace perl"> </code><code class="variable">$parent</code><code class="operator">.</code><code class="identifier">IO</code><code class="operator">.</code><code class="identifier">add</code><code class="operator">:</code><code class="whitespace perl"> </code><code class="string">'threa'</code><code class="operator">;</code><code class="whitespace perl">
    </code><code class="keyword">my</code><code class="whitespace perl"> </code><code class="variable">$order</code><code class="whitespace perl"> </code><code class="operator">=</code><code class="whitespace perl"> </code><code class="identifier">add</code><code class="whitespace perl"> </code><code class="variable">$base</code><code class="operator">:</code><code class="whitespace perl"> </code><code class="string">'order'</code><code class="operator">;</code><code class="whitespace perl">
    </code><code class="identifier">spurt</code><code class="whitespace perl"> </code><code class="variable">$order</code><code class="operator">,</code><code class="whitespace perl"> </code><code class="identifier">S</code><code class="operator">/</code><code class="variable">$choice</code><code class="operator">(</code><code class="operator">.</code><code class="operator">*</code><code class="operator">)</code><code class="operator">/</code><code class="variable builtin">$0</code><code class="variable">$choice</code><code class="operator">/</code><code class="whitespace perl"> </code><code class="identifier">with</code><code class="whitespace perl"> </code><code class="variable">$order</code><code class="operator">.</code><code class="identifier">slurp</code><code class="operator">;</code><code class="whitespace perl">
    </code><code class="keyword">my</code><code class="whitespace perl"> </code><code class="variable">$path</code><code class="whitespace perl"> </code><code class="operator">=</code><code class="whitespace perl"> </code><code class="variable">$base</code><code class="operator">.</code><code class="identifier">add</code><code class="operator">:</code><code class="whitespace perl"> </code><code class="variable">$choice</code><code class="operator">;</code><code class="whitespace perl">
    </code><code class="identifier">run</code><code class="whitespace perl"> </code><code class="string">'xclip'</code><code class="operator">,</code><code class="whitespace perl"> </code><code class="variable">$path</code><code class="whitespace perl">
</code><code class="operator">}</code><code class="whitespace perl">
</code></pre><p>The highlight here is the non-destructive substitution <code>S///</code>,
which allow regex substitution in a pointfree and pure manner.
Though, instead of moving <code>$choice</code> to top of the deque, we place it
at the bottom and use <code>xclip</code> to trigger the daemon to do it
and synchronize between selections.</p>
</section>
<section id="Front-End">
<h3>Front-End</h3>
<p>Note that <code>threac</code> does not give any output: selection history (by default) 
are stored in a standard and convenient location to be read by any front-end
of choice.  For awesome I made a menu whose each entry is wired to <code>threac</code>
and <code>xdotool</code> (to simulate primary paste with <code>S-Insert</code>) and bind
the whole thing to a keyboard shortcut.</p>
<pre><code class="keyword">local</code><code class="whitespace lua"> </code><code class="identifier">base</code><code class="whitespace lua"> </code><code class="operator">=</code><code class="whitespace lua"> </code><code class="function builtin">os.getenv</code><code class="operator">(</code><code class="string">"HOME"</code><code class="operator">)</code><code class="whitespace lua"> </code><code class="operator">..</code><code class="whitespace lua"> </code><code class="string">"/.cache/threa/"</code><code class="whitespace lua">
</code><code class="keyword">local</code><code class="whitespace lua"> </code><code class="identifier">command</code><code class="whitespace lua"> </code><code class="operator">=</code><code class="whitespace lua"> </code><code class="string">"threac %s &amp;&amp; xdotool key shift+Insert"</code><code class="whitespace lua">
</code><code class="keyword">local</code><code class="whitespace lua"> </code><code class="identifier">f</code><code class="whitespace lua"> </code><code class="operator">=</code><code class="whitespace lua"> </code><code class="function builtin">io.open</code><code class="operator">(</code><code class="identifier">base</code><code class="whitespace lua"> </code><code class="operator">..</code><code class="whitespace lua"> </code><code class="string">"order"</code><code class="operator">)</code><code class="whitespace lua">
</code><code class="keyword">local</code><code class="whitespace lua"> </code><code class="identifier">order</code><code class="whitespace lua"> </code><code class="operator">=</code><code class="whitespace lua"> </code><code class="identifier">f</code><code class="operator">:</code><code class="function method">read</code><code class="operator">(</code><code class="string">"*a"</code><code class="operator">)</code><code class="whitespace lua">
</code><code class="identifier">f</code><code class="operator">:</code><code class="function method">close</code><code class="operator">(</code><code class="operator">)</code><code class="whitespace lua">

</code><code class="keyword">local</code><code class="whitespace lua"> </code><code class="identifier">items</code><code class="whitespace lua"> </code><code class="operator">=</code><code class="whitespace lua"> </code><code class="operator">{</code><code class="operator">}</code><code class="whitespace lua">
</code><code class="keyword">for</code><code class="whitespace lua"> </code><code class="identifier">c</code><code class="whitespace lua"> </code><code class="keyword">in</code><code class="whitespace lua"> </code><code class="identifier">order</code><code class="operator">:</code><code class="function method">gmatch</code><code class="operator">(</code><code class="string">"."</code><code class="operator">)</code><code class="whitespace lua"> </code><code class="keyword">do</code><code class="whitespace lua">
  </code><code class="keyword">local</code><code class="whitespace lua"> </code><code class="identifier">f</code><code class="whitespace lua"> </code><code class="operator">=</code><code class="whitespace lua"> </code><code class="function builtin">io.open</code><code class="operator">(</code><code class="identifier">base</code><code class="whitespace lua"> </code><code class="operator">..</code><code class="whitespace lua"> </code><code class="identifier">c</code><code class="operator">)</code><code class="whitespace lua">
  </code><code class="function builtin">table.insert</code><code class="operator">(</code><code class="identifier">items</code><code class="operator">,</code><code class="whitespace lua"> </code><code class="operator">{</code><code class="identifier">f</code><code class="operator">:</code><code class="function method">read</code><code class="operator">(</code><code class="string">"*a"</code><code class="operator">)</code><code class="operator">:</code><code class="function method">gsub</code><code class="operator">(</code><code class="string">"\n"</code><code class="operator">,</code><code class="whitespace lua"> </code><code class="string">" "</code><code class="operator">)</code><code class="operator">,</code><code class="whitespace lua"> </code><code class="keyword">function</code><code class="whitespace lua"> </code><code class="operator">(</code><code class="operator">)</code><code class="whitespace lua">
    </code><code class="identifier">awful</code><code class="operator">.</code><code class="identifier">spawn</code><code class="operator">.</code><code class="function">with_shell</code><code class="operator">(</code><code class="identifier">command</code><code class="operator">:</code><code class="function method">format</code><code class="operator">(</code><code class="identifier">c</code><code class="operator">)</code><code class="operator">)</code><code class="whitespace lua">
  </code><code class="keyword">end</code><code class="operator">}</code><code class="operator">)</code><code class="whitespace lua">
  </code><code class="identifier">f</code><code class="operator">:</code><code class="function method">close</code><code class="operator">(</code><code class="operator">)</code><code class="whitespace lua">
</code><code class="keyword">end</code><code class="whitespace lua">
</code><code class="identifier">awful</code><code class="operator">.</code><code class="function">menu</code><code class="operator">{</code><code class="identifier">items</code><code class="whitespace lua"> </code><code class="operator">=</code><code class="whitespace lua"> </code><code class="identifier">items</code><code class="operator">,</code><code class="whitespace lua"> </code><code class="identifier">theme</code><code class="whitespace lua"> </code><code class="operator">=</code><code class="whitespace lua"> </code><code class="operator">{</code><code class="identifier">width</code><code class="whitespace lua"> </code><code class="operator">=</code><code class="whitespace lua"> </code><code class="number">911</code><code class="operator">}</code><code class="operator">}</code><code class="operator">:</code><code class="function method">show</code><code class="operator">(</code><code class="operator">)</code><code class="whitespace lua">
</code></pre></section>
</section>
<section id="Conclusion">
<h2>Conclusion</h2>
<p>Through writing the clipboard manager <a href="https://sr.ht/~cnx/threa">threa</a>, which is released
under <a href="https://www.gnu.org/licenses/gpl-3.0">GNU GPLv3+</a> on <a href="https://sourcehut.org">SourceHut</a>, we have discovered a few features
of Raku that make it a great <em>scripting</em> language:</p>
<ul>
<li>
Out-of-box CLI support:
<ul>
<li>
Running programs and capturing output
</li>
<li>
Environment variables
</li>
<li>
File system operations
</li>
<li>
Builtin argument parser
</li>
</ul>
</li>
<li>
Concision:
<ul>
<li>
Statement modifiers
</li>
<li>
Topic variable
</li>
<li>
First-class regex
</li>
<li>
Trivial asynchronization
</li>
</ul>
</li>
</ul>
<p>As a generic programming language, Raku has other classes of characteristics
that makes it useful in other larger projects such as grammars (i.e. regex
on steroids) and OOP for human beings.  It is a truly versatile language
and I really hope my words can convince someone new to try it out!</p>
</section>
</section>
<section role="doc-endnotes">
<hr/>
<ol>
<li id="fn1">
<p>I ain&#x2019;t proud of this, okay?<a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn2">
<p>The <em>z</em>&#x2019;s are for Zig, how original, I know.<a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn3">
<p><a><em>citation needed</em></a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn4">
<p>Wow much environment!<a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn5">
<p>By some supernatural being of course!<a href="#fnref5" role="doc-backlink">↩︎︎</a><a href="#fnref5" role="doc-backlink">↩︎︎</a><a href="#fnref5" role="doc-backlink">↩︎︎</a><a href="#fnref5" role="doc-backlink">↩︎︎</a><a href="#fnref5" role="doc-backlink">↩︎︎</a></p>
</li>
</ol>
</section>
</div>
</content>
<category term='fun'/>
<category term='recipe'/>
<category term='clipboard'/>
<published>2021-07-03T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/2020/gsoc/checkin/5/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/2020/gsoc/checkin/5/index.html</id>
<title>Fifth Check-In</title>
<summary>GSoC 2020: Fifth Check-In</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/2020/gsoc/checkin/5/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<p>+++
date = Date(2020, 7, 27)
tags = [&#x201c;gsoc&#x201d;, &#x201c;pip&#x201d;]
+++</p>
<section id="Fifth-Check-In">
<h1>Fifth Check-In</h1>
<p>Hello and I hope y&#x2019;all are still doing well!</p>
<section id="What-did-I-do-last-week">
<h2>What did I do last week?</h2>
<p>I was not really productive last week—most of the following tickets are fillers
to make use of the spare cycles I had when I was still trying to figure out
the way to implement the main work.</p>
<ul>
<li>
Finalize the <code>--use-feature=fast-deps</code> flag ({{pip 8588}})
</li>
<li>
Improve mocking of environment variables in the test suit ({{pip 8614}})
</li>
<li>
Finalize the fix for verbose/quiet options specified via
configuration files and environment variables ({{pip 8578}})
</li>
<li>
Clean up a tiny bit in the resolver internal API ({{pip 8629}})
</li>
<li>
Start working on seperating the download of wheels
from dependency resolution ({{pip 8638}})
</li>
</ul>
</section>
<section id="Did-I-get-stuck-anywhere">
<h2>Did I get stuck anywhere?</h2>
<p>I&#x2019;m struggling on refactoring the code to support separate download.
<code>pip</code>&#x2019;s codebase was not intended for this and thus there are
many execution paths and other details entangled around the relevant area.</p>
</section>
<section id="What-is-coming-up-next">
<h2>What is coming up next?</h2>
<p><code>pip</code> 20.2 is going to be released within the next few days with
<code>--use-feature=fast-deps</code> included and I&#x2019;m mentally prepare to fix
any undiscovered problem.  At the same time, I will continue working
on {{pip 8638}} and hopefully get it done soon enough to begin drafting
download parallelization strategies, mostly with the UI.</p>
</section>
</section>
</div>
</content>
<category term='gsoc'/>
<published>2020-07-27T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/nopoo/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/nopoo/index.html</id>
<title>To Poo or Not to Poo?</title>
<summary>Me experimenting with #nopoo</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/nopoo/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<section id="To-Poo-or-Not-to-Poo">
<h1>To Poo or Not to Poo?</h1>
<p>Late April 2021, Việt Nam witnessed the beginning of the fourth wave
of SARS-CoV-2 after a few months without any community case.  Soon enough,
students are told to not come to their schools&#x2019; campus.  This happens
when I was an intern at <a href="https://usth.edu.vn">USTH</a> <a href="https://ictlab.usth.edu.vn">ICTLab</a>, so I was advised to work remotely
as well.  I asked for this at the start of the internship but my supervisor
was rather reluctant, since there was multiple interns working together
and communication in person might be most effective.  Working from home
was beneficial to me in a few important ways:</p>
<ol>
<li>
I had a three-monitor setup at home and a more comfortable space.
</li>
<li>
I could have be more flexible working hours at home.
</li>
<li>
I did not have to bike back and forth to the lab (which is 4 km away)
twice a day<a id="fnref1" href="#fn1" role="doc-noteref"><sup>1</sup></a>, which could be exhausting in the hot summer.
</li>
</ol>
<p>Thanks to the last point, I also sweat a lot less and as I no longer
had to maintain a public appearance, I decided to give <code>#nopoo</code> a try.
I had been aware of such practice for quite a few years, but had never
thought of actually implementing it until I saw <a href="https://www.youtube.com/watch?v=T-_HKFjxVl0">Johnny Harris&#x2019; vlog</a>,
which I can only describe as <em>intriguing</em>.  TL;DW the journalist
maintained that generally shampoos washed away <em>his</em> scalp&#x2019;s natural oil,
and in combination with other hair products made the scalp itchy and unhealthy.
<em>His</em> solution was to drop the use of all products completely and so far
it had been working <em>for him</em>.<a id="fnref2" href="#fn2" role="doc-noteref"><sup>2</sup></a></p>
<p>Well, my head was itchy sometimes (still itchy at the time of writing),
alors, <a href="https://polytechnique.edu">pour la patrie, les sciences et la gloire</a>, let&#x2019;s do it!</p>
<section id="Day-One">
<h2>Day One</h2>
<p>I was going full no poo, no soap, no baking soda, no vinegar, <em>just water,
raw water</em>.  Everything was going as expected, my hair was not as fluffy
as usual after washing, but it was easier to get in shape.  I didn&#x2019;t really
style my hair.  Not as a fashion statement, I was (still am!) just rather lazy.
Usually this wasn&#x2019;t an issue, unless when my hair was long, it tent to cover
my forehead, ears and eyes, which was arguably an uncomfortable experience.
Having the hair stay in place was indeed a blessing!</p>
</section>
<section id="Day-Two">
<h2>Day Two</h2>
<p>My hair started to feel thicker and running hands through it no longer
felt simulating.  On the bright side it looked fabulous and did not itch.</p>
</section>
<section id="Day-Four">
<h2>Day Four</h2>
<p>My hair and scalp began to feel greasy.  I guess it was because I did not
wash it thoroughly that day.  With just water one would need to take more
effort scrubbing the hair and especially the scalps to return them
to a comfortable state.  Plus my mentality got worse so my perceived
experience could be exaggeratedly negative.</p>
</section>
<section id="Day-Five">
<h2>Day Five</h2>
<p>I worked out and paid more attention to the hair washing process.
It felt noticeably better.</p>
</section>
<section id="Day-Six">
<h2>Day Six</h2>
<p>The brief revival of my mental health did not last very long:<a id="fnref3" href="#fn3" role="doc-noteref"><sup>3</sup></a>
later that day I was completely autopiloting and accidentally poo&#x2019;ed myself.
It felt fluffy again but I was disappointed that things did not go as planned.</p>
</section>
<section id="Day-Seven">
<h2>Day Seven</h2>
<p>I decided to cut my hair.  I had been doing so for a decade when I wrote this,
but I got neither better nor faster at it, so it only happens twice or thrice
a year.  Of course I had to poo myself after to get rid of all the tiny pieces.</p>
</section>
<section id="Day-Eleven">
<h2>Day Eleven</h2>
<p>Fast forward a few days it started to feel greasy again, but this time the hair
was shorter so it was less of an issue.  I began to apply [saline] to the hair
after washing and somehow it helped a lot in improving the situation.  Saline
was also my solution for face acne in my teenage year (along with finger nails
and pillowcase hygiene).</p>
</section>
<section id="Day-Fifteen">
<h2>Day Fifteen</h2>
<p>At this point the experience had become more stable.  My scalp
still itched occasionally but seemly less often than when I was poo&#x2019;ing
more regularly.  The hair stayed in shape with merely any effort
(I didn&#x2019;t even use a comb).</p>
<p>Overall, the difference is barely noticeable otherwise but I think I will be 
continuing holding my poo for another while, probably in long term.
Do not let my experience speak for you, however, try it yourself if you are
interested, but keep observing the effect objectively.</p>
</section>
</section>
<section role="doc-endnotes">
<hr/>
<ol>
<li id="fn1">
<p>I usually had lunch at home with my parents.<a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn2">
<p>Emphases <em>his</em>.<a id="fnref4" href="#fn4" role="doc-noteref"><sup>4</sup></a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn3">
<p>I later discovered that this was due to the lack of <a href="https://www.sunlightdish.com">sunlight</a>.<a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a></p>
</li>
</ol>
</section>
</div>
</content>
<category term='lyf'/>
<category term='exp'/>
<published>2021-05-23T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/2020/gsoc/checkin/3/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/2020/gsoc/checkin/3/index.html</id>
<title>Third Check-In</title>
<summary>GSoC 2020: Third Check-In</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/2020/gsoc/checkin/3/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<p>+++
date = Date(2020, 6, 29)
tags = [&#x201c;gsoc&#x201d;, &#x201c;pip&#x201d;]
+++</p>
<section id="Third-Check-In">
<h1>Third Check-In</h1>
<p>Holla, holla, holla!  Last seven days has not been a really productive week
for me, though I think there are still some nice things to share with
you all here!  The good news is that I&#x2019;ve finish my last leçon as a somophore,
the bad news is that I have a bunch of upcoming tests, mainly in the form
of group projects and/or presentation (phew!).  Enough about me,
let&#x2019;s get back to <code>pip</code>:</p>
<section id="What-did-I-do-last-week">
<h2>What did I do last week?</h2>
<p>Not much, actually )-:</p>
<ul>
<li>
Write some tests for {{pip 8467 &#x201d;the HTTP range mapping for wheel&#x201d;}.
</li>
<li>
{{pip 8504 &#x201d;Try to bring back&#x201d;} multithreaded <code>pip list --outdated</code>
and <code>--uptodate</code>, as {{pip 8320 &#x201d;the parallel &lt;code&gt;map&lt;/code&gt;&#x201d;} was merged
earlier today.
</li>
<li>
Nitpick {{pip 8332}}
(yep it&#x2019;s a new low for me to include this to the list (-:).
</li>
</ul>
</section>
<section id="Did-I-get-stuck-anywhere">
<h2>Did I get stuck anywhere?</h2>
<p>Not exactly, since I didn&#x2019;t do much d-;  [Many of my PRs] are stalling though.
On one hand the maintainers of <code>pip</code> are all volunteers working in
their free time, on the other hand I don&#x2019;t think I have tried hard enough
to get their attention on my PRs.</p>
</section>
<section id="What-is-coming-up-next">
<h2>What is coming up next?</h2>
<p>I&#x2019;ll try my best getting the following merged upstream before
{{pip 8206 &#x201d;the upcoming beta release&#x201d;}:</p>
<ul>
<li>
Parallel networking for <code>pip list</code>: {{pip 8504}}
</li>
<li>
Lazy wheel for dependency information: {{pip 8467}}, {{pip 8411}}
(to determine if hashing is required) and {{pip 8467#issuecomment-648717032
&#x201d;a new patch introducing this as an unstable feature&#x201d;}
</li>
</ul>
</section>
</section>
</div>
</content>
<category term='gsoc'/>
<published>2020-06-29T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/2020/gsoc/article/3/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/2020/gsoc/article/3/index.html</id>
<title>I'm Not Drowning on My Own</title>
<summary>GSoC 2020: I'm Not Drowning on My Own</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/2020/gsoc/article/3/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<section id="I'm-Not-Drowning-on-My-Own">
<h1>I&#x2019;m Not Drowning on My Own</h1>
<section id="Cold-Water">
<h2>Cold Water</h2>
<p>Hello there!  My schoolyear is coming to an end, with some final assignments
and group projects left to be done.  I for sure underestimated the workload
of these and in the last (and probably next) few days I&#x2019;m drowning in work
trying to meet my deadlines.</p>
<p>One project that might be remotely relevant is <a href="https://github.com/McSinyx/cheese-shop">cheese-shop</a>, which tries to
manage the metadata of packages from the real <a href="https://pypi.org">Cheese Shop</a>.  Other than that,
schoolwork is draining a lot of my time and I can&#x2019;t remember the last time
I came up with something new for my GSoC project )-;</p>
</section>
<section id="Warm-Water">
<h2>Warm Water</h2>
<p>On the bright side, I received a lot of help and encouragement
from contributors and stakeholders of <code>pip</code>.  In the last week alone,
I had five pull requests merged:</p>
<ul>
<li>
<a href="https://github.com/pypa/pip/issues/8332">GH-8332</a>: Add license requirement to <code>_vendor/README.rst</code>
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8320">GH-8320</a>: Add utilities for parallelization
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8504">GH-8504</a>: Parallelize <code>pip list --outdated</code> and <code>--uptodate</code>
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8411">GH-8411</a>: Refactor <code>operations.prepare.prepare_linked_requirement</code>
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8467">GH-8467</a>: Add utitlity to lazily acquire wheel metadata over HTTP
</li>
</ul>
<p>In addition to helping me getting my PRs merged, my mentor Pradyun Gedam
also gave me my first official feedback, including what I&#x2019;m doing right
(and wrong too!) and what I should keep doing to increase the chance of
the project being successful.</p>
<p><a href="https://github.com/pypa/pip/issues/7819">GH-7819</a>&#x2019;s roadmap (Danny McClanahan&#x2019;s discoveries and works on lazy wheels)
is being closely tracked by <code>hatch</code>&#x2019;s maintainter Ofek Lev, which really
makes me proud and warms my heart, that what I&#x2019;m helping build is actually
needed by the community!</p>
</section>
<section id="Learning-How-To-Swim">
<h2>Learning How To Swim</h2>
<p>With <a href="https://github.com/pypa/pip/issues/8467">GH-8467</a> and <a href="https://github.com/pypa/pip/issues/8530">GH-8530</a> merged, I&#x2019;m now working on <a href="https://github.com/pypa/pip/issues/8532">GH-8532</a>
which aims to roll out the lazy wheel as the way to obtain
dependency information via the CLI flag <code>--use-feature=lazy-wheel</code>.</p>
<p><a href="https://github.com/pypa/pip/issues/8532">GH-8532</a> was failing initially, despite being relatively trivial and that
the commit it used to base on was passing.  Surprisingly, after rebasing it
on top of <a href="https://github.com/pypa/pip/issues/8530">GH-8530</a>, it suddenly became green mysteriously.  After the first
(early) review, I was able to iterate on my earlier code, which used
the ambiguous exception <code>RuntimeError</code>.</p>
<p>The rest to be done is <em>just</em> adding some functional tests (I&#x2019;m pretty sure
this will be either overwhelming or underwhelming) to make sure that
the command-line flag is working correctly.  Hopefully this can make it into
the beta of the upcoming release <a href="https://github.com/pypa/pip/issues/8511">this month</a>.</p>
<p><img alt="Lazy wheel" src="lazy-wheel.jpg"/></p>
<p>In other news, I&#x2019;ve also submitted <a href="https://github.com/pypa/pip/issues/8538">a patch improving the tests for the parallelization utilities</a>, which was really messy
as I wrote them. Better late than never!</p>
<p>Metaphors aside, I actually can&#x2019;t swim d-:</p>
</section>
<section id="Diving-Plan">
<h2>Diving Plan</h2>
<p>After <a href="https://github.com/pypa/pip/issues/8532">GH-8532</a>, I think I&#x2019;ll try to parallelize downloads of wheels
that are lazily fetched only for metadata.  By the current implementation
of the new resolver, for <code>pip install</code>, this can be injected directly
between the resolution and build/installation process.</p>
</section>
</section>
</div>
</content>
<category term='gsoc'/>
<category term='python'/>
<published>2020-07-06T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/reply/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/reply/index.html</id>
<title>Comments for Static Sites without JavaScripts</title>
<summary>Comments for Static Sites without JavaScript via Emails</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/reply/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<section id="Comments-for-Static-Sites-without-JavaScripts">
<h1>Comments for Static Sites without JavaScripts</h1>
<blockquote>
<p>I&#x2019;m open for criticism<br/>
But really, is it any room for criticism?</p>
</blockquote>
<p>Recently, I&#x2019;ve switched my <a href="https://en.wikipedia.org/wiki/Web_feed">feed</a> reader from <a href="https://newsboat.org">Newsboat</a> to <a href="https://lzone.de/liferea">Liferea</a>.
The latter has a GUI and some extra features which make the experience
a lot more comfy.  For instance, custom enclosure handling lets me
to finally migrate all of my YouTube subscriptions to <a href="https://en.wikipedia.org/wiki/Atom_(Web_standard)">Atom</a>
and <em>conveniently</em> browse and watch videos using <a href="https://mpv.io">mpv</a>.  Image support
also allows me to directly view web comics.<a id="fnref1" href="#fn1" role="doc-noteref"><sup>1</sup></a>  One of them,
<a href="https://themonsterunderthebed.net">The Monster Under the Bed</a>,<a id="fnref2" href="#fn2" role="doc-noteref"><sup>2</sup></a> does not embed the strips
in its feed, but it has comments.</p>
<p>Yes, <a href="https://www.rssboard.org/rss-specification">RSS</a> includes support for <code>&lt;comments&gt;</code>, and I was not aware of it
until <a href="https://nixnet.social/notice/AEO3fYbuzYCJl85eD2">very recently</a>.  I suppose many other people late to
the (web feed) party are neither.  Since the rise of static sites,
feeds have regain popularity, even for <a href="https://www.theregister.com/2021/05/20/google_rss_chrome_android">Google to reconsider
its direction</a>.  Compare to RSS or Atom, alternatives have
the following shortcomings:</p>
<ul>
<li>
<a href="https://en.wikipedia.org/wiki/Usenet">Usenet</a> is generally obsolete to most people.
</li>
<li>
<a href="https://en.wikipedia.org/wiki/Mailing_list">Mailing list</a> messages are immutable.
</li>
<li>
Fora and social media are silos.<a id="fnref3" href="#fn3" role="doc-noteref"><sup>3</sup></a>
</li>
<li>
Social media are designed for ephemeral discussions.
</li>
<li>
Instant messaging is awful for archival.
</li>
</ul>
<p>On the other hand, news feeds are commonly read-only: only a few readers
can render comments and even fewer are able to post one.  On the server side,
a dynamic server is needed to accept comments.  Traditionally, it&#x2019;s the same
as the system serving the website.  Although this works, it is significantly
more costly than a server dedicated to static sites, which scale a lot better.</p>
<p><a href="https://en.wikipedia.org/wiki/Hacker">Hackers</a> have came up with multiple workarounds such as using
<a href="https://carlschwan.eu/2020/12/29/adding-comments-to-your-static-blog-with-mastodon">microblogging</a> or <a href="https://cactus.chat">instant messaging</a> to add comments
to their static sites, but all require client-side code execution,
which is an option for neither RSS nor Atom.  Furthermore, <a href="https://unixsheikh.com/articles/so-called-modern-web-developers-are-the-culprits.html">JavaScript
hurts portability and performance</a> on the WWW, hence it should
be avoided unless it is absolutely impossible to implement a feature otherwise.
Commenting is not an exception.</p>
<p>Following is my adventure implementing a comment section for this very blog.
If you&#x2019;re also up to the task, I think you should view what I did
as an inspiration (rather than a reference) and don&#x2019;t be afraid
to experiment around until satisfaction.</p>
<ol>
<li>
<a href="#Choosing-Back-End">Choosing Back-End</a></li>
<li>
<a href="#Designing-Data-Flow">Designing Data Flow</a></li>
<li>
<a href="#Implementation">Implementation</a><ol type="a">
<li>
<a href="#Accepting-Replies">Accepting Replies</a></li>
<li>
<a href="#Rendering-Comments">Rendering Comments</a></li>
<li>
<a href="#Injecting-Comments">Injecting Comments</a></li>
</ol>
</li>
<li>
<a href="#Moderation">Moderation</a></li>
</ol>
<section id="Choosing-Back-End">
<h2>Choosing Back-End</h2>
<p>As mentioned earlier, static sites or not, there still needs to be
a dynamic component to accept incoming replies.  HTTP requests would be
the most portable since all netizen obviously have a web browser, but those
are what we&#x2019;re trying to replace here.  What else does everyone has nowadays?
Something so common that it can be used to identify people upon
service registrations?  Exactly, emails and phone numbers!</p>
<p>OK, Imma stop horsing around.  My back-end of choice would be emails.
It&#x2019;s global, it&#x2019;s cheap and federated.  Cellular services almost fit the bill,
except that they would cost an arm and leg for one to comment around the web
everyday via SMS, whose character limit is not facilitating thoughtful
discussions either.  As for forum, social medium or instant messaging,
no platform has nearly as large of an user base as electronic mails.</p>
<p><img alt="HTML is often a trojan horse for JavaScript" src="html5-js.png"/></p>
<p>It&#x2019;s not like any email would fit the comment section though.  Especially
not the HTML kind with a few hundred kilobytes of embedded CSS, JS
and non-content images.  From the security standpoint alone &#x2019;tis already
a no-go.  A light markup language like Markdown<a id="fnref4" href="#fn4" role="doc-noteref"><sup>4</sup></a> would be much better.</p>
<p>One great thing about using a mature technology like email is that we have
all use cases covered.  Filtering, exporting and parsing emails work out-of-box
regardless of one&#x2019;s provider, <a href="https://en.wikipedia.org/wiki/Email_client">MUA</a> and programming preferences.  I have
an SourceHut account with which I can create mailing lists on-demand
so I&#x2019;m using it; however there&#x2019;s no reason exporting from your private inbox
is any more difficult, presuming you have set up <a href="https://drewdevault.com/2021/05/17/aerc-with-mbsync-postfix.html">offline email</a>.</p>
<div class="note">
<h3 id="Tips-and-tricks">Tips and tricks</h3>
<p>Speaking of SourceHut, exporting a mailing list archive is rather easy,
one could either use the button on the web UI or download from the API.
As the operation is not exactly cost-free, the former is protected
by a <a href="https://en.wikipedia.org/wiki/Cross-site_request_forgery">CSRF</a> token and the latter by <a href="https://man.sr.ht/meta.sr.ht/oauth.md">OAuth 2.0</a>.  If you are a fellow
<a href="https://sr.ht">sr.ht</a> user, you can use <a href="https://man.sr.ht/builds.sr.ht/manifest.md#tasks">acurl</a> on the build service with the URL
from the <a href="https://lists.sr.ht/graphql">GraphQL</a> <code>query { me { lists { results { name, archive } } } }</code>.</p>
</div>
<div class="note">
<h3 id="Update">Update</h3>
<p>I stopped paying for sr.ht in May 2024 after years of Sourcehut failing
to show any measurable progress towards reaching the beta status.
I am now using public-inbox for public, eh, inboxes.</p>
</div>
</section>
<section id="Designing-Data-Flow">
<h2>Designing Data Flow</h2>
<p>I promise, this sounds bigger than it really is, but first,
let&#x2019;s have a glance at how static generators work.  Typically,
there are three times templating happens:</p>
<ol>
<li>
Conversion of individual articles into HTML <em>content</em>
</li>
<li>
Inserting each article content in a page template
to create a complete HTML document
</li>
<li>
Inserting multiple HTML contents into one RSS or Atom feed template
</li>
</ol>
<p>At completion, two kinds of output are generated: website and web feed.
Similarly, comments have to be rendered for both targets: an HTML
comment section for web browsing and a separate RSS feed for each article&#x2019;s
<code>&lt;wfw:commentRss&gt;</code>.<a id="fnref5" href="#fn5" role="doc-noteref"><sup>5</sup></a>  Therefore, injections should be done separately
at stage 2 and 3.  The overall process of static site generation
with email comments is illustrated as follows.</p>
<p><img alt="Data transformation during generation process" src="formbox.svg"/></p>
<p>For clarity, HTML and RSS input templates for comments and their parent page
and web feed are omitted.  Path to each <em>comment feed</em> output being injected
in the respective <em>web feed item</em> is also not shown in the figure.</p>
</section>
<section id="Implementation">
<h2>Implementation</h2>
<p>At the time of writing, this personal website of mine was generated
by <a href="https://julialang.org">Julia</a> <a href="https://franklinjl.org">Franklin</a>, who was neither fast<a id="fnref6" href="#fn6" role="doc-noteref"><sup>6</sup></a> nor <a href="https://github.com/tlienart/Franklin.jl/issues/936">semantic</a>,
but was the only one I knew supporting LaTeX prerendering out of the box.
Franklin is also rather <a href="https://franklinjl.org/syntax/utils">extendable</a> via Julia functions.</p>
<section id="Accepting-Replies">
<h3>Accepting Replies</h3>
<p>Let&#x2019;s start with how each article can be programmatically and uniquely
identified.  By default in RSS, a <a>GUID</a> is the permanent URL
of the associated web page.  I am not exactly a creative person, so I mirrored
this idea, although I only used the difference between URLs, i.e. minus
the scheme, network location and trailing <code>index.html</code> (Franklin always
appends it to the target path of any source file that is neither <code>index.md</code>
nor <code>index.html</code>):</p>
<pre><code class="function">dir_url</code><code class="default">(</code><code class="default">)</code><code class="whitespace julia"> </code><code class="operator">=</code><code class="whitespace julia"> </code><code class="function">strip</code><code class="default">(</code><code class="function">dirname</code><code class="default">(</code><code class="function">locvar</code><code class="default">(</code><code class="symbol">:fd_url</code><code class="default">)</code><code class="default">)</code><code class="default">,</code><code class="whitespace julia"> </code><code class="operator">'</code><code class="operator">/</code><code class="operator">'</code><code class="default">)</code><code class="whitespace julia">
</code><code class="function">message_id</code><code class="default">(</code><code class="default">)</code><code class="whitespace julia"> </code><code class="operator">=</code><code class="whitespace julia"> </code><code class="string">"%3C$(dir_url())@cnx%3E"</code><code class="whitespace julia">
</code></pre><p>For maximum portability, threading identification is used in emails&#x2019;
<code>In-Reply-To</code> header, which expects a message ID, which must match
<code>&lt;.+@.+&gt;</code>.  Once again, to avoid having to think, I opted for
the path difference for the left hand side and my nickname <code>cnx</code>
for the right.  The <code>mailto</code> URI could be then be constructed accordingly:</p>
<pre><code class="keyword">using</code><code class="whitespace julia"> </code><code class="identifier">Printf</code><code class="operator">:</code><code class="whitespace julia"> </code><code class="preprocessor">@sprintf</code><code class="whitespace julia">

</code><code class="keyword">function</code><code class="whitespace julia"> </code><code class="function">hfun_mailto_comment</code><code class="default">(</code><code class="default">)</code><code class="whitespace julia">
  </code><code class="preprocessor">@sprintf</code><code class="default">(</code><code class="string">"mailto:%s?%s=%s&amp;%s=Re: %s"</code><code class="default">,</code><code class="whitespace julia">
           </code><code class="string">"cnx.site@loa.loang.net"</code><code class="default">,</code><code class="whitespace julia">
           </code><code class="string">"In-Reply-To"</code><code class="default">,</code><code class="whitespace julia"> </code><code class="function">message_id</code><code class="default">(</code><code class="default">)</code><code class="default">,</code><code class="whitespace julia">
           </code><code class="string">"Subject"</code><code class="default">,</code><code class="whitespace julia"> </code><code class="function">locvar</code><code class="default">(</code><code class="symbol">:title</code><code class="default">)</code><code class="default">)</code><code class="whitespace julia">
</code><code class="keyword">end</code><code class="whitespace julia">
</code></pre><p>The anchor was then added to the page foot:</p>
<pre><code class="tag chars">&lt;</code><code class="tag">a</code><code class="whitespace html"> </code><code class="attribute">href</code><code class="operator">=</code><code class="string">"{{mailto_comment}}"</code><code class="whitespace html">
   </code><code class="attribute">title</code><code class="operator">=</code><code class="string">"Reply via email"</code><code class="tag chars">&gt;</code><code class="default">{</code><code class="default">{</code><code class="default">a</code><code class="default">u</code><code class="default">t</code><code class="default">h</code><code class="default">o</code><code class="default">r</code><code class="default">}</code><code class="default">}</code><code class="tag chars">&lt;/</code><code class="tag">a</code><code class="tag chars">&gt;</code><code class="whitespace html">
</code></pre></section>
<section id="Rendering-Comments">
<h3>Rendering Comments</h3>
<p>This is when the fun begins.  Julia&#x2019;s standard library does not include
an email parser, and I doubt your favorite language does either,
unless it is named after a British comedy troupe.  Python is often described
as <em>batteries included</em>, or at least it used to (seemingly the consensus among
current core devs has shifted towards <a href="https://discuss.python.org/t/adopting-recommending-a-toml-parser/4068">favoring third-party libraries</a>).</p>
<div class="note">
<h3 id="Off-topic-rambling">Off-topic rambling</h3>
<p>Standard library inclusion wasn&#x2019;t really the deal breaker here though.
I still needed a Markdown engine and a HTML sanitizer (because Markdown
can include HTML), and AFAICT no stdlib has them.  The read issue was
with the lack of Julia packaging on most distributions (apart from Guix),
and most certainly <a href="https://github.com/NixOS/nixpkgs/issues/20649">not on NixOS</a>, my current distro.  For the same reason
the idea of rewriting Franklin in Python has been running in my head
for a while now.  Python packaging is much more downstream-friendly
and unlike Julia compilation overhead is almost non-existent.</p>
</div>
<p>On the other hand, it&#x2019;s trivial to pipe an external program&#x2019;s output to Julia,
e.g. <code>readchomp(`echo foo bar`)</code> would give you the string &#x201c;foo bar&#x201d;.  Thus,
the to-be-written <em>comment generator</em> should take (the path to) a mail box,
the message ID of the article and a template, and write the result to stdout.
Argument parsing is, again, thankfully in Python&#x2019;s stdlib:</p>
<pre><code class="keyword">from</code><code class="whitespace python"> </code><code class="identifier">argparse</code><code class="whitespace python"> </code><code class="keyword">import</code><code class="whitespace python"> </code><code class="identifier">ArgumentParser</code><code class="whitespace python">
</code><code class="keyword">from</code><code class="whitespace python"> </code><code class="identifier">pathlib</code><code class="whitespace python"> </code><code class="keyword">import</code><code class="whitespace python"> </code><code class="identifier">Path</code><code class="whitespace python">
</code><code class="keyword">from</code><code class="whitespace python"> </code><code class="identifier">urllib</code><code class="operator">.</code><code class="identifier">parse</code><code class="whitespace python"> </code><code class="keyword">import</code><code class="whitespace python"> </code><code class="identifier">unquote</code><code class="whitespace python">

</code><code class="identifier">parser</code><code class="whitespace python"> </code><code class="operator">=</code><code class="whitespace python"> </code><code class="function">ArgumentParser</code><code class="operator">(</code><code class="operator">)</code><code class="whitespace python">
</code><code class="identifier">parser</code><code class="operator">.</code><code class="function method">add_argument</code><code class="operator">(</code><code class="string">'mbox'</code><code class="operator">)</code><code class="whitespace python">
</code><code class="identifier">parser</code><code class="operator">.</code><code class="function method">add_argument</code><code class="operator">(</code><code class="string">'id'</code><code class="operator">,</code><code class="whitespace python"> </code><code class="identifier">type</code><code class="operator">=</code><code class="identifier">unquote</code><code class="operator">)</code><code class="whitespace python">
</code><code class="identifier">parser</code><code class="operator">.</code><code class="function method">add_argument</code><code class="operator">(</code><code class="string">'template'</code><code class="operator">,</code><code class="whitespace python"> </code><code class="identifier">type</code><code class="operator">=</code><code class="identifier">Path</code><code class="operator">)</code><code class="whitespace python">
</code><code class="identifier">args</code><code class="whitespace python"> </code><code class="operator">=</code><code class="whitespace python"> </code><code class="identifier">parser</code><code class="operator">.</code><code class="function method">parse_args</code><code class="operator">(</code><code class="operator">)</code><code class="whitespace python">
</code></pre><p>I then parsed the <a href="https://datatracker.ietf.org/doc/html/rfc4155">mbox</a> into a mapping indexed by parent message IDs
as follows.  They would be HTML-unquoted so that was why I needed
to do the same for the input message ID.</p>
<pre><code class="keyword">from</code><code class="whitespace python"> </code><code class="identifier">collections</code><code class="whitespace python"> </code><code class="keyword">import</code><code class="whitespace python"> </code><code class="identifier">defaultdict</code><code class="whitespace python">
</code><code class="keyword">from</code><code class="whitespace python"> </code><code class="identifier">email</code><code class="operator">.</code><code class="identifier">utils</code><code class="whitespace python"> </code><code class="keyword">import</code><code class="whitespace python"> </code><code class="identifier">parsedate_to_datetime</code><code class="whitespace python">
</code><code class="keyword">from</code><code class="whitespace python"> </code><code class="identifier">mailbox</code><code class="whitespace python"> </code><code class="keyword">import</code><code class="whitespace python"> </code><code class="identifier">mbox</code><code class="whitespace python">

</code><code class="identifier">date</code><code class="whitespace python"> </code><code class="operator">=</code><code class="whitespace python"> </code><code class="keyword">lambda</code><code class="whitespace python"> </code><code class="identifier">m</code><code class="operator">:</code><code class="whitespace python"> </code><code class="function">parsedate_to_datetime</code><code class="operator">(</code><code class="identifier">m</code><code class="operator">[</code><code class="string">'Date'</code><code class="operator">]</code><code class="operator">)</code><code class="operator">.</code><code class="function method">date</code><code class="operator">(</code><code class="operator">)</code><code class="whitespace python">
</code><code class="identifier">archive</code><code class="whitespace python"> </code><code class="operator">=</code><code class="whitespace python"> </code><code class="function">defaultdict</code><code class="operator">(</code><code class="identifier">list</code><code class="operator">)</code><code class="whitespace python">
</code><code class="keyword">for</code><code class="whitespace python"> </code><code class="identifier">message</code><code class="whitespace python"> </code><code class="keyword">in</code><code class="whitespace python"> </code><code class="function builtin">sorted</code><code class="operator">(</code><code class="function">mbox</code><code class="operator">(</code><code class="identifier">args</code><code class="operator">.</code><code class="identifier">mbox</code><code class="operator">)</code><code class="operator">,</code><code class="whitespace python"> </code><code class="identifier">key</code><code class="operator">=</code><code class="identifier">date</code><code class="operator">)</code><code class="operator">:</code><code class="whitespace python">
    </code><code class="identifier">archive</code><code class="operator">[</code><code class="identifier">message</code><code class="operator">[</code><code class="string">'In-Reply-To'</code><code class="operator">]</code><code class="operator">]</code><code class="operator">.</code><code class="function method">append</code><code class="operator">(</code><code class="identifier">message</code><code class="operator">)</code><code class="whitespace python">
</code></pre><p>As said earlier, arbitrary HTML content is not exactly suitable for comments.
However, it is undeniable that HTML emails have taken over the world
and compromises must be made: allowing <code>multipart/alternative</code> of both
<code>text/plain</code> and <code>text/html</code>.  It is not the only multipart, so are
attachments and cryptographic signatures.  Since we are only interested
in the plaintext part, it is actually easier done than said to extract it:</p>
<pre><code class="keyword">from</code><code class="whitespace python"> </code><code class="identifier">bleach</code><code class="whitespace python"> </code><code class="keyword">import</code><code class="whitespace python"> </code><code class="identifier">clean</code><code class="operator">,</code><code class="whitespace python"> </code><code class="identifier">linkify</code><code class="whitespace python">
</code><code class="keyword">from</code><code class="whitespace python"> </code><code class="identifier">markdown</code><code class="whitespace python"> </code><code class="keyword">import</code><code class="whitespace python"> </code><code class="identifier">markdown</code><code class="whitespace python">

</code><code class="keyword">def</code><code class="whitespace python"> </code><code class="function">get_body</code><code class="operator">(</code><code class="identifier">message</code><code class="operator">)</code><code class="operator">:</code><code class="whitespace python">
    </code><code class="keyword">if</code><code class="whitespace python"> </code><code class="identifier">message</code><code class="operator">.</code><code class="function method">is_multipart</code><code class="operator">(</code><code class="operator">)</code><code class="operator">:</code><code class="whitespace python">
        </code><code class="keyword">for</code><code class="whitespace python"> </code><code class="identifier">payload</code><code class="whitespace python"> </code><code class="keyword">in</code><code class="whitespace python"> </code><code class="function builtin">map</code><code class="operator">(</code><code class="identifier">get_body</code><code class="operator">,</code><code class="whitespace python"> </code><code class="identifier">message</code><code class="operator">.</code><code class="function method">get_payload</code><code class="operator">(</code><code class="operator">)</code><code class="operator">)</code><code class="operator">:</code><code class="whitespace python">
            </code><code class="keyword">if</code><code class="whitespace python"> </code><code class="identifier">payload</code><code class="whitespace python"> </code><code class="keyword">is</code><code class="whitespace python"> </code><code class="keyword">not</code><code class="whitespace python"> </code><code class="keyword">None</code><code class="operator">:</code><code class="whitespace python"> </code><code class="keyword">return</code><code class="whitespace python"> </code><code class="identifier">payload</code><code class="whitespace python">
    </code><code class="keyword">elif</code><code class="whitespace python"> </code><code class="identifier">message</code><code class="operator">.</code><code class="function method">get_content_type</code><code class="operator">(</code><code class="operator">)</code><code class="whitespace python"> </code><code class="operator">=</code><code class="operator">=</code><code class="whitespace python"> </code><code class="string">'text/plain'</code><code class="operator">:</code><code class="whitespace python">
        </code><code class="identifier">body</code><code class="whitespace python"> </code><code class="operator">=</code><code class="whitespace python"> </code><code class="identifier">message</code><code class="operator">.</code><code class="function method">get_payload</code><code class="operator">(</code><code class="identifier">decode</code><code class="operator">=</code><code class="keyword">True</code><code class="operator">)</code><code class="whitespace python">
        </code><code class="keyword">return</code><code class="whitespace python"> </code><code class="function">clean</code><code class="operator">(</code><code class="function">linkify</code><code class="operator">(</code><code class="identifier">body</code><code class="operator">,</code><code class="whitespace python"> </code><code class="identifier">output_format</code><code class="operator">=</code><code class="string">'html5'</code><code class="operator">)</code><code class="operator">)</code><code class="operator">,</code><code class="whitespace python">
                     </code><code class="identifier">tags</code><code class="operator">=</code><code class="operator">.</code><code class="operator">.</code><code class="operator">.</code><code class="operator">,</code><code class="whitespace python"> </code><code class="identifier">protocols</code><code class="operator">=</code><code class="operator">.</code><code class="operator">.</code><code class="operator">.</code><code class="operator">)</code><code class="whitespace python">
    </code><code class="keyword">return</code><code class="whitespace python"> </code><code class="keyword">None</code><code class="whitespace python">
</code></pre><p>Now all that&#x2019;s left is to render that body and relevant headers
as an HTML segment or an RSS item.  This is when we revisit the template.
Jinja is probably the most popular in Python, thanks to Django and Flask,
but its complexity is rather unnecessary.  Instead, I went with the built-in
<code>str.format</code>.</p>
<p><img alt="Double braces are brilliant, but I prefer single ones" src="format.jpg"/></p>
<p>What are templates for, exactly?  Not the complete document, apparently,
because that would differs from article to article and increase the complexity
for injection.  Neither a single comment, as comments are threaded into trees
(or a forest) and their relationship can be useful.  We gotta <a href="https://genius.com/Timbaland-meet-in-tha-middle-lyrics">meet
in tha middle</a> and use recursive templates instead, e.g. for nested comments:</p>
<pre><code class="tag chars">&lt;</code><code class="tag">div</code><code class="whitespace html"> </code><code class="attribute">class</code><code class="operator">=</code><code class="default">c</code><code class="default">o</code><code class="default">m</code><code class="default">m</code><code class="default">e</code><code class="default">n</code><code class="default">t</code><code class="tag chars">&gt;</code><code class="whitespace html">
  </code><code class="default">.</code><code class="default">.</code><code class="default">.</code><code class="whitespace html">
  </code><code class="default">{</code><code class="default">c</code><code class="default">h</code><code class="default">i</code><code class="default">l</code><code class="default">d</code><code class="default">r</code><code class="default">e</code><code class="default">n</code><code class="default">}</code><code class="whitespace html">
</code><code class="tag chars">&lt;/</code><code class="tag">div</code><code class="tag chars">&gt;</code><code class="whitespace html">
</code></pre><p>To render linear comments, such as for <code>&lt;wfw:commentRss&gt;</code>, simply move
the children out of the item as follows.</p>
<pre><code class="tag">&lt;item</code><code class="tag">&gt;</code><code class="whitespace xml">
  </code><code class="default">.</code><code class="default">.</code><code class="default">.</code><code class="whitespace xml">
</code><code class="tag">&lt;/item</code><code class="tag">&gt;</code><code class="whitespace xml">
</code><code class="default">{</code><code class="default">c</code><code class="default">h</code><code class="default">i</code><code class="default">l</code><code class="default">d</code><code class="default">r</code><code class="default">e</code><code class="default">n</code><code class="default">}</code><code class="whitespace xml">
</code></pre><p>The rest substitutions are mostly just extracted from the email&#x2019;s headers.
Another bit that needs some extra decisions, though, is the parameters
for the <code>mailto</code> URI to reply to each comment:</p>
<ul>
<li>
<code>In-Reply-To</code> set to current <code>Message-Id</code>
</li>
<li>
<code>Cc</code> set to current <code>Reply-To</code> (if exists) or <code>From</code>
</li>
<li>
<code>Subject</code> is inherited, with <code>Re:</code> prepended if missing
</li>
</ul>
<p>This is getting boring with a lot of trivial code, so I&#x2019;ll leave you
with a pointer to the completed script named <a href="https://trong.loang.net/~cnx/formbox">formbox</a> and move on
to more interesting stuff.</p>
</section>
<section id="Injecting-Comments">
<h3>Injecting Comments</h3>
<p>Inserting HTML comment sections is pretty simple.  First I wrote a simple
Julia function <code>render_comments</code> calling <code>formbox</code> under the hood, then</p>
<pre><code class="function">hfun_comments_rendered</code><code class="default">(</code><code class="default">)</code><code class="whitespace julia"> </code><code class="operator">=</code><code class="whitespace julia"> </code><code class="function">render_comments</code><code class="default">(</code><code class="string">"comment.html"</code><code class="default">)</code><code class="whitespace julia">
</code></pre><p><code>comments_rendered</code> is then injected below the article.  For RSS,
it took an extra steps:</p>
<ol>
<li>
Insert <code>render_comments("comment.xml")</code> to the comment feed template
<code>comments.xml</code> (notice they are two different templates) and write it
next to the article&#x2019;s output <code>index.html</code>
</li>
<li>
Insert the path of the written comment feed to the <code>&lt;wfw:commentRss&gt;</code> tag
in the article&#x2019;s feed item
</li>
</ol>
<p>That&#x2019;s it!</p>
</section>
</section>
<section id="Moderation">
<h2>Moderation</h2>
<p>I don&#x2019;t want a <em>Terms of Services</em> page, it&#x2019;d feel too corporate
for my <em>personal</em> website, so I will list the rules here:</p>
<ol>
<li>
Please be excellent to each other.  Disagreements are okay,
personal insults are not.
</li>
<li>
Stay on topic.  If you want to publicly discuss with me
about something else, start a new thread on a <a href="https://loa.loang.net/cnx.misc">mailing list</a>
or reach me via social media.
</li>
<li>
<a href="https://useplaintext.email">Use plaintext emails</a> and do not top post.  Markdown inline markups,
block quotes, lists and code blocks are supported.
</li>
<li>
Comments are implied to be under <a href="https://creativecommons.org/licenses/by-sa/4.0">CC BY-SA 4.0</a> unless declared otherwise.
</li>
<li>
I reserve the right to remove any comment I don&#x2019;t like.
I generally don&#x2019;t delete comments, but if you want to exercise
your freedom of speech, publish it yourself.
</li>
<li>
I do not warrant the availability of the comments either.
I will try my best but one day all comments may just disappear,
just like this website itself.  Archive what you deem important.
</li>
<li>
These rules are subject to change according to my personal liking
without notice.
</li>
</ol>
<p>Replies will only be rendered on the website and feed after I see them,
so please expect a delay of at least 24 hours.  If you are eager to reply
to each other, subscribe to the <a href="https://loa.loang.net/cnx.site">site&#x2019;s mailing list</a> instead.</p>
</section>
</section>
<section role="doc-endnotes">
<hr/>
<ol>
<li id="fn1">
<p>TBF there are image preview scripts in Newsboat&#x2019;s <a href="https://drewdevault.com/2020/06/06/Add-a-contrib-directory.html">contrib</a>.<a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn2">
<p>Content warning: occasionally NSFW<a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn3">
<p>Federation is getting there for social media; not so much for fora.<a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn4">
<p>But don&#x2019;t use <a href="https://blog.brixit.nl/markdown-email">text/markdown</a> for your emails.<a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn5">
<p>Unfortunately there&#x2019;s no equivalence for Atom.<a href="#fnref5" role="doc-backlink">↩︎︎</a><a href="#fnref5" role="doc-backlink">↩︎︎</a><a href="#fnref5" role="doc-backlink">↩︎︎</a><a href="#fnref5" role="doc-backlink">↩︎︎</a><a href="#fnref5" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn6">
<p>Over 30 seconds to generate a few hundred kB of web pages.<a href="#fnref6" role="doc-backlink">↩︎︎</a><a href="#fnref6" role="doc-backlink">↩︎︎</a><a href="#fnref6" role="doc-backlink">↩︎︎</a><a href="#fnref6" role="doc-backlink">↩︎︎</a><a href="#fnref6" role="doc-backlink">↩︎︎</a></p>
</li>
</ol>
</section>
</div>
</content>
<category term='fun'/>
<category term='recipe'/>
<category term='net'/>
<published>2022-01-09T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/2020/gsoc/article/7/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/2020/gsoc/article/7/index.html</id>
<title>Outro</title>
<summary>GSoC 2020: Outro</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/2020/gsoc/article/7/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<section id="Outro">
<h1>Outro</h1>
<blockquote>
<p>Steamed fish was amazing, matter of fact<br/>
Let me get some jerk chicken to go<br/>
Grabbed me one of them lemon pie theories<br/>
And let me get some of them benchmarks you theories too</p>
</blockquote>
<section id="The-Look">
<h2>The Look</h2>
<p>At the time of writing,
<a href="https://github.com/pypa/pip/issues/8771">implementation-wise parallel download is ready</a>:</p>
<p><a href="https://asciinema.org/a/356704"><img alt="asciicast" src="gh-8771.svg"/></a></p>
<p>Does this mean I&#x2019;ve finished everything just-in-time?  This sounds to good
to be true!  And how does it perform?  Welp&#x2026;</p>
</section>
<section id="The-Benchmark">
<h2>The Benchmark</h2>
<p>Here comes the bad news: under a decent connection to the package index,
using <code>fast-deps</code> does not make <code>pip</code> faster.  For best comparison,
I will time <code>pip download</code> on the following cases:</p>
<section id="Average-Distribution">
<h3>Average Distribution</h3>
<p>For convenience purposes, let&#x2019;s refer to the commands to be used as follows</p>
<pre><code class="identifier">pip</code><code class="whitespace bash"> </code><code class="default">--no-cache-dir</code><code class="whitespace bash"> </code><code class="identifier">download</code><code class="whitespace bash"> </code><code class="operator">{</code><code class="identifier">requirement</code><code class="operator">}</code><code class="whitespace bash">  </code><code class="comment"># legacy-resolver</code><code class="whitespace bash">
</code><code class="identifier">pip</code><code class="whitespace bash"> </code><code class="default">--use-feature</code><code class="default">=</code><code class="number">2020</code><code class="default">-resolver</code><code class="whitespace bash"> </code><code class="default">\</code><code class="whitespace bash">
  </code><code class="default">--no-cache-dir</code><code class="whitespace bash"> </code><code class="identifier">download</code><code class="whitespace bash"> </code><code class="operator">{</code><code class="identifier">requirement</code><code class="operator">}</code><code class="whitespace bash">  </code><code class="comment"># 2020-resolver</code><code class="whitespace bash">
</code><code class="identifier">pip</code><code class="whitespace bash"> </code><code class="default">--use-feature</code><code class="default">=</code><code class="number">2020</code><code class="default">-resolver</code><code class="whitespace bash"> </code><code class="default">--use-feature</code><code class="default">=</code><code class="identifier">fast</code><code class="default">-deps</code><code class="whitespace bash"> </code><code class="default">\</code><code class="whitespace bash">
  </code><code class="default">--no-cache-dir</code><code class="whitespace bash"> </code><code class="identifier">download</code><code class="whitespace bash"> </code><code class="operator">{</code><code class="identifier">requirement</code><code class="operator">}</code><code class="whitespace bash">  </code><code class="comment"># fast-deps</code><code class="whitespace bash">
</code></pre><p>In the first test, I used <a href="https://sr.ht/~cnx/axuy">axuy</a> and obtained the following results</p>
<table>
<tr>
<th>legacy-resolver</th>
<th>2020-resolver</th>
<th>fast-deps</th>
</tr>
<tr>
<td>7.709s</td>
<td>7.888s</td>
<td>10.993s</td>
</tr>
<tr>
<td>7.068s</td>
<td>7.127s</td>
<td>11.103s</td>
</tr>
<tr>
<td>8.556s</td>
<td>6.972s</td>
<td>10.496s</td>
</tr>
</table>
<p>Funny enough, running <code>pip download</code> with <code>fast-deps</code> in a directory
with downloaded files already took around 7-8 seconds.  This is because
to lazily download a wheel, <code>pip</code> has to <a href="https://github.com/pypa/pip/issues/8670">make many requests</a>
which are apparently more expensive than actual data transmission on my network.</p>
<div class="note">
<h3 id="When-is-it-useful-then">When is it useful then?</h3>
<p>With unstable connection to PyPI (for some reason I am not confident enough
to state), this is what I got</p>
<table>
<tr>
<th>2020-resolver</th>
<th>fast-deps</th>
</tr>
<tr>
<td>1m16.134s</td>
<td>0m54.894s</td>
</tr>
<tr>
<td>1m0.384s</td>
<td>0m40.753s</td>
</tr>
<tr>
<td>0m50.102s</td>
<td>0m41.988s</td>
</tr>
</table>
<p>As the connection was <em>unstable</em> and that the majority of <code>pip</code> networking
is performed as CI/CD with large and stable bandwidth, I am unsure what this
result is supposed to tell (-;</p>
</div>
</section>
<section id="Large-Distribution">
<h3>Large Distribution</h3>
<p>In this test, I used <a href="https://www.tensorflow.org">TensorFlow</a> as the requirement and obtained
the following figures:</p>
<table>
<tr>
<th>legacy-resolver</th>
<th>2020-resolver</th>
<th>fast-deps</th>
</tr>
<tr>
<td>0m52.135s</td>
<td>0m58.809s</td>
<td>1m5.649s</td>
</tr>
<tr>
<td>0m50.641s</td>
<td>1m14.896s</td>
<td>1m28.168s</td>
</tr>
<tr>
<td>0m49.691s</td>
<td>1m5.633s</td>
<td>1m22.131s</td>
</tr>
</table>
</section>
<section id="Distribution-with-Conflicting-Dependencies">
<h3>Distribution with Conflicting Dependencies</h3>
<p>Some requirement that will trigger a decent amount of backtracking by
the current implementation of the new resolver <code>oslo-utils==1.4.0</code>:</p>
<table>
<tr>
<th>2020-resolver</th>
<th>fast-deps</th>
</tr>
<tr>
<td>14.497s</td>
<td>24.010s</td>
</tr>
<tr>
<td>17.680s</td>
<td>28.884s</td>
</tr>
<tr>
<td>16.541s</td>
<td>26.333s</td>
</tr>
</table>
</section>
</section>
<section id="What-Now">
<h2>What Now?</h2>
<p>I don&#x2019;t know, to be honest.  At this point I&#x2019;m feeling I&#x2019;ve failed my own
(and that of other stakeholders of <code>pip</code>) expectation and wasted the time
and effort of <code>pip</code>&#x2019;s maintainers reviewing dozens of PRs I&#x2019;ve made
in the last three months.</p>
<p>On the bright side, this has been an opportunity for me to explore the codebase
of package manager and discovered various edge cases where the new resolver
has yet to cover (e.g. I&#x2019;ve just noticed that <code>pip download</code> would save
to-be-discarded distributions, I&#x2019;ll file an issue on that soon).  Plus I got
to know many new and cool people and idea, which make me a more helpful
individual to work on Python packaging in the future, I hope.</p>
</section>
</section>
</div>
</content>
<category term='gsoc'/>
<category term='python'/>
<published>2020-08-31T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/2020/gsoc/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/2020/gsoc/index.html</id>
<title>Google Summer of Code 2020</title>
<summary>GSoC 2020 final report</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/2020/gsoc/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<section id="Google-Summer-of-Code-2020">
<h1>Google Summer of Code 2020</h1>
<p>In the summer of 2020, I worked with the contributors of <code>pip</code>,
trying to improve the networking performance of the package manager.
Admittedly, at the end of <a href="https://summerofcode.withgoogle.com/archive/2020/projects/6238594655584256">the internship</a> period,
<a href="/blog/2020/gsoc/article/7/#the_benchmark">the benchmark said otherwise</a>; though I really hope
the clean-up and minor fixes I happened to be doing to the codebase
over the summer, in addition to the implementation of parallel
utils and lazy wheel, might actually help the project.</p>
<p>Personally, I learned a lot: not just about Python packaging and
networking stuff, but also on how to work with others.  I am really
grateful to <a href="https://github.com/pradyunsg">@pradyunsg</a> (my mentor), <a href="https://github.com/chrahunt">@chrahunt</a>,
<a href="https://github.com/uranusjr">@uranusjr</a>, <a href="https://github.com/pfmoore">@pfmoore</a>, <a href="https://github.com/brainwane">@brainwane</a>,
<a href="https://github.com/sbidoul">@sbidoul</a>, <a href="https://github.com/xavfernandez">@xavfernandez</a>, <a href="https://github.com/webknjaz">@webknjaz</a>,
<a href="https://github.com/jaraco">@jaraco</a>, <a href="https://github.com/deveshks">@deveshks</a>, <a href="https://github.com/gutsytechster">@gutsytechster</a>,
<a href="https://github.com/dholth">@dholth</a>, <a href="https://github.com/dstufft">@dstufft</a>, <a href="https://github.com/cosmicexplorer">@cosmicexplorer</a>
and <a href="https://github.com/ofek">@ofek</a>.  While this feels like a long shout-out list,
it really isn&#x2019;t.  These people are the maintainers, the contributors of <code>pip</code>
and/or other Python packaging projects, and more importantly, they have been
more than helpful, encouraging and patient to me throughout my every activities,
showing me the way when I was lost, fixing me when I was wrong, putting up with
my carelessness and showing me support across different social media.</p>
<p>To best serve the community, below I have tried my best to document
what I have done, how I&#x2019;ve done it and why I&#x2019;ve done it for over
the last three months.  At the time of writing, some work is still in progress,
so these also serve as a reference point for myself and others to reason
about decisions in relevant topics.</p>
<ol>
<li>
<a href="#The-Main-Story">The Main Story</a><ol type="a">
<li>
<a href="#Act-One:-Parallelization-Utilities">Act One: Parallelization Utilities</a></li>
<li>
<a href="#Act-Two:-Lazy-Wheels">Act Two: Lazy Wheels</a></li>
<li>
<a href="#Act-Three:-Late-Downloading">Act Three: Late Downloading</a></li>
<li>
<a href="#Act-Four:-Batch-Downloading-in-Parallel">Act Four: Batch Downloading in Parallel</a></li>
</ol>
</li>
<li>
<a href="#The-Side-Quests">The Side Quests</a></li>
<li>
<a href="#The-Plot-Summary">The Plot Summary</a></li>
</ol>
<section id="The-Main-Story">
<h2>The Main Story</h2>
<p>The storyline can be divided into the following four main acts.</p>
<section id="Act-One:-Parallelization-Utilities">
<h3>Act One: Parallelization Utilities</h3>
<p>In this first act, I ensured the portibility of parallelization
measures for later use in the final act.  Multithreading and multiprocessing
<code>map</code> were properly fellback on platforms without full support.</p>
<ul>
<li>
<a href="https://github.com/pypa/pip/issues/8320">GH-8320</a>: Add utilities for parallelization (close <a href="https://github.com/pypa/pip/issues/8169">GH-8169</a>)
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8538">GH-8538</a>: Make <code>utils.parallel</code> tests tear down properly
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8504">GH-8504</a>: Parallelize <code>pip list --outdated</code> and <code>--uptodate</code>
(using <a href="https://github.com/pypa/pip/issues/8320">GH-8320</a>)
</li>
</ul>
</section>
<section id="Act-Two:-Lazy-Wheels">
<h3>Act Two: Lazy Wheels</h3>
<p>As proposed by <a href="https://github.com/cosmicexplorer">@cosmicexplorer</a> in <a href="https://github.com/pypa/pip/issues/7819">GH-7819</a>, it is possible to only
download a portion of a wheel to obtain metadata during dependency resolution.
Not only that this would reduce the total amount of data to be transmitted over
the network in case the resolver needs to perform heavy backtracking, but also
it would create a synchronization point at the end of the resolution progress
where parallel downloading can be applied to the needed wheels (some wheels
solely serve their metadata during dependency backtracking and are not needed
by the users).</p>
<ul>
<li>
<a href="https://github.com/pypa/pip/issues/8467">GH-8467</a>: Add utitlity to lazily acquire wheel metadata over HTTP
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8584">GH-8584</a>: Revise lazy wheel and its tests
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8681">GH-8681</a>: Make range requests closer to chunk size (help <a href="https://github.com/pypa/pip/issues/8670">GH-8670</a>)
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8716">GH-8716</a> and <a href="https://github.com/pypa/pip/issues/8730">GH-8730</a>: Disable caching for range requests
</li>
</ul>
</section>
<section id="Act-Three:-Late-Downloading">
<h3>Act Three: Late Downloading</h3>
<p>During this act, the main works were refactoring to integrate the <strong>lazy wheel</strong>
into <code>pip</code>&#x2019;s codebase and clean up the way for download parallelization.</p>
<ul>
<li>
<a href="https://github.com/pypa/pip/issues/8411">GH-8411</a>: Refactor <code>operations.prepare.prepare_linked_requirement</code>
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8629">GH-8629</a>: Abstract away <code>AbstractDistribution</code>
in higher-level resolver code
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8442">GH-8442</a>, <a href="https://github.com/pypa/pip/issues/8532">GH-8532</a> and <a href="https://github.com/pypa/pip/issues/8588">GH-8588</a> (later reworked by
<a href="https://github.com/chrahunt">@chrahunt</a> in <a href="https://github.com/pypa/pip/issues/8685">GH-8685</a>): Use lazy wheel to obtain
dependency information for the new resolver
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8743">GH-8743</a>: Test hash checking for <code>fast-deps</code>
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8804">GH-8804</a>: Check download directory before making range requests
</li>
</ul>
</section>
<section id="Act-Four:-Batch-Downloading-in-Parallel">
<h3>Act Four: Batch Downloading in Parallel</h3>
<p>The final act is mostly about the UI of the parallel download.
My work involved around how the progress should be displayed
and how other relevant information should be reported to the users.</p>
<ul>
<li>
<a href="https://github.com/pypa/pip/issues/8710">GH-8710</a>: Revise method fetching metadata using lazy wheels
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8722">GH-8722</a>: Dedent late download logs (fix <a href="https://github.com/pypa/pip/issues/8721">GH-8721</a>)
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8737">GH-8737</a>: Add a hook for batch downloading
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8771">GH-8771</a>: Parallelize wheel download
</li>
</ul>
</section>
</section>
<section id="The-Side-Quests">
<h2>The Side Quests</h2>
<p>In order to keep the wheel turning (no pun intended) and avoid wasting time
waiting for the pull requests above to be reviewed, I decided to create
even more PRs (as I am typing this, many of the patches listed below
are nowhere near being merged).</p>
<ul>
<li>
<a href="https://github.com/pypa/pip/issues/7878">GH-7878</a>: Fail early when install path is not writable
</li>
<li>
<a href="https://github.com/pypa/pip/issues/7928">GH-7928</a>: Fix rst syntax in Getting Started guide
</li>
<li>
<a href="https://github.com/pypa/pip/issues/7988">GH-7988</a>: Fix tabulate col size in case of empty cell
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8137">GH-8137</a>: Add subcommand alias mechanism
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8143">GH-8143</a>: Make mypy happy with beta release automation
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8248">GH-8248</a>: Fix typo and simplify ireq call
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8332">GH-8332</a>: Add license requirement to <code>_vendor/README.rst</code>
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8423">GH-8423</a>: Nitpick logging calls
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8435">GH-8435</a>: Use str.format style in logging calls
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8456">GH-8456</a>: Lint <code>src/pip/_vendor/README.rst</code>
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8568">GH-8568</a>: Declare constants in configuration.py as such
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8571">GH-8571</a>: Clean up <code>Configuration.unset_value</code> and nit <code>__init__</code>
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8578">GH-8578</a>: Allow verbose/quiet level to be specified
via config files and environment variables
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8599">GH-8599</a>: Replace tabs by spaces for consistency
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8614">GH-8614</a>: Use <code>monkeypatch.setenv</code> to mock environment variables
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8674">GH-8674</a>: Fix <code>tests/functional/test_install_check.py</code>,
when run with new resolver
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8692">GH-8692</a>: Make assertion failure give better message
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8709">GH-8709</a>: List downloaded distributions before exiting (fix <a href="https://github.com/pypa/pip/issues/8696">GH-8696</a>)
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8759">GH-8759</a>: Allow py2 deprecation warning from setuptools
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8766">GH-8766</a>: Use the new resolver for test requirements
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8790">GH-8790</a>: Mark tests using remote svn and hg as xfail
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8795">GH-8795</a>: Reformat a few spots in user guide
</li>
</ul>
</section>
<section id="The-Plot-Summary">
<h2>The Plot Summary</h2>
<p>Every Monday throughout the Summer of Code, I summarized what I had done
in the week before in the form of either a short blog or an (even shorter)
check-in.  These write-ups often contain handfuls of popular culture references
and was originally hosted on <a href="https://blogs.python-gsoc.org/en/mcsinyxs-blog">Python GSoC</a>.</p>
<ul>
<li>
<a href="https://loang.net/~cnx/blog/2020/gsoc/checkin/1">First Check-In</a>
</li>
<li>
<a href="https://loang.net/~cnx/blog/2020/gsoc/article/1">Unexpected Things When You're Expecting</a>
</li>
<li>
<a href="https://loang.net/~cnx/blog/2020/gsoc/checkin/2">Second Check-In</a>
</li>
<li>
<a href="https://loang.net/~cnx/blog/2020/gsoc/article/2">The Wonderful Wizard of O'zip</a>
</li>
<li>
<a href="https://loang.net/~cnx/blog/2020/gsoc/checkin/3">Third Check-In</a>
</li>
<li>
<a href="https://loang.net/~cnx/blog/2020/gsoc/article/3">I'm Not Drowning on My Own</a>
</li>
<li>
<a href="https://loang.net/~cnx/blog/2020/gsoc/checkin/4">Fourth Check-In</a>
</li>
<li>
<a href="https://loang.net/~cnx/blog/2020/gsoc/article/4">I've Walked 500 Miles…</a>
</li>
<li>
<a href="https://loang.net/~cnx/blog/2020/gsoc/checkin/5">Fifth Check-In</a>
</li>
<li>
<a href="https://loang.net/~cnx/blog/2020/gsoc/article/5">Sorting Things Out</a>
</li>
<li>
<a href="https://loang.net/~cnx/blog/2020/gsoc/checkin/6">Sixth Check-In</a>
</li>
<li>
<a href="https://loang.net/~cnx/blog/2020/gsoc/article/6">Parallelizing Wheel Downloads</a>
</li>
<li>
<a href="https://loang.net/~cnx/blog/2020/gsoc/checkin/7">Final Check-In</a>
</li>
<li>
<a href="https://loang.net/~cnx/blog/2020/gsoc/article/7">Outro</a>
</li>
</ul>
</section>
</section>
</div>
</content>
<category term='fun'/>
<category term='exp'/>
<category term='gsoc'/>
<category term='pkg'/>
<published>2020-08-31T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/2020/gsoc/checkin/1/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/2020/gsoc/checkin/1/index.html</id>
<title>First Check-In</title>
<summary>GSoC 2020: First Check-In</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/2020/gsoc/checkin/1/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<p>+++
date = Date(2020, 6, 1)
tags = [&#x201c;gsoc&#x201d;, &#x201c;pip&#x201d;]
+++</p>
<section id="First-Check-In">
<h1>First Check-In</h1>
<p>Hi everyone, I am McSinyx, a Vietnamese undergraduate student
who loves [free software].  This summer I am working with
the maintainers and the contributors of <code>pip</code> to make
the package manager {{pip 825 &#x201d;download in parallel&#x201d;}.</p>
<section id="What-did-I-do-during-the-community-bonding-period">
<h2>What did I do during the community bonding period?</h2>
<p>Aside from bonding with <code>pip</code>&#x2019;s maintainers and contributors as well as
with my mentors, I was also experimenting on the theoretical and technical
obstacles blocking this GSoC project.  Pradyun Gedam (a mentor of mine)
suggested making [a proof of concept] to determine if parallel downloading
can play nicely with ResolveLib_&#x2019;s abstraction and we are reviewing it
together.  On the technical side, we <code>pip</code>&#x2019;s committers are exploring
{{pip 8169 &#x201d;available options for parallelization&#x201d;} and I made an attempt to
{{pip 8320 &#x201d;make use of Python&#x2019;s standard worker pool in a portable way&#x201d;}.</p>
</section>
<section id="Did-I-get-stuck-anywhere">
<h2>Did I get stuck anywhere?</h2>
<p>Yes, of course!  Neither of the experiments above is finished as of
this moment.  Though, I am optimistic that the issues will not be
real blockers and we will figure that out in the next few days.</p>
</section>
<section id="What-is-coming-up-next">
<h2>What is coming up next?</h2>
<p>As planned, this week I am going to refactor the package downloading code
in <code>pip</code>.  The main purpose is to decouple the networking code from
the package preparation operation and make sure that it is thread-safe.</p>
<p>In addition, I am also continuing mentioned experiments to have a better
confidence on the future of this GSoC project.</p>
<p>To other GSoC students, mentors and admins reading this, I am wishing
you all good health and successful projects this summer!</p>
</section>
</section>
</div>
</content>
<category term='gsoc'/>
<published>2020-06-01T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/route/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/route/index.html</id>
<title>GNU as a Router, the Canonical Way</title>
<summary>How to set up an Ubuntu system as a router</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/route/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<section id="GNU-as-a-Router-the-Canonical-Way">
<h1>GNU as a Router, the Canonical Way</h1>
<p>A while ago I noticed that my ISP leases IPv4 addresses out indefinitely.
It was everything I&#x2019;d ever wanted and I gotta seize it to truly <em>self</em>-host.
As an experiment, I started on something cheaper, like a single-board compooter.
In 2024, support for general-purpose RISC-V chips began to ripen, so naturally
due to FOMO, I bought a board with JH-7110.  Boy, was that a mistake!
While the bootloaders&#x2019; support had been well <a href="https://rvspace.org/en/project/JH7110_Upstream_Plan">upstreamed</a>,
certain essential features like PCIe (for NVMe) has yet to reached
a mainline Linux release, even worse so on the BSDs.  I ended up flashing
the <em>only</em> distribution with official support <a href="https://loa.loang.net/chung/D16T24MXDP3T.3BR1X04I90CGT@guix/t">at the time</a>, Ubuntu.</p>
<p>Funny enough, after over a decade of daily driving <a href="https://www.gnu.org">GNU</a>, twas the first time
I installed Ubuntu on a machine of my own.  At the time of writing,
the reason for the was more apparent than ever: Canonical had been
forcing Snap<a id="fnref1" href="#fn1" role="doc-noteref"><sup>1</sup></a> down the users&#x2019; throat, even on the <em>server</em> edition.
Thankfully Snap was still managed by APT and twas easy enough to remove
prevent it from coming back.  Another annoyance was the lack of manual pages
in the minimized installation <em>and</em> that the official way to enable them
is through a script that also install other bloats SMFH (the script
is quite short and the actually necessary commands can be trivially found,
I&#x2019;d rather they&#x2019;re documented instead).</p>
<p>That being said, not everything Ubuntu includes due to NIH is bad.
Unity (not the game engine that&#x2019;s proprietary like Snap server)
was loved by many; and this article is basically an appreciation post
for some others: <a href="https://netplan.io">Netplan</a> and <a href="https://launchpad.net/ufw">ufw</a>.  Before diving in,
lemme finish the story to give you the full context of this setup.
The SBC is the VisionFive 2 which is blessed with plenty of IO:</p>
<ul>
<li>
8 GB of memory
</li>
<li>
4 USB 3.0 type-A ports
</li>
<li>
2 RJ45 ports (1 Gb and 100/10 Mb)
</li>
<li>
1 M.2 slot (I used this as an excuse to buy a larger SSD
and put the old 256 GB one here)
</li>
<li>
1 eMMC slot<a id="fnref2" href="#fn2" role="doc-noteref"><sup>2</sup></a> (eMMC are cheap, got one also with 256 GB)
</li>
<li>
1 TF slot
</li>
<li>
40 pin GP(and predefined-purpose)IO
</li>
<li>
Other stuff for interfacing with humen like HDMI, audio jack, etc.
</li>
</ul>
<p>Initially, my plan for the SBC was to host services unlisted
on the <a href="https://loang.net">loang network</a>.  Official services were not considered
because my home network has no IPv6 and sometimes I&#x2019;ll like
to have most of the bandwidth for meself.  Shortly afterwards,
I also purchased a somewhat beefy desktop compooter with even more I/O,
especially a bunch of SATA, which are a lot more attractive
than connecting hard di*ks via USB.  On the other hand, the SBC barely
consume any electricity, well under 10 W with the NVMe drive, a Wi-Fi dongle
and a fan connected.  Since it cost virtually nothing to keep it up 24/7,
I decided to hand it the following two tasks:</p>
<ul>
<li>
Reverse proxying services running on more powerful machines
in the local network.
</li>
<li>
Acting as a virtual router between nodes I manage.
This is particularly useful for tunneling to my work network
and accessing the servers, allowing me to work remotely with low latency.
</li>
</ul>
<p>Setting up the VPN with Wireguard was relatively easy, so I assumed swapping
the SBC for the home router couldn&#x2019;t be too hard.  Once again, I [chose poorly],
this little project&#x2019;d costed me so many sleepless nights so I figured
I should note down what I learned here in case it can save someone else
from the same pain.  <strong>Do not take inspiration from this!</strong></p>
<ol>
<li>
<a href="#Connecting-to-the-Internet">Connecting to the Internet</a></li>
<li>
<a href="#Local-Networking">Local Networking</a></li>
<li>
<a href="#Wireless-Access-Point">Wireless Access Point</a></li>
<li>
<a href="#Name-Resolution">Name Resolution</a></li>
</ol>
<section id="Connecting-to-the-Internet">
<h2>Connecting to the Internet</h2>
<p>My landlord handles the contract with the ISP so I don&#x2019;t know the details
of the subscription, but there&#x2019;s certainly no IPv6 nor any static IPv4 address.
Bandwidth to datacenters in the region is approximately 100 Mb/s
and the wall socket connects to a Cat 5e cable.  I know about the latter
because whatever dumb ass did the last maintenance wired that to another
short one dangling from the wall socket<a id="fnref3" href="#fn3" role="doc-noteref"><sup>3</sup></a>, and after getting stabbed
in the eyes for months I finally to open it up and made the socket
a proper socket.</p>
<p>It would not make the slightest of a difference but I connect
the SBC&#x2019;s 1 Gb port (identified in Ubuntu as end0) to the Internet
and the slower one (end1) to my desktop on the local network.
Thankfully no <a href="https://docs.fsfe.org/en/teams/router-freedom-tech-wiki">special setup</a> was needed
and here is the entire Netplan configuration to connect to the outside world:</p>
<pre><code class="string">network</code><code class="operator">:</code><code class="whitespace yaml">
  </code><code class="string">ethernets</code><code class="operator">:</code><code class="whitespace yaml">
    </code><code class="string">end0</code><code class="operator">:</code><code class="whitespace yaml">
      </code><code class="string">dhcp4</code><code class="operator">:</code><code class="whitespace yaml"> </code><code class="constant builtin">true</code><code class="whitespace yaml">
  </code><code class="string">renderer</code><code class="operator">:</code><code class="whitespace yaml"> </code><code class="default">networkd</code><code class="whitespace yaml">
  </code><code class="string">version</code><code class="operator">:</code><code class="whitespace yaml"> </code><code class="number">2</code><code class="whitespace yaml">
</code></pre></section>
<section id="Local-Networking">
<h2>Local Networking</h2>
<p>For simplicity&#x2019;s sake, I decided to use the same subnet for both Ethernet
and Wi-Fi under a bridge br0, where addressing and routing is configured:</p>
<pre><code class="string">network</code><code class="operator">:</code><code class="whitespace yaml">
  </code><code class="string">bridges</code><code class="operator">:</code><code class="whitespace yaml">
    </code><code class="string">br0</code><code class="operator">:</code><code class="whitespace yaml">
      </code><code class="string">addresses</code><code class="operator">:</code><code class="whitespace yaml">
        </code><code class="operator">-</code><code class="whitespace yaml"> </code><code class="default">192.168.147.254/25</code><code class="whitespace yaml">
      </code><code class="string">interfaces</code><code class="operator">:</code><code class="whitespace yaml">
        </code><code class="operator">-</code><code class="whitespace yaml"> </code><code class="default">end1</code><code class="whitespace yaml">
      </code><code class="string">routes</code><code class="operator">:</code><code class="whitespace yaml">
        </code><code class="operator">-</code><code class="whitespace yaml"> </code><code class="string">from</code><code class="operator">:</code><code class="whitespace yaml"> </code><code class="default">192.168.147.128/25</code><code class="whitespace yaml">
          </code><code class="string">on-link</code><code class="operator">:</code><code class="whitespace yaml"> </code><code class="constant builtin">true</code><code class="whitespace yaml">
          </code><code class="string">to</code><code class="operator">:</code><code class="whitespace yaml"> </code><code class="default">0.0.0.0/0</code><code class="whitespace yaml">
          </code><code class="string">type</code><code class="operator">:</code><code class="whitespace yaml"> </code><code class="default">nat</code><code class="whitespace yaml">
          </code><code class="string">via</code><code class="operator">:</code><code class="whitespace yaml"> </code><code class="default">192.168.147.254</code><code class="whitespace yaml">
  </code><code class="string">ethernets</code><code class="operator">:</code><code class="whitespace yaml">
    </code><code class="string">end1</code><code class="operator">:</code><code class="whitespace yaml">
      </code><code class="string">dhcp4</code><code class="operator">:</code><code class="whitespace yaml"> </code><code class="constant builtin">false</code><code class="whitespace yaml">
</code></pre><p>As Netplan doesn&#x2019;t configure any DHCP server,
that&#x2019;s done separately by udhcpd from busybox:</p>
<pre><code>interface br0
start 192.168.147.128
end 192.168.147.253
max_leases 126
option subnet 255.255.255.128
option router 192.168.147.254
</code></pre><p>I couldn&#x2019;t seem to get a concrete information on the ports used by DHCP
so I open the firewall for UDP on both 67 and 68 (I swear
this isn&#x2019;t an engagement bait to test out the new mailing list):</p>
<pre><code>ufw allow in on br0 to any port 67 proto udp
ufw allow in on br0 to any port 68 proto udp
</code></pre></section>
<section id="Wireless-Access-Point">
<h2>Wireless Access Point</h2>
<p>Thanks to systemd, the Wi-Fi dongle is recognized as wlx600dd0g8b33f.
Yes, that abomination of a name includes the chip&#x2019;s full MAC address.
That being said, I&#x2019;d like to stick to the basis of a systemd/Linux distro.
Netplan doesn&#x2019;t support Wi-Fi hotspot with systemd-networkd but NetworkManager,
so the interface had thus to be declared as Ethernet:</p>
<pre><code class="string">network</code><code class="operator">:</code><code class="whitespace yaml">
  </code><code class="string">bridges</code><code class="operator">:</code><code class="whitespace yaml">
    </code><code class="string">br0</code><code class="operator">:</code><code class="whitespace yaml">
      </code><code class="string">interfaces</code><code class="operator">:</code><code class="whitespace yaml">
        </code><code class="operator">-</code><code class="whitespace yaml"> </code><code class="default">wlx600dd0g8b33f</code><code class="whitespace yaml">
  </code><code class="string">ethernets</code><code class="operator">:</code><code class="whitespace yaml">
    </code><code class="string">wlx600dd0g8b33f</code><code class="operator">:</code><code class="whitespace yaml">
      </code><code class="string">dhcp4</code><code class="operator">:</code><code class="whitespace yaml"> </code><code class="constant builtin">false</code><code class="whitespace yaml">
</code></pre><p>Actual wireless connectivity is handled by hostapd:</p>
<pre><code>interface=wlx600dd0g8b33f
bridge=br0
ssid=YΦ
utf8_ssid=1
country_code=KR
channel=6
ieee80211d=1
ieee80211h=1
ieee80211n=1
hw_mode=g
wmm_enabled=1

wpa=2
wpa_pairwise=TKIP
wpa_passphrase=just enter random characters
</code></pre></section>
<section id="Name-Resolution">
<h2>Name Resolution</h2>
<p>My ISP is <a href="https://www.tomshardware.com/tech-industry/cyber-security/south-korean-telecom-company-attacks-torrent-users-with-malware-over-600000-people-report-missing-files-strange-folders-and-disabled-pcs">known to be evil</a> so I&#x2019;d rather rely on more reputable
resolvers like <a href="https://opennic.org">OpenNIC</a>, which also offers free-of-charge (!) domain names.
Most of their <a href="https://servers.opennic.org">tier 2</a> servers are located on the other side of the globe
(200 to 300 ms RTT), so a local cache is almost required.  <a href="https://pymumu.github.io/smartdns/en">SmartDNS</a>
seems to be the best fit for this purpose, as it queries upstream servers
simultaneously and also check for the IP with the lowest RTT among the results.
Since I don&#x2019;t trust my ISP, connections to the upstream servers are encrypted:</p>
<pre><code>bind :53@br0
server-tls 51.254.162.59 -host-name ns1-dot.iriseden.fr
server-tls 202.61.197.122 -host-name dns.furrydns.de
server-tls 80.152.203.134 -host-name dot.kekew.info
server-tls 178.201.248.159 -host-name dot.kekew.info
server-tls 178.201.248.160 -host-name dot.kekew.info
server-tls 95.216.99.249 -host-name dns.froth.zone
</code></pre><p>For the router itself, the nameserver is set in /etc/resolv.conf
and Netplan is told not to change it:</p>
<pre><code class="string">network</code><code class="operator">:</code><code class="whitespace yaml">
  </code><code class="string">ethernets</code><code class="operator">:</code><code class="whitespace yaml">
    </code><code class="string">end0</code><code class="operator">:</code><code class="whitespace yaml">
      </code><code class="string">dhcp4-use-dns</code><code class="operator">:</code><code class="whitespace yaml"> </code><code class="constant builtin">false</code><code class="whitespace yaml">
</code></pre><p>After ufw is configured to allow UDP traffic in port 53 on br0,
udhcpd is instructed to advertise this local DNS server:</p>
<pre><code>option dns 192.168.147.254
</code></pre><p>I might consider blocking ads at the domain-name level someday, but for now
uBlock Origin is working well enough on my systems and I rarely have
people over, especially not for looking at <em>their</em> electronic devices.</p>
</section>
</section>
<section role="doc-endnotes">
<hr/>
<ol>
<li id="fn1">
<p>Not <a href="http://snap.berkeley.edu">the good one</a>.<a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn2">
<p>Innovation&#x2019;s gone full circle, <em>eMMC</em> is short for <em>embedded MMC</em>.<a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn3">
<p>Basically a futanari of the RJ45 world.<a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a></p>
</li>
</ol>
</section>
</div>
</content>
<category term='fun'/>
<category term='net'/>
<category term='recipe'/>
<published>2024-08-03T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/dedep/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/dedep/index.html</id>
<title>De-Dependency December</title>
<summary>Call for Participation: De-Dependency December</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/dedep/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<section id="De-Dependency-December">
<h1>De-Dependency December</h1>
<blockquote>
<p>As we mature, the dependency graph matures with us.</p>
</blockquote>
<section id="Exposition">
<h2>Exposition</h2>
<p>In the <a href="https://www.youtube.com/watch?v=stChOsejLEQ">occasional fights</a> between system and language packagers,
<a href="https://man.sr.ht/~cnx/ipwhl">I&#x2019;m known to take the downstream camp.</a>  As a user, there are
lots of things I take for granted.  I install the stuff I need,
occasionally upgrade the system, and everything gets updated.
Vulnerability in a library used by multiple programs?  Its patched version
gets swapped in within a few hours (given it&#x2019;s not <a href="https://blogs.gentoo.org/mgorny/2021/02/19/the-modern-packagers-security-nightmare">vendored or pinned</a>).
<a href="https://wiki.debian.org/Hardening">Most</a> <a href="https://fedoraproject.org/wiki/Changes/Harden_All_Packages">distributions</a> <a href="https://wiki.archlinux.org/title/Arch_package_guidelines/Security">even</a> <a href="https://en.opensuse.org/openSUSE:Security_Features">apply</a>
<a href="https://wiki.gentoo.org/wiki/Hardened/Toolchain#Changes">hardening</a> <a href="https://nixos.org/manual/nixpkgs/stable#sec-hardening-in-nixpkgs">flags</a> that <a href="https://xeiaso.net/blog/openssl-alarm-fatigue">some bugs aren&#x2019;t even exploitable
in the first place</a>.  They create a <a href="https://www.youtube.com/watch?v=205ODJgAEik">safe place</a> for me
to comfortably express myself at work and at home.</p>
<p>Recently on my work computer, I&#x2019;ve switched to Guix System, which has yet
many packages.  Looking into the way to package programs I use
and ongoing efforts, I realized the colossal number of transitive dependencies
of <a href="https://issues.guix.gnu.org/55903">certain software</a> and the impracticality for a user union (i.e. a distro)
to maintain such set of <a href="https://raku-advent.blog/2021/12/06/unix_philosophy_without_leftpad">micro packages</a> in every language.</p>
</section>
<section id="Confrontation">
<h2>Confrontation</h2>
<p>This gave me a more serious thought on software sustainability.  Such topic
often reminds us of energy consumption, modularity, development model,
or even style (clean code).  End-users (including self-hosts),
on the other hand, ask the following questions to decide upon installing
and keeping a piece of software:</p>
<ul>
<li>
Can I <em>trust</em> installing this won&#x2019;t do anything funny to my machine?
</li>
<li>
How much <a href="https://xkcd.com/303">effort</a> I need to prevent people from doing funny things
to my machine if the software includes <a href="https://heartbleed.com">something that gets on the front page
of some magazines</a> tomorrow?
</li>
<li>
How much of my limited resources will it take to run or <a href="https://ludocode.com/blog/flatpak-is-not-the-future">simply exist</a>?
</li>
</ul>
<p>There are certain intersections in concerns of enterprises and users,
however it&#x2019;s worth noticing that distributions are almost exclusively optimized
to cater for the users&#x2019; need.  Not only they <a href="https://en.wikipedia.org/wiki/Tron">fight for the users</a>,
they <em>are</em> the users.  Suppose you don&#x2019;t want to write yellow-glowing programs,
you should <a href="https://drewdevault.com/2021/09/27/Let-distros-do-their-job.html">make the life of downstream package maintainers easier</a>.
No, it does not count if you push them to give in to run <a href="https://github.com/NixOS/nixpkgs/blob/master/pkgs/build-support/go/module.nix"><code>go mod vendor</code></a>
or <a href="https://github.com/svanderburg/node2nix">download from NPM recursively</a>.</p>
</section>
<section id="Resolution">
<h2>Resolution</h2>
<p><em>So how do I write software that is easy to package</em>, you may ask.
If you followed the articles linked above, you&#x2019;ve probably already
figured that out.  It&#x2019;s less about what you <em>write</em> and more about
what you <em>use</em>.  When someone complains a program is difficult
to build from source, certainly it&#x2019;s not about how hard it is to type,
say <code>make install</code>, but acquiring the dependencies for that to run
successfully and the result will work.</p>
<p>Lower the number of dependencies will absolutely help.  To put it bluntly,
you can&#x2019;t have a problem with dependencies if there&#x2019;s none of them.
This sounds like reinventing the wheel, but if the use case is common enough,
you might find what you need in the standard library.<a id="fnref1" href="#fn1" role="doc-noteref"><sup>1</sup></a>
I&#x2019;ve been restricting myself from using third-party libraries
for new side projects and it actually worked for my most recent ones:</p>
<ul>
<li>
<a href="https://chim.loan/phylactery">Phylactery</a>, a static comics web server on Go with <a href="https://en.wikipedia.org/wiki/Comic_book_archive">CBZ</a> parsing
and concurrent request handling
</li>
<li>
<a href="https://trong.loang.net/~cnx/fead">Fead</a>, an <a href="https://en.wikipedia.org/wiki/Static_site_generator">SSG</a> plugin in Python for advertising others’ feed
with parallel HTTP request, parsing of RSS 2 and Atom and CLI argument parsing
</li>
</ul>
<p>Even for such simple use cases, there are still many libraries in the wild
that can handle more data formats, are more convenient to use
or more performant.  On the other hand, the amount of maintenance needed
to keep the programs safe indefinitely for a user is much lower
thanks to the small dependency footprint.</p>
<p>What I&#x2019;m asking you to give a try in the advent days<a id="fnref2" href="#fn2" role="doc-noteref"><sup>2</sup></a> is not as drastic.
Look through your works, find a library you require for a small portion
of its <a href="https://www.youtube.com/watch?v=3Mpyias9ek4">power</a>, or something can be implemented specifically for your project
using reasonable effort (w.r.t. the whole codebase).  This is not just
for the sake of maintainability: <a href="https://guide.handmade-seattle.com/c/2021/context-is-everything">being less general, the new implementation
can likely outperform the replaced public library</a>.</p>
<p><img alt="Multiple types of sockets installed on the same wall" src="outlets.jpg"/></p>
<p>In many cases, you will find yourself making use of the standard library.
Standards make life much easier, <a href="https://xkcd.com/927">if only</a> people can come up
with an agreement.  Or maybe they don&#x2019;t have to.  Maybe each could choose
among the <a href="https://raku-advent.blog/2021/12/11/unix_philosophy_without_leftpad_part2">utilities libraries</a>.  At the end of the day, it&#x2019;s the total number
of packages that can have bugs to be reported upstream and patched that matters.</p>
<p>That being said, please keep an eye on the standard library the same way
you (should) watch your other dependencies, just in case what you need
is finally added.  Worry not of backward incompatibility, <a href="https://wiki.debian.org/DontBreakDebian#Don.27t_suffer_from_Shiny_New_Stuff_Syndrome">users of LTS systems
are content with older versions</a> of your software.</p>
</section>
<section id="Fall-and-Catastrophe">
<h2>Fall and Catastrophe</h2>
<p>Just kidding, I&#x2019;m offering <a href="https://en.wikipedia.org/wiki/Three-act_structure">answers</a>, not <a href="https://en.wikipedia.org/wiki/Dramatic_structure#Freytag's_pyramid">tragedies</a>.  Winter is coming,
join me in a De-Dependency December and fight for the users!</p>
</section>
</section>
<section role="doc-endnotes">
<hr/>
<ol>
<li id="fn1">
<p>Unless you use Rust.<a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn2">
<p>I&#x2019;m not Christian, but I had fun with <a href="https://adventofcode.com">AoC</a> and <a href="https://breezewiki.com/neopets/wiki/Advent_Calendar">Neopets</a> before.<a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a></p>
</li>
</ol>
</section>
</div>
</content>
<category term='fun'/>
<category term='pkg'/>
<published>2022-11-10T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/2020/gsoc/article/6/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/2020/gsoc/article/6/index.html</id>
<title>Parallelizing Wheel Downloads</title>
<summary>GSoC 2020: Parallelizing Wheel Downloads</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/2020/gsoc/article/6/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<section id="Parallelizing-Wheel-Downloads">
<h1>Parallelizing Wheel Downloads</h1>
<blockquote>
<p>And now it&#x2019;s clear as this promise<br/>
That we&#x2019;re making<br/>
Two progress bars into one</p>
</blockquote>
<p>Hello there! It has been raining a lot lately and some mosquito has given me
the Dengue fever today.  To whoever reading this, I hope it would never happen
to you.</p>
<section id="Download-Parallelization">
<h2>Download Parallelization</h2>
<p>I&#x2019;ve been working on <code>pip</code>&#x2019;s download parallelization for quite a while now.
As distribution download in <code>pip</code> was modeled as a lazily evaluated iterable
of chunks, parallelizing such procedure is as simple as submitting routines
that write files to disk to a worker pool.</p>
<p>Or at least that is what I thought.</p>
</section>
<section id="Progress-Reporting-UI">
<h2>Progress Reporting UI</h2>
<p><code>pip</code> is currently using customly defined progress reporting classes,
which was not designed to working with multithreading code.  Firstly,
I want to try using these instead of defining separate UI
for multithreaded progresses. As they use system signals for termination,
one must the progress bars has to be running the main thread.  Or sort of.</p>
<p>Since the progress bars are designed as iterators, I realized that we
can call <code>next</code> on them.  So quickly, I throw in some queues and locks,
and prototyped the first <em>working</em> <a href="https://github.com/pypa/pip/issues/8771">implementation of progress synchronization</a>.</p>
</section>
<section id="Performance-Issues">
<h2>Performance Issues</h2>
<p>Welp, I only said that it works, but I didn&#x2019;t mention the performance,
which is terrible.  I am pretty sure that the slow down is with
the synchronization, since the <code>map_multithread</code> call doesn&#x2019;t seem
to trigger anything that may introduce any sort of blocking.</p>
<p>This seems like a lot of fun, and I hope I&#x2019;ll get better tomorrow
to continue playing with it!</p>
</section>
</section>
</div>
</content>
<category term='gsoc'/>
<category term='python'/>
<published>2020-08-17T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/advent/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/advent/index.html</id>
<title>Advent of Programming Languages</title>
<summary>Doing Advent of Code in a new programming language each day</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/advent/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<section id="Advent-of-Programming-Languages">
<h1>Advent of Programming Languages</h1>
<p>Earlier this year I enrolled in a master&#x2019;s programme<a id="fnref1" href="#fn1" role="doc-noteref"><sup>1</sup></a> at <a href="https://unist.ac.kr">UNIST</a>
and joined the Programming Languages and Software Engineering lab (PLaSE) as
a student researcher.  The stipend covers the school fees and living expenses,
and I&#x2019;m given <em>an</em> academic freedom to choose what to work on and take risks.
I will review the life here in detail in another post, but (SPOILER ALERT!)
overall I&#x2019;m quite content with it.</p>
<p>That being said, PLaSE is new and small, we only do research
on software engineering and don&#x2019;t do its name justice.  Because of that,
in the first year here I decided to do each day of <a>Advent of Code</a>
in a language I&#x2019;d never used in competitive programming (CP) before.</p>
<p><img alt="Pitbull holding the globe, captioned: Mr. Worldwide" src="mr-worldwide.jpg"/></p>
<p>Here was my blacklist going in, chronologically: Pascal, Python,
Scheme, C, C++, Common Lisp, Lua, Raku, Go, Rust and Zig.  I am only
proficient in over half of the listed languages, but dura lex, sed lex,
I&#x2019;d already had my CP first time with the rest.</p>
<p>To try any new language, all I have to do is dropping into an ephemeral shell
with its implementation using <code>nix-shell</code> or <code>guix shell</code> without the fear
of bloating up my systems.  I&#x2019;m running <a href="https://loang.net/~cnx/blog/butter">NixOS on my laptop</a>
with <a href="https://search.nixos.org/packages">nixpkgs</a> being one of the largest downstream repositories, including
everything but the kitchen sink.  On the work desktop, I installed Guix System
which has a decent <a href="https://packages.guix.gnu.org">set of packages</a> and <a href="https://trong.loang.net/~cnx/dotfiles/tree/guix/system.scm?id=b53f96565b8c#n51">nix service</a>
in case something is missing.  Every update, I run <a href="https://nixos.org/manual/nix/stable/command-ref/nix-collect-garbage.html">garbage</a> <a href="https://guix.gnu.org/manual/en/html_node/Invoking-guix-gc.html">collection</a>
and get rid of all unnecessary software, i.e. those not <a href="https://trong.loang.net/~cnx/dotfiles/tree/guix">declared</a>
<a href="https://trong.loang.net/~cnx/dotfiles/tree/nix">in my config</a>.</p>
<section id="Day-One">
<h2>Day One</h2>
<p>The first day should have been the warm up so I challenged myself
with using POSIX utilities.  This is a bit irony though as the majority
of my time spent outside of <a href="https://www.vim.org"><em>the</em> editor</a> or a web browser
is inside a (<a href="https://www.youtube.com/watch?v=k5E6CExu204">Bourn-again</a>) shell.</p>
<p>The <a href="https://adventofcode.com/2022/day/1">problem</a> was indeed simple, involving only <a href="https://trong.loang.net/~cnx/cp/commit?id=ff0bb53c15dd">finding the maxima
among the sums of newline-separated numbers</a>.  I used <a href="https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sed.html">sed</a>(1p)
to turn the input into <a href="https://linux.die.net/man/1/dc">dc</a>(1) eypressions, and <a href="https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sort.html">sort</a>(1p) and <a href="https://pubs.opengroup.org/onlinepubs/9699919799/utilities/tail.html">tail</a>(1p)
for picking the largest sum.  Probably the most interesting part
was that the summation was reusable to <a href="https://larkspur.one/notice/AQALVP69wAiotsVmgC">grade an assignment</a> for a course
I was a teaching assistant for.</p>
</section>
<section id="Day-Two">
<h2>Day Two</h2>
<p>The <a href="https://adventofcode.com/2022/day/2">second problem</a> didn&#x2019;t ramp up much in difficulty.
It only called for some rather <a href="https://trong.loang.net/~cnx/cp/commit?id=ada3a69b15ff">simple arithmetic</a>,
and the input format&#x2019;s regularity convinced me to finally give <a href="https://harelang.org">Hare</a> a try.</p>
<p>For just a taste, Hare is boring in a good way.  I was excited
for the tagged union of <a href="https://harelang.org/tutorials/introduction/#defining-new-error-types">error which can include and propagate
any debugging information</a>, but unfortunately it wasn&#x2019;t needed
for programs of such complexity (nor that errors are ever handled in CP).
I&#x2019;m looking forward to a chance to write more Hare in the future.</p>
</section>
<section id="Day-Three">
<h2>Day Three</h2>
<p>The <a href="https://adventofcode.com/2022/day/3">task for day 3</a> was literally day <math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mrow><mn>1</mn><mo>&#x002B;</mo><mn>2</mn></mrow></math> in scope.
I went for another <strong>better C</strong> that is <a href="https://nim-lang.org">Nim</a>.  My first impression with it
wasn&#x2019;t positive: Nim insists on considering each source file as a module
and does not allow hyphens in identifier name, so <a href="https://forum.nim-lang.org/t/5024">filenames mustn&#x2019;t have
any hyphen</a> either.  This had led me to piping the source code
to <code>nim c -</code> and executing <code>~/.cache/nim/stdinfile_d/stdinfile</code> to keep
my solution naming convention.  <code>nim r -</code> wouldn&#x2019;t have worked either
since the convention also consists of reading the input from stdin.</p>
<p>On the bright side, <a href="https://en.wikipedia.org/wiki/Uniform_Function_Call_Syntax">uniform function call syntax</a>, identifier
case-(and underscore-)insensitivity and optional parentheses allowed me
to <a href="https://trong.loang.net/~cnx/cp/commit?id=eeb9a45346a8">dodge parentheses in calls and camelCasing altogether</a>.
Although I <em>love</em> Lisp and don&#x2019;t have any problem with brackets,
I think their placement in ALGOL style hurts the readability
of nested calls and <a href="https://www.cs.kent.edu/~jmaletic/papers/ICPC2010-CamelCaseUnderScoreClouds.pdf">camelCase is just objectively bad</a>,
pun<a id="fnref2" href="#fn2" role="doc-noteref"><sup>2</sup></a> unintended.</p>
</section>
<section id="Day-Four">
<h2>Day Four</h2>
<p>The <a href="https://adventofcode.com/2022/day/4">forth problem</a> wasn&#x2019;t any harder, only requiring
<a href="https://trong.loang.net/~cnx/cp/commit?id=8941f621840f">simple logic operations and summation</a>.  To save time,
I opted for <a href="https://julialang.org">Julia</a>, which I was kinda sorta familiar with
in building this site (at the time this is published at least).
Like Nim, it has higher-order functions and a (reference) compiler
capable of producing fast binaries.</p>
</section>
<section id="Day-Five">
<h2>Day Five</h2>
<p>The <a href="https://adventofcode.com/2022/day/5">next day&#x2019;s task</a> was finally a breath of fresh air
with <a href="https://trong.loang.net/~cnx/cp/commit?id=aa7616140a8b">matrix parsing and LIFO (literal) stacks</a>.  It begged
for a regular expression parser,<a id="fnref3" href="#fn3" role="doc-noteref"><sup>3</sup></a> hence I mined a tiny bit of Ruby
for the task.  Ruby had been designed to be an object-oriented Perl,
and expectedly it feels very similar to Raku.  To an extend, I was also
able to avoid ALGOL-style call do quite a <a href="https://www.codesections.com/blog/raku-lisp-impression">Lisp impression</a>.</p>
<p>When I was looking for a second language to learn after the peak
of my CP <strong>career</strong> in middle school, I was choosing between those
with garbage-collection that are most popularly used in <a href="https://www.gnu.org/philosophy/free-sw.html">free software</a>
at the time, namely Perl, Python and Ruby.  Perl was ruled out due to my fear
of <a href="https://raku-advent.blog/2022/12/20/sigils">sigils</a> and I picked up Python as I didn&#x2019;t want to be a <a href="https://en.wikipedia.org/wiki/Japanophilia#21st_century">weeaboo</a>.
Sometimes I wonder how my side projects would have turned out to be
had I chosen differently.</p>
</section>
<section id="Day-Six">
<h2>Day Six</h2>
<p>The <a href="https://adventofcode.com/2022/day/6">sixth problem</a> essentially asked for maintaining a finite queue
of English letters until it is distinct.  The most efficient way to do this
is employing bit shifting for the FIFO and a bit set for the letters.
<a href="https://trong.loang.net/~cnx/cp/commit?id=f82f0b1a08f1">I implemented that</a> literally in the <a href="https://dlang.org/spec/betterc.html">Better C</a> subset of <a href="https://dlang.org">D</a>.</p>
<p>Although the language is around my age and influenced the big names
like modern C++, Swift and Zig,<a id="fnref4" href="#fn4" role="doc-noteref"><sup>4</sup></a> its documentation is pretty underwhelming
and inconsistent.  For instance, the 128-bit integer type <code>cent</code> is documented
as a [basic data type], however it only exists in the <a href="https://dlang.org/phobos/core_int128.html">core.int128</a> library
with more cumbersome usage (and doesn&#x2019;t work with <code>dmd -betterC</code>).</p>
<p>Like with Nim, D compilers also don&#x2019;t allow hyphens in source filenames,
so I had to pipe the code to <code>dmd -of=a.out -</code> (the executable name
would be randomized otherwise).</p>
</section>
<section id="Day-Seven">
<h2>Day Seven</h2>
<p>On the first <a href="https://vine.co/v/iM0HnpBebd0">Wednesday</a> of the month of celebration, the <a href="https://adventofcode.com/2022/day/7">problem</a>
was parsing <code>cd</code> and <code>ls</code>-like invocation and output to reconstruct
a directory tree and do, uh, tree stuff.  <a href="https://janet-lang.org">Janet</a>&#x2019;s <a href="https://janet-lang.org/docs/peg.html">PEG module</a>
was much more <a href="https://trong.loang.net/~cnx/cp/commit?id=38d8920c7d7c">delightful</a> for parsing than regular expression
on steroids like Raku&#x2019;s <a href="https://docs.raku.org/language/grammars">grammar</a>.</p>
<p>Writing imperative S-expressions felt dirty, though it&#x2019;s IMHO
a quite better take than Lua, understandably as it was originally
a redesign of <a href="https://fennel-lang.org">Fennel</a>.</p>
</section>
<section id="Day-Eight">
<h2>Day Eight</h2>
<p>The <a href="https://adventofcode.com/2022/day/8">eighth problem</a> could be efficiently solved via dynamic programming
on multidimensional arrays so I <a href="https://trong.loang.net/~cnx/cp/commit?id=f8b0528d933f">used</a> Fortran for array programming.
There&#x2019;s not much to say other than that it werkt and, ah yea, dynamic allocation
didn&#x2019;t seem worth the effort so I ended up hardcoding the sizes.</p>
</section>
<section id="Day-Nine">
<h2>Day Nine</h2>
<p>The <a href="https://adventofcode.com/2022/day/9">ninth task</a> was about sparse matrix transformation.
Naturally I used hash table in Tcl for this purpose
and the <a href="https://trong.loang.net/~cnx/cp/commit?id=cde44cdda55d">solution</a> was straightforward enough.
I am planning on extending a video game&#x2019;s level configuration
to be programmable and the top contenders are now Lua/Fennel,
Janet and Tcl.  No idea when I&#x2019;ll get to it, but I&#x2019;mma keep ya posted.</p>
</section>
<section id="Day-Ten">
<h2>Day Ten</h2>
<p>On <a href="https://adventofcode.com/2022/day/10">day 10</a>, I needed to build a less-than-basic<a id="fnref5" href="#fn5" role="doc-noteref"><sup>5</sup></a> calculator.
I thought using AWK would spice things up a bit, but it actually simplified
the <a href="https://trong.loang.net/~cnx/cp/commit?id=5e4395eab495">solution</a>.  Instead of having to read and parse each operation,
the script is executed for each input line, even allowing interleaving
matching.  Therefore, the behavior specification could be followed closely
without any significant effort on adapting the logic for the language.</p>
<p>I used to think of AWK as just a more verbose sed(1).
I was wrong and am glad that I was.  I guess AWK can come in
pretty handy for similar real-world usages, such as log processing
or moderately complex transformation of textual data.</p>
</section>
<section id="May-Day">
<h2>May Day</h2>
<p><a>Oops!… I did it again.</a>  If you thought because I published this
right after Christmas it must be a complete advent journal, I have played you
for absolute fools!  The later problems were increasingly parsing heavy,
and while I still had languages I wanted to try, none left was designed
for text processing.  I was also busy in meatspace at the time thus I couldn&#x2019;t
find the time to write byte-level parsers in languages I didn&#x2019;t know.</p>
<p>I didn&#x2019;t try really hard nor got really far, but <a href="https://www.youtube.com/watch?v=eVTXPUF4Oz4">in the end</a>
maybe the <a href="https://www.youtube.com/watch?v=l7r-R61W1DQ">real treasure</a> was the experiences I had along the way.
I suppose the <a href="https://en.wikipedia.org/wiki/Contact_hypothesis">contact hypothesis</a> <em>might</em> be true, at least
in this context<a href="https://www.youtube.com/watch?v=M94ii6MVilw">;</a> my prejudice against many languages had been
cleared away even after surface-level interactions.  You should probably
also give it a try, who knows, it could be much <a href="https://en.wiktionary.org/wiki/gay#Middle_English">gay</a>er than you&#x2019;d expect!</p>
</section>
</section>
<section role="doc-endnotes">
<hr/>
<ol>
<li id="fn1">
<p>No, I have not been given any slave.<a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn2">
<p>camelCase was popularized by mainstream object oriented languages.<a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn3">
<p>Not really, reading byte-by-byte would also work, just less cool.<a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn4">
<p>I feel underachieved now.<a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn5">
<p>No eggs were harmed in the making of the solution.<a href="#fnref5" role="doc-backlink">↩︎︎</a><a href="#fnref5" role="doc-backlink">↩︎︎</a><a href="#fnref5" role="doc-backlink">↩︎︎</a><a href="#fnref5" role="doc-backlink">↩︎︎</a></p>
</li>
</ol>
</section>
</div>
</content>
<category term='fun'/>
<category term='exp'/>
<published>2022-12-26T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/2020/gsoc/article/5/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/2020/gsoc/article/5/index.html</id>
<title>Sorting Things Out</title>
<summary>GSoC 2020: Sorting Things Out</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/2020/gsoc/article/5/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<section id="Sorting-Things-Out">
<h1>Sorting Things Out</h1>
<p>Hi!  I really hope that everyone reading this is still doing okay,
and if that isn&#x2019;t the case, I wish you a good day!</p>
<section id="pip-202-Released">
<h2><code>pip</code> 20.2 Released!</h2>
<p>Last Wednesday, <code>pip</code> 20.2 was released, delivering the <code>2020-resolver</code>
as well as many other improvements!  I was lucky to be able
to get the <code>fast-deps</code> feature to be included as part of the release.
A brief description of this <em>experimental</em> feature as well as testing
instruction can be found on <a href="https://discuss.python.org/t/announcement-pip-20-2-release/4863/2">Python Discuss</a>.</p>
<p>The public exposure of the feature also remind me of some further
<a href="https://github.com/pypa/pip/issues/8681">optimization</a> to make on <a href="https://github.com/pypa/pip/issues/8670">the lazy wheel</a>.
Hopefully without download parallelization it would not be too slow
to put off testing by concerned users of <code>pip</code>.</p>
</section>
<section id="Preparation-for-Download-Parallelization">
<h2>Preparation for Download Parallelization</h2>
<p>As of this moment, we already have:</p>
<ul>
<li>
<a href="https://github.com/pypa/pip/issues/8162#issuecomment-667504162">Multithreading pool fallback working</a>
</li>
<li>
An opt-in to use lazy wheel to optain dependency information,
and thus getting a list of wheels at the end of resolution
ready to be downloaded together
</li>
</ul>
<p>What&#x2019;s left is <em>only</em> to interject a parallel download somewhere after
the dependency resolution step.  Still, this struggles me way more than
I&#x2019;ve ever imagined.  I got so stuck that I had to give myself a day off
in the middle of the week (and study some Rust), then I came up with
<a href="https://github.com/pypa/pip/issues/8638">something what was agreed upon as difficult to maintain</a>.</p>
<p>Indeed, a large part of this is my fault, for not communicating the design
thoroughly with <code>pip</code>&#x2019;s maintainers and not carefully noting stuff down
during (verbal) discussions with my mentor.  Thankfully <a href="https://github.com/pypa/pip/issues/8685">Chris Hunt came to the rescue</a> and did a refactoring that will
make my future work much easier and cleaner.</p>
</section>
</section>
</div>
</content>
<category term='gsoc'/>
<category term='python'/>
<published>2020-08-03T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/teredo/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/teredo/index.html</id>
<title>Teredo Tunnel Simulation</title>
<summary>Teredo tunnel simulation in virtual machines</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/teredo/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<section id="Teredo-Tunnel-Simulation">
<h1>Teredo Tunnel Simulation</h1>
<p>Internet Protocol version 6 (IPv6), the most recent version of
the Internet Protocol, was developed by the IETF to deal with
the long-anticipated problem of IPv4 address exhaustion.  Despite being
superior to IPv4 in multiple aspect (e.g. larger address space,
extension headers), IPv6 has not been widely adopted, although it has been
semi-standardized in 1998 and fully-standardized in 2017.<a id="fnref1" href="#fn1" role="doc-noteref"><sup>1</sup></a></p>
<p>During the transition period, teredo tunneling has been used to give
IPv6 connectivity for IPv6-capable hosts that are on the IPv4 Internet
but have no native connection to an IPv6 network.<a id="fnref2" href="#fn2" role="doc-noteref"><sup>2</sup></a>
In this article, I will demontrate a way to set up such tunnel up
on virtual machines, then examine the packets being sent by IPv6 nodes
connected by the tunnel.</p>
<ol>
<li>
<a href="#Configuration">Configuration</a><ol type="a">
<li>
<a href="#Virtual-Machines">Virtual Machines</a></li>
<li>
<a href="#Teredo-Tunnel-Setup">Teredo Tunnel Setup</a></li>
<li>
<a href="#Teredo-Tunnel-Usage">Teredo Tunnel Usage</a></li>
</ol>
</li>
<li>
<a href="#Analysis">Analysis</a><ol type="a">
<li>
<a href="#Packets-Capturing">Packets Capturing</a></li>
<li>
<a href="#Packet-Contents">Packet Contents</a></li>
</ol>
</li>
<li>
<a href="#Conclusion">Conclusion</a></li>
</ol>
<section id="Configuration">
<h2>Configuration</h2>
<figure>
  <a href='https://video.hardlimit.com/w/d4dYuXm6p4g9sCWm156qkg'>
    <img src='screenshot.png' alt='Screenshot'/>
  </a>
  <figcaption>Teredo Tunnel Simulation Speedrun</figcaption>
</figure>
<section id="Virtual-Machines">
<h3>Virtual Machines</h3>
<p>In order to simulate Teredo tunneling, one needs two IPv6 nodes and two routers
with both IPv4 and IPv6 access.  In total, there needs to be four
virtual machines to be set up, thus I went for Void Linux, which is known
for its low memory foot print thanks to using <code>runit</code> instead of <code>systemd</code>.
To minimize resource usage and speed up the setup process, I chose the barebone
live image which uses <code>musl</code> instead of <code>glibc</code>.  At boot, the image uses only
40 MB of memory.</p>
<p>For virtualization, I used <code>vert-manager</code>, simply because it is available
in Debian&#x2019;s repository (my host OS).  For some reason, on <code>amd64</code>, the kernel
refuses to boot until I give it over 200 MB, but apparently that is still
a really modest number.  Networking is provided to the guest OSes via NAT
with default configurations.</p>
<p>It is worth mentioning that through <code>virtio</code>, one may use SSH to log into
the guests systems from the host OS.  I find this especially convenient as
it enables me to copy and paste not only commands but also IP addresses between
host and guests as well as between guests.</p>
<p>For convenience, from now on, the outside nodes will be referred to as PC A and
PC B, on the other hand the routers are named Router A and Router B.  Upon boot,
they were given an Ethernet interface <code>eth0</code> with the following addresses.</p>
<table>
<tr>
<th style="text-align: left;">Node</th>
<th style="text-align: left;">MAC address</th>
<th style="text-align: left;">IPv4 address</th>
</tr>
<tr>
<td style="text-align: left;">Router A</td>
<td style="text-align: left;"><code>52:54:00:f0:85:c7</code></td>
<td style="text-align: left;"><code>192.168.122.127</code></td>
</tr>
<tr>
<td style="text-align: left;">Router B</td>
<td style="text-align: left;"><code>52:54:00:2b:01:cc</code></td>
<td style="text-align: left;"><code>192.168.122.134</code></td>
</tr>
<tr>
<td style="text-align: left;">PC A</td>
<td style="text-align: left;"><code>52:54:00:3b:82:36</code></td>
<td style="text-align: left;"><code>192.168.122.86</code></td>
</tr>
<tr>
<td style="text-align: left;">PC B</td>
<td style="text-align: left;"><code>52:54:00:7b:ed:c0</code></td>
<td style="text-align: left;"><code>192.168.122.255</code></td>
</tr>
</table>
<p>Local IPv6 addresses were also given but we are not going to need them.</p>
</section>
<section id="Teredo-Tunnel-Setup">
<h3>Teredo Tunnel Setup</h3>
<p>First, I set up a IPv4 tunnel between the two routers:</p>
<pre><code class="comment"># On Router A</code><code class="whitespace bash">
</code><code class="identifier">ip</code><code class="whitespace bash"> </code><code class="identifier">tunnel</code><code class="whitespace bash"> </code><code class="identifier">add</code><code class="whitespace bash"> </code><code class="identifier">tunn</code><code class="whitespace bash"> </code><code class="identifier">mode</code><code class="whitespace bash"> </code><code class="identifier">sit</code><code class="whitespace bash"> </code><code class="identifier">remote</code><code class="whitespace bash"> </code><code class="number">192.168</code><code class="number">.122</code><code class="number">.134</code><code class="whitespace bash"> </code><code class="identifier">ttl</code><code class="whitespace bash"> </code><code class="number">255</code><code class="whitespace bash">
</code><code class="identifier">ip</code><code class="whitespace bash"> </code><code class="identifier">link</code><code class="whitespace bash"> </code><code class="function builtin">set</code><code class="whitespace bash"> </code><code class="identifier">tunn</code><code class="whitespace bash"> </code><code class="identifier">up</code><code class="whitespace bash">
</code><code class="comment"># On Router B</code><code class="whitespace bash">
</code><code class="identifier">ip</code><code class="whitespace bash"> </code><code class="identifier">tunnel</code><code class="whitespace bash"> </code><code class="identifier">add</code><code class="whitespace bash"> </code><code class="identifier">tunn</code><code class="whitespace bash"> </code><code class="identifier">mode</code><code class="whitespace bash"> </code><code class="identifier">sit</code><code class="whitespace bash"> </code><code class="identifier">remote</code><code class="whitespace bash"> </code><code class="number">192.168</code><code class="number">.122</code><code class="number">.127</code><code class="whitespace bash"> </code><code class="identifier">ttl</code><code class="whitespace bash"> </code><code class="number">255</code><code class="whitespace bash">
</code><code class="identifier">ip</code><code class="whitespace bash"> </code><code class="identifier">link</code><code class="whitespace bash"> </code><code class="function builtin">set</code><code class="whitespace bash"> </code><code class="identifier">tunn</code><code class="whitespace bash"> </code><code class="identifier">up</code><code class="whitespace bash">
</code></pre><p>For this tunnel to be able to act as a Teredo one, the two routers needs
to have IPv6 addresses prefixed by <code>2001::/32</code>.<a id="fnref2" href="#fn2" role="doc-noteref"><sup>2</sup></a></p>
<pre><code class="comment"># On Router A</code><code class="whitespace bash">
</code><code class="identifier">ip</code><code class="whitespace bash"> </code><code class="number">-6</code><code class="whitespace bash"> </code><code class="identifier">addr</code><code class="whitespace bash"> </code><code class="identifier">add</code><code class="whitespace bash"> </code><code class="number">2001</code><code class="default">:</code><code class="number">2</code><code class="default">:</code><code class="default">:</code><code class="number">1</code><code class="default">/</code><code class="number">64</code><code class="whitespace bash"> </code><code class="identifier">dev</code><code class="whitespace bash"> </code><code class="identifier">eth0</code><code class="whitespace bash">
</code><code class="comment"># On Router B</code><code class="whitespace bash">
</code><code class="identifier">ip</code><code class="whitespace bash"> </code><code class="number">-6</code><code class="whitespace bash"> </code><code class="identifier">addr</code><code class="whitespace bash"> </code><code class="identifier">add</code><code class="whitespace bash"> </code><code class="number">2001</code><code class="default">:</code><code class="number">3</code><code class="default">:</code><code class="default">:</code><code class="number">1</code><code class="default">/</code><code class="number">64</code><code class="whitespace bash"> </code><code class="identifier">dev</code><code class="whitespace bash"> </code><code class="identifier">eth0</code><code class="whitespace bash">
</code></pre><p>Finally, I fellback all IPv6 lookups to the tunnel and enabled IPv6 forwarding:</p>
<pre><code class="identifier">ip</code><code class="whitespace bash"> </code><code class="number">-6</code><code class="whitespace bash"> </code><code class="identifier">route</code><code class="whitespace bash"> </code><code class="identifier">add</code><code class="whitespace bash"> </code><code class="identifier">default</code><code class="whitespace bash"> </code><code class="identifier">dev</code><code class="whitespace bash"> </code><code class="identifier">tunn</code><code class="whitespace bash">
</code><code class="identifier">sysctl</code><code class="whitespace bash"> </code><code class="default">-w</code><code class="whitespace bash"> </code><code class="identifier">net</code><code class="default">.</code><code class="identifier">ipv6</code><code class="default">.</code><code class="identifier">conf</code><code class="default">.</code><code class="identifier">all</code><code class="default">.</code><code class="identifier">forwarding</code><code class="default">=</code><code class="number">1</code><code class="whitespace bash">
</code></pre></section>
<section id="Teredo-Tunnel-Usage">
<h3>Teredo Tunnel Usage</h3>
<p>The IPv6 addresses of the PCs were set up as follows
(<code>0x8067</code> is <code>PC</code> in ASCII).</p>
<pre><code class="comment"># On PC A</code><code class="whitespace bash">
</code><code class="identifier">ip</code><code class="whitespace bash"> </code><code class="number">-6</code><code class="whitespace bash"> </code><code class="identifier">address</code><code class="whitespace bash"> </code><code class="identifier">add</code><code class="whitespace bash"> </code><code class="number">2001</code><code class="default">:</code><code class="number">2</code><code class="default">:</code><code class="default">:</code><code class="number">8067</code><code class="default">/</code><code class="number">64</code><code class="whitespace bash"> </code><code class="identifier">dev</code><code class="whitespace bash"> </code><code class="identifier">eth0</code><code class="whitespace bash">
</code><code class="comment"># On PC B</code><code class="whitespace bash">
</code><code class="identifier">ip</code><code class="whitespace bash"> </code><code class="number">-6</code><code class="whitespace bash"> </code><code class="identifier">address</code><code class="whitespace bash"> </code><code class="identifier">add</code><code class="whitespace bash"> </code><code class="number">2001</code><code class="default">:</code><code class="number">3</code><code class="default">:</code><code class="default">:</code><code class="number">8067</code><code class="default">/</code><code class="number">64</code><code class="whitespace bash"> </code><code class="identifier">dev</code><code class="whitespace bash"> </code><code class="identifier">eth0</code><code class="whitespace bash">
</code></pre><p>By giving both Router A and PC A addresses prefixed by <code>2001:2::/64</code>
(similarly for Router B and PC B), I implied that they can find each other
through the local IPv6 network, for example on PC B:</p>
<pre><code>$ ip -6 route | head -n1
2001:3::/64 dev eth0 proto kernel metric 256 pref medium
</code></pre><p>To use the newly created tunnel,
the PCs simple had to be routed directly to the routers:</p>
<pre><code class="comment"># On PC A</code><code class="whitespace bash">
</code><code class="identifier">ip</code><code class="whitespace bash"> </code><code class="number">-6</code><code class="whitespace bash"> </code><code class="identifier">route</code><code class="whitespace bash"> </code><code class="identifier">add</code><code class="whitespace bash"> </code><code class="identifier">default</code><code class="whitespace bash"> </code><code class="identifier">via</code><code class="whitespace bash"> </code><code class="number">2001</code><code class="default">:</code><code class="number">2</code><code class="default">:</code><code class="default">:</code><code class="number">1</code><code class="whitespace bash">
</code><code class="comment"># On PC B</code><code class="whitespace bash">
</code><code class="identifier">ip</code><code class="whitespace bash"> </code><code class="number">-6</code><code class="whitespace bash"> </code><code class="identifier">route</code><code class="whitespace bash"> </code><code class="identifier">add</code><code class="whitespace bash"> </code><code class="identifier">default</code><code class="whitespace bash"> </code><code class="identifier">via</code><code class="whitespace bash"> </code><code class="number">2001</code><code class="default">:</code><code class="number">3</code><code class="default">:</code><code class="default">:</code><code class="number">1</code><code class="whitespace bash">
</code></pre><p>The connection could then be verified by running on PC A:</p>
<pre><code>$ traceroute 2001:3::8067
traceroute to 2001:3::8067 (2001:3::8067), 30 hops max, 80 byte packets
 1  2001:2::1 (2001:2::1)  0.572 ms  0.441 ms  0.328 ms
 2  2001:3::1 (2001:3::1)  0.906 ms  0.888 ms  1.049 ms
 3  2001:3::8067 (2001:3::8067)  1.325 ms  1.174 ms  1.091 ms
</code></pre></section>
</section>
<section id="Analysis">
<h2>Analysis</h2>
<p>To gain further understanding on how packets are transferred over
the Teredo tunnel, I captured and took a closer look at some of them.</p>
<section id="Packets-Capturing">
<h3>Packets Capturing</h3>
<p>Fortunately for me<a id="fnref3" href="#fn3" role="doc-noteref"><sup>3</sup></a>, all traffic of guests OSes were wired
to an separate interface named <code>virbr0</code>.  To capture going through
the tunnel, I simply had to tell Wireshark to listen to the interface,
while letting PC A ping PC B though IPv6: <code>ping -c1 2001:3::8067</code>.
I then skimmed through the packets sent between the two nodes and looked for
the IPv6-in-IPv4 ones.</p>
</section>
<section id="Packet-Contents">
<h3>Packet Contents</h3>
<p>Catured IPv6-in-IPv4 looks exactly like how I would imagined it to be.
The content of the ping request can be partially decoded as follows.</p>
<section id="Ethernet-Header">
<h4>Ethernet Header</h4>
<ul>
<li>
<code>52 54 00 2b 01 cc</code>: MAC address of Router B (destination)
</li>
<li>
<code>52 54 00 f0 85 c7</code>: MAC address of Router A (source)
</li>
<li>
<code>08 00</code>: EtherType of IPv4
</li>
</ul>
</section>
<section id="IPv4-Header">
<h4>IPv4 Header</h4>
<ul>
<li>
<code>45 00 00 7c 9b 43 40 00 ff</code>: Some flags
</li>
<li>
<code>29</code>: Protocol of <em>IPv6</em>
</li>
<li>
<code>69 be</code>: Checksum
</li>
<li>
<code>c0 a8 7a 86</code>: IPv4 address of Router B (destination)
</li>
<li>
<code>c0 a8 7a 7f</code>: IPv4 address of Router A (source)
</li>
</ul>
</section>
<section id="IPv6-Header">
<h4>IPv6 Header</h4>
<ul>
<li>
<code>60 00 07 e7 00 40</code>: Some flags
</li>
<li>
<code>3a</code>: Next header (ICMPv6)
</li>
<li>
<code>3f</code>: Hop limit of 63
</li>
<li>
<code>20 01 00 02 00 00 00 00 00 00 00 00 00 00 80 67</code>: PC A&#x2019;s IPv6 address
</li>
<li>
<code>20 01 00 03 00 00 00 00 00 00 00 00 00 00 80 67</code>: PC B&#x2019;s IPv6 address
</li>
</ul>
</section>
<section id="ICMPv6">
<h4>ICMPv6</h4>
<ul>
<li>
<code>80</code>: Type of ping request
</li>
<li>
<code>00 cf be 03 d9 00 01</code>: Some flags
</li>
<li>
<code>e3 0d fe 5e 00 00 00 00 bc d6 0e 00 00 00
00 00 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d
1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d
2e 2f 30 31 32 33 34 35 36 37</code>: Binary data to be echoed
</li>
</ul>
</section>
</section>
</section>
<section id="Conclusion">
<h2>Conclusion</h2>
<p>Via the activities elaborated above, the procedure to set up a Teredo tunnel
and the content of the packets travelling through it could be well understood.
These understanding may help facilite the adoption of IPv6, even for IPv6 nodes
having no native connection to an IPv6 network.  I hope that the IPv6 will grow
fast enough that I can see the day measures like this tunnel can soon
be deprecated.</p>
</section>
</section>
<section role="doc-endnotes">
<hr/>
<ol>
<li id="fn1">
<p><a href="https://tools.ietf.org/html/rfc8200">RFC 8200</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn2">
<p><a href="https://tools.ietf.org/html/rfc4380">RFC 4380</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn3">
<p>Aside from web browsing, I also run an IPFS node
and a bunch of local servers.  I probably need to retire some of them
soon since they really clutter the traffic.<a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a></p>
</li>
</ol>
</section>
</div>
</content>
<category term='fun'/>
<category term='recipe'/>
<category term='net'/>
<published>2020-07-03T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/2020/gsoc/article/4/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/2020/gsoc/article/4/index.html</id>
<title>I've Walked 500 Miles…</title>
<summary>GSoC 2020: I've Walked 500 Miles…</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/2020/gsoc/article/4/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<section id="I've-Walked-500-Miles…">
<h1>I&#x2019;ve Walked 500 Miles…</h1>
<p><img alt="500 miles" src="500-miles.gif"/></p>
<div class="rightquote">
<p>&#x2026; and I would walk 500 more<br/>
just to be the man who walks a thousand miles<br/>
to fall down at your door</p>
</div>
<section id="The-Main-Road">
<h2>The Main Road</h2>
<p>Hi, have you met <code>fast-deps</code>?  It&#x2019;s (going to be) the name of <code>pip</code>&#x2019;s
experimental feature that may improve the speed of dependency resolution
of the new resolver.  By avoid downloading whole wheels to just
obtain metadata, it is especially helpful when <code>pip</code> has to do
heavy backtracking to resolve conflicts.</p>
<p>Thanks to <a href="https://github.com/pypa/pip/issues/8532#discussion_r453990728">Chris Hunt's review on GH-8537</a>, my mentor Pradyun Gedam and I worked out a less hacky approach
to inteject the call to lazy wheel during the resolution process.  A new PR
<a href="https://github.com/pypa/pip/issues/8588">GH-8588</a> was filed to implement it—I could have <em>just</em> worked on top
of the old PR and rebased, but my <code>git</code> skill is far from gud enuff
to confidently do it.</p>
<p>Testing this one has been a lot of fun though.  At first, integration tests
were added as a rerun of the tests for the new resolver, with an additional flag
to use feature <code>fast-deps</code>.  It indeed made me feel guilty towards <a href="https://travis-ci.com">Travis</a>,
who has to work around 30 minutes more every run. Per Chris Hunt&#x2019;s suggestion,
in the new PR, I instead write a few functional tests for the area relating
the most to the feature, namely <code>pip</code>&#x2019;s subcommands <code>wheel</code>,
<code>download</code> and <code>install</code>.</p>
<p>It was also suggested that a mock server with HTTP range requests support
might be better (in term of performance and reliablilty) than for testing.
However, <a href="https://github.com/pypa/pip/issues/8584#issuecomment-659227702">I have yet to be able to make Werkzeug do it</a>.</p>
<p>Why did I say I&#x2019;m half way there?  With the parallel utilities merged and a way
to quickly get the list of distribution to be downloaded being really close,
what left is <em>only</em> to figure out a way to properly download them in parallel.
With no distribution to be added during the download progress, the model of this
will fit very well with the architecture in <a href="/blog/2020/gsoc/pip-parallel-dl.pdf">my original proposal</a>.
A batch downloader can be implemented to track the progress of each download
and thus report them cleanly as e.g. progress bar or percentage. This is
the part I am second-most excited about of my GSoC project this summer
(after the synchronization of downloads written in my proposal, which was then
superseded by <code>fast-deps</code>) and I can&#x2019;t wait to do it!</p>
</section>
<section id="The-Side-Quests">
<h2>The Side Quests</h2>
<p>As usual, I make sure that I complete every side quest I see during the journey:</p>
<ul>
<li>
<a href="https://github.com/pypa/pip/issues/8568">GH-8568</a>: Declare constants in <code>configuration.py</code> as such
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8571">GH-8571</a>: Clean up <code>Configuration.unset_value</code>
and nit the class&#x2019; <code>__init__</code>
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8578">GH-8578</a>: Allow verbose/quite level
to be specified via config file and env var
</li>
<li>
<a href="https://github.com/pypa/pip/issues/8599">GH-8599</a>: Replace tabs by spaces for consistency
</li>
</ul>
</section>
<section id="Snap-Back-to-Reality">
<h2>Snap Back to Reality</h2>
<p>A bit about me, I actually walked 500 meters earlier today to a bank
and walked 500 more to another to prepare my Visa card for purchasing
the upcoming <a href="https://wiki.pine64.org/index.php/PinePhone">PinePhone</a> prototype.  It&#x2019;s one of the first smartphones
to fully support a GNU/Linux distribution, where one can run desktop apps
(including proper terminals) as well as traditional services like SSH,
HTTP server and IPFS node because why not?  Just a few hours ago,
I pre-ordered the <a href="https://postmarketos.org/blog/2020/07/15/pinephone-ce-preorder/">postmarketOS community edition</a> with additional hardware
for convergence.</p>
<p>If you did not come here for a PinePhone ad, please take my apologies though d-;
and to ones reading this, I hope you all can become the person who walks
a thousand miles to fall down at the door opening to all
what you ever wished for!</p>
</section>
</section>
</div>
</content>
<category term='gsoc'/>
<category term='python'/>
<published>2020-07-20T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/metsrc/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/metsrc/index.html</id>
<title>Lazy Ragù</title>
<summary>Ragù in a slow cooker</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/metsrc/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<section id="Lazy-Ragù">
<h1>Lazy Ragù</h1>
<p>Craving &#x2019;em red meat sauces but too busy?  Behold,
a ragù recipe so well optimized it can boil the blood
of every pizzaliano within a 35-centimetro radius.<a id="fnref1" href="#fn1" role="doc-noteref"><sup>1</sup></a></p>
<section id="Ingredients">
<h2>Ingredients</h2>
<p>I like to think my sauce is somewhere between Bologna and Napoli
even tho it sure ain&#x2019;t.  Imma list in the order of importance
what I usually use down here, but please adjust according to your local market
and preference, to quote chef Jean-Pierre:</p>
<blockquote>
<p>If you don&#x2019;t have/like it, don&#x2019;t use it.</p>
</blockquote>
<ol>
<li>
2 fist-sized <a href="https://en.wiktionary.org/wiki/oignon">onyi</a>
</li>
<li>
1 big carrot
</li>
<li>
A Gordon-Ramsey dash of cooking oil<a id="fnref2" href="#fn2" role="doc-noteref"><sup>2</sup></a>
</li>
<li>
600 grammi of mince from any mammal<a id="fnref3" href="#fn3" role="doc-noteref"><sup>3</sup></a>
</li>
<li>
3 tomatoes<a id="fnref4" href="#fn4" role="doc-noteref"><sup>4</sup></a>
</li>
<li>
Salt and pepper
</li>
<li>
Half a bottle of drinkable red wine<a id="fnref5" href="#fn5" role="doc-noteref"><sup>5</sup></a>
</li>
<li>
&#x2019;bout the same amount of <a href="https://en.wikipedia.org/wiki/Stock">stock</a>
</li>
<li>
A few cloves of garlic
</li>
<li>
A couple of ribs of celery
</li>
<li>
Some <a href="https://xkcd.com/282">thyme</a> and basil, preferably fresh
</li>
<li>
Some chili
</li>
<li>
Paprika powder<a id="fnref6" href="#fn6" role="doc-noteref"><sup>6</sup></a>
</li>
</ol>
<p>These should produce about 2 litri of sauce or 4–6 servings,<a id="fnref7" href="#fn7" role="doc-noteref"><sup>7</sup></a>
for which some hard cheese(s) and butter are also nice to have.</p>
<p>The following tools are also needed:</p>
<ol>
<li>
Bowls (for storing the ingredients)
</li>
<li>
Kitchen sink
</li>
<li>
Knife and cutting board
</li>
<li>
Fine grater (like for cheese or citrus jest)
</li>
<li>
<a href="https://www.blender.org">Blender</a>
</li>
<li>
Frying pan
</li>
<li>
Spatula or ladle
</li>
<li>
Sauce pan or pot
</li>
<li>
Slow cooker
</li>
</ol>
</section>
<section id="Base-Prepping">
<h2>Base Prepping</h2>
<p>Peel, wash and finely dice the onyi and caramelize &#x2018;em in a pan
with some oil under low heat.  Stir sparingly, this process takes
over half an hour and should start before anything else.<a id="fnref8" href="#fn8" role="doc-noteref"><sup>8</sup></a>
A wee of salt would help draw out the moisture and accelerate
the caramelization, which happens well above the water&#x2019;s boiling point.</p>
<p>In the meantime, wash the other vegetables, finely grate the carrot,
peel and mince the garlic, and dice the celery.  When all done,
transfer all to the pan and continue frying until the onyi are soft
and lightly browned.</p>
</section>
<section id="Broth-Prepping">
<h2>Broth Prepping</h2>
<p>While waiting for the base veggies to caramelize, reduce the wine
in half in a sauce pan to get rid of the alcohol.</p>
<p>Blend the tomatoes with the stock (it&#x2019;s supposed to be a purée without the peel
but ain&#x2019;t nobody got time for that) and the rest of the spices.  Be conservative
with the salt, you can always add more but it&#x2019;s much less easy to remove.</p>
</section>
<section id="Meat-Prepping">
<h2>Meat Prepping</h2>
<p>Move the vegetables into the slow cooker from the pan and use it<a id="fnref9" href="#fn9" role="doc-noteref"><sup>9</sup></a>
to sear the minced meat under medium heat until the bottom side is golden brown
(no need to stir).  Parfry in multiple batches if necessary: if the pan
is crowded it&#x2019;d take much longer to reach the desired temperature
for the <a href="https://en.wikipedia.org/wiki/Maillard_reaction">Maillard reaction</a>.  Remember, <em>water is the enemy</em>,
so leave it a way to retreat.<a id="fnref10" href="#fn10" role="doc-noteref"><sup>10</sup></a></p>
</section>
<section id="Cooking">
<h2>Cooking</h2>
<p>Scoop the meat into the slow cooker and pour in the reduced wine.
Gradually add the tomato smoothie while mixing until the liquids barely
covers the solids (add more stock if necessary).</p>
<p>Turn the cooker on low and stew for 4–8 hours or until the meat is tender.</p>
</section>
<section id="Serving">
<h2>Serving</h2>
<p>Serve with short pasta or rice.  Grate in a generous amount of hard cheeses
and drop in a smol slab of butter<a id="fnref11" href="#fn11" role="doc-noteref"><sup>11</sup></a> and mix well for extra creaminess.
Butter is an emulsifier, so turn off the stove before adding it to prevent
the butterfat from separating.</p>
<p>Plate with fresh basil and thyme and even moar grated cheese
if you have any left.</p>
</section>
<section id="Reflection">
<h2>Reflection</h2>
<p><img alt="Chihuahua in salsa" src="chihuahua-salsa.jpg"/></p>
<p>The recipe is not that lazy to be handy, it nor differs from a normal ragù
enough to be a rage bait, but I spent all that time typing it down
so I decided to keep the original title for the clickbait values.</p>
</section>
</section>
<section role="doc-endnotes">
<hr/>
<ol>
<li id="fn1">
<p>I&#x2019;m sorry, Steffo, but thou can&#x2019;t stop me.<a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn2">
<p>Vegetable oil, animal fat or even butter, smoke point doesn&#x2019;t matter.<a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn3">
<p>Yes, <em>any</em> mammal.<a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn4">
<p>If not ripe and soft, add a few spoons of tomato paste.<a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn5">
<p>Measure carefully!<a href="#fnref5" role="doc-backlink">↩︎︎</a><a href="#fnref5" role="doc-backlink">↩︎︎</a><a href="#fnref5" role="doc-backlink">↩︎︎</a><a href="#fnref5" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn6">
<p>Mostly for the color.<a href="#fnref6" role="doc-backlink">↩︎︎</a><a href="#fnref6" role="doc-backlink">↩︎︎</a><a href="#fnref6" role="doc-backlink">↩︎︎</a><a href="#fnref6" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn7">
<p>It&#x2019;ll last 17 years in the freezer, so just make a full pot.<a href="#fnref7" role="doc-backlink">↩︎︎</a><a href="#fnref7" role="doc-backlink">↩︎︎</a><a href="#fnref7" role="doc-backlink">↩︎︎</a><a href="#fnref7" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn8">
<p>Onyo is always number first!<a href="#fnref8" role="doc-backlink">↩︎︎</a><a href="#fnref8" role="doc-backlink">↩︎︎</a><a href="#fnref8" role="doc-backlink">↩︎︎</a><a href="#fnref8" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn9">
<p>The pan, not the slow cooker.<a href="#fnref9" role="doc-backlink">↩︎︎</a><a href="#fnref9" role="doc-backlink">↩︎︎</a><a href="#fnref9" role="doc-backlink">↩︎︎</a><a href="#fnref9" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn10">
<p>圍師必闕。<a href="#fnref10" role="doc-backlink">↩︎︎</a><a href="#fnref10" role="doc-backlink">↩︎︎</a><a href="#fnref10" role="doc-backlink">↩︎︎</a><a href="#fnref10" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn11">
<p>Butter makes everything butter.<a href="#fnref11" role="doc-backlink">↩︎︎</a><a href="#fnref11" role="doc-backlink">↩︎︎</a><a href="#fnref11" role="doc-backlink">↩︎︎</a><a href="#fnref11" role="doc-backlink">↩︎︎</a></p>
</li>
</ol>
</section>
</div>
</content>
<category term='lyf'/>
<category term='recipe'/>
<published>2024-10-10T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/system/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/system/index.html</id>
<title>System Cascade Connection</title>
<summary>Properties of cascade connected systems analyzed via anonymous functions</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/system/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<section id="System-Cascade-Connection">
<h1>System Cascade Connection</h1>
<p>Given two discrete-time systems <math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mi>A</mi></math> and <math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mi>B</mi></math> connected in cascade to form
a new system <math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mrow><mi>C</mi><mo>(</mo><mi>x</mi><mo>)</mo><mo>&#x003D;</mo><mi>B</mi><mo>(</mo><mi>A</mi><mo>(</mo><mi>x</mi><mo>)</mo><mo>)</mo></mrow></math>, we examine the following properties:</p>
<ol>
<li>
<a href="#Linearity">Linearity</a></li>
<li>
<a href="#Time-Invariance">Time Invariance</a></li>
<li>
<a href="#LTI-Ordering">LTI Ordering</a></li>
<li>
<a href="#Causality">Causality</a></li>
<li>
<a href="#BIBO-Stability">BIBO Stability</a></li>
</ol>
<section id="Linearity">
<h2>Linearity</h2>
<p>If <math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mi>A</mi></math> and <math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mi>B</mi></math> are linear, i.e. for all signals <math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><msub><mi>x</mi><mi>i</mi></msub></math>
and scalars <math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><msub><mi>a</mi><mi>i</mi></msub></math>,</p>
<p><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><mrow><mo>{</mo><mtable columnalign='left'><mtr><mtd><mrow><mi>A</mi><mo>(</mo><munder><mstyle mathsize='big'><mo>&#x03A3;</mo></mstyle><mi>i</mi></munder><msub><mi>a</mi><mi>i</mi></msub><msub><mi>x</mi><mi>i</mi></msub><mo>[</mo><mo>&#x22C5;</mo><mo>]</mo><mo>)</mo><mo>&#x003D;</mo><munder><mstyle mathsize='big'><mo>&#x03A3;</mo></mstyle><mi>i</mi></munder><msub><mi>a</mi><mi>i</mi></msub><mi>A</mi><mo>(</mo><msub><mi>x</mi><mi>i</mi></msub><mo>)</mo><mo>[</mo><mo>&#x22C5;</mo><mo>]</mo></mrow></mtd></mtr><mtr><mtd><mrow><mi>B</mi><mo>(</mo><munder><mstyle mathsize='big'><mo>&#x03A3;</mo></mstyle><mi>i</mi></munder><msub><mi>a</mi><mi>i</mi></msub><msub><mi>x</mi><mi>i</mi></msub><mo>[</mo><mo>&#x22C5;</mo><mo>]</mo><mo>)</mo><mo>&#x003D;</mo><munder><mstyle mathsize='big'><mo>&#x03A3;</mo></mstyle><mi>i</mi></munder><msub><mi>a</mi><mi>i</mi></msub><mi>B</mi><mo>(</mo><msub><mi>x</mi><mi>i</mi></msub><mo>)</mo><mo>[</mo><mo>&#x22C5;</mo><mo>]</mo></mrow></mtd></mtr></mtable><mo>(null)</mo></mrow></math></p>
<p>then <math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mi>C</mi></math> is also linear</p>
<p><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><mtable><mtr><mtd columnalign='right'><mrow><mi>C</mi><mo>(</mo><munder><mstyle mathsize='big'><mo>&#x03A3;</mo></mstyle><mi>i</mi></munder><msub><mi>a</mi><mi>i</mi></msub><msub><mi>x</mi><mi>i</mi></msub><mo>[</mo><mo>&#x22C5;</mo><mo>]</mo><mo>)</mo></mrow></mtd><mtd columnalign='left'><mrow><mo>&#x003D;</mo><mi>B</mi><mo>(</mo><mi>A</mi><mo>(</mo><munder><mstyle mathsize='big'><mo>&#x03A3;</mo></mstyle><mi>i</mi></munder><msub><mi>a</mi><mi>i</mi></msub><msub><mi>x</mi><mi>i</mi></msub><mo>[</mo><mo>&#x22C5;</mo><mo>]</mo><mo>)</mo><mo>)</mo></mrow></mtd></mtr><mtr><mtd columnalign='right'><mi>x</mi></mtd><mtd columnalign='left'><mrow><mo>&#x003D;</mo><mi>B</mi><mo>(</mo><munder><mstyle mathsize='big'><mo>&#x03A3;</mo></mstyle><mi>i</mi></munder><msub><mi>a</mi><mi>i</mi></msub><mi>A</mi><mo>(</mo><msub><mi>x</mi><mi>i</mi></msub><mo>)</mo><mo>[</mo><mo>&#x22C5;</mo><mo>]</mo><mo>)</mo></mrow></mtd></mtr><mtr><mtd columnalign='right'><mi>x</mi></mtd><mtd columnalign='left'><mrow><mo>&#x003D;</mo><munder><mstyle mathsize='big'><mo>&#x03A3;</mo></mstyle><mi>i</mi></munder><msub><mi>a</mi><mi>i</mi></msub><mi>B</mi><mo>(</mo><mi>A</mi><mo>(</mo><msub><mi>x</mi><mi>i</mi></msub><mo>)</mo><mo>)</mo><mo>[</mo><mo>&#x22C5;</mo><mo>]</mo></mrow></mtd></mtr></mtable></math></p>
</section>
<section id="Time-Invariance">
<h2>Time Invariance</h2>
<p>If $A$ and $B$ are time invariant,
i.e. for all signals $x$ and integers $k$,</p>
<p>[\begin{aligned}
A(n \mapsto x[n - k]) &amp;= n \mapsto A(x)[n - k]\
B(n \mapsto x[n - k]) &amp;= n \mapsto B(x)[n - k]
\end{aligned}]</p>
<p>then $C$ is also time invariant</p>
<p>[\begin{aligned}
C(n \mapsto x[n - k])
&amp;= B(A(n \mapsto x[n - k]))\
&amp;= B(n \mapsto A(x)[n - k])\
&amp;= n \mapsto B(A(x))[n - k]\
&amp;= n \mapsto C(x)[n - k]
\end{aligned}]</p>
</section>
<section id="LTI-Ordering">
<h2>LTI Ordering</h2>
<p>If $A$ and $B$ are linear and time-invariant, there exists
signals $g$ and $h$ such that for all signals $x$,
$A = x \mapsto x * g$ and $B = x \mapsto x * h$, thus</p>
<p>[B(A(x)) = B(x * g) = x * g * h = x * h * g = A(x * h) = A(B(x))]</p>
<p>or interchanging $A$ and $B$ order does not change $C$.</p>
</section>
<section id="Causality">
<h2>Causality</h2>
<p>If $A$ and $B$ are causal,
i.e. for all signals $x$, $y$ and any choise of integer $k$,</p>
<p>[\begin{aligned}
\forall n &lt; k, x[n] = y[n]\quad
\Longrightarrow &amp;;\begin{cases}
\forall n &lt; k, A(x)[n] = A(y)[n]\
\forall n &lt; k, B(x)[n] = B(y)[n]
\end{cases}\
\Longrightarrow &amp;;\forall n &lt; k, B(A(x))[n] = B(A(y))[n]\
\Longleftrightarrow &amp;;\forall n &lt; k, C(x)[n] = C(y)[n]
\end{aligned}]</p>
<p>then $C$ is also causal.</p>
</section>
<section id="BIBO-Stability">
<h2>BIBO Stability</h2>
<p>If $A$ and $B$ are stable, i.e. there exists a signal $x$
and scalars $a$ and $b$ that for all integers $n$,</p>
<p>[\begin{aligned}
|x[n]| &lt; a &amp;\Longrightarrow |A(x)[n]| &lt; b\
|x[n]| &lt; a &amp;\Longrightarrow |B(x)[n]| &lt; b
\end{aligned}]</p>
<p>then $C$ is also stable, i.e. there exists a signal $x$
and scalars $a$, $b$ and $c$ that for all integers $n$,</p>
<p>[\begin{aligned}
|x[n]| &lt; a\quad
\Longrightarrow &amp;;|A(x)[n]| &lt; b\
\Longrightarrow &amp;;|B(A(x))[n]| &lt; c\
\Longleftrightarrow &amp;;|C(x)[n]| &lt; c
\end{aligned}]</p>
</section>
</section>
</div>
</content>
<category term='fun'/>
<category term='math'/>
<published>2020-04-15T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/butter/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/butter/index.html</id>
<title>NixOS on Btrfs+tmpfs</title>
<summary>How I reinstalled NixOS on Btrfs with an amnesiac root and backed up my data</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/butter/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<section id="NixOS-on-Btrfstmpfs">
<h1>NixOS on Btrfs+tmpfs</h1>
<p>In 2018, dad bought me a new laptop to replace the good ole Compaq nx7010
whose screen unfortunately got infected by some sort of microbe and dieded
shortly afterwards.  The new one, whilst having a considerably worse build
quality (like all other late-2010s ones when compared to mid-2000s models),
had a dozen times as much storage: a 250 GB M.2 SSD and a 500 GB SATA HDD.</p>
<p>My data hoarding habit has grown exponentially ever since.  Initially,
I used to back up the data from the SSD to the HDD but after a few years,
I ran out of space and decided to get some more storage.  Instead of buying
a portable hard disk like a normal person would, I went for an SATA SSD,
as it was rather difficult to find a 7200 rpm 2.5-inch<a id="fnref1" href="#fn1" role="doc-noteref"><sup>1</sup></a> HDD
in the market at the time.</p>
<p>I then asked my father for a spare SATA-to-USB case (he switched to using
a dock a while ago, and like other dads, nothing is ever thrown away)
and prepared to swap the drives.  As cloning the data would have been too easy,
I decided to <em>spice things up</em> by reinstalling the OS.  Back then I was
dual-booting Debian and NixOS, but the former had hardly been ever booted
for months so it was time to let it go:</p>
<p><img alt="Elsa rolling on the floor crying" src="let-it-go.png"/></p>
<p>In addition, I wanted to hop on the new and shinny<a id="fnref2" href="#fn2" role="doc-noteref"><sup>2</sup></a> train of Btrfs.
It has compression, snapshots and subvolumes, what&#x2019;s not to love?  Let&#x2019;s
replace something I&#x2019;d been using for nearly a decade with a file system
I had absolutely zero experience with, what could possibly go wrong, right?</p>
<p>\toc</p>
<section id="Reinstallation">
<h2>Reinstallation</h2>
<p>I was going to reinstall NixOS with an ephemeral root,
which had been covered to death in the following brilliant resources:</p>
<ul>
<li>
<a href="https://grahamc.com/blog/erase-your-darlings">Erase your darlings: immutable infrastructure for mutable systems</a>
</li>
<li>
<a href="https://elis.nu/blog/2020/05/nixos-tmpfs-as-root">NixOS ❄: tmpfs as root</a>
</li>
<li>
<a href="https://github.com/nix-community/impermanence">Nix community&#x2019;s impermanence modules</a>
</li>
<li>
<a href="https://christine.website/blog/paranoid-nixos-2021-07-18">Paranoid NixOS Setup</a>
</li>
</ul>
<p>The only twist here is that I was using Btrfs instead of ZFS or ext4 like in
other guides.  This choice would influence how to back up in the later section.</p>
<section id="Preparation">
<h3>Preparation</h3>
<p>First of all, I temporarily copied data to the SATA SSD from the M.2,
including <a href="https://trong.loang.net/~cnx/dotfiles/tree/nix">my Nix configurations</a>.  Using either <code>cp</code> or <code>rsync</code> didn&#x2019;t
seem to have any effect on the performance, and in the mean time I also
went ahead and grabbed a <a href="https://channels.nixos.org/nixos-unstable">NixOS unstable live image</a> and <code>dd</code>&#x2019;ed it to
a flash drive.  As I&#x2019;m tracking unstable, installing from the same version
would allowed me to skip switching the channel and a lot of downloading.</p>
</section>
<section id="Partitioning">
<h3>Partitioning</h3>
<p>After booting up the live image, I opened up a root shell with <code>sudo -i</code>.
As expected, <code>fdisk</code> reports the M.2 SSD as <code>/dev/nvme0n1</code>.  Paranoid as always,
I decided to give the EFI system partition a whole gibibyte, swap eight
to match memory<a id="fnref3" href="#fn3" role="doc-noteref"><sup>3</sup></a> and the rest as a single chonky Btrfs partition:</p>
<pre><code>parted /dev/nvme0n1 -- mklabel gpt
parted /dev/nvme0n1 -- mkpart ESP fat32 1MiB 1GiB
parted /dev/nvme0n1 -- set 1 boot on
mkfs.vfat /dev/nvme0n1p1

parted /dev/nvme0n1 -- mkpart Swap linux-swap 1GiB 9GiB
mkswap -L Swap /dev/nvme0n1p2
swapon /dev/nvme0n1p2

parted /dev/nvme0n1 -- mkpart primary 9GiB 100%
mkfs.btrfs -L Butter /dev/nvme0n1p3
</code></pre><p>As I typed this, I realized that I should have set up encryption
for the last partition so I would probably need to reinstall in the near future
to fix this mistake.  Anyway, with the target system&#x2019;s root mounted as tmpfs,
I would need to persist <code>/nix</code> (obviously), <code>/etc</code> (mostly for authentication
and other secret stuff not included in <code>configuration.nix</code> that I was too lazy
to opt in individually), <code>/var/log</code>, <code>/root</code> and <code>/home</code>:</p>
<pre><code>mount /dev/nvme0n1p3 /mnt
btrfs subvolume create /mnt/nix
btrfs subvolume create /mnt/etc
btrfs subvolume create /mnt/log
btrfs subvolume create /mnt/root
btrfs subvolume create /mnt/home
umount /mnt
</code></pre><p>Most subvolumes can be mounted with <code>noatime</code>, except for <code>/home</code>
where I frequently need to sort files by modification time.  All of them
should have forced compression though:</p>
<pre><code>mount -t tmpfs -o mode=755 none /mnt
mkdir -p /mnt/{boot,nix,etc,var/log,root,home}
mount /dev/nvme0n1p1 /mnt/boot
mount -o subvol=nix,compress-force=zstd,noatime /dev/nvme0n1p3 /mnt/nix
mount -o subvol=etc,compress-force=zstd,noatime /dev/nvme0n1p3 /mnt/etc
mount -o subvol=log,compress-force=zstd,noatime /dev/nvme0n1p3 /mnt/var/log
mount -o subvol=root,compress-force=zstd,noatime /dev/nvme0n1p3 /mnt/root
mount -o subvol=home,compress-force=zstd /dev/nvme0n1p3 /mnt/home
</code></pre></section>
<section id="Configuration">
<h3>Configuration</h3>
<p>With everything mounted, <code>nixos-generate-config --root /mnt</code> could be run
to generate a basic configuration.  But wait, didn&#x2019;t I say something
about my dot files?  That&#x2019;s correct, but it&#x2019;s not easy to handcraft
the <code>hardware-configuration.nix</code>.  After making sure all are mounted
with the right options and <code>services.fstrim.enable</code> is <code>true</code>, I copied
other configuration files to <code>/etc/nixos</code> and finished this step.</p>
</section>
<section id="Installation">
<h3>Installation</h3>
<p>NixOS installation is as simple as running <code>nixos-install</code>.  But my job was
not done after setting the root password and rebooting into the new system.
It was working, but not functional.  There was nothing meaningful for me
to do on it, so I had to log in (as root), <code>passwd</code>&#x2019;ed the user and copied
the home folder back from the temporary drive.</p>
<p>After freeing the new SATA SSD, I also filled it with butter.  Yes,
all the way, no GPT, no MBR, just Btrfs, whose subvolumes were used
in place of partitions:</p>
<pre><code>mkfs.btrfs -f -L Fly /dev/sdb
mkdir -p /mnt
mount /dev/sdb /mnt
btrfs subvolume create /mnt/movies
</code></pre><p>At that time the only disposable data I had were my movies collection.
The HDD also contained other data but they were rebalanced at <code>/home</code>
(on the M.2).  After swapping the SATA SSD inside the laptop, I logged in
as the normal user and get the exact same environment before the reinstallation.</p>
</section>
<section id="Profits">
<h3>Profits</h3>
<p>Thanks to subvolumes and compression, the free spaces were no longer fragmented
and I think I gained like 100 GB (not counting the old Debian&#x2019;s root).  Backup
would also be less painful with Btrfs snapshots (instead of plain <code>rsync</code>
like I used to) as shown as follows.</p>
</section>
</section>
<section id="Backup">
<h2>Backup</h2>
<p>With all data migrated, the HDD could be used for backing up.  First,
some legacy data I no longer access were moved there, then I started to back up
my <code>/home</code> partition:</p>
<section id="Initialization">
<h3>Initialization</h3>
<p>Having learned my lesson, I did not forget to set up <a href="https://gitlab.com/cryptsetup/cryptsetup">LUKS</a> this time:</p>
<pre><code>cryptsetup luksFormat /dev/sdb
cryptsetup luksOpen /dev/sdb backup
</code></pre><p>To make use of snapshots, the backup drive gotta be Btrfs as well.
The compression level was turned up to 14 this time (default was 3):</p>
<pre><code>mkfs.btrfs -L Backup /dev/mapper/backup
mkdir /backup
mount -o noatime,compress-force=zstd:14 /dev/mapper/backup /backup
</code></pre><p>Following <a href="https://btrfs.wiki.kernel.org/index.php/Incremental_Backup">Btrfs Wiki</a>, I made the first <code>/home</code> snapshot and sent it
to the backup drive:</p>
<pre><code>btrfs subvolume create /backup/home
today=$(date --iso-8601)
btrfs subvolume snapshot -r /home /home/$today
sync
btrfs send /home/$today | btrfs receive /backup/home
sync
</code></pre></section>
<section id="Repetition">
<h3>Repetition</h3>
<p>For next backups, I also mounted the drive and created a snapshot:</p>
<pre><code>cryptsetup luksOpen /dev/sdb backup
mkdir -p /backup
mount -o noatime,compress-force=zstd:14 /dev/mapper/backup /backup
today=$(date --iso-8601)
btrfs subvolume snapshot -r /home /home/$today
sync
</code></pre><p>Say the latest snapshot was on the <code>$previous</code> day, I only needed to send
the difference between the old and new backup.  Afterwards, it is safe to delete the local <code>$previous</code> snapshot to save some space.</p>
<pre><code>btrfs send -p /home/$previous /home/$today | btrfs receive /backup/home
btrfs subvolume delete /home/$previous
sync
</code></pre><p>Finally, unmount the drive and close the LUKS volume:</p>
<pre><code>umount /backup
cryptsetup luksClose backup
</code></pre><p>Is this more complicated than good ole <code>rsync</code>?  Yes.  Is it safer?  Also yes,
thanks to copy-on-write.  Would I bother using one of the tools suggested
in the wiki?  Probably not, I&#x2019;ve already documented everything in this article
in case I forget anything.</p>
</section>
</section>
</section>
<section role="doc-endnotes">
<hr/>
<ol>
<li id="fn1">
<p>63.5 mm for those outside of the land of guns and burgers<a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn2">
<p>OK, maybe not new, but certainly shinny<a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn3">
<p>Slightly larger since some of the memory is dedicated to graphics<a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a></p>
</li>
</ol>
</section>
</div>
</content>
<category term='fun'/>
<category term='recipe'/>
<category term='nix'/>
<published>2021-11-14T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/pixml/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/pixml/index.html</id>
<title>XML and Photo Gallery Generation: A Love Story</title>
<summary>How I make my photo gallery in XML and what's lovely about it</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/pixml/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<section id="XML-and-Photo-Gallery-Generation:-A-Love-Story">
<h1>XML and Photo Gallery Generation: A Love Story</h1>
<blockquote>
<p>I&#x2019;m just a language, whose style sheets are good<br/>
Oh, Lord, please, don&#x2019;t let me be misunderstood</p>
</blockquote>
<div class="note">
<h3 id="Tips">Tips</h3>
<p>As usual, the article starts with a text wall of random rambling.
If you are only interested in the technical aspects, feel free to skip
the first two sections.</p>
</div>
<ol>
<li>
<a href="#Introduction">Introduction</a></li>
<li>
<a href="#Motivation">Motivation</a></li>
<li>
<a href="#Preliminary">Preliminary</a></li>
<li>
<a href="#Approach">Approach</a></li>
<li>
<a href="#Implementation">Implementation</a><ol type="a">
<li>
<a href="#Page-Generation">Page Generation</a></li>
<li>
<a href="#Feed-Generation">Feed Generation</a></li>
<li>
<a href="#Thumbnail-Generation">Thumbnail Generation</a></li>
</ol>
</li>
<li>
<a href="#Discussion">Discussion</a></li>
<li>
<a href="#Conclusion">Conclusion</a></li>
</ol>
<section id="Introduction">
<h2>Introduction</h2>
<p>Neural-optic live streaming probably, no, definitely offers
the most photorealistic graphics one can set eyes on.  <a href="https://en.wikipedia.org/wiki/Computer-generated_imagery">CGI</a> is just
a pathetic mimic, and photography or videography is no more
than a poor plagiarism attempt when compared to quantum ray-tracing
and other advanced physics simulations^W happenings.</p>
<p>On the other hand, we humen are rather shite at replaying visual memories,
whilst (<a href="https://en.wikipedia.org/wiki/Data_degradation">bit rot</a> aside) media can be archived <a href="https://xkcd.com/1683">for forever</a>.  Besides,
many of us are too busy to <em>touch grass</em> or go see cool things
as regularly as we wish to.  This is how an industry based on showing us
<a href="https://en.wikipedia.org/wiki/Drama">mundane stuff</a> or <a href="https://en.wikipedia.org/wiki/Fiction">obvious bullcrap</a> can still manage to make tens
of thousands of <a href="https://antifandom.com/how-i-met-your-mother/wiki/Crapload">craploads</a> each year any why the interwebs are flooded
with pictures of cats, kitties and pussies.</p>
<p>Finding new shits means dopamine dispensation and that&#x2019;s why
<a href="https://www.youtube.com/watch?v=1SNRULEnTVQ">they are dope</a>.  As a model netizen, I adhere
to the web&#x2019;s social contract of mutual <a href="https://fe.disroot.org/@mcsinyx">shitposting</a> so that everyone
can have a piece.  Every blue moon, I also enjoy posting more quality
stuff like what you are reading right now, should you ignore the number
of <a href="https://peervideo.club/w/uByA7Czy7PWYMqnu8FgXvW">Mozart</a> references in the last three paragraphs.</p>
</section>
<section id="Motivation">
<h2>Motivation</h2>
<p>Some other times, I also want to share the living things and sceneries
I encounter in the <a href="https://github.com/zig-community/user-map/pull/120">new</a> place.  My camera was gifted by father
before I moved and yet I shared more photos <a href="https://fotofed.nl/cnx">with strangers</a>
than with my family.  The PixelFed instance I landed on irreversibly
shrank and lossily compressed them, while dumping 5 MB images to the family
chat room just feels weird, hence I decided to gather the decency
to build a photo gallery to show my loved ones (and admittedly,
flex with online strangers).</p>
<p>There are not many <a href="https://en.wikipedia.org/wiki/Content_management_system">CMS</a> in the wild for photo hosting,
and they often acts as a wall garden and/or a social network.
Building and hosting a new one is quite overkill, thus the obvious
solution left would be generating a static site.  Out of the gazillion <a href="https://en.wikipedia.org/wiki/Static_site_generator">SSG</a>,
I couldn&#x2019;t found any that meets the my requirements:</p>
<ol>
<li>
Generate a <a href="https://en.wikipedia.org/wiki/Web_feed">web feed</a>
</li>
<li>
Automate filling <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Img">image</a> title and alt text
</li>
<li>
Offer fine-grain control for permanent <a href="https://en.wikipedia.org/wiki/Pagination">pagination</a>
</li>
<li>
Generate thumbnails with custom size and name
</li>
</ol>
<p>I mean, they perhaps exist, but the number I had to try and fight through
would cost more time than writing the web pages and feed by hand.
So I wrote them from scratch.  Y&#x2019;all can stand up and clap now!</p>
</section>
<section id="Preliminary">
<h2>Preliminary</h2>
<p>Yes, I really started with writing <a href="https://en.wikipedia.org/wiki/XHTML">XHTML</a> and <a href="https://www.rfc-editor.org/rfc/rfc4287">Atom</a> by hand.
A web page has the following structure with namespaces omitted
and denoted in WXML (<a href="https://www.draketo.de/software/wisp">Wisp</a><math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mo>&#x00D7;</mo></math><a href="https://okmij.org/ftp/Scheme/SXML.html">SXML</a>) so I don&#x2019;t have
to close the tags (have I given up on XML too early?-).</p>
<div class="note">
<h3 id="Syntax-hints">Syntax hints</h3>
<p>For the uninitiated, any indentation or colon in Wisp represents
an additional nest level, while a dot escape the nesting.  The at signs
are used by SXML to denote attributes, which may remind you of <a href="https://www.w3.org/TR/xpath">XPath</a>.
For example, the anchor to the previous page is <code>&lt;a href=41&gt;PREV&lt;/a&gt;</code>.</p>
</div>
<pre><code>html
  head
    link
      @ : rel "alternate"
          type "application/atom+xml"
          href "/atom.xml"
    ...
  body
    nav
      a : @ : href "41"
        . "PREV"
      h1 "PAGE 42"
      a : @ : href "43"
        . "NEXT"
    article
      @ : id "foobar"
      h2
        a : @ : href "#foobar"
          . "foobar"
      a : @ : href "/42/foo.jpg"
          img
            @ : src "/42/foo.small.jpg"
                alt "pic of foo"
                title "pic of foo"
      a : @ : href "/42/bar.jpg"
          ...
    article ...
    ...
    footer ...
</code></pre><p>So far, adding an <code>article</code> is not yet too cumbersome, there&#x2019;s only a bit
of redundancy for permanent links and the nesting level is acceptable
with the deepest being <code>/html/body/article/a/img</code>.  It gets more repetitive
once we publish it to to the linked Atom feed:</p>
<pre><code>feed
  entry
    link
      @ : rel "alternate"
          type "application/xhtml+xml"
          href "https://gallery.example/42/#foobar"
    id "https://gallery.example/42/#foobar"
    title "foobar"
    content
      @ : type "xhtml"
      div
        img
          @ : src "https://gallery.example/42/foo.jpg"
              alt "pic of foo"
              title "pic of foo"
        img ...
    updated ...
  entry ...
  ...
</code></pre><p>Since web feeds are standalone documents, they must always use absolute URLs.
(Welp that&#x2019;s not entirely true, <a href="https://www.w3.org/TR/xmlbase">XML Base</a> does exists, but not all readers
support it, and more importantly, certain elements such as <code>atom:id</code> disallow
relative references.)  In addition, whilst the web page links a thumbnail
to the original image to save bandwidths, the feed can be consumed one post
at a time, which thus points to the full size version.  Therefore,
copying the markup to embed it inside the Atom is error-prone and doesn&#x2019;t
exactly spark joy.</p>
<div class="note">
<h3 id="Fun-fact">Fun fact</h3>
<p>What does spark joy is that we can embed XHTML directly into the web feed,
which means the content is still XML and we don&#x2019;t need to quote it in CDATA.
For other sites where contents don&#x2019;t accumulate up to hundreds of megabytes,
this will allow us to slap some (SPOILER ALERT!) stylesheet on the Atom feed
and let the user agent render it in a <a href="https://simonesilvestroni.com/blog/build-a-human-readable-rss-with-jekyll">human-readable form</a>.</p>
</div>
</section>
<section id="Approach">
<h2>Approach</h2>
<p>I actually already spoiled it in the epigraph,<a id="fnref1" href="#fn1" role="doc-noteref"><sup>1</sup></a> but for the sake
of completeness let us <a href="https://xkcd.com/1445">discuss a few possible solutions</a>.
What I wanted was to reduce the redundancy of manual input, in other words,
a system transforming a custom information-dense format to standard
yet sparser ones, which in this case are XHTML and Atom.  Given some new photos
and their relevant data, the purpose was to minimize the publishing friction.</p>
<p>It&#x2019;s worth mentioning that the goal was not to minimize the input format,
the transformation speed, or feedback latency, but all of the above,
plus the cost of constructing the tool, incrementally as our requirements
slightly changes over time.  Our choice for the base <a href="https://programming-journal.org/2023/7/13">programming system</a>
shall affect each and every of these aspects and more.</p>
<p>Some technical dimensions are <a href="https://en.wikipedia.org/wiki/Animal_Farm">more equal</a> than others, though.
For this use case, IMHO immediate feedback loop should be given
the number one priority, not only because it&#x2019;d be frustrating
to have to complete multiple rituals just to preview the changes,
but also as watching and reflecting file system changes is (sadly still)
a difficult problem.</p>
<p>For Linux<a id="fnref2" href="#fn2" role="doc-noteref"><sup>2</sup></a> there&#x2019;s <a href="https://man7.org/linux/man-pages/man7/inotify.7.html">inotify</a> which doesn&#x2019;t suck,
except when it does and misses events,<a id="fnref3" href="#fn3" role="doc-noteref"><sup>3</sup></a> and the standard POSIX build tool
<a href="https://pubs.opengroup.org/onlinepubs/9699919799/utilities/make.html">make</a> relies on <a href="https://apenwarr.ca/log/20181113">mtime which is also flaky</a>.  Some SSG
work around this by spawning up a server with more sophisticated
caching mechanism and even include a HTTP server sending out refresh events.
Implementing such system is easily <a href="https://xkcd.com/1319">more expensive</a> than doing
the original task manually.</p>
<p>Luckily, there is another way.  <em>After</em> the birth of imperative
DOM manipulation programs running on VM inside browsers (Ecma scripts),
there came a (now forgotten) art of purely functional DOM transformation.
More specifically, <a href="https://www.w3.org/standards/xml/transformation">XSLT</a> can declaratively transform any XML document
to another, and its best part is that modern browsers natively support it,
i.e. there&#x2019;s no difference between editing the input document
and the hypothetical output XHTML.  For better portability
and rendering performance, we can still generate the latter
ahead-of-time (AoT) during deployment.</p>
</section>
<section id="Implementation">
<h2>Implementation</h2>
<p>Going back to the example, the input format could boil down
to a more concise XML file, e.g. <code>42/index.xml</code>:</p>
<pre><code>page
  @ : prev "41"
      curr "42"
      next "43"
  post
    @ : title "foobar"
        time ...
    picture
      @ : filename "foo"
          desc "pic of foo"
    picture ...
    ...
  post ...
  ...
</code></pre><section id="Page-Generation">
<h3>Page Generation</h3>
<p>The stylesheet should then be declared at the beginning of the file,
so that the user agent can automatically fetch and apply it
to render the output XHML:</p>
<pre><code class="tag pi">&lt;?xml-stylesheet</code><code class="whitespace xml"> </code><code class="attribute">href</code><code class="operator">=</code><code class="string">"/page.xslt"</code><code class="whitespace xml"> </code><code class="attribute">type</code><code class="operator">=</code><code class="string">"text/xsl"</code><code class="tag pi">?&gt;</code><code class="whitespace xml">
</code></pre><p>XSLT is essentially a templating language, similar to PHP (which is also older)
and template libraries in your favorite languages.  For the ease of reading,
I will let the target document&#x2019;s namespace be the default, while aliasing
the transformation one as <code>xsl</code>.  The stylesheet for the web pages would
look something like the following, which should be self-explanatory.</p>
<pre><code>xsl:stylesheet
  xsl:template : @ : match "/page"
    xsl:variable : @ : name "base"
      xsl:text "/"
      xsl:value-of : @ : select "@curr"
      xsl:text "/"
    html
      head ...
      body
        nav
          xsl:if : @ : test "@prev != ''"
            a : @ : href "/{@prev}/"
              . "PREV"
          h1 : xsl:text "PAGE "
               xsl:value-of : @ : select "@curr"
          xsl:if : @ : test "@next != ''"
            ...
        xsl:for-each : @ : select "post"
          xsl:variable : @ : name "id"
            xsl:value-of
              @ : select "translate(@title, ' ', '-')"
          article
            @ : id "{$id}"
            h2
              a : @ : href "#{$id}"
                  xsl:value-of : @ : select "@title"
            xsl:for-each : @ : select "picture"
              a : @ : href "{$base}{@filename}.jpg"
                  img
                    @ : src "{$base}{@filename}.small.jpg"
                        alt "{@desc}"
                        title "{@desc}"
        footer ...
</code></pre></section>
<section id="Feed-Generation">
<h3>Feed Generation</h3>
<p>Similarly, for Atom entries on a single page,</p>
<pre><code>xsl:stylesheet
  xsl:variable : @ : name "root"
    . "https://gallery.example/"
  xsl:template : @ : match "/page"
    xsl:variable : @ : name "base"
      xsl:value-of : @ : select "$root"
      xsl:value-of : @ : select "@curr"
      xsl:text "/"
    xsl:for-each : @ : select "post"
      xsl:variable : @ : name "url"
        xsl:value-of : @ : select "$base"
        xsl:text "#"
        xsl:value-of
          @ : select "translate(@title, ' ', '-')"
      entry
        link
          @ : rel "alternate"
              type "application/xhtml+xml"
              href "{$url}"
        id : xsl:value-of : @ : select "$id"
        title : xsl:value-of : @ : select "@title"
        content
          @ : type "xhtml"
          div
            xsl:for-each : @ : select "picture"
              img
                @ : src "{$base}{@filename}.jpg"
                    alt "{@desc}"
                    title "{@desc}"
        updated : xsl:value-of : @ : select "@time"
</code></pre><p>The trickier part here is concatenating the entries together.
Simple enough, instead of linking to the stylesheet in the data,
we can read XML files directly from XSLT.</p>
<pre><code>xsl:template
  @ : match "/"
  ...
  xsl:apply-templates
    @ : select "document('42/index.xml')/page"
  xsl:apply-templates ...
  ...
</code></pre><p>This allows us to do other cool things, such as embedding SVG in XHTML
to make use of the parent element&#x2019;s <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#currentcolor_keyword">currentcolor</a>, while keeping
the source files separate.  It is especially useful for monochromatic icons,
e.g.</p>
<pre><code>xsl:copy-of : @ : select "document('cc.svg')/*"
xsl:copy-of : @ : select "document('by.svg')/*"
xsl:copy-of : @ : select "document('sa.svg')/*"
</code></pre></section>
<section id="Thumbnail-Generation">
<h3>Thumbnail Generation</h3>
<p>So far, we have met three out of the <a href="#Motivation">four requirements</a>,
only thing left is creating the thumbnails.  Inspired by Ethan Dalool,
I am going for <a href="https://voussoir.net/writing/sharing_photos">fairly large ones of 1024 px in width</a>,</p>
<blockquote>
<p>large enough to comfortably browse the photos without clicking through
to the big version of each, and the thumbnails are decently light
and not too jpeggy at about 125-150 kilobytes on average.</p>
</blockquote>
<p>At such size, I can aim for around ten photoes<a id="fnref4" href="#fn4" role="doc-noteref"><sup>4</sup></a> per page
while maintaining a somewhat decent load time.  Plus, since the width
of images are hardcoded, page <a href="https://en.wikipedia.org/wiki/Margin_(typography)">margin</a> could be automatically inferred
to never stretch them.</p>
<pre><code class="tag">html</code><code class="whitespace css"> </code><code class="operator">{</code><code class="whitespace css">
    </code><code class="property">box-sizing</code><code class="operator">:</code><code class="whitespace css"> </code><code class="identifier">border-box</code><code class="operator">;</code><code class="whitespace css">
    </code><code class="property">margin</code><code class="operator">:</code><code class="whitespace css"> </code><code class="constant builtin">auto</code><code class="operator">;</code><code class="whitespace css">
    </code><code class="property">max-width</code><code class="operator">:</code><code class="whitespace css"> </code><code class="function builtin">calc</code><code class="operator">(</code><code class="number">1024px</code><code class="whitespace css"> </code><code class="operator">+</code><code class="whitespace css"> </code><code class="number">2ch</code><code class="operator">)</code><code class="operator">;</code><code class="whitespace css">
</code><code class="operator">}</code><code class="whitespace css">
</code><code class="tag">body</code><code class="whitespace css"> </code><code class="operator">{</code><code class="whitespace css"> </code><code class="property">margin</code><code class="operator">:</code><code class="whitespace css"> </code><code class="number">0</code><code class="whitespace css"> </code><code class="number">1ch</code><code class="whitespace css"> </code><code class="operator">}</code><code class="whitespace css">
</code></pre><p>To generate the thumbnails, I use <a href="https://github.com/mattes/epeg">epeg</a> together with <code>make</code> for wildcarding:</p>
<pre><code class="variable">PICTURES</code><code class="whitespace makefile"> </code><code class="operator">:=</code><code class="whitespace makefile"> </code><code class="operator">$(</code><code class="function builtin">filter-out</code><code class="whitespace makefile"> </code><code class="identifier">%.small.jpg</code><code class="whitespace makefile"> </code><code class="operator">$(</code><code class="variable">PREFIX</code><code class="operator">)</code><code class="identifier">/%.jpg</code><code class="default">,</code><code class="whitespace makefile"> </code><code class="operator">$(</code><code class="function builtin">wildcard</code><code class="whitespace makefile"> </code><code class="identifier">*/*.jpg</code><code class="operator">)</code><code class="operator">)</code><code class="whitespace makefile">
</code><code class="variable">THUMBNAILS</code><code class="whitespace makefile"> </code><code class="operator">:=</code><code class="whitespace makefile"> </code><code class="operator">$(</code><code class="function builtin">patsubst</code><code class="whitespace makefile"> </code><code class="identifier">%.jpg</code><code class="default">,</code><code class="identifier">%.small.jpg</code><code class="default">,</code><code class="operator">$(</code><code class="variable">PICTURES</code><code class="operator">)</code><code class="operator">)</code><code class="whitespace makefile">
</code><code class="whitespace makefile">
</code><code class="identifier">%.small.jpg</code><code class="operator">:</code><code class="whitespace makefile"> </code><code class="identifier">%.jpg</code><code class="whitespace makefile">
</code><code class="whitespace makefile">	</code><code class="identifier">epeg</code><code class="whitespace bash"> </code><code class="default">-w</code><code class="whitespace bash"> </code><code class="number">1024</code><code class="whitespace bash"> </code><code class="default">-p</code><code class="whitespace bash"> </code><code class="default">-q</code><code class="whitespace bash"> </code><code class="number">80</code><code class="whitespace bash"> </code><code class="operator">$</code><code class="variable builtin">&lt;</code><code class="whitespace bash"> </code><code class="operator">$</code><code class="variable builtin">@</code><code class="whitespace makefile">
</code></pre><p>The Makefile also define rules for AoT compilation using <a href="https://gnome.pages.gitlab.gnome.org/libxslt/xsltproc.html">xsltproc</a>
for the web pages and feed.  Apparently no feed reader supports XSLT,
and for pages runtime processing negatively affect the performance
due to the multiple round trips for the stylesheet and the vector icons.</p>
<pre><code class="variable">DATA</code><code class="whitespace makefile"> </code><code class="operator">:=</code><code class="whitespace makefile"> </code><code class="operator">$(</code><code class="function builtin">wildcard</code><code class="whitespace makefile"> </code><code class="identifier">*/index.xml</code><code class="operator">)</code><code class="whitespace makefile"> </code><code class="identifier">index.xml</code><code class="whitespace makefile">
</code><code class="variable">PAGES</code><code class="whitespace makefile"> </code><code class="operator">:=</code><code class="whitespace makefile"> </code><code class="operator">$(</code><code class="function builtin">patsubst</code><code class="whitespace makefile"> </code><code class="identifier">%.xml</code><code class="default">,</code><code class="identifier">%.xhtml</code><code class="default">,</code><code class="operator">$(</code><code class="variable">DATA</code><code class="operator">)</code><code class="operator">)</code><code class="whitespace makefile">
</code><code class="variable">OUTPUTS</code><code class="whitespace makefile"> </code><code class="operator">:=</code><code class="whitespace makefile"> </code><code class="operator">$(</code><code class="variable">THUMBNAILS</code><code class="operator">)</code><code class="whitespace makefile"> </code><code class="operator">$(</code><code class="variable">PAGES</code><code class="operator">)</code><code class="whitespace makefile"> </code><code class="identifier">atom.xml</code><code class="whitespace makefile">
</code><code class="whitespace makefile">
</code><code class="identifier">all</code><code class="operator">:</code><code class="whitespace makefile"> </code><code class="operator">$(</code><code class="variable">OUTPUTS</code><code class="operator">)</code><code class="whitespace makefile">
</code><code class="whitespace makefile">
</code><code class="identifier">index.xml</code><code class="operator">:</code><code class="whitespace makefile"> </code><code class="operator">$(</code><code class="variable">LATEST</code><code class="operator">)</code><code class="identifier">/index.xml</code><code class="whitespace makefile">
</code><code class="whitespace makefile">	</code><code class="identifier">ln</code><code class="whitespace bash"> </code><code class="default">-fs</code><code class="whitespace bash"> </code><code class="operator">$</code><code class="variable builtin">&lt;</code><code class="whitespace bash"> </code><code class="operator">$</code><code class="variable builtin">@</code><code class="whitespace makefile">
</code><code class="whitespace makefile">
</code><code class="identifier">%.xhtml</code><code class="operator">:</code><code class="whitespace makefile"> </code><code class="identifier">%.xml</code><code class="whitespace makefile"> </code><code class="identifier">page.xslt</code><code class="whitespace makefile">
</code><code class="whitespace makefile">	</code><code class="identifier">xsltproc</code><code class="whitespace bash"> </code><code class="identifier">page</code><code class="default">.</code><code class="identifier">xslt</code><code class="whitespace bash"> </code><code class="operator">$</code><code class="variable builtin">&lt;</code><code class="whitespace bash"> </code><code class="operator">&gt;</code><code class="whitespace bash"> </code><code class="operator">$</code><code class="variable builtin">@</code><code class="whitespace makefile">
</code><code class="whitespace makefile">
</code><code class="identifier">atom.xml</code><code class="operator">:</code><code class="whitespace makefile"> </code><code class="identifier">atom.xslt</code><code class="whitespace makefile"> </code><code class="operator">$(</code><code class="variable">DATA</code><code class="operator">)</code><code class="whitespace makefile"> </code><code class="operator">$(</code><code class="function builtin">wildcard</code><code class="whitespace makefile"> </code><code class="identifier">*.svg</code><code class="operator">)</code><code class="whitespace makefile">
</code><code class="whitespace makefile">	</code><code class="identifier">xsltproc</code><code class="whitespace bash"> </code><code class="identifier">atom</code><code class="default">.</code><code class="identifier">xslt</code><code class="whitespace bash"> </code><code class="operator">&gt;</code><code class="whitespace bash"> </code><code class="identifier">atom</code><code class="default">.</code><code class="identifier">xml</code><code class="whitespace makefile">
</code></pre><p>The <a href="https://trong.loang.net/~cnx/px">full implementation</a> is deployed to <a href="https://px.cnx.gdn">px.cnx.gdn</a>,
mirrored to the <a href="https://www.opennic.org">OpenNIC</a> domain <a href="https://pix.sinyx.indy">pix.sinyx.indy</a> reusing
the former&#x2019;s TLS certificate, because CA/Browser Forum
disallows support for domains not recognized by ICANN and no
<a href="https://wiki.opennic.org/opennic/tls">CA for OpenNIC</a> is mature enough.</p>
</section>
</section>
<section id="Discussion">
<h2>Discussion</h2>
<blockquote>
<p>Okay you built your site using XML macros, so what?
The syntax is clunky and you hate it so much yourself
that not even a single line of code example here is in actual XML.
Doesn&#x2019;t seem like a love story to me!</p>
</blockquote>
<p>Like all relationships, it&#x2019;s not that simple.  I&#x2019;ve learned to not judge
a book by its cover and come to the understanding that XML is the (ugly)
equivalence of <a href="https://en.wikipedia.org/wiki/S-expression">sexp</a><a id="fnref5" href="#fn5" role="doc-noteref"><sup>5</sup></a>  Unlike afterthoughts such as C preprocessors,
<a href="https://docs.djangoproject.com/en/dev/topics/templates">Django</a>-like templates, or even the Wisp-lookalike syntax of <a href="https://github.com/slim-template/slim">Slim</a>,
XML stylesheets is in the same data structure.  To put it another way,
one can use XSLT to generate XSLT from XSLT.  Do I need it in this case
or ever at all?  Probably not, but that certainly makes XSL a lot more
attractive in my eyes.</p>
<p>Furthermore, the tooling for XML is highly mature, from editors to linters
and processors to rendering engines.  It&#x2019;d be lying to say you ain&#x2019;t
fascinated that tis possible to directly feed browsers pure data
instead of markup representations.  More than that, one can have
entirely static API endpoints that are both human- and machine-readable.</p>
<blockquote>
<p>XSL is just declarative JS!  You are so blinded
by your lust for functional programming that you have
become <a href="https://loang.net/~cnx/blog/reply">the very thing you swore to destroy</a>!</p>
</blockquote>
<p>My distaste for Ecma scripts is not due to DOM manipulation.
Sure, I do find in-place modification inelegant for documents,
but if only that&#x2019;s the only issue.  I block them on most sites
because they can interact with many things other than just the DOM,
imposing <a href="https://en.wikipedia.org/wiki/Mouse_tracking">privacy</a> and <a href="https://react-etc.net/entry/exploiting-speculative-execution-meltdown-spectre-via-javascript">security</a> risks while <a href="https://meta.stackexchange.com/q/2980/698165">fucking up the UX</a>.</p>
<p>Architecturally, Ecma scripts enable the absolute bloody worst possible
kind of web pages with zero data at all, fetching tiny pieces of content
in JSON and turn performance <a href="https://unixsheikh.com/articles/so-called-modern-web-developers-are-the-culprits.html">to shit</a>.  The user agents then try to salvage
efficiency by turning themselves into a distributed system component
and adding optimizations that shall never be (ab)used for the sake of users.
O ye <a href="https://en.wikipedia.org/wiki/Wirth%27s_law">cycle of doom</a>!</p>
<p>Note that one can make a similar mistake with XSL regarding the number
of round trips, and XML stylesheets can provide the same front-end/back-end
separation.  Both can be used to provide hot loading during development
and AoT rendering in production (if not all, then many JS libraries support
pre-rendering, ignoring the monstrous <a href="https://loang.net/~cnx/blog/dedep">dependency graph</a>).
At the end of the day, it&#x2019;s not the matter of technology but principle:
to be in the <a href="https://pluralistic.net/2023/01/21/potemkin-ai/#hey-guys">users&#x2019; best interest</a>.</p>
<blockquote>
<p>There is nothing complex about the photo gallery,
any existing SSG can do the same with minor tweaks!
You never needed to write a new one to begin with!</p>
</blockquote>
<p>I am wondering the same myself, but keep in mind there are details
I&#x2019;ve been hiding from in the example.  I went all-in for the semantic web
with the hope for best portability and accessibility.  One thing
I haven&#x2019;t mentioned is the <code>lang</code> attribute, e.g. <code>en</code>, <code>vi</code> or <code>fr</code>
depending on the post.  Adding this to the web pages requires the SSG
to be somewhat modular, and even harder for the web feed.</p>
<p>Moreover, generic SSG are not designed to handle the difference
in content between a page&#x2019;s <code>article</code> and the feed&#x2019;s corresponding <code>entry</code>,
neither for having multiple posts in a single page.  Pagination is
also commonly implemented backwards, i.e. page 2 being the second latest one,
making it impossible to avoid link rot.</p>
<p>Not to suggest that the majority of SSG are poorly designed, just that
from a certain amount of <a href="https://guide.handmade-seattle.com/c/2021/context-is-everything">context</a> difference, tis cheaper to just redesign
from scratch.  This is not about XSL vs Go/Python/JS for SSG or web dev
in general, but this specific and happen-to-be-far-from-complex case.</p>
</section>
<section id="Conclusion">
<h2>Conclusion</h2>
<p>At the time of writing, XML has pretty much been superseded by JSON or YAML,
for the better or worse.  I have no love for YAML for obvious reasons,
but it also saddens me to sometimes see JSON being solely used as a container
for HTML.  I hope that this essay can <a href="https://www.youtube.com/watch?v=F3QPWrLFsOA">awaken something in you</a> about XML
and remind you about the semantic web in your next project.  It worked out
for me, maybe it&#x2019;ll work out for you too!</p>
<p>The story between XML and my photo gallery is a fond love story.
They were born for each other, there was no drama, everything just werkt.
Their romance inspire me to better appreciate stability and maturity,
and value those right in front of my eyes yet I had been <em>too blind to see</em>.
Anyway, this is getting too long, so Imma end it with another <a href="https://www.youtube.com/watch?v=5LvOdWi3Qno">song</a>.</p>
<blockquote>
<p>Lookin&#x2019; for perfect<br/>
Surrounded by artificial<br/>
You&#x2019;re the closest thing to real I&#x2019;ve seen<br/>
Sure, everyone has their problems<br/>
That&#x2019;s a given<br/>
Yours are the easiest to tolerate</p>
</blockquote>
</section>
</section>
<section role="doc-endnotes">
<hr/>
<ol>
<li id="fn1">
<p>If you know, you know.<a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn2">
<p>Yup, just the kernel.<a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn3">
<p>But in case it works for you, check out <a href="https://eradman.com/entrproject">entr</a>.<a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn4">
<p><em>Thumb</em>nails, pho<em>toes</em>, get it?-)<a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn5">
<p>Or conventionally in most Lisp 1&#x2019;s, <code>sex?</code>.<a href="#fnref5" role="doc-backlink">↩︎︎</a><a href="#fnref5" role="doc-backlink">↩︎︎</a><a href="#fnref5" role="doc-backlink">↩︎︎</a><a href="#fnref5" role="doc-backlink">↩︎︎</a><a href="#fnref5" role="doc-backlink">↩︎︎</a></p>
</li>
</ol>
</section>
</div>
</content>
<category term='fun'/>
<category term='net'/>
<category term='recipe'/>
<published>2023-03-17T12:00:00Z</published>
<updated>2026-08-03T07:31:00Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/2020/gsoc/article/1/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/2020/gsoc/article/1/index.html</id>
<title>Unexpected Things When You're Expecting</title>
<summary>GSoC 2020: Unexpected Things When You're Expecting</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/2020/gsoc/article/1/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<section id="Unexpected-Things-When-You're-Expecting">
<h1>Unexpected Things When You&#x2019;re Expecting</h1>
<p>Hi everyone, I hope that you are all doing well and wishes you all good health!
The last week has not been really kind to me with a decent amount of
academic pressure (my school year is lasting until early Jully).
It would be bold to say that I have spent 10 hours working on my GSoC project
since the last check-in, let alone the 30 hours per week requirement.
That being said, there were still some discoveries that I wish to share.</p>
<ol>
<li>
<a href="#The-multiprocessingdummy-wrapper">The <code>multiprocessing[.dummy]</code> wrapper</a></li>
<li>
<a href="#The-change-in-direction">The change in direction</a></li>
</ol>
<section id="The-multiprocessingdummy-wrapper">
<h2>The <code>multiprocessing[.dummy]</code> wrapper</h2>
<p>Most of the time I spent was to finalize the multi{processing,threading}
wrapper for <code>map</code> function that submit tasks to the worker pool.
To my surprise, it is rather difficult to write something that is
not only portable but also easy to read and test.</p>
<p>By <a href="https://github.com/pypa/pip/issues/8320">the latest commit</a>, I realized the following:</p>
<ol>
<li>
The <code>multiprocessing</code> module was not designed for the implementation
details to be abstracted away entirely.  For example, the lazy <code>map</code>&#x2019;s
could be really slow without specifying suitable chunk size
(to cut the input iterable and distribute them to workers in the pool).
By <em>suitable</em>, I mean only an order smaller than the input.  This defeats
half of the purpose of making it lazy: allowing the input to be
evaluated lazily.  Luckily, in the use case I&#x2019;m aiming for, the length of
the iterable argument is small and the laziness is only needed for the output
(to pipeline download and installation).
</li>
<li>
Mocking <code>import</code> for testing purposes can never be pretty.  One reason
is that we (Python users) have very little control over the calls of
<code>import</code> statements and its lower-level implementation <code>__import__</code>.
In order to properly patch this built-in function, unlike for others
of the same group, we have to <code>monkeypatch</code> the name from <code>builtins</code>
(or <code>__builtins__</code> under Python 2) instead of the module that import stuff.
Furthermore, because of the special namespacing, to avoid infinite recursion
we need to alias the function to a different name for fallback.
</li>
<li>
To add to the problem, <code>multiprocessing</code> lazily imports the fragile module
during pools creation.  Since the failure is platform-specific
(the lack of <code>sem_open</code>), it was decided to check upon the import
of the <code>pip</code>&#x2019;s module.  Although the behavior is easier to reason
in human language, testing it requires invalidating cached import and
re-import the wrapper module.
</li>
<li>
Last but not least, I now understand the pain of keeping Python 2
compatibility that many package maintainers still need to deal with
everyday (although Python 2 has reached its end-of-life, <code>pip</code>, for
example, <a href="https://github.com/pypa/pip/issues/6148">will still support it for another year</a>).
</li>
</ol>
</section>
<section id="The-change-in-direction">
<h2>The change in direction</h2>
<p>Since last week, my mentor Pradyun Gedam and I set up weekly real-time
meeting (a fancy term for video/audio chat in the worldwide quarantine
era) for the entire GSoC period. During the last session, we decided to
put parallelization of download during resolution on hold, in favor of a
more beneficial goal: <a href="https://github.com/pypa/pip/issues/7819">partially download the wheels during dependency resolution</a>.</p>
<p><img alt="Me going around the problem" src="swirl.png"/></p>
<p>As discussed by Danny McClanahan and the maintainers of <code>pip</code>, it is feasible
to only download a few kB of a wheel to obtain enough metadata for
the resolution of dependency.  While this is only applicable to wheels
(i.e. prebuilt packages), other packaging format only make up less than 20%
of the downloads (at least on PyPI), and the figure is much less for
the most popular packages.  Therefore, this optimization alone could make
<a href="https://www.ei8fdb.org/test-pips-alpha-resolver-and-help-us-document-dependency-conflicts">the upcoming backtracking resolver</a>&#x2019;s performance par with the legacy one.</p>
<p>During the last few years, there has been a lot of effort being poured into
replacing <code>pip</code>&#x2019;s current resolver that is unable to resolve conflicts.
While its correctness will be ensured by some of the most talented and
hard-working developers in the Python packaging community, from the users&#x2019;
point of view, it would be better to have its performance not lagging
behind the old one.  Aside from the increase in CPU cycles for more
rigorous resolution, more I/O, especially networking operations is expected
to be performed.  This is due to <a href="https://github.com/pypa/pip/issues/7406#issuecomment-583891169">the lack of a standard and efficient way to acquire the metadata</a>.
Therefore, unlike most package managers we are familiar with, <code>pip</code> has
to fetch (and possibly build) the packages solely for dependency informations.</p>
<p>Fortunately, <a href="https://peps.python.org/pep-427#recommended-archiver-features">PEP 427#recommended-archiver-features</a> recommends
package builders to place the metadata at the end of the archive.
This allows the resolver to only fetch the last few kB using
<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests">HTTP range requests</a> for the relevant information.
Simply appending <code>Range: bytes=-8000</code> to the request header
in <code>pip._internal.network.download</code> makes the resolution process
<em>lightning</em> fast.  Of course this breaks the installation but I am confident
that it is not difficult to implement this optimization cleanly.</p>
<p>One drawback of this optimization is the compatibility.  Not every Python
package index support range requests, and it is not possible to verify
the partial wheel.  While the first case is unavoidable, for the other,
hashes checking is usually used for pinned/locked-version requirements,
thus no backtracking is done during dependency resolution.</p>
<p>Either way, before installation, the packages selected by the resolver
can be downloaded in parallel.  This warranties a larger crowd of packages,
compared to parallelization during resolution, where the number of downloads
can be as low as one during trail of different versions of the same package.</p>
<p>Unfortunately, I have not been able to do much other than
<a href="https://github.com/pypa/pip/issues/8411">a minor clean up</a>.  I am looking forward to accomplishing
more this week and seeing what this path will lead us too!  At the moment,
I am happy that I&#x2019;m able to meet the blog deadline, at least in UTC!</p>
</section>
</section>
</div>
</content>
<category term='gsoc'/>
<category term='python'/>
<published>2020-06-09T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/greens/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/greens/index.html</id>
<title>Green Leaf Soup</title>
<summary>An easy template for making savory soup from green leaf vegetables</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/greens/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<section id="Green-Leaf-Soup">
<h1>Green Leaf Soup</h1>
<p>At the time of writing, I am sharing a kitchen with around 40 people
from all parts of the world.  Very often, someone asks me to share
a recipe from my cuisine, and I usually have to decline, blaming
the lack of fresh ingredients,<a id="fnref1" href="#fn1" role="doc-noteref"><sup>1</sup></a> while I can only afford to shop
for groceries weekly.  While most Vietnamese dishes call for fresh meat
(I can only buy refrigerated one), in certain case it doesn&#x2019;t really matter.
One quick dish that could tolerate days-old meat is a soup of green leaf
vegetables.</p>
<p>Back when I was still at home, a meal almost always consists of a soup.
When we are about to finish a bowl of rice, we mix in the soup to wash
all of the gelatinized starch (into our mouth).  The soup could be
anything from boiled vegetable broth to <a href="https://en.wikipedia.org/wiki/Basella_alba">vine spinach</a> and jute soup
with crab juice.  In that range of difficulty, I would rate the following
recipe somewhere in the lower middle.</p>
<section id="Ingredients">
<h2>Ingredients</h2>
<p>For a vegetable soup, of course you need a lot of veggie.  As much
as you can eat.  I would recommend at least two handful per serving<a id="fnref2" href="#fn2" role="doc-noteref"><sup>2</sup></a>
of any <a href="https://en.wikipedia.org/wiki/Brassica">Brassica</a> leafs, e.g. mustard greens, spoon cabbage
or regular cabbage.<a id="fnref3" href="#fn3" role="doc-noteref"><sup>3</sup></a>  The greener the plant the less starchy it is
and the better it blends with the umami of the meat.</p>
<p>As for the animal product, minced or ground pork is a common choice.
Minced chicken, fish or dried shrimp also works, but IMHO beef, lamb
or goat could overpower the veggie.  Meat is not the star of the show
and should be used moderately, 50 grams<a id="fnref4" href="#fn4" role="doc-noteref"><sup>4</sup></a> would be generous.
There is no vegan variation of this dish AFAICT, except for
reducing to just water, leafs and seasoning, but even a child
could cook that without a recipe.</p>
<p>In addition, a shallot is required for searing and <a href="https://en.wikipedia.org/wiki/Fish_sauce#Vietnam">fish sauce</a> for seasoning.
It is OK-ish to use onion in place of shallot;<a id="fnref5" href="#fn5" role="doc-noteref"><sup>5</sup></a> I am not a fan
of using soy sauce in this dish though.  Super salt (table salt and MSG 9:1 mix)
is a better substitution in case you can&#x2019;t get your hands on <em>the</em> signature
Vietnamese seasoning.</p>
<p>Last but not least, it would not be a soup without water.
A cup should be enough to emerge the cooked veggie.</p>
</section>
<section id="Preparation">
<h2>Preparation</h2>
<p>First, wash and slice the vegetable and throw it in a colander
to let the water rinse of.  Next, chop the shallot <em>thinly</em>.</p>
<p>If you bought minced or ground meat, you are done preparing.
Otherwise, it&#x2019;s mincing/grinding time, duh!</p>
</section>
<section id="Cooking">
<h2>Cooking</h2>
<p>Turn the stovetop to medium high and put on a stainless steel pan or pot.
Doesn&#x2019;t have to stainless steel, anything smooth without a polymer coating
would do.  Pour in a touch of cooking oil (or a tiny spoon of lard)
and start sautéing the shallot.</p>
<p>As soon as the pot is hot enough, immediately add the meat (don&#x2019;t wait
for the shallot to turn golden brown, the slices are thin enough to
be caramelized as the meat is seared).  You don&#x2019;t need to stir since
we don&#x2019;t need evenly cook it right now, but don&#x2019;t let it stick together.
Use a spoon or a scraper to break it up and press it down for faster searing.</p>
<p>If you have fish sauce, pour it in after the meat finishes browning
to develop even more flavor for a few seconds.  Then, deglaze the pot
using water and bring it to a boil.  Throw the leafs into the pot
and get the water boiling again.  In case you use salt for seasoning,
now is time to sprinkle it in the soup.  Let it cook for another
two or three minutes (radiant or thermal conductive coil could be
switched off and maintain the heat for that duration) and it&#x2019;s ready to serve!</p>
</section>
</section>
<section role="doc-endnotes">
<hr/>
<ol>
<li id="fn1">
<p><em>Good</em> Vietnamese food I grow up eating are always from the freshest.<a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn2">
<p>From now on, the amount of each ingredient is listed for one serving.<a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn3">
<p>Outside of the genus, <a href="https://en.wikipedia.org/wiki/Sauropus_androgynus">rau ngót</a> is awesome if available.<a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn4">
<p>Or about a dozen bullets in eagle and burger unit.<a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a><a href="#fnref4" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn5">
<p>Not a whole onion, but around the size of your thumb per serving.<a href="#fnref5" role="doc-backlink">↩︎︎</a><a href="#fnref5" role="doc-backlink">↩︎︎</a><a href="#fnref5" role="doc-backlink">↩︎︎</a><a href="#fnref5" role="doc-backlink">↩︎︎</a></p>
</li>
</ol>
</section>
</div>
</content>
<category term='lyf'/>
<category term='recipe'/>
<published>2022-09-11T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
<entry>
<link href='https://loang.net/~cnx/blog/youtu/index.html' rel='alternate' type='text/html'/>
<id>https://loang.net/~cnx/blog/youtu/index.html</id>
<title>Best Ways to Watch YouTube Videos</title>
<summary>Do you know de wey?  Lemme show you de wey!</summary>
<content type='xhtml' xml:base='https://loang.net/~cnx/blog/youtu/index.html'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<section id="Best-Ways-to-Watch-YouTube-Videos">
<h1>Best Ways to Watch YouTube Videos</h1>
<p>In today&#x2019;s episode of <a href="https://www.alexmolas.com/2023/07/15/nobody-cares-about-your-blog.html">guides nobody asked for and likely having
been covered by someone more qualified</a>, lemme show you
the <em>correct</em> ways to view videos hosted on YouTube and other
hostile, tracker-riddled hellscapes.  Whilst I despise Google&#x2019;s
mass surveillance practices, it stores a large proportion
of culturally significant videos and clips that would be difficult
to mirror to user-respecting services due to copyright.  Hell, even YouTube
doesn&#x2019;t have the right to distribute many of them in the first place.</p>
<p>Because of YouTube&#x2019;s circumvention of advertisement blockers,
the ad-blocking arm race finally caught mainstream media attention
and tis kool to talk about that now.  Hence I&#x2019;m happy to jump
on the bandwagon, albeit a wee bit late, but this ain&#x2019;t just that.
Since I feed you poison—over 4% of the pages linked from my site
are on YouTube—the least I can do is sell you my cures.</p>
<section id="Using-a-Proper-Media-Player">
<h2>Using a Proper Media Player</h2>
<p>The most popular solutions are either to use for a good blocker
on a browser with (supposedly) long-term support for <a href="https://github.com/uBlockOrigin/uBlock-issues/issues/338#issuecomment-1332300191">Manifest V2</a>
like <a href="https://ublockorigin.com">uBlock Origin</a> on Firefox, or use alternative front-ends
such as <a href="https://invidious.io">Invidious</a> or <a href="https://github.com/TeamPiped/Piped">Piped</a>.  Although uBlock Origin is essential
for a pleasant experience on the modern interwebs and alternative frontends
offers the best UX for browsing videos, in-browser and service-specific
media players are inferior anyway when compared to programs properly designed
for a decent playback experience.</p>
<p>My favorite has been <a href="https://mpv.io">mpv</a> for as long as I can remember,
as it makes it easy to adjust video brightness/contrast/etc.,
playback speed, subtitle size and placements, and to overamplify
quiet audios.  Out of the box, it integrates with <a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a>,
a time shifter with support for <a href="https://github.com/yt-dlp/yt-dlp/raw/master/supportedsites.md">most online media services</a>.
Just drop the URL into an mpv window and <a href="https://www.youtube.com/watch?v=_FNzL5nW_u4">boom</a>, it werks!</p>
<p>Either <a href="https://uno.starshard.studio/notes/9nmgldtdgghu8m2n">drag-and-drop</a> or invoking <code>mpv $url</code> is quite convenient,
but not that close to following an anchor, is it?  You&#x2019;d need
to first open mpv or a program launcher<a id="fnref1" href="#fn1" role="doc-noteref"><sup>1</sup></a>, then drag the URL there,
or perhaps copy and paste it for the latter cases.
What if you <a href="https://ziglang.org/perf">gotta go fast</a>, aye?  As a <a href="https://video.hardlimit.com/c/morbiwars">hedgehog-maxxer</a> meself,
of course I can do better, and here&#x2019;s how.</p>
</section>
<section id="With-a-Browser-Add-on">
<h2>With a Browser Add-on</h2>
<p>While drafting this article, I noticed that the <em>ff2mpv</em> extension I was using
had <a href="https://github.com/woodruffw/ff2mpv/commit/2397193b36e6.patch">technically been non-free</a> for a while.  Albeit I understand
and respect the author&#x2019;s noble intention against violence, I believe
discrimination never ends up helping those oppressed due to the power imbalance
for the exclusion false-positives to be worth it.</p>
<p>For this reason, I switched to <a href="https://addons.mozilla.org/en-US/firefox/addon/iina-open-in-mpv">Open in mpv</a> and recommend it instead.
The usage is practically the same: open context menu at the video URL
and select <em>Open this link in mpv</em>.  The internal mechanism
is a bit different though, and because it influences the installation process,
I will try to briefly explain <a href="https://www.youtube.com/watch?v=1Fl2sMV7Hcc">how it works</a>.</p>
<p>The way <em>Open in mpv</em> works is a bit convoluted.  First, it wraps
the specified URL in a <code>mpv</code> scheme.  The new URL starts with <code>mpv://</code>
is then passed back to Firefox, which must have been configured to open it
in the native program <code>open-in-mpv</code>.  This program parses the URL
into the equivalent mpv command and execute it.  If you are not on NixOS,
see the <a href="https://github.com/Baldomo/open-in-mpv/raw/master/README.md">extension&#x2019;s README</a> to set it up yourself.</p>
<p>Otherwise, it can be declared in <a href="https://nixos.org/manual/nixos/stable/options#opt-programs.firefox.policies">configuration.nix(5)</a> as follows.
The declarations should be self-explanatory after referencing
Firefox&#x2019;s documentation for <a href="https://mozilla.github.io/policy-templates">policies.json</a>.  If you have trouble finding
an extension&#x2019;s ID and download URL, search for it in <a href="https://gnuzilla.gnu.org/mozzarella">Mozzarella</a>.</p>
<pre><code class="operator">{</code><code class="whitespace nix"> </code><code class="identifier">pkgs</code><code class="operator">,</code><code class="whitespace nix"> </code><code class="keyword">...</code><code class="whitespace nix"> </code><code class="operator">}</code><code class="operator">:</code><code class="whitespace nix">
</code><code class="operator">{</code><code class="whitespace nix">
  </code><code class="identifier">programs</code><code class="operator">.</code><code class="identifier">firefox</code><code class="whitespace nix"> </code><code class="operator">=</code><code class="whitespace nix"> </code><code class="operator">{</code><code class="whitespace nix">
    </code><code class="identifier">enable</code><code class="whitespace nix"> </code><code class="operator">=</code><code class="whitespace nix"> </code><code class="constant builtin">true</code><code class="operator">;</code><code class="whitespace nix">
    </code><code class="identifier">policies</code><code class="whitespace nix"> </code><code class="operator">=</code><code class="whitespace nix"> </code><code class="operator">{</code><code class="whitespace nix">
      </code><code class="identifier">ExtensionSettings</code><code class="operator">.</code><code class="string">"{d66c8515-1e0d-408f-82ee-2682f2362726}"</code><code class="whitespace nix"> </code><code class="operator">=</code><code class="whitespace nix"> </code><code class="operator">{</code><code class="whitespace nix">
        </code><code class="identifier">default_area</code><code class="whitespace nix"> </code><code class="operator">=</code><code class="whitespace nix"> </code><code class="string">"menupanel"</code><code class="operator">;</code><code class="whitespace nix">
        </code><code class="identifier">installation_mode</code><code class="whitespace nix"> </code><code class="operator">=</code><code class="whitespace nix"> </code><code class="string">"normal_installed"</code><code class="operator">;</code><code class="whitespace nix">
        </code><code class="identifier">install_url</code><code class="whitespace nix"> </code><code class="operator">=</code><code class="whitespace nix">
          </code><code class="string">"https://addons.mozilla.org/firefox"</code><code class="whitespace nix">
          </code><code class="operator">+</code><code class="whitespace nix"> </code><code class="string">"/downloads/latest/iina-open-in-mpv/latest.xpi"</code><code class="operator">;</code><code class="whitespace nix">
      </code><code class="operator">}</code><code class="operator">;</code><code class="whitespace nix">
      </code><code class="identifier">Handlers</code><code class="operator">.</code><code class="identifier">scheme</code><code class="operator">.</code><code class="identifier">mpv</code><code class="whitespace nix"> </code><code class="operator">=</code><code class="whitespace nix"> </code><code class="operator">{</code><code class="whitespace nix">
        </code><code class="identifier">action</code><code class="whitespace nix"> </code><code class="operator">=</code><code class="whitespace nix"> </code><code class="string">"useHelperApp"</code><code class="operator">;</code><code class="whitespace nix">
        </code><code class="identifier">ask</code><code class="whitespace nix"> </code><code class="operator">=</code><code class="whitespace nix"> </code><code class="constant builtin">false</code><code class="operator">;</code><code class="whitespace nix">
        </code><code class="identifier">handlers</code><code class="whitespace nix"> </code><code class="operator">=</code><code class="whitespace nix"> </code><code class="operator">[</code><code class="whitespace nix"> </code><code class="operator">{</code><code class="whitespace nix">
          </code><code class="identifier">name</code><code class="whitespace nix"> </code><code class="operator">=</code><code class="whitespace nix"> </code><code class="string">"open-in-mpv"</code><code class="operator">;</code><code class="whitespace nix">
          </code><code class="identifier">path</code><code class="whitespace nix"> </code><code class="operator">=</code><code class="whitespace nix"> </code><code class="string">"${pkgs.open-in-mpv}/bin/open-in-mpv"</code><code class="operator">;</code><code class="whitespace nix">
        </code><code class="operator">}</code><code class="whitespace nix"> </code><code class="operator">]</code><code class="operator">;</code><code class="whitespace nix">
      </code><code class="operator">}</code><code class="operator">;</code><code class="whitespace nix">
    </code><code class="operator">}</code><code class="operator">;</code><code class="whitespace nix">
  </code><code class="operator">}</code><code class="operator">;</code><code class="whitespace nix">
</code><code class="operator">}</code><code class="whitespace nix">
</code></pre><p>Even though Mozzarella is supposed to only show libre add-ons, be aware
that the metadata it crawls from <a href="https://addons.mozilla.org">addons.mozzila.org</a> might not always
be <a href="https://issues.guix.gnu.org/68361">correct</a>.  Ideally, browser extensions should be packaged
in the distribution&#x2019;s repository, but packaging discipline is not exactly
NixOS&#x2019;s strong suit.  I will probably post an update on how to declare
<code>policies.json</code> in Guix once I figure that out.</p>
</section>
<section id="From-a-Feed-Reader">
<h2>From a Feed Reader</h2>
<p>Now we can properly watch videos while browsing the web,
but subscribing to YouTube channels on its web interface
would require creating an account and subjecting one&#x2019;s self
to more surveillance.  Fortunately, at the time of writing,
YouTube still provide Atom <a href="https://en.wikipedia.org/wiki/Web_feed">feeds</a> for syndication.
Funny enough, they are advertised on the channel pages as RSS:</p>
<pre><code class="tag chars">&lt;</code><code class="tag single">link</code><code class="whitespace html"> </code><code class="attribute">rel</code><code class="operator">=</code><code class="string">"alternate"</code><code class="whitespace html">
      </code><code class="attribute">type</code><code class="operator">=</code><code class="string">"application/rss+xml"</code><code class="whitespace html">
      </code><code class="attribute">title</code><code class="operator">=</code><code class="string">"RSS"</code><code class="whitespace html">
      </code><code class="attribute">href</code><code class="operator">=</code><code class="string">"https://www.youtube.com/feeds/videos.xml?channel_id=…"</code><code class="tag chars">&gt;</code><code class="whitespace html">
</code></pre><p>The referenced feed employ <a href="https://www.rssboard.org/media-rss">Media RSS</a> to communicate the video URL.
This extension is widely supported by feed readers, as well
as the previously mentioned feed-discovery mechanism.  I use <a href="https://lzone.de/liferea">Liferea</a>,
which allows me to directly paste the YouTube channel&#x2019;s URL<a id="fnref2" href="#fn2" role="doc-noteref"><sup>2</sup></a>,
and displays each video&#x2019;s description, thumbnail and enclosed media, e.g.</p>
<p><img alt="Liferea in action" src="liferea.png"/></p>
<p>For each MIME type to, enclosures can be configured to be opened
by a user-preferred program.  In this case, I set <code>mpv --ytdl-format=b</code>
for <code>application/x-shockwave-flash</code> (a reminiscence of a time
when browsers needed <a href="https://ruffle.rs">Flash</a> to play videos and animations)
for the <em>second</em> best quality to save some bandwidth.  YouTube encodes
the highest resolution video separate from the audio, so the best combined
format <code>b</code> is one level lower than yt-dlp&#x2019;s default best video
and best audio together.</p>
</section>
<section id="Via-Clipboard-Integration">
<h2>Via Clipboard Integration</h2>
<p>People also share videos with me via instant messaging.
I find it cumbersome to open the URL in the browser then redirect it
to the media player, so the clipboard is used as the bridge instead.
To do this, I simply create a key binding to the command below.<a id="fnref3" href="#fn3" role="doc-noteref"><sup>3</sup></a></p>
<pre><code class="identifier">mpv</code><code class="whitespace bash"> </code><code class="default">--ytdl-format</code><code class="default">=</code><code class="identifier">b</code><code class="whitespace bash"> </code><code class="string">"$(xclip -out -selection clipboard)"</code><code class="whitespace bash">
</code></pre></section>
<section id="Musing">
<h2>Musing</h2>
<p>There, I shared how I do it so <a href="/blog/youtu">you can too</a>!  If they seem
needlessly complex, you share my disappointment on the UX evolution
of the mainstream web.  I dream of a more semantic web, not necessarily
web 3.0, perhaps just more explicitly typed, where e.g. a YouTube URL
for embedding would be a <code>video/webm</code> instead of a <code>text/html</code>.</p>
<p>If <code>mailto</code> URIs can launch our email client, and social media pages
can bug us to open the post in their own app, why can&#x2019;t we have
interoperable media handling?  Maybe we should, but I&#x2019;m not sure if we can.
<a href="https://www.searchenginejournal.com/youtube-is-showing-ads-on-non-monetized-channels/388674">Greed</a> stands in our way.  Providers force us to use their proprietary
<a href="https://pluralistic.net/2023/01/21/potemkin-ai">malware</a> to consume their service.  <a href="https://www.defectivebydesign.org">DRM</a> has become the foundation
of media distribution.  Grassroots movements like <a href="https://framasoft.org">Framasoft</a> might never
reach mainstream status.</p>
<p>I don&#x2019;t mean to tell you to give up though, just to direct your energy
to where it matters.  Spend less on developing <a href="https://sr.ht/~benbusby/farside">alternative front-ends</a>
than on ethical replacements, bridges and inviting people over.
We need more <a href="https://sepiasearch.org">videos</a>, more <a href="https://www.funkwhale.audio">music</a>, more <a href="https://castopod.org">podcasts</a>, more <a href="https://en.wikipedia.org/wiki/Open_access">knowledge</a>,
better <a href="https://xmpp.org">instant</a> <a href="https://matrix.org">messaging</a>, better <a href="https://seirdy.one/posts/2021/03/10/search-engines-with-own-indexes">search engines</a>,
better <a href="https://browser.mt">translations</a>, better <a href="https://www.home-assistant.io">home</a> <a href="https://platypush.tech">automation</a>, and whatnot.
Against all odds, maybe things will finally start to improve
even for those outside of our bubble.  <a href="https://fe.disroot.org/@mcsinyx/posts/ALaW77HgCSPq4pLxpo">Perchance.</a></p>
</section>
</section>
<section role="doc-endnotes">
<hr/>
<ol>
<li id="fn1">
<p>Or a terminal emulator<a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a><a href="#fnref1" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn2">
<p>Something starting with https://www.youtube.com/@<a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a><a href="#fnref2" role="doc-backlink">↩︎︎</a></p>
</li>
<li id="fn3">
<p>On [Wayland], replace <code>xclip</code> with something equivalent<a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a><a href="#fnref3" role="doc-backlink">↩︎︎</a></p>
</li>
</ol>
</section>
</div>
</content>
<category term='fun'/>
<category term='net'/>
<category term='recipe'/>
<category term='nix'/>
<category term='clipboard'/>
<published>2024-01-17T12:00:00Z</published>
<updated>2026-08-03T04:32:18Z</updated>
</entry>
</feed>
