<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://push.cx/feed.xml" rel="self" type="application/atom+xml" /><link href="https://push.cx/" rel="alternate" type="text/html" /><updated>2026-08-20T00:00:00-05:00</updated><id>https://push.cx/feed.xml</id><title type="html">Push.cx</title><subtitle>Blog of [Peter Bhat Harkins](https://malaprop.org)</subtitle><entry><title type="html">Fixing tags on 17k mp3s</title><link href="https://push.cx/fixing-tags-on-17k-mp3s" rel="alternate" type="text/html" title="Fixing tags on 17k mp3s" /><published>2026-08-20T00:00:00-05:00</published><updated>2026-08-20T00:00:00-05:00</updated><id>https://push.cx/fixing-tags-on-17k-mp3s</id><content type="html" xml:base="https://push.cx/fixing-tags-on-17k-mp3s"><![CDATA[<p>Beets is a python CLI tool for fixing up metadata in music collections.
I wrote up a guide from my own use on my ~17k track collection because <a href="/rovyvon-a5r-flashlight-diagram">writing instructions is a good way to learn</a>.
But also: it’s a power tool built by hobbyists over 15+ years, so the UI is clunky and often misleading.
It doesn’t really respect the user’s time and attention, but it’s the least bad tool I could find.</p>

<p>This post has been hanging out in my drafts folder a long time.
I started writing these work notes in second person back when I thought this would be a straightforward project, maybe I’d finish and have a tutorial that highlighted the one or two rough spots.
That didn’t work out, but I’m not putting in even more time to rewrite it.</p>

<h2 id="install">Install</h2>

<p>Despite spending a few years of my career as a Python developer, I’ve never been able to keep tools written in Python working for more than a few months without some update blowing it up (let alone a dev setup).
I’ve tried many of the various packaging tools over the decades, no real change in MTBF.
I’ve been using a new installer called <a href="https://docs.astral.sh/uv/">uv</a> that does keep them working.
To install with aome very useful optional dependencies: <code class="language-plaintext highlighter-rouge">uv tool install beets --with bs4 --with librosa --with pylast --with pyacoustid --with resampy --with soundfile</code></p>

<p>Run: <code class="language-plaintext highlighter-rouge">beet --help</code>.</p>

<p>It will load your plugins and they’ll complain about <em>most</em> missing dependencies.
If you see an exception, puzzle out which python module it wants (eg <code class="language-plaintext highlighter-rouge">acoustid</code> is in <code class="language-plaintext highlighter-rouge">pyacoustid</code>), run <code class="language-plaintext highlighter-rouge">uv tool uninstall beets</code>, and rerun the <code class="language-plaintext highlighter-rouge">uv tool install</code> adding the missing library to a <code class="language-plaintext highlighter-rouge">--with</code>.</p>

<p>Install your distro’s packages for <code class="language-plaintext highlighter-rouge">ffmpeg</code> and <code class="language-plaintext highlighter-rouge">oggz</code> (may be named <code class="language-plaintext highlighter-rouge">liboggz</code>) to check for corrupt files with the <code class="language-plaintext highlighter-rouge">badfiles</code> plugin.</p>

<h2 id="first-run-auto-tagging">First run: auto-tagging</h2>

<p>Using beets is broken up into 3+ runs of the software to shake out issues with the config or your library before making any edits to music files.
This first run is about collecting info on your library into the beets database (<code class="language-plaintext highlighter-rouge">~/.config/beets/library.db</code>).</p>

<p>Here’s my <a href="/uploads/2026/08/config.yaml"><code class="language-plaintext highlighter-rouge">.config/beets/config.yaml</code></a> and <a href="https://docs.beets.io/en/stable/reference/config.html">the config docs</a>.</p>

<p>The important parts to highlight are:</p>

<ul>
  <li>The <code class="language-plaintext highlighter-rouge">paths</code> uses filenames I’ve used for ages for compilations and single tracks, you probably didn’t organize your music like this.</li>
  <li><a href="https://docs.beets.io/en/stable/plugins/index.html#pst-primary-sidebar">Plugins</a> are configured to write into the music files and <em>will</em> do so on this first run. Surprise! So you get to review each plugin’s config individually.</li>
  <li>The <code class="language-plaintext highlighter-rouge">import</code> section has <code class="language-plaintext highlighter-rouge">copy</code>, <code class="language-plaintext highlighter-rouge">move</code>, and <code class="language-plaintext highlighter-rouge">write</code> all set to <code class="language-plaintext highlighter-rouge">false</code> to avoid rewriting tags on the first pass. This is to work around a UX problem (described below), but also to give time to build confidence in how <code class="language-plaintext highlighter-rouge">beets</code> works.</li>
</ul>

<p>Optional: review <a href="https://docs.beets.io/en/stable/plugins/index.html#using-metadata-source-plugins">Metadata Source Plugins</a> to configure searching more music info databases.</p>

<p>Run: <code class="language-plaintext highlighter-rouge">beet import --autotag --quiet --incremental-skip-later ~/music</code></p>

<p>This will take hours and print a lot of debugging info.
My ~17k tracks took a little over 5 hours, roughly 1 second per track.</p>

<p><code class="language-plaintext highlighter-rouge">--quiet</code> doesn’t quiet output, it skips the process of making manual decisions.
That’ll be the second run, explained next.</p>

<p>On the first run, it will incorrectly print <code class="language-plaintext highlighter-rouge">Resuming interrupted import of ~/music</code>.
This is harmless.</p>

<p>If there are errors, <code class="language-plaintext highlighter-rouge">rm ~/.config/beets/import.log and ~/.config/beets/library.db</code> to start over from scratch.
But if it crashes or you have to ctrl-c it to finish later, just re-run the same command to resume where you left off.</p>

<p>(Speaking of which, I ran into a <a href="https://github.com/beetbox/beets/issues/5218">crash bug</a> with <code class="language-plaintext highlighter-rouge">fromfilename</code>; I re-ran with <code class="language-plaintext highlighter-rouge">-v</code> and an added <code class="language-plaintext highlighter-rouge">print</code> to the plugin to try to get the filename, but it ran clean. Weird.)</p>

<h2 id="go-looking-for-problems">Go looking for problems</h2>

<p>Run <code class="language-plaintext highlighter-rouge">beet bad | grep "WARNING" | grep -v "No supported tags in the file"</code> at some point to find corrupt and truncated tracks.</p>

<p>The first <code class="language-plaintext highlighter-rouge">grep</code> strips out redundant “checker found N errors or warning” messages; the second strips out a warning for untagged files because that’s fine, we haven’t gotten to the tagging run yet.
Don’t enable the <code class="language-plaintext highlighter-rouge">badfiles</code> <code class="language-plaintext highlighter-rouge">check_on_import</code> config because it ignores <code class="language-plaintext highlighter-rouge">--quiet</code>.</p>

<p>Good luck, hopefully you don’t have too many damaged files.
Around 4% of my mp3s had minor issues, none unplayable.</p>

<p>There’s a tool called <a href="https://mp3val.sourceforge.net/">mp3val</a> that can repair mp3s.
Putting it together: <code class="language-plaintext highlighter-rouge">beet bad | grep "WARNING" | grep -v "No supported tags in the file" | cut -d'"' -f 2 | grep -i .mp3$ | xargs -d '\n'  mp3val -t -nb -f</code></p>

<p>I had a handful of tracks in other formats (flac, m4a, mp4, ogg) with minor errors, but I was able to quickly convert them: <code class="language-plaintext highlighter-rouge">find . -name "*.m4a" -exec bash -c 'for file; do output="${file%.mp4}.mp3"; ffmpeg -i "$file" -vn -c:a libmp3lame -q:a 2 "$output"; done' _ {} \;</code></p>

<h2 id="second-run-manual-tagging">Second run: manual tagging</h2>

<p>The primary value of beets is “auto-tagger”, which <em>mostly</em> automatically identifies and tags files.
When it can’t make a confident decision it asks you, so you use the “auto-tagger” to make manual tagging decisions.
The <a href="https://docs.beets.io/en/stable/guides/tagger.html#similarity">Using the Auto-Tagger</a> doc describes the process.</p>

<p>I split manual tagging out into a separate run because of its UI problems.
When the auto-tagger is waiting on a decision from you, it continues scanning files in the background.
On your first run there will be a lot of noisy messages (often from plugins), so your decision prompts will be randomly scrolling up and off the screen while you’re trying to read them.
There’s no way to pause the background work or repeat the prompt.</p>

<p>Extract a clean list of what was skipped:</p>

<p>Run: <code class="language-plaintext highlighter-rouge">grep "^skip " ~/.config/beets/import.log | sed 's/^skip //' | sort -u &gt; /tmp/skipped.txt</code></p>

<p>Then work through them individually with <code class="language-plaintext highlighter-rouge">beet import -I ~/music/whoever</code> or all with <code class="language-plaintext highlighter-rouge">cat /tmp/skipped.txt | xargs -d '\n' beet import -I</code>.
The <code class="language-plaintext highlighter-rouge">-I</code> ignores if you skipped a directory previously.
Inconveniently, beets says “skipped” to mean both “this is already imported, nothing to do” and “you skipped this and didn’t pass <code class="language-plaintext highlighter-rouge">-I</code>”.</p>

<p>Tagging is a slow process with very frustrating UI.
I’m not going to make myself mad writing out all the details because nobody would read it.
(This situation is why I’ve given away a dozen copies of <a href="https://bookshop.org/p/books/badass-making-users-awesome-kathy-sierra/b36b88b536c72c32">Badass by Kathy Sierra</a> and occasionally been driven to impart its wisdom via percussive osmosis.)</p>

<p>Between albums beets will stall for seconds or minutes as it waits on API rate limits.
Usually it will continue to print out-of-context and non-actionable debugging messages to the terminal.
While it’s a little nice to know beets hasn’t frozen, when you come back to the terminal you’ll may have no idea what prompt scrolled off the screen.
The only safe thing to type is <code class="language-plaintext highlighter-rouge">S</code> to <code class="language-plaintext highlighter-rouge">Skip</code> and then hope to find the album on a later run.</p>

<p>If you’re curious how far behind your editing beet has fallen, you can keep <code class="language-plaintext highlighter-rouge">sudo strace -p $(pgrep -f beet) -e trace=openat -f 2&gt;&amp;1 | grep -oP '"\K[^"]+\.mp3' | awk '!seen[$0]++'</code> running in another terminal.</p>

