<?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>2025-11-18T00:00:00-06: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">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><entry><title type="html">NixOS on prgmr and Failing to Learn Nix</title><link href="https://push.cx/nixos" rel="alternate" type="text/html" title="NixOS on prgmr and Failing to Learn Nix" /><published>2018-07-04T13:30:05-05:00</published><updated>2018-07-04T13:30:05-05:00</updated><id>https://push.cx/nixos</id><content type="html" xml:base="https://push.cx/nixos"><![CDATA[<p>This is a writeup of my notes on how to get <a href="https://nixos.org">NixOS</a> running on a VPS at <a href="https://prgmr.com">prgmr</a>, followed by more general notes on this experiment in learning nix.</p>

<h3 id="provision">Provision</h3>

<p>I went with the lowest tier, currently 1.25 GiB RAM, 15 GiB Disk for $5/month. I’m only running <a href="https://weechat.org">weechat</a> for irc/twitter/fediverse/slack and some miscellaneous small things. For “pre-installed distribution” I chose “None (HVM)”.</p>

<h3 id="netboot-to-start-install">Netboot to start install</h3>

<p>I ssh’d into the management console, <code class="language-plaintext highlighter-rouge">ssh </code><em><code class="language-plaintext highlighter-rouge">[hostname]</code></em><code class="language-plaintext highlighter-rouge">@</code><em><code class="language-plaintext highlighter-rouge">[hostname]</code></em><code class="language-plaintext highlighter-rouge">.console.xen.prgmr.com</code></p>

<ul>
  <li>6 bootloader</li>
  <li>4 netboot installer, pick nixos</li>
  <li>0 twice for main menu</li>
  <li>4 to power off</li>
  <li>2 to start (see “Booting” below)</li>
  <li>1 to log in as root with no password (relax, ssh is off)</li>
</ul>

<h3 id="partition">Partition</h3>

<p>Surprisingly, the included 1.25 GB of RAM was not enough to run some nix commands.
I had to back up and recreate the box with some swap space.
I didn’t think too hard about it, just guessed at 2 GB and it worked OK.<label for="bug1681" class="margin-toggle"> ⊕</label><input type="checkbox" id="bug1681" class="margin-toggle" /><span class="marginnote">2018-07-09: Vaibhav Sagar suggested this is probably <a href="https://github.com/NixOS/nix/issues/1681">this known bug</a>.</span></p>

<p>` gdisk /dev/xvda`{lang=”bash”}</p>

<p>` o to create gpt`{lang=”bash”}</p>

<p><code class="language-plaintext highlighter-rouge">n to create swap partition</code>{lang=”bash”}</p>

<p><code class="language-plaintext highlighter-rouge">Command (? for help): n Partition number (1-128, default 1): 1 First sector (34-31457246, default = 2048) or {+-}size{KMGTP}: Last sector (2048-31457246, default = 31457246) or {+-}size{KMGTP}: +32M Current type is 'Linux filesystem' Hex code or GUID (L to show codes, Enter = 8300): EF02 Changed type of partition to 'BIOS boot partition'</code>{lang=”bash”}</p>

<p><code class="language-plaintext highlighter-rouge">Command (? for help): n Partition number (2-128, default 2): First sector (34-31457246, default = 67584) or {+-}size{KMGTP}: Last sector (67584-31457246, default = 31457246) or {+-}size{KMGTP}: -2G Current type is 'Linux filesystem' Hex code or GUID (L to show codes, Enter = 8300): Changed type of partition to 'Linux filesystem'</code>{lang=”bash”}</p>

<p><code class="language-plaintext highlighter-rouge">Command (? for help): n Partition number (3-128, default 3): First sector (34-31457246, default = 27262976) or {+-}size{KMGTP}: Last sector (27262976-31457246, default = 31457246) or {+-}size{KMGTP}: Current type is 'Linux filesystem' Hex code or GUID (L to show codes, Enter = 8300): 8200 Changed type of partition to 'Linux swap'</code>{lang=”bash”}</p>

<p><code class="language-plaintext highlighter-rouge">w to write and exit</code>{lang=”bash”}</p>

<p><code class="language-plaintext highlighter-rouge">mkswap -L swap /dev/xvda3</code>{lang=”bash”}</p>

<p><code class="language-plaintext highlighter-rouge">swapon /dev/xvda3</code>{lang=”bash”}</p>