<p>Honestly, I couldn’t finish this run.
It’s painfully slow, it often misses metadata clues in the filename so I have to manually search and suffer the delay again.
And there’s no utility, no incremental progress unless I wanted to take off the safeties and start writing to my collection.
So I backed up the database and threw Claude at it, told it to leave existing tags on a couple albums I had tagged deliberately, to apply fixes that seemed real likely, and to make a list for human review of anything seemed genuinely weird.
It churned, I spent maybe an hour reviewing the esoterica, done.
The point of the project was to take the error rate down from 60%; if an LLM took it down to 1% that’s a lot better than an Sisyphean slog where a human fails to take it down to 0%.</p>

<h2 id="third-run-reviewing-and-applying-tags">Third run: reviewing and applying tags</h2>

<p>Now that the music metadata is correct in the beets database, you can use <code class="language-plaintext highlighter-rouge">beet list</code> and <code class="language-plaintext highlighter-rouge">beet stats</code> to <a href="https://docs.beets.io/en/stable/guides/main.html#seeing-your-music">query the your beets database</a>.</p>

<p>For example <code class="language-plaintext highlighter-rouge">beet list -f '"$path","$album","$year","$track","$artist","$title","$genre"' &gt; compare.csv</code> to generate spreadsheet of your key metadata.</p>

<p>The big value is writing this metadata into the mp3s themselves so that every music player can read them.
To see all potential changes, run: <code class="language-plaintext highlighter-rouge">beet write --pretend</code>, probably piping its infinite output into a csv or some grep pipeline to cut it down to meaningful decisions.</p>

<p>If everything looks good, run it again without <code class="language-plaintext highlighter-rouge">--pretend</code>.</p>

<h2 id="n-runs-expansion-and-maintenance">N runs: expansion and maintenance</h2>

<p>Run <code class="language-plaintext highlighter-rouge">beet import ~/music</code> when you add new music.</p>

<p>Run <code class="language-plaintext highlighter-rouge">beet remove -f</code> to tell beet to forget about files that no longer exist.</p>

<p>Run <code class="language-plaintext highlighter-rouge">beet duplicates</code> to find duplicated tracks.</p>

<p>Run <code class="language-plaintext highlighter-rouge">uv tool upgrade beets</code> to get new versions without losing hours to python packaging stack traces.</p>

<p>Use <a href="https://docs.beets.io/en/stable/plugins/mbsync.html">mbsync</a> to pull in updated metadata.
Run <code class="language-plaintext highlighter-rouge">beet mbsync -p</code> to preview changes; run again without <code class="language-plaintext highlighter-rouge">-p</code> when happy.</p>]]></content><author><name></name></author><category term="Life" /><category term="beets" /><category term="mp3s" /><category term="libraries" /><category term="uv" /><summary type="html"><![CDATA[Beets is a python CLI tool for fixing up metadata in music collections. I wrote up a guide from my own use on my ~17k track collection because writing instructions is a good way to learn. But also: it’s a power tool built by hobbyists over 15+ years, so the UI is clunky and often misleading. It doesn’t really respect the user’s time and attention, but it’s the least bad tool I could find.]]></summary></entry><entry><title type="html">Payoff Progress of an Amortizated Loan</title><link href="https://push.cx/payoff-progress" rel="alternate" type="text/html" title="Payoff Progress of an Amortizated Loan" /><published>2026-06-15T00:00:00-05:00</published><updated>2026-06-15T00:00:00-05:00</updated><id>https://push.cx/payoff-progress</id><content type="html" xml:base="https://push.cx/payoff-progress"><![CDATA[<p>15 years ago I took an education course for becoming a <a href="https://en.wikipedia.org/wiki/Certified_Financial_Planner">Certified Financial Planner</a> from Northwestern University.<label for="northwestern" class="margin-toggle"> ⊕</label><input type="checkbox" id="northwestern" class="margin-toggle" /><span class="marginnote">Which is bullshit, NU rents its prestige to an <a href="https://dalton-education.com/university/northwestern-university">independent training company</a>.</span>
This was before the <a href="https://www.youtube.com/watch?v=gvZSpET11ZY">popularization of fee-only financial planners</a> and I didn’t want to learn how to manage my finances from salespeople.
I also had no interest in <em>working</em> as a CFP, so I didn’t take the exam and have never held this accreditation,
but it does mean I often get tapped to explain things to friends who also don’t want to learn finance from salespeople.</p>

<p>A friend of mine is getting a mortgage for the first time.
American mortgages are “amortized”, which is a suspicious concept.</p>

<p>In short, you pay the same amount every month but your payments are split between “principal” that pays back what you borrowed and “interest” that is the bank’s revenue.
The split changes over time: your early payments are mostly interest, and over the life of the loan that split dwindles until your money goes entirely to principal.</p>

<p>This feels pretty weird, like maybe you’re getting ripped off at the beginning.
Your money isn’t effectively paying off the loan right from the start?
If you sell the house 10 years into a 30 year mortgage, shouldn’t you have a third of the equity?
Now, there are reasons that amortization benefits both you and the bank, but I’m not going down that enticing rabbithole because I want to talk about something even more thrilling: charts.</p>

<p>If you try to read <a href="https://www.fidelity.com/learning-center/personal-finance/what-is-amortization">an intro to amortization</a> you will see a lot of tedious tables<label for="schedules" class="margin-toggle"> ⊕</label><input type="checkbox" id="schedules" class="margin-toggle" /><span class="marginnote">Finance jargon calls a table a "schedule" if it has dates in the future.</span> and, in the nicer ones, a useless chart like this:</p>

<p><img src="/uploads/2026/06/dumb-fidelity-chart.png" alt="A stupid amortization chart with three curves: balance sweeps down, interest and principal arch up over the life of the loan." /></p>

<p>I appreciate an accurate but uninformative explanation as much as the next reader of man pages, but these charts are especially useless because they’re drawn from the bank’s perspective.
The loan is their product, so they’re thinking about it top-down, the total values over the loan’s lifetime.<label for="redundant" class="margin-toggle"> ⊕</label><input type="checkbox" id="redundant" class="margin-toggle" /><span class="marginnote">The chart is also redundant. The balance is the inverse of the principal. Your eye is drawn to where the curves intersect, but those points don’t indicate anything meaningful.</span>
You think bottom-up, from the monthly payment that will become a very important feature in your life.</p>

<p>I was thinking about how to explain amortization better and I remembered how much I love fillable progress trackers and <a href="https://en.wikipedia.org/wiki/Burndown_chart">burndown charts</a>.
I once had a job at company with a <a href="https://equitymatrix.io/blog/vesting-schedules-explained">typical four year equity grant</a>: one year cliff, then quarterly vesting.
About a year and a half in, I wanted a vivid reminder of each day’s value.
On a little whiteboard I wrote how many days left until the next vest, and empty boxes for the each vesting.</p>

<p><img src="/uploads/2026/06/equity-progress-tracker.jpg" alt="Small whiteboard with a big '46' and four rows of boxes, the first row is one box but the other rows are 4 boxes. The first two rows and 3 of the 4 boxes on the next row are colored in." /></p>

<p>Each morning I’d count down one more day, and once a quarter I filled in a box to mark one more grant.
It helped.</p>

<p>I rolled these two topics around in my head and came up with a visual presentation to combine them.
I sketched them out in Claude Code using Opus 4.8, and got this hybrid where the years run down the page and each box is sized to the principal you’re paying off that month.</p>

<figure class="fullwidth">
<img src="/uploads/2026/06/sample-tracker.png" alt="Sort of a stacked bar chart on its side, each year has 12 boxes. The cells in the first couple rows/years are shaded, as if filled in with a pencil." />
</figure>

<p>The void between the principal payments and the right edge of the chart is the part of your payment that goes to interest.
If you lay your head on your right shoulder you’ll see the boxes form the same shape as the “principal” line on the useless chart above.</p>

<p><a href="/uploads/2026/06/payoff-progress.html">It’s online if you’d like to play</a>.</p>

<p>The number down the right column is the remaining principal to pay off; $300k in this example. I considered a running total of equity in this column, but it felt misleading when the final property value will be different than the original sale price. Percentage could work.</p>

<p>Interactive visualizations of how dynamic systems respond to varying input (“video games”) can be pretty <a href="https://theoryoffun.com/">instructive</a>.
This one sufficed for for talking through the parts that my friend was struggling with.</p>

<p>Continuing to tinker in CC, I threw on on some display options.
Maybe the most useful is the ‘principal + interest’ mode that visualizes that your full payment with a reducing percent shaded to indicate the interest payments.
(Though I’m not happy with how that forms vertical shapes which falsely imply each column of months has its own change over the years.)</p>

<p>The weakest pedagogy of this visualization is that it doesn’t really beat you over the head with how small changes to the interest rate have very large changes to the overall cost of the loan (that is, the total interest payments).
You have to play with the interest rate a bit to get it.
Maybe it’s worth showing the sum of the interest payments, or allow entering two interest rates.</p>

<p>Similarly, there’s a missed opportunity to teach the value of making additional principal payments.
Entering one-off or recurring additional payments would move the right edge of the principal boxes to the right for that year <em>and every following year</em>.<label for="tsiolkovsky" class="margin-toggle"> ⊕</label><input type="checkbox" id="tsiolkovsky" class="margin-toggle" /><span class="marginnote">An inverse of the <a href="https://en.wikipedia.org/wiki/Tsiolkovsky_rocket_equation">rocket equation</a>, to give another vivid example.</span>
Maybe it would make visual sense to indent the left side of all future years to show the effect of prepayment.</p>

<p>If you’d like to tinker with these or other ideas, or use it in any way, knock yourself out.
Between the <a href="https://www.congress.gov/crs_external_products/LSB/PDF/LSB10922/LSB10922.8.pdf">CRS report</a> and
<a href="https://en.wikipedia.org/wiki/Artificial_intelligence_and_copyright">Thaler v. Perlmutter</a> it seems that vibecoded software is in the public domain.</p>]]></content><author><name></name></author><category term="Code" /><category term="vibecoding" /><category term="finance" /><summary type="html"><![CDATA[15 years ago I took an education course for becoming a Certified Financial Planner from Northwestern University. ⊕Which is bullshit, NU rents its prestige to an independent training company. This was before the popularization of fee-only financial planners and I didn’t want to learn how to manage my finances from salespeople. I also had no interest in working as a CFP, so I didn’t take the exam and have never held this accreditation, but it does mean I often get tapped to explain things to friends who also don’t want to learn finance from salespeople.]]></summary></entry><entry><title type="html">Rovyvon A5R flashlight diagram</title><link href="https://push.cx/rovyvon-a5r-flashlight-diagram" rel="alternate" type="text/html" title="Rovyvon A5R flashlight diagram" /><published>2025-11-18T00:00:00-06:00</published><updated>2025-11-18T00:00:00-06:00</updated><id>https://push.cx/rovyvon-a5r-flashlight-diagram</id><content type="html" xml:base="https://push.cx/rovyvon-a5r-flashlight-diagram"><![CDATA[<p>I bought a <a href="https://www.rovyvon.com/products/aurora-a5-usb-c-gitd-keychain-flashlight-4th-generation">Rovyvon A5R flashlight</a>.
It’s a great little flashlight that charges by USB C and has many useful modes.
While I’m generally quite happy <a href="https://vimdoc.sourceforge.net/htmldoc/intro.html#vim-modes-intro">with modes</a>, I spend a lot less time using a flashlight than editing text and was a little confused about how to switch to the one I wanted.
The <a href="/uploads/2025/11/RovyVon%20Aurura%20A5R%20flashlight%20manual.pdf">manual for the flashlight</a> is accurate but not clearly written.</p>

<p>So I used <a href="https://graphviz.org/">graphviz</a> to knock out a state diagram showing the number of clicks to switch between modes:</p>

<p><img src="/uploads/2025/11/rovyvon-a5r-flashlight.png" alt="state diagram" /></p>

<p>This is missing that the ‘Regular’ mode will start in the mode that it last spent 3 minutes in.
I couldn’t think of a way to represent that graphically that would be clear at a glance, so if I had to use words to explain the graphic I might as well just have written out that previous sentence.</p>

<p>While this diagram may be useful in the unlikely case you buy the exact same flashlight, my own familiarity came more from the <a href="https://www.scotthyoung.com/blog/ultralearning/">effortful study</a> of creating the diagram than looking at the diagram.
So I’m really sharing this as an example of a useful study technique.</p>

<p>Here’s the source, if you’d like to use it as a starting point for your own diagrams:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>digraph FlashlightStateMachine {
    rankdir=TB;
    node [shape=rectangle, style=rounded];

    // Main states
    locked [label="Locked Off\n(blinks twice)" style="rounded,dashed"];
    unlocked [label="Unlocked Off\n(blinks three times)" style="rounded,dashed"];
    momentary [label="Momentary On", style="rounded,bold"];
    regular [label="Regular On", style="rounded,bold"];
    sidelight [label="Sidelight On", style="rounded,bold"];

    // Bidirectional edge between locked and unlocked
    locked -&gt; unlocked [label=" 5", dir="both"];

    // Momentary transitions
    unlocked -&gt; momentary [xlabel="hold", dir=both];

    // Regular on transitions
    unlocked -&gt; regular [label="2"];
    regular -&gt; unlocked [label=" long"];

    // Sidelight transitions
    unlocked -&gt; sidelight [label="3"];
    regular -&gt; sidelight [label="3"];
    sidelight -&gt; unlocked [label=" long"];

    // Subgraph for Regular On brightness levels
    subgraph cluster_regular {
        label="Regular Modes";
        style=dashed;

        low [label="Low"];
        med [label="Medium"];
        high [label="High"];
        moon [label="Moon"];

        low -&gt; med;
        med -&gt; high;
        high -&gt; moon;
        moon -&gt; low;
    }

    // Subgraph for Sidelight modes
    subgraph cluster_sidelight {
        label="Sidelight Modes";
        style=dashed;

        white_low [label="White Low"];
        white_high [label="White High"];
        red [label="Red"];
        red_flash [label="Red Flash"];

        white_low -&gt; white_high;
        white_high -&gt; red;
        red -&gt; red_flash;
        red_flash -&gt; white_low;
    }

    // Connect main states to substates
    regular -&gt; low [style=dotted, arrowhead=none];
    sidelight -&gt; white_low [style=dotted, arrowhead=none];

    // Layout
    {rank = same; locked; unlocked;}
    {rank = same; momentary; regular; sidelight;}
}
</code></pre></div></div>]]></content><author><name></name></author><category term="Code" /><category term="documentation" /><category term="graph" /><category term="graphviz" /><category term="study" /><summary type="html"><![CDATA[I bought a Rovyvon A5R flashlight. It’s a great little flashlight that charges by USB C and has many useful modes. While I’m generally quite happy with modes, I spend a lot less time using a flashlight than editing text and was a little confused about how to switch to the one I wanted. The manual for the flashlight is accurate but not clearly written.]]></summary></entry><entry><title type="html">TypeID in Lua</title><link href="https://push.cx/typeid-in-lua" rel="alternate" type="text/html" title="TypeID in Lua" /><published>2025-05-21T00:00:00-05:00</published><updated>2025-05-21T00:00:00-05:00</updated><id>https://push.cx/typeid-in-lua</id><content type="html" xml:base="https://push.cx/typeid-in-lua"><![CDATA[<p>I’ve published a Lua implementation of TypeId:</p>

<p><a href="https://github.com/pushcx/typeid-lua">https://github.com/pushcx/typeid-lua</a></p>

<p>TypeID is a nice standard for creating unique id tokens with a Stripe-like<label for="stripe" class="margin-toggle"> ⊕</label><input type="checkbox" id="stripe" class="margin-toggle" /><span class="marginnote">An aside to Stripe from an Xtripe: Please write a <a href="https://stripe.com/blog/engineering">eng blog</a> post about the features and history of tokens. It would be pillar content that would be enormously popular for meaningfully advancing the state of the art to a new standard (Not the post about <a href="https://dev.to/stripe/designing-apis-for-humans-object-ids-3o5a">using them</a>.)</span>
<a href="https://en.m.wikipedia.org/wiki/Hungarian_notation">hungarian notation</a>:</p>

<blockquote>
  <p>TypeIDs are a modern, type-safe extension of UUIDv7. Inspired by a similar use of prefixes in Stripe’s APIs.</p>
</blockquote>

<blockquote>
  <p>TypeIDs are canonically encoded as lowercase strings consisting of three parts:</p>
</blockquote>

<blockquote>
  <ol>
    <li>A type prefix (at most 63 characters in all lowercase snake_case ASCII [a-z_]).</li>
    <li>An underscore ‘_’ separator</li>
    <li>A 128-bit UUIDv7 encoded as a 26-character string using a modified base32 encoding.</li>
  </ol>
</blockquote>

<blockquote>
  <p>Here’s an example of a TypeID of type <code class="language-plaintext highlighter-rouge">user</code>:</p>
</blockquote>

<blockquote>
  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>user_2x4y6z8a0b1c2d3e4f5g6h7j8k
└──┘ └────────────────────────┘
type    uuid suffix (base32)
</code></pre></div>  </div>
</blockquote>

<blockquote>
  <p>A <a href="https://github.com/jetify-com/typeid/tree/main/spec">formal specification</a> defines the encoding in more detail.</p>
</blockquote>

<p>Cleverly, the spec comes with a suite of labeled <a href="https://github.com/jetify-com/typeid/tree/main/spec">test
cases</a> of valid and invalid
examples.
I wish more specs did this!</p>

<p>I’m happy with the functionality my library offers, and there was the <a href="https://www.youtube.com/watch?v=ducG55pfCMQ">familiar delight of making things</a> the first time I round-tripped a TypeID.</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">TypeID</span> <span class="o">=</span> <span class="nb">require</span><span class="p">(</span><span class="s2">"typeid"</span><span class="p">)</span>
<span class="c1">-- or in dev: TypeID = require("./typeid")</span>

<span class="n">t</span> <span class="o">=</span> <span class="n">TypeID</span><span class="p">.</span><span class="n">generate</span><span class="p">(</span><span class="s2">"comment"</span><span class="p">)</span>
<span class="c1">-- t = {</span>
<span class="c1">--   prefix = "comment",</span>
<span class="c1">--   suffix = "01jvbhbbdje07rnyqkvstpvcge"</span>
<span class="c1">-- }</span>

<span class="c1">-- TypeID tables implement __tostring</span>
<span class="nb">print</span><span class="p">(</span><span class="n">t</span><span class="p">)</span> <span class="c1">-- "comment_01jvbhbbdje07rnyqkvstpvcge"</span>

<span class="c1">-- You can extract a standard UUID string</span>
<span class="n">t</span><span class="p">:</span><span class="n">uuid</span><span class="p">()</span> <span class="c1">-- "0196d715-adb2-700f-8afa-f3de756db20e"</span>

<span class="c1">-- and round trip that back into a TypeID</span>
<span class="n">TypeID</span><span class="p">.</span><span class="n">from_uuid_string</span><span class="p">(</span><span class="s2">"comment"</span><span class="p">,</span> <span class="s2">"0196d715-adb2-700f-8afa-f3de756db20e"</span><span class="p">)</span>

<span class="c1">-- parse and validate a TypeID from a string</span>
<span class="n">TypeID</span><span class="p">.</span><span class="n">parse</span><span class="p">(</span><span class="s2">"comment_01jvbhbbdje07rnyqkvstpvcge"</span><span class="p">)</span>