<p><code class="language-plaintext highlighter-rouge">mkfs.ext4 -L root /dev/xvda2</code>{lang=”bash”}</p>

<p>` <code class="language-plaintext highlighter-rouge">{lang="bash"}</code>mount /dev/xvda2 /mnt `{lang=”bash”}</p>

<h3 id="configuring-nix">Configuring nix</h3>

<p>I generated the initial config and added a few <a href="https://wiki.prgmr.com/mediawiki/index.php/NixOS">prgmr-specific tweaks</a>:</p>

<p>` nixos-generate-config –root /mnt`{lang=”bash”}</p>

<p><code class="language-plaintext highlighter-rouge">cd /mnt/etc/nixos</code>{lang=”bash”}</p>

<p>` <code class="language-plaintext highlighter-rouge">{lang="bash"}</code>vi configuration.nix `{lang=”bash”}</p>

<p>Here’s my tweaks:</p>

<p>` boot.loader.grub.device = “/dev/xvda”`{lang=”nix”}</p>

<p>` # prgmr console config: boot.loader.grub.extraConfig = “serial –unit=0 –speed=115200 ; terminal_input serial console ; terminal_output serial console”; boot.kernelParams = [“console=ttyS0”];`{lang=”nix”}</p>

<p><code class="language-plaintext highlighter-rouge">environment.systemPackages = with pkgs; [ bitlbee tmux weechat wget vim ];</code>{lang=”nix”}</p>

<p><code class="language-plaintext highlighter-rouge">services.openssh.enable = true;</code>{lang=”nix”}</p>

<p><code class="language-plaintext highlighter-rouge">networking.firewall.allowedTCPPorts = [ 22 ];</code>{lang=”nix”}</p>

<p><code class="language-plaintext highlighter-rouge">sound.enable = false; services.xserver.enable = false; services.openssh.enable = true;</code>{lang=”nix”}</p>

<p>` <code class="language-plaintext highlighter-rouge">{lang="nix"}</code> users.extraUsers.pushcx = { name = “pushcx”; isNormalUser = true; extraGroups = [ “wheel” “disk” “systemd-journal” ]; uid = 1000; openssh.authorizedKeys.keys = [ “[ssh public key here]” ]; }; `{lang=”nix”}</p>

<p>Then I ran <code class="language-plaintext highlighter-rouge">nixos-install</code> to install the system.</p>

<h3 id="booting">Booting</h3>

<p>The NixOS manual says you should be able to run <code class="language-plaintext highlighter-rouge">reboot</code> to boot to the new system, but something in xen doesn’t reload the new boot code and I got the netboot again rather than the new system.
After talking to prgmr I found it worked if I pulled up the management console and did:</p>

<ul>
  <li>6 -&gt; 1 boot from disk</li>
  <li>then 4 to fully poweroff</li>
  <li>then 2 to create/start</li>
</ul>

<p>After this I had a running system that I could ssh into as a regular user.</p>

<p><a href="https://prgmr.com">Prgmr</a> donates hosting to <a href="https://lobste.rs">Lobsters</a>,
but because <a href="https://lobste.rs/u/alynpost">Alan</a> configured the hosting, this was my first time really using the system.
It was painless and getting support in #prgmr on Freenode was comfortable for me as a longtime IRC user.
I liked them before, and now I’m happy to recommend them for no-nonsense VPS hosting.</p>

<h2 id="nixnixos">Nix/NixOS</h2>

<p>I did this setup because I’ve been meaning to learn nix (the package manager) and NixOS (the Linux distribution built on nix) for a while.
As I <a href="https://lobste.rs/s/p2b1gn/arch_linux_developer_friendly_operating#c_gda5mm">commented on Lobsters</a>, they look like they didn’t start from manual configuration and automate that, they started from thinking hard about what system configuration is and encoded that.
(The final impetus was that I ran out of stored credit at Digital Ocean, hit several billing bugs trying to pay them, and couldn’t contact support - six tries in four mediums only got roboresponses.)</p>

<p>The <a href="https://nixos.org/nixos/manual/">NixOS manual</a> is solid and I had little trouble installing the OS.
It did a great job of working through a practical installation while explaining the underlying concepts.</p>