<span class="c1">-- finally, you can generate with a unix timestamp in ms:</span>
<span class="n">TypeID</span><span class="p">.</span><span class="n">generate</span><span class="p">(</span><span class="s2">"comment"</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="c1">-- "comment_0000000001e8avt0nh7a68v2jc"</span>
</code></pre></div></div>

<p>This was a fun practice project for me.
I’ve used Lua more and more over the last few years in video game scripts and <a href="https://awesomewm.org/">my window manager</a>,
and while 1-based array indexes will always feel odd, I think there’s a lot of potential in the language.</p>

<p>I experimented with style while implementing, and a lot of what I’m taking away from it is idioms I’m ignorant of.
The <code class="language-plaintext highlighter-rouge">TypeID</code> is more OO style and returns an object with a method; the <code class="language-plaintext highlighter-rouge">Base32</code> and <code class="language-plaintext highlighter-rouge">UUID7</code> modules work on primitives.
After implementing, I guess users would probably prefer getting a primitive back, as there doesn’t seem to be an idiomatic way to type-check.
A module can export a trusted constructor, but without types there’s no way to
use that to prevent instantiating invalid objects; everything is a table anyways.
Coming from Ruby and ActiveRecord it’s frustrating to have most of a solution to the pervasive problem of passing around invalid objects but not be able to complete it.</p>

<p>I guess have to read popular libraries to get a feel for style.
I don’t really know what level to aim at between “data-hiding high-level interface” and “yolo, all primitives and seams showing for perf”.
Maybe it’s different inside and outside of games.</p>

<p>Along those lines, I ported <code class="language-plaintext highlighter-rouge">Base32</code> from the official TypeID Golang implementation and then wrote <code class="language-plaintext highlighter-rouge">UUID7</code> in bytes to match it.
But all that intermediate bit twiddling could be simplified by generating a UUID7 directly into the Base32 encoding if I wanted to spend a lot more time on this.</p>

<p>Maybe I’m looking under the wrong name, but it seems odd there isn’t a bitfield type I could use, given Lua’s popularity in games.
Some searching turned up <a href="https://github.com/JohnHind/Lua_Bitfield">a library</a> but the absence of multi-bit operations seems inconvenient.
Which points to:</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- typeid.lua</span>
<span class="n">uuid</span> <span class="o">=</span> <span class="k">function</span><span class="p">(</span><span class="n">self</span><span class="p">)</span>
  <span class="k">return</span> <span class="n">UUID7</span><span class="p">.</span><span class="n">to_string</span><span class="p">(</span><span class="n">Base32</span><span class="p">.</span><span class="n">decode</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">suffix</span><span class="p">))</span>
<span class="k">end</span>
</code></pre></div></div>

<p>There’s a code smell in <code class="language-plaintext highlighter-rouge">TypeID</code>: the nested conversions in the metatable <code class="language-plaintext highlighter-rouge">uuid</code> function suggest the internal representation of <code class="language-plaintext highlighter-rouge">suffix</code> is wrong.
The syntactic distinction between <code class="language-plaintext highlighter-rouge">.field</code> and <code class="language-plaintext highlighter-rouge">:method()</code> means duplicating the data into two fields, exposing the internal representation by it being a field and the other a method, or getting away from what seems like common struct-y style and making both into methods.
I’ve really grown to like the way Ruby’s optional parenthesis blur the line on fields and methods.</p>

<p>I wrote this library because I’d like to add a TypeID request identifier as a trace ID that nginx would generate and log, and pass along through Rails logs to MariaDB logs.
It’s overkill for <a href="https://github.com/lobsters/lobsters">Lobsters</a> but once a year I really wanted the ability to correlate logs like that.
While the ROI may not really justify the time, it was a uniquely well-scoped small practice project.</p>

<p>Writing in Lua and adding the Lua module support to nginx seemed an easier path than writing in C and adding that compilation step to the deploy pipeline.
Ultimately though, I’m not going to write that wrapper module.</p>

<p>On a parallel track, our <a href="https://github.com/lobsters/lobsters-ansible">ansible setup</a> has slowly been succumbing to bit rot and my inexpert maintenance.
I learned that <a href="https://hatchbox.io">Hatchbox</a> could fill the same role and paying a couple bucks a month means it’s maintained by an expert professional.
So <a href="https://lobste.rs/~355e3b">355e3b</a> are going to move our hosting over soon, and it uses <a href="https://en.m.wikipedia.org/wiki/Caddy_\(web_server\)">Caddy</a> instead of nginx, so I guess I’ll wrap the official TypeID <a href="https://github.com/jetify-com/typeid-go">Golang implementation</a> in a Caddy module instead.
Still, it’s rewarding to <a href="https://github.com/jetify-com/opensource/pull/494">contribute to TypeID’s list of supported languages</a>.</p>]]></content><author><name></name></author><category term="Code" /><category term="Lua" /><category term="TypeID" /><category term="nginx" /><category term="Caddy" /><category term="Hatchbox" /><category term="Lobsters" /><category term="Ansible" /><summary type="html"><![CDATA[I’ve published a Lua implementation of TypeId:]]></summary></entry><entry><title type="html">Broken Poker</title><link href="https://push.cx/broken-poker" rel="alternate" type="text/html" title="Broken Poker" /><published>2025-03-28T00:00:00-05:00</published><updated>2025-03-28T00:00:00-05:00</updated><id>https://push.cx/broken-poker</id><content type="html" xml:base="https://push.cx/broken-poker"><![CDATA[<p>Some friends and I have played a friendly, low-stakes Texas Hold ‘Em poker game for years.
One player wanted to play a night of silly, rule-breaking poker for his birthday.</p>

<p>I put together 15 variants, with inspiration and copying from
<a href="https://forumserver.twoplustwo.com/24/home-poker/special-add-house-rules-spice-up-game-1758503/">here</a>,
<a href="https://wizardofvegas.com/forum/gambling/poker/35137-silly-poker-based-games/">here</a>,
and <a href="https://old.reddit.com/r/poker/comments/16iqrbu/what_is_a_weird_poker_variant_you_made_up_at_your/">especially here</a>.
These aren’t intended to be long-term playable games with polished rules, just wacky variations for a silly night.</p>

<p>I wrote these out on a stack of index cards.
We played each variant for one round of dealing, so 6-7 hands of each.
I only revealed one variant before we played:</p>

<p><img src="/uploads/2025/03/regret.jpg" alt="regret rule card, text is listed below" title="regret rule card, text is listed below" /></p>

<p>I put the variants in order to escalate complexity, to set up the Fuck Yeah/Fuck No joke, to provide a breather after the complexity of Auction House, and to end with Double Trouble because it was the idea that kicked all this off.</p>

<ol>
  <li><strong>Bonus</strong> Start with 1 extra hole card, face up.</li>
  <li><strong>Roswell</strong> Start with 2 hole cards down and 1 up. After river betting: draw an extra hole card, do an extra betting round.</li>
  <li><strong>Trashy</strong> Start with 3 hole cards. After each betting round: players select 1 hole card and simultaneously pass it to the player on their left.</li>
  <li><strong>Stairstep</strong> Five betting rounds, each round you get 1 hole card and 1 community card.</li>
  <li><strong>Fuck Yeah</strong> After each betting round: draw 1 hole card, then discard 1.</li>
  <li><strong>Fuck No</strong> After each betting round: discard 1 hole card, then draw 1.</li>
  <li><strong>My Ship Will Come In</strong> After the turn betting: reveal 2 cards. Those ranks are wild.</li>
  <li><strong>Regret</strong> Start with 5 hole cards. After the flop, turn, and river betting: discard 1 hole card face up near you.</li>
  <li><strong>Dead Man’s Chest</strong> All folded cards go to the chest, down. If the hand ends early, nothing special happens. After river betting, reveal the chest. If the dead hand wins, the pot rolls over.</li>
  <li><strong>Swapsies</strong> At start, turn up a 5 card sideboard. After each betting round, each player may swap 1 hole card with a sideboard card. Keep it face up.</li>
  <li><strong>Auction House</strong> Deal 3 boards. Before each betting round is bidding. Going around once, each active player may bid chips to any 1 board. The board with the most chips is the real one. All bid chips go to the pot.</li>
  <li><strong>Death on the Nile</strong> After river betting: roll a die. Count from the first turn card to discard and replace 1 board card. (Ignore 6, nothing changes.)</li>
  <li><strong>Haggle</strong> Any 2 players, even folded, may agree to a <em>table stakes</em> price or for free, swap 1 hole card, seen or unseen.</li>
  <li><strong>River of Blood</strong> If the final community card is black, bet and end normally. If it’s red, bet and then <em>repeat</em> until you get a black card.</li>
  <li><strong>Double Trouble</strong> Shuffle together 2 full decks, include the 4 jokers as wilds. A pair/trip/quad with a suited pair outranks one without.</li>
</ol>

<p>Except where noted otherwise, it’s Texas Hold ‘Em rules, hole cards are face down, and actions are limited to the players who haven’t folded.</p>]]></content><author><name></name></author><category term="Games" /><category term="poker" /><category term="humor" /><summary type="html"><![CDATA[Some friends and I have played a friendly, low-stakes Texas Hold ‘Em poker game for years. One player wanted to play a night of silly, rule-breaking poker for his birthday.]]></summary></entry><entry><title type="html">TV Setup</title><link href="https://push.cx/tv-setup" rel="alternate" type="text/html" title="TV Setup" /><published>2025-01-28T00:00:00-06:00</published><updated>2025-01-28T00:00:00-06:00</updated><id>https://push.cx/tv-setup</id><content type="html" xml:base="https://push.cx/tv-setup"><![CDATA[<p>I got a TV for the first time since 2001 and a couple friends asked me to explain the setup.</p>

<p>Mostly I got it because the only movie theater near me is badly managed, but a new apartment’s layout means that if I’m playing games at my desk I can’t talk to my spouse sitting in the front room.</p>

<h2 id="privacy-goal">Privacy goal</h2>

<p>I care about personal privacy.
Basically every TV manufacturer <a href="https://arxiv.org/abs/2409.06203">IDs what you’re watching and sells that to advertisers</a>, who can correlate it to you personally because the TV will have the same IP as your web browsing.
Creepy shit.</p>

<p>People occasionally suggest buying “commercial panels” but the ones I found were more expensive, lower-quality, and had weird limitations.
The solution is to never connect a TV to the internet.<label for="open-wifi" class="margin-toggle"> ⊕</label><input type="checkbox" id="open-wifi" class="margin-toggle" /><span class="marginnote">There’s a persistent conspiracy theory that TVs connect to open wifi. This would be very easy to test but I couldn’t find anyone who claimed to have observed it. Luckily TV surveillance is so inefficient about bandwidth that manufacturers can’t afford to put cellular modems in <a href="https://foundation.mozilla.org/en/privacynotincluded/articles/its-official-cars-are-the-worst-product-category-we-have-ever-reviewed-for-privacy/">like car manufacturers</a>.</span></p>

<p>So what are the options for an external device?</p>

<ul>
  <li>All the popular open source media servers have clunky UI or were unreliable; mostly both.<label for="clunky" class="margin-toggle"> ⊕</label><input type="checkbox" id="clunky" class="margin-toggle" /><span class="marginnote">This <a href="https://news.ycombinator.com/item?id=43063167">HN thread on Jellyfin</a> hits all of the issues I had with the open source options and also hits all of the “you’re doing it wrong” nitpicking I was not interested in participating in.</span></li>
  <li><a href="https://foundation.mozilla.org/en/privacynotincluded/amazon-fire-tv-family/">Amazon Fire</a> and <a href="https://foundation.mozilla.org/en/privacynotincluded/roku-streaming-sticks/">Roku</a> are invasive <em>and</em> janky.</li>
  <li>The <a href="https://foundation.mozilla.org/en/privacynotincluded/google-chromecast-with-google-tv/">Google Chromecast</a> was made by Google, and so will be the <a href="https://en.m.wikipedia.org/wiki/Chromecast#Discontinuation_and_successor">Google TV Streamer</a>.</li>
  <li>The <a href="https://foundation.mozilla.org/en/privacynotincluded/nvidia-shield-tv/">NVIDIA Shield</a> is not bad.</li>
  <li>The <a href="https://foundation.mozilla.org/en/privacynotincluded/apple-tv-4k/">Apple TV 4k</a> is good and has a good ecosystem.</li>
</ul>

<p>OK!</p>

<h2 id="choosing-a-tv-model">Choosing a TV model</h2>

<p>My last TV was a 15” CRT with a failing vertical hold that took a couple minutes to warm up before the picture would stop rolling.
A 4k OLED is a <em>stunning upgrade</em>.
I’ve been rewatching all my favorite movies because they look so good.</p>

<p>I don’t have an opinion on manufacturer besides never buying Samsung because of low reliability and poor UX.</p>

<p>Use <a href="https://www.rtings.com/tv">Rtings</a> to find a good current model.
Use their <a href="https://www.rtings.com/tv/reviews/by-size/size-to-distance-relationship">size to distance calculator</a> to pick a size for your room.</p>

<h2 id="apple-tv-setup">Apple TV setup</h2>

<p>Mount the <a href="https://www.apple.com/apple-tv-4k/">Apple TV 4k</a> to the back of the TV with velcro tape.
Ethernet is almost mandatory; more on this below.</p>

<p>Conveniently, you can use the terrible remote that comes with your TV to turn off motion smoothing and then put it in a drawer to never use.
The Apple TV remote is nice, though once I had to <a href="https://support.apple.com/en-mide/108769">reset</a> it when it forgot how to control the TV volume.
There’s a <a href="https://support.apple.com/en-us/108778">virtual remote</a> that is nice for typing searches but otherwise is a peculiar demonstration that directly translating a physical device to a touchscreen feels clunky.</p>

<h3 id="settings">Settings:</h3>

<ul>
  <li>General
    <ul>
      <li>Restrictions: This is the one missing stair on Apple TV. It has the parental controls you’d expect <em>except</em> you can’t lock the Settings app, so curious kids can change every setting that isn’t about watching a TV show that might have a curse word in it. Whyyyyyyyyy.</li>
      <li>Siri: off. It doesn’t run on-device.</li>
    </ul>
  </li>
  <li>Users and Accounts -&gt; Default User
    <ul>
      <li>Shared with You: off, distracting.</li>
      <li>Require password for purchases and free downloads. You can auth with an iOS device, which is clever.</li>
    </ul>
  </li>
  <li>Video and Audio
    <ul>
      <li>Format: 4k Dolby Vision</li>
      <li>Match Content: Range &amp; Frame Rate</li>
      <li>Check HDMI Connection: run after setting the previous</li>
      <li>Audio: has several accessibility settings, no idea why they’re here instead</li>
    </ul>
  </li>
  <li>Notifications -&gt; Search, TV: off</li>
  <li>AirPlay and HomeKit: weird that nobody’s gone after them for antitrust about it, but there’s the usual Apple practice of having a wonderful experience for their hardware like AirPods and meh for competitors.
    <ul>
      <li>Conference Room Display: on, so guests can show things from their phones</li>
      <li>Require a PIN for AirPlay: on, unless you live somewhere rural</li>
    </ul>
  </li>
  <li>Remotes and Devices -&gt; Bluetooth: Apple doesn’t make a video game controller so they’re clunky, but controllers that claim to work on Apple TV/iPad/iPhone work fine, with only the usual Bluetooth pain switching between them between multiple devices.</li>
  <li>Accessibility: There is so much good stuff in here, it’s an incredible amount of thoughtful design.
    <ul>
      <li>Physical and Motor -&gt; Tap to Navigate: off. Guests and kids struggle with the too-clever touchpad on the remote.</li>
    </ul>
  </li>
</ul>

<h3 id="home-screen">Home Screen:</h3>

<p>Apple would really like you to subscribe to their things, so there’s a bunch of shovelware you can’t delete like Music, TV, Arcade, etc.
You can at least make a folder named “Shovelware” and move it to be the last thing on the home screen.
You can’t set Restrictions to keep curious kids out of these, either.</p>

<h3 id="apps">Apps</h3>

<p><a href="https://tailscale.com/">Tailscale</a>, on the off chance you don’t already use it for your home network.
You don’t have to expose your NAS to the internet.
The TV app can act as an exit node, which is convenient for dealing with bank “security” that panics if you log in from a coffeeshop.
Beyond TV setup, Tailscale continually impresses me with its features, reliability, and polish.
It has solved a ton of hassles that have come with having multiple devices and working remotely.</p>

<p><a href="https://apps.apple.com/us/app/infuse-video-player/id1136220934">Infuse</a> for playing any kind of media files from <a href="https://firecore.com/infuse">most any storage</a>.
Excellent UI with automatic metadata download,<label for="infuse-subtitles" class="margin-toggle"> ⊕</label><input type="checkbox" id="infuse-subtitles" class="margin-toggle" /><span class="marginnote">Subtitles are only OK. Infuse pulls from <a href="https://www.opensubtitles.org/en/search/subs">OpenSubtitles</a> but could do a much better job of sorting them based on frame rate, resolution, and keywords in the video filename. The UI for styling subtitles lacks a live preview, and the process of adjusting a delay is downright bad.</span> overall much better experience than the various streaming services.
Calmer, too, it’s trivial to turn off “Watch Next” suggestions and autoplay of the next episode.</p>

<p>We made a category for “Watch Together” and then a category each for a personal queue.</p>

<p>If Apple didn’t limit apps ability to cache so strictly, Infuse could start playback in 0.1s instead of 2s.
But I have to assume most apps would be terrible at filling and pruning their caches, so I guess it’s fair.</p>

<p><a href="https://apps.apple.com/in/app/speedtest/id1564125757">SpeedTest</a> for diagnosing network issues.</p>

<p><a href="https://apps.apple.com/us/app/steam-link/id1246969117">Steam Link</a> for playing games from your desktop.<label for="UFO-50" class="margin-toggle"> ⊕</label><input type="checkbox" id="UFO-50" class="margin-toggle" /><span class="marginnote"><a href="https://50games.fun/">UFO 50</a> is an incredible love letter to the weirdness of the NES/Genesis era with a few modern genres reimagined. If you played back then, don’t read or watch anything about it, just go get the buddy whose basement couch you used to play on and fire this up.</span>
It’s really a general purpose remote desktop tool, you can “Add a Non-Steam Game to My Library” to run anything.
I’ve used it to run <a href="https://calibre-ebook.com/">calibre</a> and <a href="https://push.cx/installing-you-need-a-budget-ynab-on-arch-linux">YNAB</a> from my iPad.
One bug/limitation: many games get letterboxed if the aspect ratio of your monitor doesn’t match the TV. I wrote a small <a href="/uploads/2025/01/disp">shell script</a> and added it three times for switching ratios to match the device I’m using.</p>

<p><a href="https://8bitdo.com/">8BitDo</a> makes the best controllers.</p>

<p>While the latency is low enough for action games, it won’t be low enough for competitive shooters, fighting games, or precision platformers.
The Apple TV doesn’t have ports for USB and the max cable length is only 2 meters anyways.
If you don’t run ethernet to the Apple TV, you’ll get an annoyingly disruptive lag spike when your neighbor turns on their crappy microwave.</p>

<p>I’m surprised to say it, but it’s much more reliable to play games from a Linux desktop than Windows.
Windows is swarming with intrusive upsell ads and will randomly reboot, so I found myself having to walk over to the desktop fairly often to unbreak it.
Valve has put a ton of work into the <a href="https://www.steamdeck.com/en/verified">Steam Deck Compatibility program</a> and <a href="https://en.m.wikipedia.org/wiki/Proton_\(software\)">Proton</a> so almost every game I play <a href="https://www.protondb.com/">just works</a>.
It’s worth noting I get motion sick from 3d first-person and over-the-shoulder games and those are the AAA blockbusters, so I wouldn’t know if those run worse.<label for="hdr" class="margin-toggle"> ⊕</label><input type="checkbox" id="hdr" class="margin-toggle" /><span class="marginnote">A <a href="https://lobste.rs/s/sn8buz/tv_setup#c_3nznqc">comment on Lobsters</a> reports Linux is bad at HDR. I’ve never played an HDR game and this falls directly in this blind spot.</span></p>

<p><a href="https://apps.apple.com/us/app/steam-link/id1246969117">RetroArch</a> has an Apple TV app, but tvOS is still trapped in the old iPhone misdesign that users can’t be trusted with files.
Loading games or retrieving savegames is a chore.
It’s acceptable if you <em>only</em> play on the TV but if you want to carry a game between platforms you’ll have to play via Steam Link.<label for="iOS" class="margin-toggle"> ⊕</label><input type="checkbox" id="iOS" class="margin-toggle" /><span class="marginnote">At least on iPhone/iPad you can use the <a href="https://mobiussync.com/">Möbius Sync</a> app for <a href="https://syncthing.net/">Syncthing</a> to share your RetroArch ROMs and saves. Use an Ignore Pattern for <code class="language-plaintext highlighter-rouge">retroarch.cfg</code> to keep configs separate.</span></p>

<p>There’s some good settings <a href="https://retrohandhelds.gg/retroarch-setup-guide-for-ios-devices/">in this article</a> once you scroll past the affiliate ads.</p>

<h2 id="syncthing">Syncthing</h2>

<p>One small caveat with Tailscale and Syncthing: many packages bind its web interface to the local network interface so it won’t be visible across your intranet.
This is annoying for headless devices.
Find Syncthing’s <code class="language-plaintext highlighter-rouge">config.xml</code> and change the <code class="language-plaintext highlighter-rouge">&lt;address&gt;</code> to <code class="language-plaintext highlighter-rouge">0.0.0.0</code> or, better, the machine’s Tailscale IP (<code class="language-plaintext highlighter-rouge">100.x.x.x</code>).</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;gui enabled="true" tls="true" debugging="false" sendBasicAuthPrompt="false"&gt;
  &lt;address&gt;0.0.0.0:8384&lt;/address&gt;
  &lt;user&gt;...&lt;/user&gt;
  &lt;password&gt;...&lt;/password&gt;
  &lt;apikey&gt;...&lt;/apikey&gt;
  &lt;theme&gt;default&lt;/theme&gt;
&lt;/gui&gt;
</code></pre></div></div>