<p>I then turned to the <a href="https://nixos.org/nix/manual">Nix manual</a> to learn more about working with and creating packages and failed, even with help from the nixos IRC channel and issue tracker.
I think the fundamental cause is that it wasn’t written for newbies to learn nix from; there’s a man-page like approach where it only makes sense if you already understand it.</p>

<p>Ultimately I was stopped because I needed to create a package for <a href="https://github.com/kensanata/bitlbee-mastodon">bitlbee-mastodon</a> and <a href="https://github.com/wee-slack/wee-slack">weeslack</a>. As is normal for a small distro, it hasn’t packaged these kind of uncommon things (or complex desktop stuff like Chrome<label for="secret" class="margin-toggle"> ⊕</label><input type="checkbox" id="secret" class="margin-toggle" /><span class="marginnote">2018-07-16: I’ve learned that Nix <em>does</em> have a package for Chrome, but it doesn’t appear in <code class="language-plaintext highlighter-rouge">nix-env</code> searches or the <a href="https://nixos.org/nixos/packages.html#chrome">official package list</a> because it’s hidden by an option that is not referenced in system config files, user config files, the NixOS Manual, the Nix Manual, the man page for <code class="language-plaintext highlighter-rouge">nix-env</code>, the package search site, or the the documentation of any other tool it hides packages from.</span>) but I got the impression the selection grows daily.
I didn’t want to install them manually (which I doubt would really work on NixOS), I wanted an exercise to learn packaging so I could package my own software and run NixOS on servers (the recent issues/PRs/commits on <a href="https://github.com/lobsters/lobsters-ansible">lobsters-ansible</a> tell the tale of my escalating frustration at its design limitations).</p>

<p>The manual’s instructions to build and package GNU’s “hello world” binary <a href="https://github.com/NixOS/nix/issues/2259">don’t actually work</a> (gory details there).
I got the strong impression that no one has ever sat down to watch a newbie work through this doc and see where they get confused; not only do fundamentals go unexplained and the samples not work, there’s no discussion of common errors.
Frustratingly, it also conflates building a package with contributing to nixpkgs, the official NixOS package repository.</p>

<p>Either this is a fundamental confusion in nix documentation or there’s some undocumented assumption about what tools go where that I never understood.
As an example, I tried to run <code class="language-plaintext highlighter-rouge">nix-shell</code> (which I think is the standard tool for debugging builds but it has expert-only docs) and it was described over in the <a href="https://nixos.org/nixpkgs/manual/">Nixpkgs Manual</a> even though it’s for all packaging issues.
To use the shell I have to understand “<a href="https://nixos.org/nixpkgs/manual/#sec-stdenv-phases">phases</a>”, but some of the ones listed simply don’t exist in the shell environment.
I can’t guess if this a bug, out-dated docs, or incomplete docs.
And that’s before I got to confusing “you just have to know it” issues like the <code class="language-plaintext highlighter-rouge">src</code> attribute becoming <code class="language-plaintext highlighter-rouge">unpackPhase</code> rather than <code class="language-plaintext highlighter-rouge">srcPhase</code>, or “learn from bitter experience” issues like <code class="language-plaintext highlighter-rouge">nix-shell</code> polluting the working directory and carrying state between build attempts.
(This is where I gave up.)</p>

<p>I don’t know how the NixOS Manual turned out so well; the rest of the docs have this fractal issue where, at every level of detail, every part of the system is incompletely or incorrectly described somewhere other than expected.
I backed up and reread the homepages and about pages to make sure I didn’t miss a tutorial or other introduction that might have helped make sense of this, but found nothing besides these manuals.
If I sound bewildered and frustrated, then I’ve accurately conveyed the experience.
I gave up trying to learn nix, even though it still looks like the only packaging/deployment system with the right perspective on the problems.</p>

<p>I’d chalk it up to nix being young, but there’s some oddities that look like legacy issues.
For example, commands vary: it’s <code class="language-plaintext highlighter-rouge">nix-env -i</code> to install a package, but <code class="language-plaintext highlighter-rouge">nix-channel</code> only has long options like <code class="language-plaintext highlighter-rouge">--add</code>, and <code class="language-plaintext highlighter-rouge">nix-rebuild switch</code> uses the more modern “subcommand” style.
With no coherent style, you have to memorize which commands use which syntax - again, one of those things newbies stumble on but experts don’t notice and may not even recognize as a problem.</p>

<p>Finally, there’s two closely-related issues in nix that look like misdesigns, or at least badly-missed opportunities.
I don’t have a lot of confidence in these because, as recounted, I was unable to learn to use nix.
Mostly these are based on my 20 years of administrating Linux systems, especially the provisioning and devops work I’ve done with Chef, Puppet, Ansible, Capistrano, and scripting that I’ve done in the last 10.
Experience has led me to think that the hard parts of deployment and provisioning boil down to a running system being like a running program making heavy use of mutable global variables (eg. the filesystem):
the pain comes from unmanaged changes and surprisingly complex moving parts.</p>

<p>The first issue is that Nix templatizes config files.
There’s an example in my <code class="language-plaintext highlighter-rouge">configuration.nix</code> notes above: rather than editing the grub config file, the system lifts copies from this config file to paste into a template of of grub’s config file that must be hidden away somewhere.
So now instead of just knowing grub’s config, you have to know it <em>plus</em> what interface the packager decided to design on top of it by reading the package source (and I had to google to find that).
There’s warts like <code class="language-plaintext highlighter-rouge">extraConfig</code> that throw up their hands at the inevitable uncaptured complexity and offer a interface to inject arbitrary text into the config.
I hope “inject” puts you in a better frame of mind than “interface”: this is <a href="http://wiki.c2.com/?StringlyTyped">stringly-typed</a> text interpolation and a typo in the value means an error from grub rather than nix.
This whole thing must be a ton of extra work for packagers, and if there’s a benefit over <code class="language-plaintext highlighter-rouge">vi /etc/default/grub</code> it’s not apparent (maybe in provisioning, though I never got to nixops).</p>

<p>This whole system is both complex and incomplete, and it would evaporate if nix configured packages by providing a default config file in a package with a command to pull it into <code class="language-plaintext highlighter-rouge">/etc/nix</code> or <code class="language-plaintext highlighter-rouge">/etc/nixos</code> for you to edit and nix to copy back into the running system when you upgrade or switch.
This would lend itself very well to keeping the system config under version control, which is never suggested in the manual and doesn’t seem to be integrated at any level of the tooling - itself a puzzling omission, given the emphasis on repeatability.</p>

<p>Second, to support this complexity, they developed their own programming language.
(My best guess - I don’t actually know which is the chicken and which is the egg.)
A nix config file isn’t data, it’s a turning-complete language with conditionals, loops, closures, scoping, etc.
Again, this must have been a ton of work to implement and a young, small-team programming language has all the obvious issues like no debugger, confusing un-googleable error messages that don’t list filenames and line numbers, etc.; and then there’s the learning costs to users.
Weirdly for a system inspired by functional programming, it’s dynamically typed, so it feels very much like the featureset and limited tooling/community of JavaScript circa 1998.
In contrast to JavaScript, the nix programming language is only used by one project, so it’s unlikely to see anything like the improvements in JS in last 20 years.
And while JavaScript would be an improvement over inventing a language, using Racket or Haskell to create a DSL would be a big improvement.</p>

<p>These are two apparent missed opportunities, not fatal flaws.
Again, I wasn’t able to learn nix to the level that I understand how and why it was designed this way, so I’m not setting forth a strongly-held opinion.
They’re really strange, expensive decisions that I don’t see a compelling reason for, and they look like they’d be difficult to change.
Probably they have already been beaten to death on a mailing list somewhere, but I’m too frustrated by how much time I’ve wasted to go looking.</p>

<p>I’ve scheduled a calendar reminder for a year from now to see if the manual improves or if <a href="https://twitter.com/lucperkins/status/999007471141240832">Luc Perkins’s book</a> is out.</p>

<p>2018-08-09: I wasted another two days trying Nix from the other direction.
Rather than build up from the basics I tried to start from the top down and create a “Hello World” Rails app.
It’s hard to tell around the bugs and docs, but I’m pretty sure it’s not possible to run a Rails app on NixOS.</p>