<h2 id="controllers-are-a-cake-with-too-many-layers">Controllers are a cake with too many layers</h2>

<p>This is a button on my controller:</p>

<p><img src="/uploads/2025/01/controller-1.jpg" alt="photo" /></p>

<p>Then I can rebind it in the firmware:</p>

<p><img src="/uploads/2025/01/controller-2.jpg" alt="screenshot" /></p>

<p>Then I can rebind it in the Apple TV Settings:</p>

<p><img src="/uploads/2025/01/controller-3.jpg" alt="screenshot" /></p>

<p>Then I can rebind it in Steam Link:</p>

<p><img src="/uploads/2025/01/controller-4.jpg" alt="screenshot" /></p>

<p>Then I can rebind it in Linux:</p>

<p><img src="/uploads/2025/01/controller-5.png" alt="screenshot" /></p>

<p>Then I can rebind it in Steam:</p>

<p><img src="/uploads/2025/01/controller-6.png" alt="screenshot" /></p>

<p>Then I can rebind it in a game (here, RetroArch):</p>

<p><img src="/uploads/2025/01/controller-7.png" alt="screenshot" /></p>

<p>Figuring out which layer misconfigs a button is terrible, and it’s just sloppy that none of them know what the controller <a href="https://shop.8bitdo.com/products/8bitdo-pro-2-bluetooth-controller?variant=42511821177009">looks like</a> or how its buttons are labeled.</p>

<p>Valve, you are the only company with market position to drag manufacturers, OS developers, and game developers into the same room to sort this out.
I know several of them take Not Invented Here to the extreme, but it would be great if you would abuse your monopoly power for my convenience by dragging them kicking and screaming into pleasant interoperability.<label for="steam-controller" class="margin-toggle"> ⊕</label><input type="checkbox" id="steam-controller" class="margin-toggle" /><span class="marginnote">And while you’re at it, bring back the <a href="https://en.m.wikipedia.org/wiki/Steam_Controller">Steam Controller</a>.<br /><br /> If you still have one in a drawer, dear reader, here’s <a href="https://steamcommunity.com/app/353370/discussions/1/6516193260178656983/?ctp=7#c4041481833164119785">my notes on flashing the latest firmware</a>.</span>
Please and thank you.</p>]]></content><author><name></name></author><category term="Life" /><category term="TV" /><category term="privacy" /><category term="video games" /><category term="home networking" /><category term="Tailscale" /><summary type="html"><![CDATA[I got a TV for the first time since 2001 and a couple friends asked me to explain the setup.]]></summary></entry><entry><title type="html">Google Ad Injection</title><link href="https://push.cx/google-ad-injection" rel="alternate" type="text/html" title="Google Ad Injection" /><published>2024-11-24T20:01:00-06:00</published><updated>2024-11-24T20:01:00-06:00</updated><id>https://push.cx/google-ad-injection</id><content type="html" xml:base="https://push.cx/google-ad-injection"><![CDATA[<p><em>This post is getting updates - I’m trying to collect ad samples and investigate with a site owner so I can give away js that site owners can use to detect and block the ads. Please help!</em></p>

<p>On November 19, Google <a href="https://support.google.com/websearch/thread/308719098/page-annotation-in-google-app-browser-for-ios?hl=en">announced</a> the Google App for iOS is injecting unlabeled ads into pages.
They look like the author created them, as seen in Google’s own screenshot:</p>

<p><img src="/uploads/2024/11/google-screenshot.png" alt="Google's own screenshot of their ad injected onto the description of a historic monument. It looks a link on the words &quot;Osaka Castle&quot;, with no warning that google placed it and no disclosure of being an ad." /></p>

<p>Google claims they are injecting ads live now, so I’d like to quickly turnaround a js snippet that sites can use to detect the tampering.
I’m strongly reminded of how a motivation for the big lift to HTTPS was slimy ISPs injecting ads into pages.</p>

<p>This system is nearly identical to Microsoft Smart Tags, a 2001 Internet Explorer feature that <a href="https://www.theregister.com/2001/06/13/have_you_been_smart_tagged/">injected links to Microsoft sites into pages</a>, but without the ability for site owners to disable them with a <code class="language-plaintext highlighter-rouge">meta</code> tag.
(Recognized by <a href="https://news.ycombinator.com/item?id=42242129">esprehn</a>.)</p>

<p>It’s also similar to <a href="https://www.theregister.com/2005/03/03/google_autolink/">Google AutoLink</a>, a 2005 Google Toolbar feature a user could click to inject links into pages.</p>

<p>Google has published no information on how it targets the ads.
It may target sites based on Googlebot crawls or may send the url or text of pages that require a login to Google.
(Thanks <a href="https://ruby.social/@tasket@infosec.exchange/113547455327604023">@tasket@infosec.exchange</a>.)</p>

<p>Initial reporting by <a href="https://www.seroundtable.com/google-ios-app-page-annotation-38451.html">SERoundtable</a> and <a href="https://9to5google.com/2024/11/25/google-ios-app-link-annotations-search/">9to5google</a>.</p>

<h3 id="do-you-have-the-ios-google-app-installed-can-you-find-an-inserted-ad-that-looks-and-acts-like-the-screenshot-above">Do you have the iOS Google App installed? Can you find an inserted ad that looks and acts like the screenshot above?</h3>

<p>If you have the app and own a site, please check your site.
Maybe start with high-ad-value terms like tourist destinations, insurance, loans, attorneys/lawyers, donations, hosting, trading, consumer electronics, or rehab.
I would especially appreciate <a href="https://push.cx/contact">hearing from you</a> so we could iterate on a couple possibilities for detecting this via the DOM.</p>

<p>(I found the <a href="https://japanobjects.com/features/japanese-castles">victim in their screenshot</a> and have collected 3 negative reports from people unable to reproduce the ad, so it may have been a one-off demo for the announcement.)</p>

<h3 id="this-isnt-ads">“This isn’t ads”</h3>

<p>Oddly, a common response has been that the world’s largest advertising company adding links to keywords directed at their own sites isn’t advertising.
Sometimes with the caveat that it’s because they’re currently only house ads and not yet for sale to third parties.</p>

<p>A bit of useful history might be Google’s own press release, “<a href="https://www.google.com/about/honestresults/">Why we sell advertising, not search results</a>”.
This was written when AdWords were placed to the side of search results with a blue background to be unambiguously ads.
Despite saying that results wouldn’t be for sale, Google <a href="https://searchengineland.com/search-ad-labeling-history-google-bing-254332">slowly iterated the design</a> to make the ads nearly indistinguishable from results.</p>

<p>Today, these injected links <em>start</em> nearly indistinguishable from the author’s own links, with no “Ad” label or icon, and only a faint pastel background similar to the 2010 AdWords treatment.
Like AdWords, every experiment Google runs on engagement will show “improvement” as the ads become harder to distinguish,</p>

<p>If you found that an advertising company was adding links to your site without your knowledge or consent, would you consider it a useful service?</p>

<p>Do you think it acceptable that, to disable the injected links, you have to agree to the ad company’s continually-updated terms of service and ask to individually opt-out each of your sites?</p>

<h3 id="ongoing">Ongoing</h3>

<p><em>There may be more on <a href="https://bsky.app/profile/push.cx/post/3lbsze4iqp222">Bluesky</a> or <a href="https://ruby.social/@pushcx/113546957556429539">Mastodon</a> where I’ve tried to find help.</em></p>

<p>Here’s the first rough draft of a js snippet that a site owner who sees an ad could add to a page to see some more info.
It needs text from the link inserted.
There’s some assumptions noted for how the injection might appear in the DOM.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">logDiv</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nf">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">div</span><span class="dl">'</span><span class="p">);</span>
  <span class="nx">div</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">cssText</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">position: fixed; top: 0; left: 0; right: 0;</span><span class="dl">'</span> <span class="o">+</span>
                      <span class="dl">'</span><span class="s1">background: #fee; color: #000; font-family: monospace; </span><span class="dl">'</span> <span class="o">+</span>
                      <span class="dl">'</span><span class="s1">z-index: 999; padding: 10px; max-height: 50vh; overflow: auto;</span><span class="dl">'</span><span class="p">;</span>
  <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nf">prepend</span><span class="p">(</span><span class="nx">div</span><span class="p">);</span>

  <span class="kd">function</span> <span class="nf">log</span><span class="p">(</span><span class="nx">msg</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">entry</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nf">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">div</span><span class="dl">'</span><span class="p">);</span>
    <span class="nx">entry</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="nx">msg</span><span class="p">;</span>
    <span class="nx">logDiv</span><span class="p">.</span><span class="nf">appendChild</span><span class="p">(</span><span class="nx">entry</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="k">try</span> <span class="p">{</span>
    <span class="cm">/* edit 'castle' to include the text of an ad link */</span>
    <span class="kd">const</span> <span class="nx">text</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">osaka castle</span><span class="dl">'</span><span class="p">;</span>

    <span class="cm">/* if it's not an 'a', try 'span' and then 'div' */</span>
    <span class="k">for </span><span class="p">(</span><span class="kd">const</span> <span class="nx">ad</span> <span class="k">of</span> <span class="nb">document</span><span class="p">.</span><span class="nf">querySelectorAll</span><span class="p">(</span><span class="dl">'</span><span class="s1">a</span><span class="dl">'</span><span class="p">);)</span> <span class="p">{</span>
      <span class="k">if </span><span class="p">(</span><span class="nx">a</span><span class="p">.</span><span class="nx">textContent</span><span class="p">.</span><span class="nf">toLowerCase</span><span class="p">().</span><span class="nf">includes</span><span class="p">(</span><span class="nx">text</span><span class="p">))</span> <span class="p">{</span>
        <span class="nf">log</span><span class="p">(</span><span class="nx">ad</span><span class="p">.</span><span class="nx">outerHTML</span><span class="p">);</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span> <span class="k">catch</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if </span><span class="p">(</span><span class="nx">logDiv</span><span class="p">)</span> <span class="p">{</span>
      <span class="nf">log</span><span class="p">(</span><span class="s2">`Error: </span><span class="p">${</span><span class="nx">e</span><span class="p">.</span><span class="nx">message</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">})();</span>
</code></pre></div></div>]]></content><author><name></name></author><category term="Code" /><category term="Google" /><category term="ad injection" /><summary type="html"><![CDATA[This post is getting updates - I’m trying to collect ad samples and investigate with a site owner so I can give away js that site owners can use to detect and block the ads. Please help!]]></summary></entry><entry><title type="html">Streaming Weekly Lobsters Office Hours</title><link href="https://push.cx/streaming-weekly" rel="alternate" type="text/html" title="Streaming Weekly Lobsters Office Hours" /><published>2024-08-12T12:38:00-05:00</published><updated>2024-08-12T12:38:00-05:00</updated><id>https://push.cx/streaming-lobsters-office-hours</id><content type="html" xml:base="https://push.cx/streaming-weekly"><![CDATA[<p>Reposting the <a href="https://lobste.rs/s/zzprkr/weekly_lobsters_office_hours">announcement posted on Lobsters</a> for my blog’s rss feed:</p>

<blockquote>
  <p>Hey folks,</p>

  <p>When this post is two hours old, and indefinitely twice a week, I’m going to stream Lobsters office hours and development on Twitch.
The <a href="https://twitch.tv/pushcx">channel is here</a> and more info + an archive is <a href="https://push.cx/stream">on my blog</a>. (You don’t have to sign up to Twitch to watch.)</p>

  <p>The office hours are largely to support iteratively running <a href="https://lobste.rs/about#queries">queries</a>.
People don’t take me up on this very often except via IRC because it’s hard to write these things perfectly out of the gate.
Having a fast feedback loop is essential.
Also, maybe having an open office hours session will be a nicely informal opportunity to answer questions and generally demystify the site.</p>

  <p>If nobody has questions I’ll hack on the Lobsters codebase.
Hopefully this encourages more activity on the <a href="https://github.com/lobsters/lobsters">repo</a>.
I’m also open to support people using the code to start a sister site, which is another activity that benefits from a tight feedback loop.
(<a href="https://github.com/lobsters/lobsters/issues/1265">example</a>)</p>

  <p>I plan to stream weekly on (US Central) Monday afternoons at 2 PM and Thursday mornings at 9 AM in the hopes that this gives reasonable timezone coverage.
Sorry for the inconvenience if you are antipodal to Chicago, though the site has <a href="https://github.com/lobsters/lobsters/blob/f2d7f4a8465ddf9141057c293ac4bb6d253143f3/config/application.rb#L40">always run on Chicago time</a>.
Sessions will probably run 1.5 to 3 hours and I’ll put summaries and transcripts in <a href="https://push.cx/stream">my archive</a> for easy searching.
I’ll try to keep the <a href="https://www.twitch.tv/pushcx/schedule">twitch schedule</a> up-to-date and I already know I’ll miss 9/1 for a vacation.</p>

  <p>This is pretty experimental!
I did it a few streams back in summer of 2020 that went well, but everything was pretty chaotic that year <a href="https://en.wikipedia.org/wiki/COVID-19_pandemic">for some reason</a> and I dropped it.
I did a technical rehearsal this weekend that <a href="https://push.cx/stream/2024-08-10-test-stream">went reasonably smoothly</a>.
If this continues to be interesting and useful, I’ll keep it up and try new things.
Feedback here, in the stream chat, or by email (peter@ <a href="https://push.cx">my domain</a>) is much appreciated.</p>

  <p>Special guest host will be my cat (subject to his schedule and whim). See y’all in our weird parasocial future.</p>
</blockquote>]]></content><author><name></name></author><category term="Code" /><category term="stream" /><category term="Twitch" /><category term="Lobsters" /><summary type="html"><![CDATA[Reposting the announcement posted on Lobsters for my blog’s rss feed:]]></summary></entry><entry><title type="html">Discord vs IRC Rough Notes</title><link href="https://push.cx/discord-vs-irc-notes" rel="alternate" type="text/html" title="Discord vs IRC Rough Notes" /><published>2024-07-11T14:00:00-05:00</published><updated>2024-07-11T14:00:00-05:00</updated><id>https://push.cx/discord-vs-irc-notes</id><content type="html" xml:base="https://push.cx/discord-vs-irc-notes"><![CDATA[<p>Lobsters has had a chat room on <a href="https://libera.chat">Libera Chat</a>
for <a href="https://en.wiktionary.org/wiki/dance_with_the_one_that_brought_you">9 years today</a>.
Lobsters itself is <a href="https://lobste.rs/s/slfdci/one_dozen_lobsters">12</a>,
and I <a href="https://lobste.rs/s/1z77ly/libera_chat#c_vwmpgx">see Libera Chat as continuous</a> with a rename a few years ago.</p>

<p>There’s a <a href="https://lobste.rs/chat">more thorough description</a> but <code class="language-plaintext highlighter-rouge">#lobsters</code> has three big purposes:</p>

<ol>
  <li>share a feed of links and have a lighter, ephemeral discussion on them</li>
  <li>give potential new users a place to connect to existing ones for invites<label for="border" class="margin-toggle"> ⊕</label><input type="checkbox" id="border" class="margin-toggle" /><span class="marginnote">2024-08-08: To make an implicit value explicit, a big value of the chat is that it keeps our border a little porous, it helps Lobsters avoid becoming stagnant</span></li>
  <li>some off-topic chat and community bonding</li>
</ol>

<p>So we care a lot about text chat with a bit of custom functionality and a great new user experience.
IRC is no longer a good experience for new users and
a couple times a year <code class="language-plaintext highlighter-rouge">#lobsters</code> rehashes a discussion on IRC’s features and prospects.
I finally realized I should collect my notes/logs into something linkable even if it’s only a braindump.</p>

<ul>
  <li>Most of this is me comparing Discord to IRC because it’s the alternative that’s most-used by current chatters, but the Rebuttal section is pretty universal to any discussion of IRC’s shortcomings.</li>
  <li><strong>I’m not seriously considering moving Lobsters chat to Discord</strong>, and the possibility is less attractive now that I’ve collected a list of its problems, which has built a compelling case it’s a bad culture fit.</li>
  <li>This is all pretty rough and contains a significant amount of frustrated venting.</li>
  <li>Items are not in priority order.</li>
  <li>Nothing here is urgent.</li>
  <li>We’ll use a chat for decades so any decision is less about current parity and more about trend lines.</li>
  <li>I’m compiling many discussions so we can rehash less in the future, whatever happens.</li>
</ul>

<h2 id="desirable-discord-features">Desirable Discord Features</h2>

<ul>
  <li>good new user experience
    <ul>
      <li>GUI</li>
      <li>familiar signup</li>
      <li>if user already has an account, joining server is a link to a one-click join dialog</li>
      <li>scrollback on join so the channel doesn’t look dead unless the new user joins in the ten seconds before an existing user hits enter</li>
      <li>doesn’t leak IP</li>
    </ul>
  </li>
  <li>desktop streaming (1080p)
    <ul>
      <li>I want to resume streaming Lobsters coding + office hours (esp for <a href="https://lobste.rs/about#queries">queries</a>)<label for="stream" class="margin-toggle"> ⊕</label><input type="checkbox" id="stream" class="margin-toggle" /><span class="marginnote">2024-10-15: I’ve been <a href="https://push.cx/stream">streaming</a> as hoped for a few months.</span></li>
      <li>My attempts to contact Twitch eng about their login bug failed <label for="twitchbug" class="margin-toggle"> ⊕</label><input type="checkbox" id="twitchbug" class="margin-toggle" /><span class="marginnote">2024-07-18: Found it: Twitch’s login is broken if Firefox’s <a href="https://support.mozilla.org/en-US/kb/firefox-protection-against-fingerprinting">fingerprinting protection</a> is on. I have to make a separate profile to use on-stream anyways, so it’s easy enough to leave off there.</span></li>
    </ul>
  </li>
  <li>text formatting: links, bold, underline, italic, code blocks</li>
  <li>security - MFA, alert emails, active login list</li>
  <li>message editing</li>
  <li>emoji reactions (and allergic users can hide them)</li>
  <li>mod tools
    <ul>
      <li>reasonable banning (+b is garbage; separation of kick is misdesign)</li>
      <li>no @ on mod names (https://libera.chat/guidelines/#channel-operators-are-users-too)</li>
      <li>highlight/filter on keywords w great default lists</li>
      <li>many high-quality 3rd-party tools (eg https://carl.gg/)</li>
      <li>public modlog (might req carl.gg, I forget)</li>
    </ul>
  </li>
  <li>mobile app</li>
  <li>good UI for muting channels/groups/users</li>
  <li>threads for breaking out overlapping/side conversations</li>
  <li>good user docs with screenshots</li>
  <li>per-server user profiles w bio, links</li>
  <li>I can pay for it and it improves over time</li>
  <li>can oauth to link account from lobsters profile</li>
  <li>file attachments</li>
  <li>higher discoverability/much better new user onboarding
    <ul>
      <li>#lobsters gets traffic from being ~25th in libera’s webchat top channel list</li>
      <li>but discord is staggeringly popular and has 3p public directories</li>
    </ul>
  </li>
</ul>

<h2 id="discord-downsides">Discord Downsides</h2>

<p>big stuff, potentially blockers:</p>

<ul>
  <li>might not be able to disable <a href="https://support.discord.com/hc/en-us/articles/360028038352-Server-Boosting-FAQ">donations</a> (<a href="https://lobste.rs/s/95uler/would_there_be_interest_patreon_for#c_9l58ia">reasoning</a>)</li>
  <li>mobile client sends a notification by default for any activity
    <ul>
      <li>painfully user-hostile and inappropriate for <a href="https://support.discord.com/hc/en-us/articles/360047132851-Enabling-Your-Community-Server">Community Servers</a></li>
    </ul>
  </li>
  <li>slow even for a desktop GUI
    <ul>
      <li>a loading interstitial! takes seconds on a fast connection and powerful desktop</li>
      <li>changing servers/channels takes x00ms and regularly much more</li>
      <li>animated placeholders during delays</li>
      <li>mobile app is noticeably slower at everything</li>
    </ul>
  </li>
  <li>countless distracting animations, mouseover popouts, and <a href="https://en.wikipedia.org/wiki/Mystery_meat_navigation">mystery meat</a></li>
  <li>broken activity UI
    <ul>
      <li>not clear whether a channel is selected or has activity, both use bold</li>
      <li>very flaky about marking channels as read (much worse on web than mobile)</li>
      <li>messages from blocked users still highlight channel as active</li>
    </ul>
  </li>
  <li>actively hostile to 3rd party clients/general hackery</li>
  <li>a serious culture clash that prompts most of the UI problems
    <ul>
      <li>Discord is oriented to mass-appeal to passive consumption of games, gossip, and memes</li>
      <li>Lobsters is about creating, learning, sharing experiences/expertise</li>
    </ul>
  </li>
</ul>

<p>smaller stuff, antifeatures:</p>

<ul>
  <li>no active ruby bot library
    <ul>
      <li>so almost every integration and workaround requires a 3rd-party service</li>
      <li>I’m sick of trying to keep a js service running, but maybe I’m just underwhelmed by the IRC library</li>
      <li>355e3b notes <a href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks">webhooks</a> get us ~all current functionality so I demoted this out of blockers</li>
    </ul>
  </li>
  <li>threads/replies are clunky af
    <ul>
      <li>feels like they were bolted on the UI and never really integrated; even slack is better</li>
    </ul>
  </li>
  <li>much more spam</li>
  <li>stickers, inline images, animated emoji</li>
  <li>enormous obnoxious link preview cards (no setting; maybe could delete by bot)</li>
  <li>intrusive upsells in clients (these go away on paid servers, right?)</li>
  <li>can’t turn off external link <a href="https://lobste.rs/s/eg2erk">warnings</a> even solely for our own site (sharing links is a core activity; we have sophisticated users)</li>
  <li>big company support (vs a libera admin is a lobsters user and active in the channel)
    <ul>
      <li>forum is overwhelmed by support requests/mental health crises</li>
      <li>no chance of reporting bugs or influencing features</li>
    </ul>
  </li>
  <li>complicated subscription structure with unclear pricing (but probably &lt;$20/m)</li>
  <li>users can’t export logs</li>
  <li>no TUI</li>
  <li>no Reactions page collecting emoji reactions to own posts</li>
  <li>can’t disable X integration on profiles</li>
  <li>it’s so slow I have to list it a second time</li>
</ul>

<h2 id="irc-rebuttals">IRC Rebuttals</h2>

<ul>
  <li>Fix/script your client
    <ul>
      <li>This is “baby why do you make me hit you” levels of helpful.</li>
      <li>I would rather have one reasonable UI than have every user fail to reinvent the wheel.</li>
      <li>Bad out-of-the-box UI is why IRC has rounds-to-100% new user churn</li>
    </ul>
  </li>
  <li>So the server/client split…
    <ul>
      <li>I am a user, not an implementer. This is a technical decision that seems to have permanently hamstrung development.</li>
      <li>It’s about as interesting and useful as a customer support line that refuses to help because you have the wrong department.</li>
    </ul>
  </li>
  <li>Nickserv/chanserv/etc
    <ul>
      <li>Bolting on features/services via chatbot is powerful but bad UI (even when Discord does it!)</li>
      <li>The big difference is that IRC never integrates these into the core product</li>
    </ul>
  </li>
  <li>IRC is open source/open standard.
    <ul>
      <li>It’s nice and I’m willing to take a haircut for it, but I’m ready to take off the hair shirt.</li>
      <li>But maybe being a protocol instead of a product is why it’s so far behind and not improving.</li>
    </ul>
  </li>
  <li>IRC is volunteers/PRs welcome
    <ul>
      <li>IRC is a heroic accomplishment and should not be one</li>
      <li>My time is more valuable than my money. For our non-toy usage $50/month barely registers as a cost.</li>
      <li>Especially compared to the cost of joining a multi-stakeholder consensus-mandatory process about the redesign of legacy software.</li>
    </ul>
  </li>
  <li>IRCv3
    <ul>
      <li>Mostly technical underpinnings with little addressing the feature gap.</li>
      <li>IRC is not closing the gap. Discord is very actively widening the gap.</li>
    </ul>
  </li>
  <li>Discord is a single service run by a VC-funded business
    <ul>
      <li>Lots of big, familiar privacy/sustainability/control risks here</li>
      <li>This also prompts <a href="https://nothinghuman.substack.com/p/the-tyranny-of-the-marginal-user">lots of the UI problems</a></li>
      <li>VC B2C orients to growth metrics, so quality will nosedive hard when growth plateaus and the established processes can’t adapt to that “failure”</li>
    </ul>
  </li>
</ul>

<h2 id="underinformed-pontificating">Underinformed Pontificating</h2>

<p>Libera Chat isn’t at fault, incompetent, foolish, or anything else negative.
Neither is the broader IRC community (well, aside from that one infamous guy, who is all of those things and more).</p>

<p>Most of IRC’s problems are structural.
<a href="https://en.wikipedia.org/wiki/Network_effect">Network effects</a> make chat
valuable.
But feature development has stalled beacuse it’s brutally hard to reach consensus.
Maybe email and the web managed it because of competitive commercial use.
Maybe the protocol isn’t as extensibile because it’s not as forgiving of
unsupported features.
There’s probably an amazing book waiting to be written about how open
protocols and standards thrive or die.</p>

<p>Text-oriented group chat has products like
Discord, Slack, Zulip, WhatsApp, Telegram, Messenger, WeChat, iMessage, GChat, Skype, Teams, Kik, Mattermost, Snapchat, Wickr, and then, you know, some small ones that only have tens of millions of active users.
Nearly every human with a phone uses at least one.
That’s a lot of room for open source and standards, and IRC seems to have attracted and extinguished potential development.
Maybe the pressing thing to design is not a revised protocol but a process for sustaining consensus over revisions.</p>

<p>2024-07-12 Edit:
Chat discussion has pointed out to me that Matrix is an open standard.
I really only know it from the not-so-pleasant bridge to Libera Chat.
I feel pretty good about putting “underinformed” in this section heading.</p>

<p>2024-07-26 Edit:
When I published this blog post, the feed-reading bot shared it in the <a href="https://zulip.com/case-studies/recurse-center/">Recurse Center Chat</a>.
I had wandered away from it <a href="/return-statement">after my batch</a>, mostly because Zulip was unpleasantly slow and janky.
That’s been fixed in the years since, it’s a smooth and polished experience.
I think it has all of the features listed here and none of the downsides (even
an <a href="https://github.com/zulip/zulip-terminal#readme">official TUI!</a>), this is a
plausible alternative.
<a href="https://blog.zulip.com/2024/07/25/zulip-9-0-released/">Zulip 9.0 was released</a> today.</p>]]></content><author><name></name></author><category term="Code" /><category term="Discord" /><category term="IRC" /><category term="Libera Chat" /><category term="Lobsters" /><summary type="html"><![CDATA[Lobsters has had a chat room on Libera Chat for 9 years today. Lobsters itself is 12, and I see Libera Chat as continuous with a rename a few years ago.]]></summary></entry><entry><title type="html">Wrapping Large-Scale Refactors</title><link href="https://push.cx/large-refactors" rel="alternate" type="text/html" title="Wrapping Large-Scale Refactors" /><published>2024-01-27T11:10:05-06:00</published><updated>2024-01-27T11:10:05-06:00</updated><id>https://push.cx/large-refactors</id><content type="html" xml:base="https://push.cx/large-refactors"><![CDATA[<p>I really liked a “<a href="https://max.engineer/long-term-refactors">Long Term Refactors</a>” by Max Chernyak explaining a nice development practice.
I was reminded of a thing that surprised me about refactors and dependency management.</p>

<p>My last job had a codebase large enough (~50M LOC<label for="loc" class="margin-toggle"> ⊕</label><input type="checkbox" id="loc" class="margin-toggle" /><span class="marginnote">Edit 2025-02: I recently reread this post. I think this number must be too high, but I can’t remember what the right number was anymore, maybe 7M? But I don’t know how I would’ve made this mistake, it’s not clearly a simple typo. I guess I’ll just reiterate that it was by far the largest codebase I’ve worked in and not alter this post.</span>) that there were always large-scale refactors in flight, which was a new experience for me.</p>

<p>One thing this article doesn’t specifically call out is that any kind of dependency update or replacement, whether an internal or external library, is a large-scale refactor.
You benefit enormously if you can do this incrementally as the author describes instead of a <a href="https://en.wikipedia.org/wiki/Flag_day_(computing)">flag day</a> or One Giant Merge to update all uses.
A counterintuitive result is that replacing one dep with another (foolib -&gt; barlib) is <em>easier</em> than updating one (foolib 1 -&gt; foolib 2)!
Most languages do not allow you to depend on multiple versions of a package and have different sections of your codebase call different versions.
Sometimes internally-maintained dependencies will rename just to get around this limitation.</p>

<p>There’s a style of managing dependencies that mandates you must wrap usage of libraries or APIs.
Rather than calling <code class="language-plaintext highlighter-rouge">Foolib::Thing.new</code>, you’ll create your own <code class="language-plaintext highlighter-rouge">FooThing</code> (maybe using the decorator or facade patterns) and that class is the only place allowed to import from or call into foolib.
With less exposure of foolib, it’s easier to create internal documentation, audit or control usage, or replace foolib with barlib.
I don’t find this a cost worth paying in smaller codebases, but easily worth it in large ones.</p>

<p>Part of why it’s worthwhile is that it gives you two new methods for dealing with dependency updates.
First (hopefully), you have a single codesite that uses foolib so a single team can make a small change to update foolib.
Or second, if there are extensive changes that mandate changes at callsites, you can rename <code class="language-plaintext highlighter-rouge">FooThing</code> to <code class="language-plaintext highlighter-rouge">FooThing1</code> (usually an easy, if large diff), introduce <code class="language-plaintext highlighter-rouge">FooThing2</code> with the new API, and then use a process like the one this article describes to make that change incrementally to the entire codebase.
Either you update foolib at the start of this process and <code class="language-plaintext highlighter-rouge">FooThing1</code> maps old usage to new, or <code class="language-plaintext highlighter-rouge">FooThing2</code> maps new usage to old and you bump foolib at the end.
This process works quite well, whether foolib is an internal or external dependency.
Whereas, say, emailing all-dev@example.com a link to the foolib release notes and dictum that on some particular date that all foolib usage must be updated will inevitably produce significant internal discord and never, ever an on-schedule completion.
An even worse and more common failure mode for internal libs is to quietly mark foolib deprecated and direct people to rewrite to barlib when they show up with urgent questions about foolib during an outage - but of course good sense and steps 6-8 of the process described in the post would avoid such an outlandish footgunning.</p>

<p>(This post was originally a <a href="https://lobste.rs/s/bi2b1j">comment on Lobste.rs</a>
but then I realized it’s a nice excuse to break the 5.5 year dry spell here.)</p>]]></content><author><name></name></author><category term="Code" /><category term="refactoring" /><category term="large codebases" /><category term="practices" /><summary type="html"><![CDATA[I really liked a “Long Term Refactors” by Max Chernyak explaining a nice development practice. I was reminded of a thing that surprised me about refactors and dependency management.]]></summary></entry></feed>