<p>2019-12-01: New attempt. Got an incorrect error message that <code class="language-plaintext highlighter-rouge">/sbin/bash</code> didn’t exist. Paved and reinstalled, then tried to build a <a href="https://github.com/zetavg/rails-nix-sample">Rails demo</a> but I got a useless error message when it failed to install 1password (!?). The two errors came while pasting directly from the install docs and I was told “lmk when you figure out which command you were just calling wrong”. The documentation’s <a href="https://github.com/NixOS/nix/issues/2259">first example is still broken</a> and following install steps invariably leads to errors. I’m sick of being told it’s my fault nix doesn’t work and I’m giving up on it.</p>]]></content><author><name>{&quot;display_name&quot; =&gt; &quot;Peter Harkins&quot;, &quot;login&quot; =&gt; &quot;admin&quot;, &quot;email&quot; =&gt; &quot;ph@malaprop.org&quot;, &quot;url&quot; =&gt; &quot;http://malaprop.org&quot;}</name><email>ph@malaprop.org</email></author><category term="Code" /><category term="nix" /><category term="prgmr" /><category term="docs" /><summary type="html"><![CDATA[This is a writeup of my notes on how to get NixOS running on a VPS at prgmr, followed by more general notes on this experiment in learning nix.]]></summary></entry><entry><title type="html">House Rules</title><link href="https://push.cx/house-rules" rel="alternate" type="text/html" title="House Rules" /><published>2018-06-13T16:38:43-05:00</published><updated>2018-06-13T16:38:43-05:00</updated><id>https://push.cx/house-rules</id><content type="html" xml:base="https://push.cx/house-rules"><![CDATA[<p>I really enjoy playing board games with friends, as you can probably guess from my <a href="/media-reviews">media reviews</a>. Over the last ~25 years of playing we’ve evolved a couple house rules that are worth formalizing and sharing.</p>

<h2 id="1-yes-take-backsies">1. Yes Take-backsies</h2>

<p>We’re playing for the fun of learning new games and competing. If you make a mistake and we can unwind it, you can take it back and do what you meant to do or realize you should have done.</p>

<p>We don’t do take-backsies of things we can’t easily and fairly unwind. For example, in Risk, if you roll the dice to attack another player’s army and get wiped out, well, that’s not fair to unwind. Similarly, if you learn hidden information like turning over the next card on the deck, we can’t make everyone forget that (but mayyyybe if it’s someone’s first time playing a game).</p>

<p>Making mistakes is a normal part of learning. By helping keep them cheap and emphasizing that it’s socially rewarded to admit and correct them, everyone learns and plays better, and gets to relax and enjoy themself more. Games are fun because they’re an <a href="https://www.raphkoster.com/2018/03/16/the-trust-spectrum/">exercise in trust</a> as much as exercise in formal systems.</p>

<p>We very often have players with uneven experience and take-backsies helps the newbies get into a game and keep the moderately-experienced competitive with the experts. We encourage suggestions and polite criticism of in-progress mistakes, too. When learning a complex game it’s hard to recognize the legal moves available and what their tradeoffs might be. So when someone looks particularly stumped, it’s normal to hear something like “Sooo... it looks like you have five or maybe six things you could do here, depending on what’s in your hand?” to offer help.</p>

<p>Suggestions show up even with competitive, experienced players, in part because some of us are so competitive we <a href="https://en.wikipedia.org/wiki/Integer_overflow">wrap around</a> into helping our opponents make the best moves for their strategies so that we’re ourselves pushed into <a href="http://www.sirlin.net/ptw">improving our own play</a>. Suggestions aren’t a rule because some people don’t like hearing them and it’s more common in competitive play to want opponents to make mistakes, but I wanted to go into it because it’s foundational to the Yes Take-Backsies rule.</p>

<h2 id="2-public-stays-public">2. Public Stays Public</h2>

<p>Information that’s revealed during gameplay stays public information and can be reviewed at any time unless doing so severely inconveniences the flow of gameplay.</p>

<p>What this means in practice is cards get discarded face-up and spread out for anyone to look through, or other things that have been played can be reviewed. The limit is that we might run out of table space or it would be distinctly un-fun to dig through that many cards, but when there’s that much information laying around it’s probably not particularly important what’s been played.</p>

<p>For example, <a href="https://boardgamegeek.com/boardgame/929/great-dalmuti">The Great Dalmuti</a> is a light trick-taking card game that’s a longtime favorite. Players play cards to try to empty their hands before everyone else, and between hands they shift to sit in the order they finished in to get benefits for the next hand. The last player is punished with the chore of clearing away the cards from each trick (and they have some other chores) so we task the next-to-last player. Instead of the game rule that all played cards are flipped face-down, our house rule is to arrange the best six ranks (numbered 1-6 of the deck’s 12 ranks) face-up along the side of the table. We don’t do this for ranks 7+ to balance the work vs how little strategy is at work in high cards (&gt;85% of the time the correct play is “dump any of them at first opportunity”). This has been a big success. Beginners graduate from learning the rules to start exercising strategy in 2-3 hands instead of 5-6 hands, experts can experiment more effectively, and everyone’s happier not trying to remember “wait... did I see <em>all</em> of the threes, or just two of them?”</p>

<p>Information that’s derivable from public information is also public. In <a href="https://boardgamegeek.com/boardgame/5/acquire">Acquire</a> every player starts with the same amount of money and buys and sells stock in public transactions... but the rulebook suggests keeping it secret to make the game “even more challenging”. Arithmetic is not a fun game, it is a chore.</p>

<p>These “memory subgames” crop up all over, are stressful and uninteresting, and seem an unfair advantage for those who are better at this or spend a few days learning mnemonic techniques. When public information stays public, players make fewer uninformed and mistaken decisions.</p>

<p>The common downside is that sometimes players bog down the game while sifting through old cards or falling into <a href="https://en.wikipedia.org/wiki/Analysis_paralysis">analysis paralysis</a>. We solve this through good-natured grumbling and some smoothly-worn old jokes that reference getting on with things before the mountains crumble into the seas, etc. In rare cases [(or rare players)]{.small} we’ll set a turn timer on someone’s phone. Or give suggestions! Once you’ve looked at all the things an opponent might do and thought through how you’d respond to them and gotten bored, there’s no harm in talking to the opponent about their options.</p>

<p>To keep things moving along (especially when playing someone with very good recall of previous game state), it’s common to ask something like, “Wait, have you played all your aces?” rather than spend twenty seconds flipping through their discards. An honesty norm has developed: you can answer honestly, you can fib and say “I’m not sure” (if you think it gives you a competitive advantage and they won’t check), in an fiercely competitive game you can say “count for yourself”, but you can’t lie. If you’ve played all your aces but say you haven’t, or vice-versa, or otherwise deliberately give false information, it’s considered very rude, unsportsmanlike conduct and is treated almost as negatively as cheating. It’s OK to want to win and normal not to want to help your opponent, but we’ve established lying about public information as something that gets you a lot of frowning friends. We’re good friends or becoming them, so social disapproval means this basically never happens. (Exception: <a href="https://en.wikipedia.org/wiki/Diplomacy_(game)">Diplomacy</a> is a <a href="https://en.wikipedia.org/wiki/Blood_sport">blood sport</a>.)</p>

<p>Public Stays Public is a much younger and more explicit rule than Yes Take-backsies, which grew out of long habit. We had only very small, limited experiments with it until about six months ago (December 2017), when I heard <a href="https://push.cx/2017/attending-recurse-center">at Recurse Center</a> that someone knew of a gaming club (maybe at MIT?) that had it as a universal house rule. I decided it was worth trying and we’ve enjoyed it in every game since.</p>

<h2 id="more">More?</h2>

<p>Our process of developing house rules is a very much like developing traditions. We’re doing this slowly and sometimes only recognizing in retrospect that we have one because we learn another group doesn’t. I’m finally getting around to writing this up today because it came up in an online chat and my response kept getting longer and longer, so maybe I’ll add more in the future.</p>

<p>I suppose one thing worth noting is that in the last decade as games have generally gotten much better (and we’ve gotten more patient), we’ve become much more reluctant to add house rules to individual games. Usually something that seems weird and wrong is a corner of gameplay worth exploring and deliberating rather than something we feel we comfortable immediately trying to prune. We have almost no per-game house rules, really.</p>]]></content><author><name>{&quot;display_name&quot; =&gt; &quot;Peter Harkins&quot;, &quot;login&quot; =&gt; &quot;admin&quot;, &quot;email&quot; =&gt; &quot;ph@malaprop.org&quot;, &quot;url&quot; =&gt; &quot;http://malaprop.org&quot;}</name><email>ph@malaprop.org</email></author><category term="Games" /><category term="tabletop games" /><category term="board games" /><category term="card games" /><summary type="html"><![CDATA[I really enjoy playing board games with friends, as you can probably guess from my media reviews. Over the last ~25 years of playing we’ve evolved a couple house rules that are worth formalizing and sharing.]]></summary></entry></feed>