<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="../feed.xsl" type="text/xsl"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">

<channel>
<title>Susam's Technology Pages</title>
<link>https://susam.net/tag/technology.html</link>
<atom:link rel="self" type="application/rss+xml" href="https://susam.net/tag/technology.xml"/>
<description>Feed for Susam's Technology Pages</description>

<item>
<title>Wander Console 0.4.0</title>
<link>https://susam.net/code/news/wander/0.4.0.html</link>
<guid isPermaLink="false">wnzfz</guid>
<pubDate>Sat, 04 Apr 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  Wander Console 0.4.0 is the fourth release of Wander, a small,
  decentralised, self-hosted web console that lets visitors to your
  website explore interesting websites and pages recommended by a
  community of independent website owners.  To try it, go
  to <a href="../../../wander/">susam.net/wander/</a>.
</p>
<figure>
  <img src="https://susam.github.io/blob/img/wander/wander-0.4.0.png"
       title="A screenshot of Wander Console 0.4.0">
  <figcaption>A screenshot of Wander Console 0.4.0</figcaption>
</figure>
<p>
  This release brings a few small additions as well as a few minor
  fixes.  You can find the previous release pages here:
  <a href="./">/code/news/wander/</a>.  The sections below
  discuss the current release.
</p>
<h2 id="contents">Contents<a href="#contents"></a></h2>
<ul>
  <li><a href="#wildcard-patterns">Wildcard Patterns</a></li>
  <li><a href="#the-via-query-parameter">The 'via' Query Parameter</a></li>
  <li><a href="#console-picker-algorithm">Console Picker Algorithm</a></li>
  <li><a href="#allow-links-that-open-in-new-tab">Allow Links that Open in New Tab</a></li>
  <li><a href="#community">Community</a></li>
</ul>
<h2 id="wildcard-patterns">Wildcard Patterns<a href="#wildcard-patterns"></a></h2>
<p>
  Wander Console now supports wildcard patterns in ignore lists.  An
  asterisk (<code>*</code>) anywhere in an ignore pattern matches zero
  or more characters in URLs.  For example, an ignore pattern
  like <code>https://*.midreadpopup.example/</code> can be used to
  ignore URLs such as this:
</p>
<ul>
  <li><code>https://alice.midreadpopup.example/</code></li>
  <li><code>https://bob.jones.midreadpopup.example/</code></li>
</ul>
<p>
  These ignore patterns are specified in a console's
  <a href="../../../wander/wander.js">wander.js</a> file.  These are
  very important for providing a good wandering experience to
  visitors.  The owner of a console decides what links they want to
  ignore in their ignore patterns.  The ignore list typically contains
  commercial websites that do not fit the spirit of the small web, as
  well as defunct or incompatible websites that do not load in the
  console.  A console with a well maintained ignore list ensures that
  a visitor to that console has a lower likelihood of encountering
  commercial or broken websites.
</p>
<p>
  For a complete description of the ignore patterns, see
  <a href="https://codeberg.org/susam/wander#customise-ignore-list">Customise
  Ignore List</a>.
</p>
<h2 id="the-via-query-parameter">The 'via' Query Parameter<a href="#the-via-query-parameter"></a></h2>
<p>
  By <a href="https://codeberg.org/susam/wander/issues/1#issuecomment-11795493">popular
  demand</a>, Wander now adds a <code>via=</code> query parameter
  while loading a recommended web page in the console.  The value of
  this parameter is the console that loaded the recommended page.  For
  example, if you encounter <a href="https://midnight.pub/">midnight.pub/</a>
  while using the console at <a href="../../../wander/">susam.net/wander/</a>,
  the console loads the page using the following URL:
</p>
<pre><code>https://midnight.pub/?via=https://susam.net/wander/</code></pre>
<p>
  This allows the owner of the recommended website to see, via their
  access logs, that the visit originated from a Wander Console.  While
  this is the default behaviour now, it can be customised in two ways.
  The value can be changed from the full URL of the Wander Console to
  a small identifier that identifies the version of Wander Console
  used (e.g. <code>via=wander-0.4.0</code>).  The query parameter can
  be disabled as well.  For more details,
  see <a href="https://codeberg.org/susam/wander#customise-via-parameter">Customise
  'via' Parameter</a>.
</p>
<h2 id="console-picker-algorithm">Console Picker Algorithm<a href="#console-picker-algorithm"></a></h2>
<p>
  In earlier versions of the console, when a visitor came to your
  console to explore the Wander network, it picked the first
  recommendation from the list of recommended pages in it
  (i.e. your <code>wander.js</code> file).  But subsequent
  recommendations came from your neighbours' consoles and then their
  neighbours' consoles and so on recursively.  Your console (the
  starting console) was not considered again unless some other console
  in the network linked back to your console.
</p>
<p>
  A common way to ensure that your console was also considered in
  subsequent recommendations too was to add a link to your console in
  your own console (i.e. in your <code>wander.js</code>).  Yes, this
  created self-loops in the network but this wasn't considered a
  problem.  In fact, this was considered desirable, so that when the
  console picked a console from the pool of discovered consoles to
  find the next recommendation, it considered itself to be part of the
  pool.  This workaround is no longer necessary.
</p>
<p>
  Since version 0.4.0 of Wander, each console will always consider
  itself to be part of the pool from which it picks consoles.  This
  means that the web pages recommended by the starting console have a
  fair chance of being picked for the next web page recommendation.
</p>
<h2 id="allow-links-that-open-in-new-tab">Allow Links that Open in New Tab<a href="#allow-links-that-open-in-new-tab"></a></h2>
<p>
  The Wander Console loads the recommended web pages in an
  <code>&lt;iframe&gt;</code> element that has sandbox restrictions
  enabled.  The sandbox properties restrict the side effects the
  loaded web page can have on the parent Wander Console window.  For
  example, with the sandbox restrictions enabled, a loaded web page
  cannot redirect the parent window to another website.  In fact,
  these days most modern browsers block this and show a warning
  anyway, but we also block this at a sandbox level too in the console
  implementation.
</p>
<p>
  It turned out that our aggressive sandbox restrictions also blocked
  legitimate websites from opening a link in a new tab.  We decided
  that opening a link in a new tab is harmless behaviour and we have
  relaxed the sandbox restrictions a little bit to allow it.  Of
  course, when you click such a link within Wander console, the link
  will open in a new tab of your web browser (not within Wander
  Console, as the console does not have any notion of tabs).
</p>
<h2 id="community">Community<a href="#community"></a></h2>
<p>
  Although I developed this project on a whim, one early morning while
  taking a short break from my <a href="../../../26c.html">ongoing
  studies</a> of algebraic graph theory, the subsequent warm
  reception <a href="https://news.ycombinator.com/item?id=47422759">on
  Hacker News</a> and <a href="https://lobste.rs/s/hjipba">Lobsters</a>
  has led to a growing community of Wander Console owners.  There are
  two places where the community hangs out at the moment:
</p>
<ul>
  <li>
    New consoles are announced in this thread on Codeberg:
    <a href="https://codeberg.org/susam/wander/issues/1">Share Your
    Wander Console</a>.
  </li>
  <li>
    We also have an Internet Relay Chat (IRC) channel
    named <a href="http://web.libera.chat/#wander">#wander</a> on the
    Libera IRC network.  This is a channel for people who enjoy
    building personal websites and want to talk to each other.  You
    are welcome to join this channel, share your console URL, link to
    your website or recent articles as well as share links to other
    non-commercial personal websites.
  </li>
</ul>
<p>
  If you own a personal website but you have not set up a Wander
  Console yet, I suggest that you consider setting one up for
  yourself.  You can see what it looks like by visiting mine at
  <a href="../../../wander/">/wander/</a>.  To set up your
  own, follow these
  instructions: <a href="https://codeberg.org/susam/wander#install">Install</a>.
  It just involves copying two files to your web server.  It is about
  as simple as it gets.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/code/news/wander/0.4.0.html">Read on website</a> |
  <a href="https://susam.net/tag/web.html">#web</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a>
</p>
]]>
</description>
</item>
<item>
<title>Mar '26 Notes</title>
<link>https://susam.net/26c.html</link>
<guid isPermaLink="false">mtsnt</guid>
<pubDate>Mon, 30 Mar 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  This is my third set of <a href="tag/notes.html">monthly notes</a>
  for this year.  In these notes, I capture various interesting facts
  and ideas I have stumbled upon during the month.  Like in the last
  two months, I have been learning and exploring algebraic graph
  theory.  The two main books I have been reading are <em>Algebraic
  Graph Theory</em> by Godsil and Royle and <em>Algebraic Graph
  Theory</em>, 2nd ed. by Norman Biggs.  Much of what appears here
  comes from my study of these books as well as my own explorations
  and attempts to distill the ideas.  This post is quite heavy on
  mathematics but there are some non-mathematical, computing-related
  notes towards the end.
</p>
<p>
  The level of exposition is quite uneven throughout these notes.
  After all, they aren't meant to be a polished exposition but rather
  notes I take for myself.  In some places I build concepts from first
  principles, while in others I gloss over details and focus only on
  the main results.
</p>
<p>
  Sometime during the second half of the month, I also
  developed an open-source tool called
  <a href="https://codeberg.org/susam/wander">Wander Console</a> on a
  whim.  It lets anyone with a website host a decentralised web
  console that recommends interesting websites from the 'small web' of
  independent, personal websites.  Check my console
  here: <a href="wander/">wander/</a>.
</p>
<p>
  Although the initial version was ready after just about 1.5 hours of
  development during a break I was taking from studying algebraic
  graph theory, the
  subsequent <a href="https://news.ycombinator.com/item?id=47422759">warm
  reception on Hacker News</a> and a
  <a href="https://codeberg.org/susam/wander/issues/1">growing
  community</a> around it, along with the resulting feature requests
  and bug fixes, ended up taking more time than I had anticipated, at
  the expense of my algebraic graph theory studies.  With a full-time
  job, it becomes difficult to find time for both open source
  development and mathematical studies.  But eventually, I managed to
  return to my studies while making Wander Console improvements only
  occasionally during breaks from my studies.
</p>
<h2 id="contents">Contents<a href="#contents"></a></h2>
<ol>
  <li><a href="#group-theory">Group Theory</a>
    <ol type="a">
      <li><a href="#permutation">Permutation</a></li>
      <li><a href="#group-homomorphism">Group Homomorphism</a></li>
      <li><a href="#group-homomorphism-preserves-identities">Group Homomorphism Preserves Identity</a></li>
      <li><a href="#group-homomorphism-preserves-inverses">Group Homomorphism Preserves Inverses</a></li>
      <li><a href="#image-of-a-group-homomorphism">Image of a Group Homomorphism</a></li>
      <li><a href="#group-monomorphism">Group Monomorphism</a>
        <ol type="i">
          <li><a href="#standard-proof">Standard Proof</a></li>
          <li><a href="#alternate-arrangement">Alternate Proof</a></li>
        </ol>
      </li>
      <li><a href="#permutation-representation">Permutation Representation</a></li>
      <li><a href="#group-action">Group Action</a>
        <ol type="i">
          <li><a href="#why-right-action">Why Right Action?</a></li>
          <li><a href="#group-action-example-1">Example 1</a></li>
          <li><a href="#group-action-example-2">Example 2</a></li>
        </ol>
      </li>
      <li><a href="#group-actions-induce-permutations">Group Actions Induce Permutations</a></li>
      <li><a href="#group-actions-determine-permutation-representations">Group Actions Determine Permutation Representations</a></li>
      <li><a href="#permutation-representations-determine-group-actions">Permutation Representations Determine Group Actions</a></li>
      <li><a href="#bijection-between-group-actions-and-permutation-representations">Bijection Between Group Actions and Permutation Representations</a></li>
      <li><a href="#orbits">Orbits</a></li>
      <li><a href="#stabilisers">Stabilisers</a></li>
      <li><a href="#orbit-stabiliser-theorem">Orbit-Stabiliser Theorem</a></li>
      <li><a href="#faithful-actions">Faithful Actions</a></li>
      <li><a href="#semiregular-actions">Semiregular Actions</a></li>
      <li><a href="#transitive-actions">Transitive Actions</a></li>
      <li><a href="#conjugacy">Conjugacy</a>
        <ol type="i">
          <li><a href="#conjugation-as-group-action">Conjugation as Group Action</a></li>
          <li><a href="#right-conjugation-vs-left-conjugation">Right Conjugation vs Left Conjugation</a></li>
        </ol>
      </li>
      <li><a href="#conjugate-groups">Conjugate Subgroups</a></li>
      <li><a href="#conjugacy-of-stabilisers">Conjugacy of Stabilisers</a></li>
    </ol>
  </li>
  <li><a href="#algeraic-graph-theory">Algebraic Graph Theory</a>
    <ol type="a">
      <li><a href="#stabiliser-index">Stabiliser Index</a></li>
      <li><a href="#strongly-connected-directed-graph">Strongly Connected Directed Graph</a></li>
      <li><a href="#shunting">Shunting</a></li>
      <li><a href="#automorphisms-preserve-successor-relation">Automorphisms Preserve Successor Relation</a></li>
      <li><a href="#test-of-s-arc-transitivity">Test of \( s \)-arc Transitivity</a></li>
      <li><a href="#moore-graphs">Moore Graphs</a></li>
      <li><a href="#generalised-polygons">Generalised Polygons</a></li>
    </ol>
  </li>
  <li><a href="#computing">Computing</a>
    <ol type="a">
      <li><a href="#select-between-lines-inclusive">Select Between Lines, Inclusive</a></li>
      <li><a href="#select-between-lines-exclusive">Select Between Lines, Exclusive</a></li>
      <li><a href="#signing-and-verification-with-ssh-key">Signing and Verification with SSH Key</a></li>
      <li><a href="#block-ip-address-with-nftables">Block IP Address with nftables</a></li>
      <li><a href="#debian-logrotate-setup">Debian Logrotate Setup</a></li>
    </ol>
  </li>
</ol>
<h2 id="group-theory">Group Theory<a href="#group-theory"></a></h2>
<h3 id="permutation">Permutation<a href="#permutation"></a></h3>
<p>
  A <em>permutation</em> of a set \( X \) is a bijection \( X \to X
 .  \)
</p>
<p>
  For example, take \( X = \{ 1, 2, 3, 4, 5, 6 \} \) and define the
  map

  \[
    \pi : X \to X; \; x \mapsto 1 + ((x + 1) \bmod 6).
  \]

  This maps

  \begin{align*}
    1 &amp;\mapsto 3, \\
    2 &amp;\mapsto 4, \\
    3 &amp;\mapsto 5, \\
    4 &amp;\mapsto 6, \\
    5 &amp;\mapsto 1, \\
    6 &amp;\mapsto 2.
  \end{align*}
</p>
<p>
  We can describe permutations more succinctly using cycle notation.
  The cycle notation of a permutation \( \pi \) consists of one or
  more sequences written next to each other such that the sequences
  are pairwise disjoint and \( \alpha \) maps each element in a
  sequence to the next element on its right.  If the sequence is
  finite, then \( \alpha \) maps the final element back to the first
  one.  Any element that does not appear in any sequence is mapped to
  itself.  For example the cycle notation for the above permutation is
  \( (1 3 5)(2 4 6).  \)
</p>
<h3 id="group-homomorphism">Group Homomorphism<a href="#group-homomorphism"></a></h3>
<p>
  A map \( \phi : G \to H \) from a group \( (G, \ast) \) to a group
  \( (H, \cdot) \) is a <em>group homomorphism</em> if, for all \( x, y
  \in G, \)

  \[
    \phi(x \ast y) = \phi(x) \cdot \phi(y).
  \]

  We say that a group homomorphism is a map between groups that
  <em>preserves</em> the group operation.  In other words, a group
  homomorphism <em>sends</em> products in \( G \) to products in \( H
 .  \)  For example, consider the groups \( (\mathbb{Z}, +) \) and \(
  (\mathbb{Z}_3, +).  \)  Then the map

  \[
    \phi : \mathbb{Z} \to \mathbb{Z}_3; \; n \mapsto n \bmod 3
  \]

  is a group homomorphism because

  \[
    \phi(x + y)
    = (x + y) \bmod 3
    = (x \bmod 3) + (y \bmod 3)
    = \phi(x) + \phi(y)
  \]

   for all \( x, y \in \mathbb{Z}.  \)  As another example, consider
   the groups \( (\mathbb{R}_{\gt 0}, \times) \) and \( (\mathbb{R},
   +).  \)  Then the map

  \[
    \log : \mathbb{R}_{\gt 0} \to \mathbb{R}
  \]

  is a group homomorphism because

  \[
    \log(m \times n) = \log m + \log n.
  \]

  Note that a group homomorphism preserves the identity element.  For
  example, \( 1 \) is the identity element of \( (\mathbb{R}_{\gt 0},
  \times) \) and \( 0 \) is the identity element of \( (\mathbb{R}, +)
  \) and indeed \( \log 1 = 0.  \)  Also, a group homomorphism
  preserves inverses.  Indeed \( \log m^{-1} = -\log m \) for all \( m
  \in \mathbb{R}_{\gt 0}.  \)  These observations are proved in the
  next two sections.
</p>
<h3 id="group-homomorphism-preserves-identities">Group Homomorphism Preserves Identity<a href="#group-homomorphism-preserves-identities"></a></h3>
<p>
  Let \( \phi : G \to H \) be a group homomorphism from \( (G, \ast)
  \) to \( (H, \cdot).  \)  Let \( e_1 \) be the identity in \( G \)
  and let \( e_2 \) be the identity in \( H.  \)  Then \( \phi(e_1) =
  e_2.  \)


  The proof is straightforward.  Note first that

  \[
    \phi(e_1) \cdot \phi(e_1)
    = \phi(e_1 \ast e_1)
    = \phi(e_1)
  \]

  Multiplying both sides on the right by \( \phi(e_1)^{-1}, \) we get

  \[
    (\phi(e_1) \cdot \phi(e_1)) \cdot \phi(e_1)^{-1}
    = \phi(e_1) \cdot \phi(e_1)^{-1}.
  \]

  Using the associative and inverse properties of groups, we can
  simplify both sides to get

  \[
    \phi(e_1) = e_2.
  \]
</p>
<h3 id="group-homomorphism-preserves-inverses">Group Homomorphism Preserves Inverses<a href="#group-homomorphism-preserves-inverses"></a></h3>
<p>
  Let \( \phi : G \to H \) be a group homomorphism from \( (G, \ast)
  \) to \( (H, \cdot).  \)  Let \( e_1 \) be the identity in \( G \)
  and let \( e_2 \) be the identity in \( H.  \)  Then for all \( x \in
  G, \) \(\phi(x^{-1}) = (\phi(x))^{-1}.  \)

  The proof of this is straightforward too.  Note that

  \[
    \phi(x) \cdot \phi(x^{-1})
    = \phi(x \ast x^{-1})
    = \phi(e_1)
    = e_2.
  \]

  Thus \( \phi(x^{-1}) \) is an inverse of \( \phi(x), \) so

  \[
    \phi(x^{-1}) = (\phi(x))^{-1}.
  \]

  The image of the inverse of an element is the inverse of the image
  of that element.
</p>
<h3 id="image-of-a-group-homomorphism">Image of a Group Homomorphism<a href="#image-of-a-group-homomorphism"></a></h3>
<p>
  Let \( \phi : G \to H \) be a group homomorphism.  Then the image of
  the \( \phi, \) denoted

  \[
    \phi(G) = \{ \phi(x) : x \in G \}
  \]

  is a subgroup of \( H.  \)  We will prove this now.
</p>
<p>
  Let \( a, b \in \phi(G).  \)  Then \( a = \phi(x) \) and \( b =
  \phi(y) \) for some \( x, y \in G.  \)  Now \( ab = \phi(x)\phi(y) =
  \phi(xy) \in \phi(G).  \)  Therefore \( \phi(G) \) satisfies the
  closure property.
</p>
<p>
  Let \( e_1 \) and \( e_2 \) be the identities in \( G \) and \( H \)
  respectively.  Since a group homomorphism preserves the identity, \(
  \phi(e_1) = e_2.  \)  Hence the identity of \( H \) lies in \(
  \phi(G).  \)
</p>
<p>
  Finally, let \( a \in \phi(G).  \)  Then \( a = \phi(x) \) for some
  \( x \in G.  \)  Then \( a^{-1} = \phi(x)^{-1} = \phi(x^{-1}) \in
  \phi(G).  \)  Therefore \( \phi(G) \) satisfies the inverse property
  as well.  Therefore \( \phi(G) \) is a subgroup of \( H.  \)
</p>
<h3 id="group-monomorphism">Group Monomorphism<a href="#group-monomorphism"></a></h3>
<p>
  A map \( \phi : G \to H \) from a group \( (G, \ast) \) to a group
  \( (H, \cdot) \) is a <em>group monomorphism</em> if \( \phi \) is a
  homomorphism and is injective.  In other words, a homomorphism \(
  \phi \) is called a monomorphism if, for all \( x, y \in G, \)

  \[
  \phi(x) = \phi(y) \implies x = y.
  \]

  Let \( e_1 \) be the identity element of \( G \) and let \( e_2 \)
  be the identity element of \( H.  \)  A useful result in group theory
  states that a homomorphism \( \phi : G \to H \) is a monomorphism if
  and only if its kernel is trivial, i.e.

  \[
    \ker(\phi) = \{ x \in G : \phi(x) = e_2 \} = \{ e_1 \}.
  \]

  Let us prove this now.
</p>
<h4 id="standard-proof">Standard Proof<a href="#standard-proof"></a></h4>
<p>
  Suppose \( \phi : G \to H \) is a monomorphism.  Since a
  homomorphism preserves the identity element, we have \( \phi(e_1) =
  e_2.  \)  Therefore

  \[
    e_1 \in \ker(\phi).
  \]

  Let \( x \in \ker(\phi).  \)  Then \( \phi(x) = e_2 = \phi(e_1).  \)
  Since \( \phi \) is injective, \( x = e_1.  \)  Therefore

  \[
    \ker(\phi) = \{ e_1 \}.
  \]

  Conversely, suppose \( \ker(\phi) = \{ e_1 \}.  \)  Let \( x, y \in G
  \) such that \( \phi(x) = \phi(y).  \)  Then

  \[
    \phi(x \ast y^{-1})
    = \phi(x) \cdot \phi(y^{-1})
    = \phi(x) \cdot (\phi(y))^{-1}
    = \phi(y) \cdot (\phi(y))^{-1}
    = e_2.
  \]

  Hence

  \[
    x \ast y^{-1} \in \ker(\phi) = \{ e_1 \},
  \]

  so

  \[
    x \ast y^{-1} = e_1.
  \]

  Multiplying both sides on the right by \( y, \) we obtain

  \[
    x = y.
  \]

  This completes the proof.
</p>
<h4 id="alternate-arrangement">Alternate Proof<a href="#alternate-arrangement"></a></h4>
<p>
  Here I briefly discuss an alternate way to think about the above
  proof.  The above proof is how most texts usually present these
  arguments.  In particular, the proof of injectivity typically
  proceeds by showing that equal images imply equal preimages.  It's a
  standard proof technique.  When I think about these proofs, however,
  the contrapositive argument feels more intuitive to me.  I prefer to
  think about how unequal preimages must have unequal images.
  Mathematically, there is no difference at all but the contrapositive
  argument has always felt the most natural to me.  Let me briefly
  describe how this proof runs in my mind when I think about it more
  intuitively.
</p>
<p>
  Suppose \( \phi \) is a monomophorism.  Since a homomorphism
  preserves the identity element, clearly \( \phi(e_1) = e_2.  \)
  Since \( \phi \) is injective, it cannot map two distinct elements
  of \( G \) to \( e_2.  \)  Thus \( e_1 \) is the only element of \( G
  \) that \( \phi \) maps to \( e_2 \) which means \( \ker(\phi) = \{
  e_1 \}.  \)
</p>
<p>
  To prove the converse, suppose \( \ker(\phi) = \{ e_1 \}.  \)
  Consider distinct elements \( x, y \in G.  \)  Since \( x \ne y, \)
  we have \( x \ast y^{-1} \ne e_1.  \)  Therefore \( x \ast y^{-1}
  \notin \ker(\phi).  \)  Thus \( \phi(x \ast y^{-1}) \ne e_2.  \)
  Since \( \phi \) is a homomorphism,

  \[
    \phi(x \ast y^{-1})
    = \phi(x) \cdot \phi(y^{-1})
    = \phi(x) \cdot \phi(y)^{-1}.
  \]

  Therefore \( \phi(x) \cdot \phi(y)^{-1} \ne e_2 \) which implies

  \[
    \phi(x) \ne \phi(y).
  \]

  This proves that \( \ker(\phi) = \{ e_1 \} \) implies that \( \phi
  \) is injective and thus a monomorphism.
</p>
<h3 id="permutation-representation">Permutation Representation<a href="#permutation-representation"></a></h3>
<p>
  Let \( G \) be a group and \( X \) a set.  Then a homomorphism

  \[
    \phi : G \to \operatorname{Sym}(X)
  \]

  is called a <em>permutation representation</em> of \( G \) on \( X
 .  \)  The homomorphism \( \phi \) maps each \( g \in G \) to a
  permutation of \( X.  \)  We say that each \( g \in G \)
  <em>induces</em> a permutation of \( X.  \)
</p>
<p>
  For example, let \( G = (\mathbb{Z}_3, +) \) and \( X = \{ 0, 1, 2,
  3, 4, 5 \}.  \)  Define the map \( \phi : G \to \operatorname{Sym}(X)
  \) by

  \begin{align*}
    \phi(0) &amp;= (), \\
    \phi(1) &amp;= (024)(135), \\
    \phi(2) &amp;= (042)(153).
  \end{align*}

  It is easy to verify that this is a homomorphism.  Here is one way
  to verify it:

  \begin{align*}
    \phi(0)\phi(1) &amp;= ()(024)(135) = (024)(135) = \phi(0 + 1), \\
    \phi(0)\phi(2) &amp;= ()(042)(153) = (042)(153) = \phi(0 + 2), \\
    &amp;\;\,\vdots \\
    \phi(2)\phi(1) &amp;= (042)(153)(024)(135) = () = \phi(0) = \phi(2 + 1), \\
    \phi(2)\phi(2) &amp;= (042)(153)(042)(153) = (024)(135) = \phi(1) = \phi(2 + 2).
  \end{align*}

  We will meet this homomorphism again in the form of group action \(
  \alpha \) in the next section.
</p>
<h3 id="group-action">Group Action<a href="#group-action"></a></h3>
<p>
  Let \( G \) be a group with identity element \( e.  \)  Let \( X \)
  be a set.  A right action of \( G \) on \( X \) is a map

  \[
    \alpha : X \times G \to X
  \]

  such that

  \begin{align*}
    \alpha(x, e)            &amp;= x, \\
    \alpha(\alpha(x, g), h) &amp;= \alpha(x, gh)
  \end{align*}

  for all \( x \in X \) and all \( g, h \in G.  \)  The two conditions
  above are called the identity and compatibility properties of the
  group action respectively.  Note that in a right action, the product
  \( gh \) is applied left to right: \( g \) acts first and then \( h
  \) acts.  If we denote \( \alpha(x, g) \) as \( x^g, \) then the
  notation for the two conditions can be simplified to \( x^e = x \)
  and \( (x^g)^h = x^{gh} \) for all \( g, h \in G.  \)
</p>
<h4 id="why-right-action">Why Right Action?<a href="#why-right-action"></a></h4>
<p>
  We discuss right group actions here instead of left group actions
  because we want to use the notation \( \alpha(x, g) = x^g, \) which
  is quite convenient while studying permutations and graph
  automorphisms.  It is perfectly possible to use left group actions
  to study permutations as well.  However, we lose the benefit of the
  convenient \( x^g \) notation.  In a left group action, the
  compatibility property is \( \alpha(g, \alpha(h, x)) = \alpha(gh, x)
 , \) so if we were to use the notation \( \alpha(g, x) = x^g, \) the
  compatibility property would look like \( (x^h)^g = x^{gh}.  \)  This
  reverses the order of exponents which can be confusing.  Right group
  actions avoid this notational inconvenience.
</p>
<h4 id="group-action-example-1">Example 1<a href="#group-action-example-1"></a></h4>
<p>
  Let \( G = \mathbb{Z}_3 \) be the group under addition modulo \( 3
 .  \)  Let \( X = \{ 0, 1, 2, 3, 4, 5 \}.  \)  Define an action \(
  \alpha \) of \( G \) on \( X \) by

  \[
    \alpha(x, g) = x^g = (x + 2g) \bmod 6.
  \]

  Each \( g \in G \) acts as a permutation of \( X.  \)  For example,
  the element \( 0 \in \mathbb{Z}_3 \) acts as the identity
  permutation.  The element \( 1 \in \mathbb{Z}_3 \) acts as the
  permutation \( (0 2 4)(1 3 5).  \)  The element \( 2 \in \mathbb{Z}_3
  \) acts as the permutation \( (0 4 2)(1 5 3).  \)  The following
  table shows how each \( g \in G \) permutes \( X.  \)

  \[
    \begin{array}{c|ccc}
      x_{\downarrow} \backslash g_{\rightarrow} &amp; 0 &amp; 1 &amp; 2 \\
      \hline
      0 &amp; 0 &amp; 2 &amp; 4 \\
      1 &amp; 1 &amp; 3 &amp; 5 \\
      2 &amp; 2 &amp; 4 &amp; 0 \\
      3 &amp; 3 &amp; 5 &amp; 1 \\
      4 &amp; 4 &amp; 0 &amp; 2 \\
      5 &amp; 5 &amp; 1 &amp; 3 \\
    \end{array}
  \]

  From the table we see that each \( g \in G \) permutes the elements
  of \( \{ 0, 2, 4 \} \) among themselves.  Similarly, the elements of
  \( \{ 1, 3, 5 \} \) are permuted among themselves.  These sets \(
  \{0, 2, 4 \} \) and \( \{ 1, 3, 5 \} \) are called the
  <em>orbits</em> of the action.  The concept of orbits is formally
  introduced in its <a href="#orbits">own section further below</a>.
</p>
<h4 id="group-action-example-2">Example 2<a href="#group-action-example-2"></a></h4>
<p>
  Now let \( G = \mathbb{Z}_6 \) be the group under addition modulo \(
  6.  \)  Let \( X = \{ 0, 1, \dots, 8 \}.  \)  Define an action \(
  \beta \) of \( G \) on \( X \) by

  \[
    \beta(x, g) = x^g = (x + 3g) \bmod 9.
  \]

  Now the table for the action looks like this:

  \[
    \begin{array}{c|cccccc}
      x_{\downarrow} \backslash g_{\rightarrow} &amp; 0 &amp; 1 &amp; 2 &amp; 3 &amp; 4 &amp; 5 \\
      \hline
      0 &amp; 0 &amp; 3 &amp; 6 &amp; 0 &amp; 3 &amp; 6 \\
      1 &amp; 1 &amp; 4 &amp; 7 &amp; 1 &amp; 4 &amp; 7 \\
      2 &amp; 2 &amp; 5 &amp; 8 &amp; 2 &amp; 5 &amp; 8 \\
      3 &amp; 3 &amp; 6 &amp; 0 &amp; 3 &amp; 6 &amp; 0 \\
      4 &amp; 4 &amp; 7 &amp; 1 &amp; 4 &amp; 7 &amp; 1 \\
      5 &amp; 5 &amp; 8 &amp; 2 &amp; 5 &amp; 8 &amp; 2 \\
      6 &amp; 6 &amp; 0 &amp; 3 &amp; 6 &amp; 0 &amp; 3 \\
      7 &amp; 7 &amp; 1 &amp; 4 &amp; 7 &amp; 1 &amp; 4 \\
      8 &amp; 8 &amp; 2 &amp; 5 &amp; 8 &amp; 2 &amp; 5
    \end{array}
  \]

  This action splits \( X \) into three orbits \( \{ 0, 3, 6 \}, \) \(
  \{ 1, 4, 7 \} \) and \( \{ 2, 5, 8 \}.  \)
</p>
<h3 id="group-actions-induce-permutations">Group Actions Induce Permutations<a href="#group-actions-induce-permutations"></a></h3>
<p>
  Earlier, we saw an example of a group action and observed that each
  element of the group acts as a permutation.  That was not merely a
  coincidence.  It is indeed a general property of group actions.
  Whenever a group \( G \) acts on a set \( X, \) each element \( g
  \in G \) determines a bijection \( X \to X.  \)  In other words,
  every element of \( G \) acts as a permutation of \( X.  \)  Let us
  see why this must be the case.
</p>
<p>
  Consider the group action \( \alpha : X \times G \to X.  \)  Fix \( g
  \in G \) and let \( x \) vary over \( X \) to obtain the map

  \[
    \alpha_g : X \to X; \; x \mapsto \alpha(x, g).
  \]

  We show that \( \alpha_g \) is a bijection.  First we prove
  injectivity.  Let \( e \) be the identity element of \( G.  \)
  Let \( x, y \in X.  \)  Then

  \begin{align*}
    \alpha_g(x) = \alpha_g(y)
    &amp; \implies \alpha(x, g) = \alpha(y, g) \\
    &amp; \implies \alpha(\alpha(x, g), g^{-1}) = \alpha(\alpha(y, g), g^{-1}) \\
    &amp; \implies \alpha(x, gg^{-1}) = \alpha(y, gg^{-1}) \\
    &amp; \implies \alpha(x, e) = \alpha(y, e) \\
    &amp; \implies x = y.
  \end{align*}

  The \( x^g \) notation allows us to write the above proof more
  conveniently as follows:

  \begin{align*}
    \alpha_g(x) = \alpha_g(y)
    &amp; \implies \alpha(x, g) = \alpha(y, g) \\
    &amp; \implies (x^g)^{g^{-1}} = (y^g)^{g^{-1}} \\
    &amp; \implies x^{g g^{-1}} = y^{g g^{-1}} \\
    &amp; \implies x^e = y^e \\
    &amp; \implies x = y.
  \end{align*}

  This completes the proof of injectivity.  Now we prove surjectivity.
  Let \( y \in X.  \)  Take \( x = \alpha(y, g^{-1}).  \)  Then

  \[
    \alpha_g(x)
    = \alpha(x, g)
    = \alpha(\alpha(y, g^{-1}), g)
    = \alpha(y, g^{-1} g)
    = \alpha(y, e)
    = y.
  \]

  Again, if we write \( x = y^{g^{-1}}, \) the above step can be
  written more succinctly as

  \[
    \alpha_g(x) = x^g = (y^{g^{-1}})^g = y^{(g^{-1} g)} = y^e = y.
  \]

  Thus every element \( y \in X \) has a preimage in \( X \) under \(
  \alpha_g.  \)  Hence \( \alpha_g \) is surjective.  Since we have
  already shown that \( \alpha_g \) is injective, we now conclude that
  \( \alpha_g \) is bijective.  Therefore \( \alpha_g \) is a
  permutation of \( X.  \)  Stated symbolically,

  \[
    \alpha_g \in \operatorname{Sym}(X).
  \]

  Note that

  \[
    \alpha_g(x) = \alpha(x, g) = x^g.
  \]

  Thus both \( \alpha_g(x) \) and \( x^g \) serve as convenient
  shorthands for \( \alpha(x, g).  \)
</p>
<h3 id="group-actions-determine-permutation-representations">Group Actions Determine Permutation Representations<a href="#group-actions-determine-permutation-representations"></a></h3>
<p>
  We have seen that each group element \( g \in G \) induces (acts as)
  a permutation of \( X.  \)  Precisely speaking, each \( g \in G \)
  determines a permutation \( \alpha_g \) of \( X.  \)  Now define a
  map

  \[
    \phi: G \to \operatorname{Sym}(X); \; g \mapsto \alpha_g.
  \]

  We now show that this map is a homomorphism.  This means that we
  want to show that \( \phi(gh) = \phi(g) \phi(h).  \)  Since \(
  \phi(g), \phi(h) \in \operatorname{Sym}(X), \) the right-hand side
  is a product of permutations of \( X.  \)  We first define the
  product of two permutations \( \pi, \rho : X \to X \) by

  \[
    \pi \rho : X \to X; \; x \mapsto \rho(\pi(x)).
  \]

  In other words, \( \pi \rho = \rho \circ \pi.  \)  Now

  \begin{align*}
    \phi(gh)(x)
    &amp; = \alpha_{gh}(x) \\
    &amp; = \alpha(x, gh) \\
    &amp; = \alpha(\alpha(x, g), h) \\
    &amp; = \alpha_h(\alpha_g(x)) \\
    &amp; = (\alpha_h \circ \alpha_g)(x) \\
    &amp; = (\alpha_g \alpha_h)(x) \\
    &amp; = (\phi(g) \phi(h))(x).
  \end{align*}

  Since the above equality holds for all \( x \in X, \) we conclude
  that

  \[
    \phi(gh) = \phi(g) \phi(h).
  \]

  Hence \( \phi \) is a group homomorphism from \( G \) to \(
  \operatorname{Sym}(X).  \)  Therefore \( \phi \) is a permutation
  representation of \( G \) on \( X.  \)  It maps each group element \(
  g \in G \) to a permutation \( \alpha_g \in \operatorname{Sym}(X).  \)
</p>
<p>
  Note the multiple levels of abstraction here.  The group action \(
  \alpha : X \times G \to X \) determines a permutation representation
  \( \phi : G \to \operatorname{Sym}(X).  \)  Each element \( g \in G
  \) together with the group action \( \alpha \) determines a
  permutation \( \alpha_g : X \to X.  \)
</p>
<p>
  Also note that \( \phi(g)(x) = \alpha_g(x) = \alpha(g, x) = x^g.  \)
  In fact, \( \phi(g) = \alpha_g.  \)
</p>
<h3 id="permutation-representations-determine-group-actions">Permutation Representations Determine Group Actions<a href="#permutation-representations-determine-group-actions"></a></h3>
<p>
  Consider a permutation representation \( \phi : G \to
  \operatorname{Sym}(X).  \)  Define a map

  \[
    \alpha : X \times G \to X; \; (x, g) \mapsto \phi(g)(x).
  \]

  First we verify the identity property of group actions.  Since \(
  \phi \) is a homomorphism, it preserves the identity element.
  Therefore \( \phi(e) \) is the identity permutation.  Hence

  \[
    \alpha(x, e) = \phi(e)(x) = x
  \]

  Now we verify the compatibility property of the action.  For all \(
  g, h \in G \) and \( x \in X, \) we have

  \begin{align*}
    \alpha(\alpha(x, g), h)
    &amp; = \alpha(\phi(g)(x), h) \\
    &amp; = \phi(h)(\phi(g)(x)) \\
    &amp; = (\phi(h) \circ \phi(g))(x) \\
    &amp; = (\phi(g)\phi(h))(x) \\
    &amp; = \phi(gh)(x) \\
    &amp; = \alpha(x, gh).
  \end{align*}

  This completes the proof of the fact that every permutation
  representation determines a group action.
</p>
<h3 id="bijection-between-group-actions-and-permutation-representations">Bijection Between Group Actions and Permutation Representations<a href="#bijection-between-group-actions-and-permutation-representations"></a></h3>
<p>
  There is a bijection between the group actions \( \alpha : X \times
  G \to X \) and permutation representations \( \phi : G \to
  \operatorname{Sym}(X).  \)  We now show that these two constructions
  are inverses of each other.
</p>
<p>
  Given a right action \( \alpha : X \times G \to X, \) define

  \[
    \phi_{\alpha} : G \to \operatorname{Sym}(X)
    \quad \text{by} \quad
    \phi_{\alpha}(g)(x) = \alpha(x, g).
  \]

  Given a permutation representation \( \phi : G \to
  \operatorname{Sym}(X), \) define

  \[
    \alpha_{\phi} : X \times G \to X
    \quad \text{by} \quad
    \alpha_{\phi}(x, g) = \phi(g)(x).
  \]

  We now show that these two constructions undo each other.  Take an
  arbitrary group action \( \alpha : X \times G \to X \) and construct
  the corresponding permutation representation \( \phi_{\alpha}.  \)
  Then take this permutation representation and construct the group
  action \( \alpha_{\phi_{\alpha}}.  \)  But

  \[
    \alpha_{\phi_{\alpha}}(x, g)
    = \phi_{\alpha}(g)(x)
    = \alpha(x, g).
  \]

  Therefore \( \alpha_{\phi_{\alpha}} = \alpha.  \)  Similarly,
  starting with the permutation representation \( \phi, \) we get

  \[
    \phi_{\alpha_{\phi}}(g)(x)
    = \alpha_{\phi}(x, g)
    = \phi(g)(x).
  \]

  Therefore \( \phi_{\alpha_{\phi}} = \phi.  \)  Hence there is a
  bijection between group actions \( \alpha : X \times G \to X \) and
  permutation representations: \( \phi : G \to \operatorname{Sym}(X)
 .  \)  In fact, a group action and the corresponding permutation
  representation contain the same information, namely how the elements
  \( g \in G \) acts as permutations of \( X.  \)  For this reason,
  many advanced texts do not make any distinction between the group
  action and its permutation representation.  They often use them
  interchangeably even though technically they have different domains.
</p>
<h3 id="orbits">Orbits<a href="#orbits"></a></h3>
<p>
  Let \( G \) act on a set \( X.  \)  For an element \( x \in X, \) the
  <em>orbit</em> of \( x \) under the action of \( G \) is the set of
  all elements of \( X \) that can be reached from \( x \) by the
  action of elements of \( G.  \)  Symbolically, the orbit of \( x \)
  is the set

  \[
    x^G = \{ x^g : g \in G \}.
  \]

  In other words, the orbit of \( x \) contains every element of \( X
  \) that \( x \) can be moved to by the group action.  If \( y \in
  x^G, \) then there exists some \( g \in G \) such that \( y = x^g
 .  \)
</p>
<p>
  The orbits of a group action partition the set \( X.  \)  That is,
  every element of \( X \) lies in exactly one orbit and two orbits
  are either identical or disjoint.  Thus the group action decomposes
  the set \( X \) into disjoint subsets (the orbits), each consisting
  of elements that can be transformed into one another by the action
  of \( G.  \)
</p>
<h3 id="stabilisers">Stabilisers<a href="#stabilisers"></a></h3>
<p>
  Let \( G \) be a group acting on a set \( X.  \)  For an element
  \( x \in X, \) the <em>stabiliser</em> of \( x \) is the set

  \[
    G_x = \{ g \in G : x^g = x \}.
  \]

  The stabiliser \( G_x \) consists of all elements of \( G \) that
  fix the element \( x.  \)  The stabiliser \( G_x \) is a subgroup of
  \( G.  \)  Indeed, the identity element \( e \in G \) satisfies \(
  x^e = x, \) so \( e \in G_x.  \)  If \( g, h \in G_x, \) then \(
  x^{gh} = (x^g)^h = x.  \)  If \( g \in G_x, \) then \( x^{g^{-1}} =
  (x^g)^{g^{-1}} = x.  \)
</p>
<p>
  Intuitively, the stabiliser measures how much symmetry of the group
  action leaves the element \( x \) unchanged.  The larger the
  stabiliser, the more elements of \( G \) fix \( x.  \)
</p>
<h3 id="orbit-stabiliser-theorem">Orbit-Stabiliser Theorem<a href="#orbit-stabiliser-theorem"></a></h3>
<p>
  Let \( G \) be a group acting on a set \( X.  \)  The
  orbit-stabiliser theorem states that for any \( x \in X, \)

  \[
    \lvert G_x \rvert \cdot \lvert x^G \rvert = \lvert G \rvert.
  \]

  Stated differently, the index of the stabiliser \( G_x \) in the
  group \( G \) is given by

  \[
    [ G : G_x ]
    = \lvert G_x \backslash G \rvert
    = \lvert G \rvert / \lvert G_x \rvert
    = \lvert x^G \rvert.
  \]

  There is a bijection between the right cosets of \( G_x \) and the
  elements of \( x^G.  \)  Demonstrating this bijection proves the
  above equation.  We will work with right cosets of \( G_x.  \)
  Define

  \[
    \phi : G_x \backslash G \to x^G; \; G_x g \mapsto x^g.
  \]

  We want to show that \( \phi \) is a bijection.  But first we need
  to show that \( \phi \) is well defined.  A coset \( G_x g \in G_x
  \backslash G \) can also be written as

  \[
    G_x g = G_x h
  \]

  for some \( h \in G.  \)  If \( x^g \ne x^h, \) then \( \phi \) would
  not be well defined, since \( \phi \) must assign each coset in \(
  G_x \backslash G \) to exactly one element in the orbit \( x^G \) in
  order to be a function.  This can be shown using the following
  equivalences:

  \begin{align*}
    G_x g = G_x h
    &amp; \iff hg^{-1} \in G_x \\
    &amp; \iff x = x^{h g^{-1}} \\
    &amp; \iff x^g = x^h \\
  \end{align*}

  This proves two things at once.  The fact that

  \[
    G_x g = G_x h \implies x^g = x^h
  \]

  proves that when the same coset is written using two different
  representatives, the image does not change.  Therefore \( \phi \) is
  well defined.  Further

  \[
    x^g = x^h \implies G_x g = G_x h
  \]

  proves that \( \phi \) is injective.  To show that \( \phi \) is
  surjective, let \( y \in x^G.  \)  Then \( y = x^g \) for some \( g
  \in G.  \)  Since \( \phi(G_x g) = x^g, \) we get

  \[
    \phi(G_x g) = y.
  \]

  Thus every element of \( x^G \) is the image of some right coset \(
  G_x g \) under \( \phi.  \)  This completes the proof of a bijection
  between the right cosets of \( G_x \) and the elements of \( x^G.  \)
  Therefore \( \lvert G_x \backslash G \rvert = \lvert x^G \rvert \)
  and hence \( \lvert G \rvert / \lvert G_x \rvert = \lvert x^G \rvert
 , \) which establishes the orbit-stabiliser theorem.
</p>
<h3 id="faithful-actions">Faithful Actions<a href="#faithful-actions"></a></h3>
<p>
  Let \( G \) act on a set \( X.  \)  The action is called
  <em>faithful</em> if distinct elements of \( G \) induce distinct
  permutations of \( X.  \)  In other words, the only element of \( G
  \) that acts as the identity permutation of \( X \) is the identity
  element \( e \in G.  \)  Symbolically, the action is faithful if

  \[
    g \ne e \implies \exists x \in X, \; x^g \ne x.
  \]

  Equivalently,

  \[
    \forall x \in X, \; x^g = x \implies g = e.
  \]

  The action is faithful if the only element of \( G \) that fixes
  every element of \( X \) is the identity, i.e.

  \[
    \bigcap_{x \in X} G_x = \{ e \}.
  \]

  Recall that every group action determines a permutation
  representation \( \phi : G \to \operatorname{Sym}(X).  \)  From this
  point of view, the action is faithful precisely when the permutation
  representation is faithful, that is, when the homomorphism \( \phi
  \) is injective (or equivalently when \( \ker(\phi) = \{ e \} \)).
  In other words, the action is faithful if and only if the associated
  homomorphism \( \phi \) is a monomorphism.
</p>
<h3 id="semiregular-actions">Semiregular Actions<a href="#semiregular-actions"></a></h3>
<p>
  A group action of \( G \) on \( X \) is called <em>semiregular</em>
  if no non-identity element of \( G \) fixes any element of \( X.  \)
  In other words, whenever \( g \ne e, \) the permutation of \( X \)
  induced by \( g \) moves every element of \( X.  \)  Symbolically,

  \[
    g \ne e \implies \forall x \in X, \; x^g \ne x.
  \]

  Equivalently,

  \[
    \exists x \in X, \; x^g = x \implies g = e.
  \]

  The action is semiregular if

  \[
    \forall x \in X, \; G_x = \{ e \}.
  \]

  This is a stronger property than faithfulness.  Faithfulness only
  guarantees that when \( g \ne e, \) the element \( g \) moves at
  least one element of \( X.  \)  But semiregularity guarantees that
  when \( g \ne e, \) the element \( g \) moves every element of \( X
 .  \)  Therefore every semiregular action is faithful, but not every
  faithful action is semiregular.
</p>
<h3 id="transitive-actions">Transitive Actions<a href="#transitive-actions"></a></h3>
<p>
  Let \( G \) act on a set \( X.  \)  The action is called
  <em>transitive</em> if there is only one orbit.  In other words, the
  action is transitive if every element of \( X \) can be reached from
  any other element by the action of some element of \( G.  \)
  Symbolically, the action is transitive if

  \[
    \forall x, y \in X \; \exists g \in G, \; x^g = y.
  \]

  Equivalently, the action is transitive if

  \[
    x^G = X
  \]

  for some (and hence every) \( x \in X.  \)
</p>
<h3 id="conjugacy">Conjugacy<a href="#conjugacy"></a></h3>
<p>
  Let \( G \) be a group.  Let \( x, g \in G.  \)  The element

  \[
    g^{-1} x g
  \]

  is called a <em>conjugate</em> of \( x \) by \( g.  \)  Any element
  \( y \in G \) that can be written as \( g^{-1} x g \) for some \( g
  \in G \) is said to be a conjugate of \( x.  \)  The conjugacy class
  of \( x \) in \( G \) is the set

  \[
    x^G = \{ g^{-1} x g : g \in G \}.
  \]

  In other words, the conjugacy class of \( x \) is the set of all
  elements of \( G \) that are conjugate to \( x.  \)  At first,
  reusing the orbit notation \( x^G \) for the conjugacy class may
  seem like an abuse of notation.  However, we will see in the next
  section that the conjugacy class is precisely the orbit of \( x \)
  under the action of \( G \) on itself by conjugation.  Thus \( x^G
  \) is in fact a natural and accurate notation for the conjugacy
  class.
</p>
<h4 id="conjugation-as-group-action">Conjugation as Group Action<a href="#conjugation-as-group-action"></a></h4>
<p>
  Conjugation can be seen as an action of a group on itself.  Define
  the map

  \[
    \alpha : G \times G \to G; \; (x, g) \mapsto g^{-1} x g.
  \]

  Note that

  \[
    \alpha(x, e) = e^{-1} x e = x
  \]

  and

  \[
    \alpha(\alpha(x, g), h)
    = h^{-1} (g^{-1} x g) h
    = (gh)^{-1} x (gh)
    = \alpha(x, gh).
  \]

  Therefore \( \alpha \) satisfies the two defining properties of a
  right group action.  The conjugacy class \( x^G \) is precisely the
  orbit of \( x \) under the conjugation action.  Therefore the orbits
  of the conjugation action of \( G \) on itself are the conjugacy
  classes of \( G.  \)
</p>
<h4 id="right-conjugation-vs-left-conjugation">Right Conjugation vs Left Conjugation<a href="#right-conjugation-vs-left-conjugation"></a></h4>
<p>
  We observed above that the conjugation action is a right action of a
  group on itself.  Let \( x, g \in G \) and let

  \[
    y = g^{-1} x g.
  \]

  Now let \( h = g^{-1}.  \)  Then we can write the above equation as

  \[
    y = h x h^{-1}.
  \]

  According to the previous section, \( y \) is the conjugate of \( x
  \) by \( g.  \)  However, many texts call \( y \) the conjugate of \(
  x \) by \( h.  \)  Both are valid perspectives.  In both
  perspectives, \( x \) and \( y \) are conjugates of each other.
  Precisely,
</p>
<ul>
  <li>
    In the first perspective, we have \( y = g^{-1} x g \) and we say
    that \( y \) is a conjugate of \( x \) by \( g.  \)  A corollary is
    that \( x \) is a conjugate of \( y \) by \( g^{-1}.  \)
  </li>
  <li>
    In the second perspective, we have \( y = h x h^{-1} \) and we say
    that \( y \) is a conjugate of \( x \) by \( h.  \)  A corollary is
    that \( x \) is a conjugate of \( y \) by \( h^{-1}.  \)
  </li>
</ul>
<p>
  Although in both perspectives, \( x \) and \( y \) are conjugates of
  each other, the group element by which one is conjugated to the
  other is different.  This leads to different group actions as well.
</p>
<p>
  When we say that \( y = g^{-1} x g \) is a conjugate of \( x \) by
  \( g, \) the group action

  \[
    \alpha : G \times G \to G; \; (x, g) \mapsto g^{-1} x g.
  \]

  is a right group action as demonstrated in the previous section.
  But when we say that \( y = h x h^{-1} \) is a conjugate of \( x \)
  by \( h, \) the conjugation action is no longer a right group action
  because the compatibility property is violated:

  \[
    \alpha(\alpha(x, g), h)
    = h ( g x g^{-1} ) h^{-1}
    = (hg) x (hg)^{-1}
    = \alpha(x, hg).
  \]

  We get \( \alpha(x, hg) \) instead of the required \( \alpha(x, gh)
 .  \)  So with the second perspective, the group action is no longer a
  right action.  Instead it is a left action since

  \[
    \alpha(g, \alpha(h, x))
    = g (h x h^{-1}) g^{-1}
    = (gh) x (gh)^{-1}
    = \alpha(gh, x).
  \]

  In this post we will work only with the first perspective because we
  will use right actions throughout.
</p>
<h3 id="conjugate-groups">Conjugate Subgroups<a href="#conjugate-groups"></a></h3>
<p>
  Let \( G \) be a group.  Let \( H \le G.  \)  Define

  \[
    g^{-1} H g = \{ g^{-1} h g : h \in H \}.
  \]

  We say that \( g^{-1} H g \) is a conjugate of \( H \) by \( g.  \)
</p>
<h3 id="conjugacy-of-stabilisers">Conjugacy of Stabilisers<a href="#conjugacy-of-stabilisers"></a></h3>
<p>
  Let \( G \) be a group acting on a set \( X.  \)  Let \( x \in X \)
  and \( g \in G.  \)  Then

  \[
    g^{-1} G_x g = G_{x^g}.
  \]

  That is, \( G_{x^g} \) is a conjugate of \( G_x \) by \( g.  \)  This
  result can be summarised as follows: stabilisers of elements in the
  same orbit are conjugate.  Or more explicitly: the stabiliser of \(
  x^g \) is a conjugate of the stabiliser of \( x \) by \( g.  \)  The
  proof is straightforward.  Let \( h \in G.  \)  Then

  \begin{align*}
    h \in g^{-1} G_x g
    &amp; \iff g^{-1} (g h g^{-1}) g \in g^{-1} G_x g \\
    &amp; \iff g h g^{-1} \in G_x \\
    &amp; \iff x^{g h g^{-1}} = x \\
    &amp; \iff (x^g)^h = x^g \\
    &amp; \iff h \in G_{x^g}.
  \end{align*}

  Therefore \( g^{-1} G_x g = G_{x^g}.  \)
</p>
<h2 id="algeraic-graph-theory">Algebraic Graph Theory<a href="#algeraic-graph-theory"></a></h2>
<h3 id="stabiliser-index">Stabiliser Index<a href="#stabiliser-index"></a></h3>
<p>
  In a vertex-transitive graph \( \Gamma, \) for any \( x \in
  V(\Gamma) \) and all \( y \in V(\Gamma), \) there exists \( g \in G
  \) such that \( x^g = y.  \)  Therefore \( x^G = V(\Gamma).  \)  Thus
  by the <a href="#orbit-stabiliser-theorem">orbit-stabiliser
  theorem</a>,

  \[
    [ G : G_x ]
    = \lvert G_x \backslash G \rvert
    = \lvert x^G \rvert
    = \lvert V(\Gamma) \rvert.
  \]
</p>
<h3 id="strongly-connected-directed-graph">Strongly Connected Directed Graph<a href="#strongly-connected-directed-graph"></a></h3>
<p>
  A <em>path</em> in a directed graph \( \Gamma \) is a sequence of
  vertices \( v_0, \dots, v_r \) of distinct vertices such that \(
  (v_{i - 1}, v_i) \) is an arc of \( \Gamma \) for \( i = 1, \dots, r
 .  \)
</p>
<p>
  A directed graph is <em>strongly connected</em> if for every ordered
  pair of vertices \( (u, v) \) there is a path from \( u \) to \( v
 .  \)
</p>
<h3 id="shunting">Shunting<a href="#shunting"></a></h3>
<p>
  Let \( \alpha = ( \alpha_0, \dots, \alpha_s ) \) and \( \beta = (
  \beta_0, \dots, \beta_s ) \) be two \( s \)-arcs in a graph \(
  \Gamma.  \)  We say that \( \beta \) is a successor of \( \alpha \)
  if \( \beta_i = \alpha_{i + 1} \) for \( 0 \le i \le s - 1.  \)  We
  also say that \( \alpha \) can be <em>shunted</em> onto \( \beta.  \)
</p>
<p>
  In section 4.2 of Godsil and Royle, there is a rather technical
  setup which first defines \( X^{(s)} \) as the directed graph with
  the \( s \)-arcs of a graph \( X \) as its vertices such that \(
  (\alpha, \beta) \) is an arc of \( X^{(s)} \) if and only if \(
  \alpha \) can be shunted onto \( \beta \) in \( X.  \)  Then it goes
  on to show that if \( X \) is a connected graph with a minimum
  degree two and \( X \) is not a cycle, then \( X^{(s)} \) is
  strongly connected for all \( s \ge 0.  \)
</p>
<p>
  That is a very technical way of saying that in a connected graph \(
  X \) that is not a cycle and has a minimum degree two, any \( s
  \)-arc \( \alpha \) can be sent to any \( s \)-arc \( \beta \) by
  repeated shunting.  The proof is quite technical too and pretty
  long, so I'll omit it here.
</p>
<h3 id="automorphisms-preserve-successor-relation">Automorphisms Preserve Successor Relation<a href="#automorphisms-preserve-successor-relation"></a></h3>
<p>
  We will obtain a nifty result here that will prove to be very useful
  in the next section.  Let \( S(\gamma) \) denote the set of all
  successors of the \( s \)-arc \( \gamma \) of a graph.  Let \( g \)
  be an automorphism of the graph.  Then

  \[
    \delta \in S(\gamma) \iff \delta^g \in S(\gamma^g).
  \]

  This follows directly from the fact that automorphisms preserve
  adjacency, so they must preserve successor relation as well.  A
  corollary of this is that for an automorphism \( h, \) we have

  \[
    \delta^{h^{-1}} \in S(\gamma) \iff \delta \in S(\gamma^h).
  \]

  This is the form that will be useful soon.
</p>
<h3 id="test-of-s-arc-transitivity">Test of \( s \)-arc Transitivity<a href="#test-of-s-arc-transitivity"></a></h3>
<p>
  The results in the previous two sections lead to a remarkably simple
  proof of the fact that the Petersen graph is \( 3 \)-arc transitive.
  Let us see how.
</p>
<p>
  Let \( P \) be the Petersen graph whose vertices are the \( 2
  \)-subsets of \( \{ 1, 2, 3, 4, 5 \} \) with adjacency given by
  disjointness of the \( 2 \)-subsets.  Then \( \operatorname{Aut}(P)
  \cong S_5 \) since any permutation of \( \{ 1, 2, 3, 4, 5 \} \)
  induces a permutation of the vertices that preserves disjointness
  and hence adjacency.  We will use the shorthand \( ab \) to
  represent each vertex \( \{ a, b \} \) of \( P.  \)  Consider the \(
  3 \)-arc

  \[
    \alpha = (12, 34, 15, 23).
  \]

  It has exactly two successors, namely

  \[
    \beta_1 = (34, 15, 23, 14), \quad \beta_2 = (34, 15, 23, 45).
  \]

  Let \( g_1 = (13)(245) \) and \( g_2 = (13524).  \)  Then

  \begin{align*}
    \alpha^{g_1}
    &amp; = (12, 34, 15, 23)^{(13)(245)} = (34, 15, 23, 14) = \beta_1, \\
    \alpha^{g_2}
    &amp; = (12, 34, 15, 23)^{(13524)} = (34, 51, 23, 45) = \beta_2.
  \end{align*}

  Let \( H = \langle g_1, g_2 \rangle \le \operatorname{Aut}(P).  \)
  Consider an \( s \)-arc \( \alpha^h \) for some \( h \in H.  \)  Let
  \( \delta \in S(\alpha^h).  \)  Then by the result in the previous
  section, we get

  \[
    \delta^{h^{-1}} \in S(\alpha)
    = \{ \beta_1, \beta_2 \}
    = \{ \alpha^{g_1}, \alpha^{g_2} \}.
  \]

  Therefore

  \[
    \delta \in \{ \alpha^{g_1 h}, \alpha^{g_2 h} \}.
  \]

  Thus

  \[
    \delta \in \alpha^{H}.
  \]

  We started with an \( s \)-arc \( \alpha^h \in \alpha^H \) and
  showed that its successors \( \delta \) also lie in \( \alpha^H.  \)
  Thus the orbit \( \alpha^H \) is closed under taking successors.
</p>
<p>
  Now by the <a href="#shunting">shunting result</a> discussed
  previously, \( \alpha \) can be sent to any \( 3 \)-arc of \( P \)
  by repeated shunting.  Therefore all \( 3 \)-arcs of \( P \) belong
  to \( \alpha^H.  \)  Therefore the automorphisms in \( H \) can send
  any \( 3 \)-arc of \( P \) to any other thus making \( P \) \( 3
  \)-arc transitive.
</p>
<h3 id="moore-graphs">Moore Graphs<a href="#moore-graphs"></a></h3>
<p>
  Graphs with diameter \( d \) and girth \( 2d + 1 \) are known as
  Moore graphs.
</p>
<p>
  There are an infinite number of Moore graphs with diameter \( 1 \)
  since the complete graphs \( K_n, \) where \( n \ge 3, \) have
  diameter \( 1 \) and girth \( 3.  \)
</p>
<p>
  There are three known Moore graphs of diameter \( 2.  \)  They are \(
  C_5, \) \( J(5, 2, 0) \) also known as the Petersen graph and the
  Hoffman-Singleton graph.  They are respectively \( 2 \)-regular, \(
  3 \)-regular and \( 7 \)-regular.  There is a famous result that
  proves that a Moore graph must be \( 2 \)-regular, \( 3 \)-regular,
  \( 7 \)-regular or \( 57 \)-regular.  It is unknown currently
  whether a \( 57 \)-regular Moore graph of diameter \( 2 \) exists.
</p>
<p>
  There are infinitely many Moore graphs of diameter \( d \ge 3 \)
  because the odd cycles \( C_{2d + 1} \) are \( 2 \)-regular graphs
  with diameter \( d \) and girth \( 2d + 1 \) for all \( d \ge 1.  \)
  However, there are no \( k \)-regular Moore graphs for diameter \( d
  \ge 3 \) when \( k \ge 3.  \)
</p>
<h3 id="generalised-polygons">Generalised Polygons<a href="#generalised-polygons"></a></h3>
<p>
  Bipartite graphs with diameter \( d \) and girth \( 2d \) are known
  as generalised polygons.  This is easy to understand.  If we take a
  classical \( d \)-gon and create the incidence graph of its vertices
  and edges, then the incidence graph is the cycle \( C_{2d} \) which
  has diameter \( d \) and girth \( 2d.  \)
</p>
<p>
  The converse is not always true.  For example,
  the <a href="https://en.wikipedia.org/wiki/Heawood_graph">Heawood
  graph</a> which has diameter \( d = 3 \) and girth \( 2d = 6.  \)  It
  is the incidence graph of Fano plane, which is a projective plane
  rather than a classical \( d \)-gon.
</p>
<p>
  Although a generalised polygon is not always the incidence graph of
  a classical polygon, the idea behind the definition comes from a
  simple observation.  If we take a classical \( d \)-gon and form the
  incidence graph of its vertices and edges, we obtain the cycle \(
  C_{2d}.  \)  This graph is bipartite and has diameter \( d \) and
  girth \( 2d.  \)  The definition of a generalised polygon abstracts
  these properties.  Any bipartite graph with diameter \( d \) and
  girth \( 2d \) is called a generalised polygon, even when it is not
  the incidence graph of a classical \( d \)-gon.  In this way the
  definition allows much richer graphs than simple cycles.
</p>
<h2 id="computing">Computing<a href="#computing"></a></h2>
<h3 id="select-between-lines-inclusive">Select Between Lines, Inclusive<a href="#select-between-lines-inclusive"></a></h3>
<p>
  Select text between two lines, including both lines:
</p>
<pre><code>sed '/pattern1/,/pattern2/!d'</code></pre>
<pre><code>sed -n '/pattern1/,/pattern2/p'</code></pre>
<p>
  Here are some examples:
</p>
<pre><samp>$ <kbd>printf 'A\nB\nC\nD\nE\nF\nG\nH\n' | sed '/C/,/F/!d'</kbd>
C
D
E
F
$ <kbd>printf 'A\nB\nC\nD\nE\nF\nG\nH\n' | sed -n '/C/,/F/p'</kbd>
C
D
E
F</samp></pre>
<h3 id="select-between-lines-exclusive">Select Between Lines, Exclusive<a href="#select-between-lines-exclusive"></a></h3>
<p>
  Select text between two lines, excluding both lines:
</p>
<pre><code>sed '/pattern1/,/pattern2/!d; //d'</code></pre>
<p>
  Here is an example usage:
</p>
<pre><samp>$ <kbd>printf 'A\nB\nC\nD\nE\nF\nG\nH\n' | sed '/C/,/F/!d; //d'</kbd>
D
E</samp></pre>
<p>
  The negated command <code>!d</code> deletes everything not matched
  by the 2-address range <code>/C/,/F/</code>, i.e. it deletes
  everything before the line matching <code>/C/</code> as well as
  everything after the line matching <code>/F/</code>.  So we are left
  with only the lines from <code>C</code> to <code>F</code>,
  inclusive.  Finally, <code>//</code> (the empty regular expression)
  reuses the most recently used regular expression.  So
  when <code>/C/,/F/</code> matches <code>C</code>, the
  command <code>//d</code> also matches <code>C</code> and deletes it.
  Similarly, <code>F</code> is deleted too.  That's how we are left
  with the lines between <code>C</code> and <code>F</code>, exclusive.
</p>
<p>
  Here are some excerpts from
  <a href="https://pubs.opengroup.org/onlinepubs/9799919799/utilities/sed.html">POSIX.1-2024</a>
  that help understand the <code>!d</code> and <code>//d</code>
  commands better:
</p>
<blockquote>
  A function can be preceded by a <code>'!'</code> character, in which
  case the function shall be applied if the addresses do not select
  the pattern space.  Zero or more &lt;blank&gt; characters shall be
  accepted before the <code>'!'</code> character.  It is unspecified
  whether &lt;blank&gt; characters can follow the <code>'!'</code>
  character, and conforming applications shall not follow
  the <code>'!'</code> character with &lt;blank&gt; characters.
</blockquote>
<blockquote>
  If an RE is empty (that is, no pattern is specified) <em>sed</em>
  shall behave as if the last RE used in the last command applied
  (either as an address or as part of a substitute command) was
  specified.
</blockquote>
<h3 id="signing-and-verification-with-ssh-key">Signing and Verification with SSH Key<a href="#signing-and-verification-with-ssh-key"></a></h3>
<p>
  Here are some minimal commands to demonstrate how we can sign some
  text using SSH key and then later verify it.
</p>
<pre><code>ssh-keygen -t ed25519 -f key
echo hello &gt; hello.txt
ssh-keygen -Y sign -f key.pub -n file hello.txt
echo "jdoe $(cat key.pub)" &gt; allowed.txt
ssh-keygen -Y verify -f allowed.txt -I jdoe -n file -s hello.txt.sig &lt; hello.txt</code></pre>
<p>
  Here are some examples that demonstrate what the outputs and
  signature file look like:
</p>
<pre><samp>$ <kbd>ssh-keygen -Y sign -f key.pub -n file hello.txt</kbd>
Signing file hello.txt
Write signature to hello.txt.sig</samp></pre>
<pre><samp>$ <kbd>cat hello.txt.sig</kbd>
-----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgAwP6RnmFVrZO0m/nRIHyvr2S19
itsKegj9p/BZKqP1sAAAAEZmlsZQAAAAAAAAAGc2hhNTEyAAAAUwAAAAtzc2gtZWQyNTUx
OQAAAEB8ylqjCLgInF8DvROnLSm1UUWd0VuLPesI+1NhMrV9BjH5lf0w20kHunJW3qRIjw
Jfs9+q/e47KdlR8wBQaHYD
-----END SSH SIGNATURE-----</samp></pre>
<pre><samp>$ <kbd>ssh-keygen -Y verify -f allowed.txt -I jdoe -n file -s hello.txt.sig &lt; hello.txt</kbd>
Good "file" signature for jdoe with ED25519 key SHA256:9ZJuUJNMy1UXo3AlQy8L7baD3LOfEbgQ30ELIt+8wWc</samp></pre>
<h3 id="block-ip-address-with-nftables">Block IP Address with nftables<a href="#block-ip-address-with-nftables"></a></h3>
<p>
  Here is a sequence of commands to create an nftables rule from
  scratch to block an IP address:
</p>
<pre><samp>$ <kbd>sudo nft list ruleset</kbd>
$ <kbd>sudo nft add table inet filter</kbd>
$ <kbd>sudo nft list ruleset</kbd>
table inet filter {
}
$ <kbd>sudo nft add chain inet filter input { type filter hook input priority 0 \; }</kbd>
$ <kbd>sudo nft list ruleset</kbd>
table inet filter {
        chain input {
                type filter hook input priority filter; policy accept;
        }
}
$ <kbd>sudo nft add rule inet filter input ip saddr 172.236.0.216 drop</kbd>
$ <kbd>sudo nft list ruleset</kbd>
table inet filter {
        chain input {
                type filter hook input priority filter; policy accept;
                ip saddr 172.236.0.216 drop
        }
}</samp></pre>
<p>
  Here is how to undo the above setup step by step:
</p>
<pre><samp>$ <kbd>sudo nft -a list ruleset</kbd>
table inet filter { # handle 1
        chain input { # handle 1
                type filter hook input priority filter; policy accept;
                ip saddr 172.236.0.216 drop # handle 2
        }
}
$ <kbd>sudo nft delete rule inet filter input handle 2</kbd>
$ <kbd>sudo nft list ruleset</kbd>
table inet filter {
        chain input {
                type filter hook input priority filter; policy accept;
        }
}
$ <kbd>sudo nft delete chain inet filter input</kbd>
$ <kbd>sudo nft list ruleset</kbd>
table inet filter {
}
$ <kbd>sudo nft delete table inet filter</kbd>
$ <kbd>sudo nft list ruleset</kbd>
$</samp></pre>
<p>
  Finally, the following command deletes all rules, chains and tables.
  It wipes the entire ruleset, so use it with care.
</p>
<pre><samp>$ <kbd>sudo nft flush ruleset</kbd>
$ <kbd>sudo nft list ruleset</kbd>
$</samp></pre>
<p>
  All outputs above were obtained using nftables v1.1.3 on Debian 13.2
  (Trixie).
</p>
<h3 id="debian-logrotate-setup">Debian Logrotate Setup<a href="#debian-logrotate-setup"></a></h3>
<p>
  Observed on Debian 11.5 (Bullseye) that <code>logrotate</code> is
  set up on it via <code>systemd</code>.  Here are some outputs that
  show what the setup is like:
</p>
<pre><samp>$ <kbd>sudo systemctl status logrotate.service</kbd>
● logrotate.service - Rotate log files
     Loaded: loaded (/lib/systemd/system/logrotate.service; static)
     Active: inactive (dead) since Mon 2026-03-30 00:00:17 UTC; 19h ago
TriggeredBy: <span class="c2">●</span> logrotate.timer
       Docs: man:logrotate(8)
             man:logrotate.conf(5)
    Process: 2148235 ExecStart=/usr/sbin/logrotate /etc/logrotate.conf (code=exited, status=0/SUCCESS)
   Main PID: 2148235 (code=exited, status=0/SUCCESS)
        CPU: 574ms

Mar 30 00:00:16 spweb systemd[1]: Starting Rotate log files...
Mar 30 00:00:17 spweb systemd[1]: logrotate.service: Succeeded.
Mar 30 00:00:17 spweb systemd[1]: Finished Rotate log files.
$ <kbd>sudo systemctl status logrotate.timer</kbd>
● logrotate.timer - Daily rotation of log files
     Loaded: loaded (/lib/systemd/system/logrotate.timer; enabled; vendor preset: enabled)
     Active: active (waiting) since Mon 2026-01-19 19:19:34 UTC; 2 months 9 days ago
    Trigger: Tue 2026-03-31 00:00:00 UTC; 4h 7min left
   Triggers: <span class="c2">●</span> logrotate.service
       Docs: man:logrotate(8)
             man:logrotate.conf(5)

Warning: journal has been rotated since unit was started, output may be incomplete.
$ <kbd>sudo systemctl list-timers logrotate</kbd>
NEXT                        LEFT         LAST                        PASSED  UNIT            ACTIVATES
Tue 2026-03-31 00:00:00 UTC 4h 7min left Mon 2026-03-30 00:00:16 UTC 19h ago logrotate.timer logrotate.service

1 timers listed.
Pass --all to see loaded but inactive timers, too.
$ <kbd>head /lib/systemd/system/logrotate.service</kbd>
[Unit]
Description=Rotate log files
Documentation=man:logrotate(8) man:logrotate.conf(5)
RequiresMountsFor=/var/log
ConditionACPower=true

[Service]
Type=oneshot
ExecStart=/usr/sbin/logrotate /etc/logrotate.conf

$ <kbd>cat /lib/systemd/system/logrotate.timer</kbd>
[Unit]
Description=Daily rotation of log files
Documentation=man:logrotate(8) man:logrotate.conf(5)

[Timer]
OnCalendar=daily
AccuracySec=1h
Persistent=true

[Install]
WantedBy=timers.target
$ <kbd>grep -vE '^#|^$' /etc/logrotate.conf</kbd>
weekly
rotate 4
create
include /etc/logrotate.d
$ <kbd>ls -l /etc/logrotate.d/</kbd>
total 40
-rw-r--r-- 1 root root 120 Aug 21  2022 alternatives
-rw-r--r-- 1 root root 173 Jun 10  2021 apt
-rw-r--r-- 1 root root 130 Oct 14  2019 btmp
-rw-r--r-- 1 root root  82 May 26  2018 certbot
-rw-r--r-- 1 root root 112 Aug 21  2022 dpkg
-rw-r--r-- 1 root root 128 May  4  2021 exim4-base
-rw-r--r-- 1 root root 108 May  4  2021 exim4-paniclog
-rw-r--r-- 1 root root 329 May 29  2021 nginx
-rw-r--r-- 1 root root 374 May 20  2022 rsyslog
lrwxrwxrwx 1 root root  28 Mar 17 01:52 <span class="c3">susam</span> -&gt; /opt/susam.net/etc/logrotate
-rw-r--r-- 1 root root 145 Oct 14  2019 wtmp</samp></pre>
<p>
  To force log rotation right now, execute:
</p>
<pre><code>sudo systemctl start logrotate.service</code></pre>
<!-- ### -->
<p>
  <a href="https://susam.net/26c.html">Read on website</a> |
  <a href="https://susam.net/tag/notes.html">#notes</a> |
  <a href="https://susam.net/tag/mathematics.html">#mathematics</a> |
  <a href="https://susam.net/tag/linux.html">#linux</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a>
</p>
]]>
</description>
</item>
<item>
<title>Accessing Fork Commits via Original Repository</title>
<link>https://susam.net/fork-commits-via-original-repo.html</link>
<guid isPermaLink="false">wfcar</guid>
<pubDate>Sat, 28 Mar 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  I ran a small experiment with Git hosting behaviour using two demo
  repositories:
</p>
<ul>
  <li>
    <code>cuppa</code>: The original repository.
  </li>
  <li>
    <code>muppa</code>: Fork of <code>cuppa</code> with questionable changes.
  </li>
</ul>
<p>
  Here is a table with links to these repositories on Codeberg and
  GitHub:
</p>
<table class="grid center">
  <thead>
    <tr>
      <th>Name</th>
      <th>Codeberg</th>
      <th>GitHub</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>cuppa</code></td>
      <td><a href="https://codeberg.org/spxy/cuppa">codeberg.org/spxy/cuppa</a></td>
      <td><a href="https://github.com/spxy/cuppa">github.com/spxy/cuppa</a></td>
    </tr>
    <tr>
      <td><code>muppa</code></td>
      <td><a href="https://codeberg.org/spxy/muppa">codeberg.org/spxy/muppa</a></td>
      <td><a href="https://github.com/spxy/muppa">github.com/spxy/muppa</a></td>
    </tr>
  </tbody>
</table>
<p>
  It is well known that GitHub lets us access a commit that exists
  only on the fork via the original repository using a direct commit
  URL.  I wanted to find out if Codeberg behaves the same.
</p>
<p>
  The commit <code>f79ef5a</code> exists only on the fork
  (<code>muppa</code>) but not on the original repo
  (<code>cuppa</code>).  Let us see how the two hosting services
  handle direct URLs to this commit.
</p>
<table class="grid center">
  <thead>
    <tr>
      <th>Name</th>
      <th>Codeberg</th>
      <th>GitHub</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>
        <code>cuppa</code>
      </td>
      <td>
        <a href="https://codeberg.org/spxy/cuppa/commit/f79ef5a03cfe2aa429a9207d8b60f5779544d072">f79ef5a</a>
      </td>
      <td>
        <a href="https://github.com/spxy/cuppa/commit/f79ef5a03cfe2aa429a9207d8b60f5779544d072">f79ef5a</a>
      </td>
    </tr>
    <tr>
      <td>
        <code>muppa</code>
      </td>
      <td>
        <a href="https://codeberg.org/spxy/muppa/commit/f79ef5a03cfe2aa429a9207d8b60f5779544d072">f79ef5a</a>
      </td>
      <td>
        <a href="https://github.com/spxy/muppa/commit/f79ef5a03cfe2aa429a9207d8b60f5779544d072">f79ef5a</a>
      </td>
    </tr>
  </tbody>
</table>
<p>
  If we look at the second row, both commit URLs for Codeberg and
  GitHub work because that is where the commit was actually created.
  The commit belongs to the fork named <code>muppa</code>.
</p>
<p>
  Now if we look at the first row, the commit URL for Codeberg returns
  a 404 page.  This reflects the fact that the
  commit <code>f79ef5a</code> does not exist on <code>cuppa</code>.
  However, GitHub returns a successful response and shows the commit.
  It shows the following warning at the top:
</p>
<blockquote>
  This commit does not belong to any branch on this repository, and
  may belong to a fork outside of the repository.
</blockquote>
<p>
  There is no particular point to this experiment.
  I just wanted to know.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/fork-commits-via-original-repo.html">Read on website</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a>
</p>
]]>
</description>
</item>
<item>
<title>Wander 0.3.0</title>
<link>https://susam.net/code/news/wander/0.3.0.html</link>
<guid isPermaLink="false">wnzth</guid>
<pubDate>Wed, 25 Mar 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  Wander 0.3.0 is the third release of Wander, a small, decentralised,
  self-hosted web console that lets visitors to your website explore
  interesting websites and pages recommended by a community of
  independent website owners.  To try it, go
  to <a href="../../../wander/">susam.net/wander/</a>.
</p>
<p>
  This release brings small but important bug fixes.  The previous
  release, <a href="0.2.0.html">version 0.2.0</a> introduced a number
  of new features.  Unfortunately, two of them caused issues for some
  users.  A new feature in the previous release was
  the <code>ignore</code> list feature.  The <code>ignore</code> list
  defines console URLs and page URLs that the console never uses while
  discovering page recommendations.  While this feature works fine,
  due to a bug in the implementation, the <strong>Console</strong>
  dialog fails to load in consoles that do not define
  any <code>ignore</code> list.  This has now been fixed.
</p>
<p>
  There was another issue due to which the <code>&lt;iframe&gt;</code>
  that displays discovered websites and pages could not load certain
  websites.  In particular, any website that relied on same-origin
  context to load its own resources failed to load in the console.
  This has been fixed as well.  Please see
  <a href="https://codeberg.org/susam/wander/issues/7">codeberg.org/susam/wander/issues/7</a>
  for a detailed discussion on this issue.
</p>
<p>
  Apart from these two important fixes, there are a few other minor
  fixes too pertaining to preventing horizontal scrolling in small
  devices and preventing duplicate recommendations from appearing too
  close to each other.  Please
  see <a href="https://codeberg.org/susam/wander/src/branch/main/CHANGES.md">CHANGES.md</a>
  for a detailed changelog.
</p>
<p>
  To learn more about Wander, how it works and how to set it up,
  please read the project README at
  <a href="https://codeberg.org/susam/wander#readme">codeberg.org/susam/wander</a>.
  To try it out right now, go to
  <a href="../../../wander/">susam.net/wander/</a>.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/code/news/wander/0.3.0.html">Read on website</a> |
  <a href="https://susam.net/tag/web.html">#web</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a>
</p>
]]>
</description>
</item>
<item>
<title>Wander 0.2.0</title>
<link>https://susam.net/code/news/wander/0.2.0.html</link>
<guid isPermaLink="false">wnztz</guid>
<pubDate>Tue, 24 Mar 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  Wander 0.2.0 is the second release of Wander, a small,
  decentralised, self-hosted web console that lets visitors to your
  website explore interesting websites and pages recommended by a
  community of independent personal website owners.  To try it, go
  to <a href="../../../wander/">susam.net/wander</a>.
</p>
<p>
  This release brings a number of improvements.  When I released
  version 0.1.0, it was the initial version of the software I was
  using for my own website.  Naturally, I was the only user initially
  and I only added trusted web pages to the recommendation list of my
  console.  But ever since I
  <a href="https://news.ycombinator.com/item?id=47422759">announced
  this project on Hacker News</a>, it has received a good amount of
  attention.  It has been less than a week since I announced it there
  but over 30 people have set up a Wander console on their personal
  websites.  There are now over a hundred web pages being recommended
  by this network of consoles.  With the growth in the number of
  people who have set up Wander console, came several feature
  requests, most of which have been implemented already.  This release
  makes these new features available.
</p>
<p>
  Since Wander 0.2.0, the <code>wander.js</code> file of remote
  consoles is executed in a sandbox <code>iframe</code> to ensure that
  it has no side effects on the parent Wander console page.
  Similarly, the pages recommended by the network are also loaded into
  a sandbox <code>iframe</code>.
</p>
<p>
  This release also brings several customisation features.  Console
  owners can customise their Wander console by adding custom CSS or
  JavaScript.  Console owners can also block certain URLs from ever
  being recommended on their console.  This is especially important in
  providing a good wandering experience to visitors.  Since this
  network is completely decentralised, console owners can add any web
  page they like to their console.  Sometimes they inadvertently add
  pages that do not load successfully in the console due to frame
  embedding restrictions.  This leads to an uneven wandering
  experience because these page recommendations occasionally make it
  to other consoles where they fail to load.  Console owners can now
  block such URLs in their console to decrease the likelihood of these
  failed page loads.  This helps make the wandering experience smoother.
</p>
<p>
  Another significant feature in this release is the
  expanded <strong>Console</strong> dialog box.  This dialog box now
  shows various details about the console and the current wandering
  session.  For example, it shows the console's configuration:
  recommended pages, ignored URLs and linked consoles.  It also shows
  a wandering history screen where you can see each link that was
  recommended to you along with the console that recommendation came
  from.  There is another screen that shows all the consoles
  discovered during the discovery process.  Those who care about how
  Wander works would find this dialog box quite useful.  To check it
  out, go to
  <a href="../../../wander/">my Wander console</a> and
  explore.
</p>
<p>
  To learn more about Wander, how it works and how to set it up,
  please read the project README at
  <a href="https://codeberg.org/susam/wander#readme">codeberg.org/susam/wander</a>.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/code/news/wander/0.2.0.html">Read on website</a> |
  <a href="https://susam.net/tag/web.html">#web</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a>
</p>
]]>
</description>
</item>
<item>
<title>Wander Console</title>
<link>https://susam.net/wander/</link>
<guid isPermaLink="false">wtswb</guid>
<pubDate>Wed, 18 Mar 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  I have put together a small tool to explore the small web of
  personal websites.  It is called <em>Wander</em>.  Please
  visit <a href="https://susam.net/wander/">susam.net/wander/</a> to
  try out my Wander console.
</p>
<p>
  If you have your own website, please consider joining this community
  by hosting your own Wander console.  To do so, visit
  <a href="https://codeberg.org/susam/wander#readme">codeberg.org/susam/wander</a>
  and follow the instructions there.  Thank you!
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/wander/">Read on website</a> |
  <a href="https://susam.net/tag/web.html">#web</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a>
</p>
]]>
</description>
</item>
<item>
<title>Wander 0.1.0</title>
<link>https://susam.net/code/news/wander/0.1.0.html</link>
<guid isPermaLink="false">wnzoz</guid>
<pubDate>Wed, 18 Mar 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  Wander 0.1.0 is the first release of Wander, a small, decentralised,
  self-hosted web console that lets visitors to your website explore
  interesting websites and pages recommended by a community of
  independent personal website owners.
</p>
<p>
  Anyone with a personal website can take this tool and host an
  instance of a Wander console.  Each Wander console loads personal
  websites and pages recommended by the Wander community.  Further,
  each Wander console can link to other Wander consoles, forming a
  lightweight, decentralised network for browsing the small web of
  personal websites.
</p>
<p>
  Setting up an instance of a Wander console involves copying just two
  static files from the Wander project at
  <a href="https://codeberg.org/susam/wander">codeberg.org/susam/wander</a>.
  The most interesting aspect of the Wander console is that discovery
  of new links from other consoles happens on the client side in the
  user's web browser.  As a website owner, you do not need to set up
  any server-side components beyond a basic web server.  In fact, you
  can host a Wander console on GitHub Pages or Codeberg Pages too.
</p>
<p>
  To learn more about Wander, how it works and how to set it up,
  please read the project README at
  <a href="https://codeberg.org/susam/wander#readme">codeberg.org/susam/wander</a>.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/code/news/wander/0.1.0.html">Read on website</a> |
  <a href="https://susam.net/tag/web.html">#web</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a>
</p>
]]>
</description>
</item>
<item>
<title>Git Checkout, Reset and Restore</title>
<link>https://susam.net/git-checkout-reset-restore.html</link>
<guid isPermaLink="false">gcrrp</guid>
<pubDate>Thu, 12 Mar 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  I have always used the <code>git checkout</code> and <code>git
  reset</code> commands to reset my working tree or index but since
  Git 2.23 there has been a <code>git restore</code> command available
  for these purposes.  In this post, I record how some of the 'older'
  commands I use map to the new ones.  Well, the new commands aren't
  exactly new since Git 2.23 was released in 2019, so this post is
  perhaps six years too late.  Even so, I want to write this down for
  future reference.  It is worth noting that the old and new commands
  are not always equivalent.  I'll talk more about this briefly as we
  discuss the commands.  However, they can be used to perform similar
  tasks.  Some of these tasks are discussed below.
</p>
<h2 id="contents">Contents<a href="#contents"></a></h2>
<ul>
  <li><a href="#experimental-setup">Experimental Setup</a></li>
  <li><a href="#reset-working-directory">Reset the Working Tree</a></li>
  <li><a href="#reset-index">Reset the Index</a></li>
  <li><a href="#reset-working-directory-and-index">Reset the Working Tree and Index</a></li>
  <li><a href="#summary">Summary</a></li>
</ul>
<h2 id="experimental-setup">Experimental Setup<a href="#experimental-setup"></a></h2>
<p>
  To experiment quickly, we first create an example Git repository.
</p>
<pre><code>mkdir foo/; cd foo/; touch a b c
git init; git add a b c; git commit -m hello</code></pre>
<p>
  Now we make changes to the files and stage some of the changes.  We
  then add more unstaged changes to one of the staged files.
</p>
<pre><code>date | tee a b c d; git add a b d; echo &gt; b</code></pre>
<p>
  At this point, the working tree and index look like this:
</p>
<pre><samp>$ <kbd>git status</kbd>
On branch main
Changes to be committed:
  (use "git restore --staged &lt;file&gt;..." to unstage)
        <span class="c2">modified:   a
        modified:   b
        new file:   d</span>

Changes not staged for commit:
  (use "git add &lt;file&gt;..." to update what will be committed)
  (use "git restore &lt;file&gt;..." to discard changes in working directory)
        <span class="c4">modified:   b
        modified:   c</span></samp></pre>
<p>
  File <code>a</code> has staged changes.  File <code>b</code> has
  both staged and unstaged changes.  File <code>c</code> has only
  unstaged changes.  File <code>d</code> is a new staged file.  In
  each experiment below, we will work with this setup.
</p>
<p>
  All results discussed in this post were obtained using Git 2.47.3 on
  Debian 13.2 (Trixie).
</p>
<h2 id="reset-working-directory">Reset the Working Tree<a href="#reset-working-directory"></a></h2>
<p>
  As a reminder, we will always use the following command between
  experiments to ensure that we restore the experimental setup each
  time:
</p>
<pre><code>date | tee a b c d; git add a b d; echo &gt; b</code></pre>
<p>
  To discard the changes in the working tree and reset the files in
  the working tree from the index, I typically run:
</p>
<pre><code>git checkout .</code></pre>
<p>
  However, the modern way to do this is to use the following command:
</p>
<pre><code>git restore .</code></pre>
<p>
  Both commands leave the working tree and the index in the following
  state:
</p>
<pre><samp>$ <kbd>git status</kbd>
On branch main
Changes to be committed:
  (use "git restore --staged &lt;file&gt;..." to unstage)
        <span class="c2">modified:   a
        modified:   b
        new file:   d</span></samp></pre>
<p>
  Both commands operate only on the working tree.  They do not alter
  the index.  Therefore the staged changes remain intact in the index.
</p>
<h2 id="reset-index">Reset the Index<a href="#reset-index"></a></h2>
<p>
  Another common situation is when we have staged some changes but
  want to unstage them.  First, we restore the experimental setup:
</p>
<pre><code>date | tee a b c d; git add a b d; echo &gt; b</code></pre>
<p>
  I normally run the following command to do so:
</p>
<pre><code>git reset</code></pre>
<p>
  The modern way to do this is:
</p>
<pre><code>git restore -S .</code></pre>
<p>
  Both commands leave the working tree and the index in the following
  state:
</p>
<pre><samp>$ <kbd>git status</kbd>
On branch main
Changes not staged for commit:
  (use "git add &lt;file&gt;..." to update what will be committed)
  (use "git restore &lt;file&gt;..." to discard changes in working directory)
        <span class="c4">modified:   a
        modified:   b
        modified:   c</span>

Untracked files:
  (use "git add &lt;file&gt;..." to include in what will be committed)
        <span class="c4">d</span>

no changes added to commit (use "git add" and/or "git commit -a")</samp></pre>
<p>
  The <code>-S</code> (<code>--staged</code>) option tells <code>git
  restore</code> to operate on the index (not the working tree) and
  reset the index entries for the specified files to match the
  version in <code>HEAD</code>.  The unstaged changes remain intact as
  modified files in the working tree.  With the <code>-S</code>
  option, no changes are made to the working tree.
</p>
<p>
  From the arguments we can see that the old and new commands are not
  exactly equivalent.  Without any arguments, the <code>git
  reset</code> command resets the entire index to <code>HEAD</code>,
  so all staged changes become unstaged.  Similarly, when we run
  <code>git restore -S</code> without specifying a commit, branch or
  tag using the <code>-s</code> (<code>--source</code>) option, it
  defaults to resetting the index from <code>HEAD</code>.
  The <code>.</code> at the end ensures that all paths under the
  current directory are affected.  When we run the command at the
  top-level directory of the repository, all paths are affected and
  the entire index gets reset.  As a result, both the old and the new
  commands accomplish the same result.
</p>
<h2 id="reset-working-directory-and-index">Reset the Working Tree and Index<a href="#reset-working-directory-and-index"></a></h2>
<p>
  Once again, we restore the experimental setup.
</p>
<pre><code>date | tee a b c d; git add a b d; echo &gt; b</code></pre>
<p>
  This time we not only want to unstage the changes but also discard
  the changes in the working tree.  In other words, we want to reset
  both the working tree and the index from <code>HEAD</code>.  This is
  a dangerous operation because any uncommitted changes discarded in
  this manner cannot be restored using Git.
</p>
<pre><code>git reset --hard</code></pre>
<p>
  The modern way to do this is:
</p>
<pre><code>git restore -WS .</code></pre>
<p>
  The working tree is now clean:
</p>
<pre><samp>$ <kbd>git status</kbd>
On branch main
nothing to commit, working tree clean</samp></pre>
<p>
  The <code>-W</code> (<code>--worktree</code>) option makes the
  command operate on the working tree.  The <code>-S</code>
  (<code>--staged</code>) option resets the index as described in the
  previous section.  As a result, this command unstages any changes
  and discards any modifications in the working tree.
</p>
<p>
  Note that when neither of these options is specified,
  <code>-W</code> is implied by default.  That's why the
  bare <code>git restore .</code> command in the previous section
  discards the changes in the working tree.
</p>
<h2 id="summary">Summary<a href="#summary"></a></h2>
<p>
  The following table summarises how the three pairs of commands
  discussed above affect the working tree and the index, assuming the
  commands are run at the top-level directory of a repository.
</p>
<div style="overflow: auto">
  <table class="grid" style="margin: 0">
    <thead>
      <tr>
        <th>Old</th>
        <th>New</th>
        <th>Working Tree</th>
        <th>Index</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td class="pre"><code>git checkout .</code></td>
        <td class="pre"><code>git restore .</code></td>
        <td>Reset to match the index.</td>
        <td>No change.</td>
      </tr>
      <tr>
        <td class="pre"><code>git reset</code></td>
        <td class="pre"><code>git restore -S .</code></td>
        <td>No change.</td>
        <td>Reset to match <code>HEAD</code>.</td>
      </tr>
      <tr>
        <td class="pre"><code>git reset --hard</code></td>
        <td class="pre"><code>git restore -SW .</code></td>
        <td>Reset to match <code>HEAD</code>.</td>
        <td>Reset to match <code>HEAD</code>.</td>
      </tr>
    </tbody>
  </table>
</div>
<p>
  The <code>git restore</code> command is meant to provide a clearer
  interface for resetting the working tree and the index.  I still use
  the older commands out of habit.  Perhaps I will adopt the new ones
  in another six years, but at least I have the mapping written down
  now.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/git-checkout-reset-restore.html">Read on website</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a> |
  <a href="https://susam.net/tag/how-to.html">#how-to</a>
</p>
]]>
</description>
</item>
<item>
<title>HN Skins 0.4.0</title>
<link>https://susam.net/code/news/hnskins/0.4.0.html</link>
<guid isPermaLink="false">hnsfr</guid>
<pubDate>Tue, 10 Mar 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  HN Skins 0.4.0 is a minor update to HN Skins, a web browser
  userscript that adds custom themes to Hacker News and lets you
  browse HN with a variety of visual styles.  This release introduces
  a small fix to preserve the commemorative black bar that
  occasionally appears at the top of the page.
</p>
<p>
  When a notable figure in technology or science passes away, Hacker
  News places a thin black bar at the top of the page in tribute.
  Previously some skins could obscure this element.  This update
  ensures that the bar remains visible and clearly noticeable.  In
  dark themed skins, the black bar is rendered as a lighter shade of
  grey so that it maintains sufficient contrast and remains
  conspicuous.
</p>
<p>
  Today Hacker News has
  <a href="https://news.ycombinator.com/item?id=47324054">a story
  about Tony Hoare passing away</a>, which made me notice that the
  commemorative black bar was not rendered properly with some skins.
  This prompted me to investigate the issue and implement the fix
  included in this release.
</p>
<p>
  Screenshots showing how the bar appears with different skins are
  available at
  <a href="https://susam.github.io/blob/img/hnskins/0.4.0/">susam.github.io/blob/img/hnskins/0.4.0/</a>.
</p>
<p>
  To install HN Skins,
  visit <a href="https://github.com/susam/hnskins#readme">github.com/susam/hnskins</a>
  and follow the instructions there.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/code/news/hnskins/0.4.0.html">Read on website</a> |
  <a href="https://susam.net/tag/web.html">#web</a> |
  <a href="https://susam.net/tag/programming.html">#programming</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a>
</p>
]]>
</description>
</item>
<item>
<title>HN Skins 0.3.0</title>
<link>https://susam.net/code/news/hnskins/0.3.0.html</link>
<guid isPermaLink="false">hnsth</guid>
<pubDate>Sat, 07 Mar 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  HN Skins 0.3.0 is a minor update to HN Skins, a web browser
  userscript that adds custom themes to Hacker News and allows you to
  browse HN with a variety of visual styles.  This release includes
  fixes for a few issues that slipped through earlier versions.  For
  example, the comment input textbox now uses the same font face and
  size as the rest of the active theme.  The colour of visited links
  has also been slightly muted to make it easier to distinguish them
  from unvisited links.  In addition, some skins have been renamed:
  Teletype is now called Courier and Nox is now called Midnight.
</p>
<p>
  Further, the font face of several monospace based themes is now set
  to <code>monospace</code> instead of <code>courier</code>.  This
  allows the browser's preferred monospace font to be used.  The font
  face of the Courier skin (formerly known as Teletype) remains set
  to <code>courier</code>.  This will never change because the sole
  purpose of this skin is to celebrate this legendary font.
</p>
<p>
  To view screenshots of HN Skins or install it, visit
  <a href="https://github.com/susam/hnskins#readme">github.com/susam/hnskins</a>.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/code/news/hnskins/0.3.0.html">Read on website</a> |
  <a href="https://susam.net/tag/web.html">#web</a> |
  <a href="https://susam.net/tag/programming.html">#programming</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a>
</p>
]]>
</description>
</item>
<item>
<title>HN Skins 0.2.0</title>
<link>https://susam.net/code/news/hnskins/0.2.0.html</link>
<guid isPermaLink="false">hnskt</guid>
<pubDate>Sun, 01 Mar 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  HN Skins 0.2.0 is a minor update of HN Skins.  It comes a day after
  its <a href="0.1.0.html">initial release</a> in order to fine tune a
  few minor issues with the styles in the initial release.  HN Skins
  is a web browser userscript that adds custom themes to Hacker News
  and allows you to browse HN with different visual styles.
</p>
<p>
  This update removes excessive vertical space below the 'reply'
  links, sorts the skin options alphabetically in the selection dialog
  and fixes the background colour of the navigation bar in the
  Terminal skin by changing it from a dark grey to a dark green.
</p>
<p>
  Soon after making this release, I discovered a few other minor
  issues, such as the Cafe and Terminal themes using Courier when I
  intended them to use the system monospace font.  This has already
  been fixed in the development version currently available on GitHub.
  However, I will make a formal release later.
</p>
<p>
  See the <a href="https://github.com/susam/hnskins/blob/main/CHANGES.md">changelog</a>
  for more details.  To see some screenshots of HN Skins or to install
  it, visit <a href="https://github.com/susam/hnskins#readme">github.com/susam/hnlinks</a>
  and follow the instructions there.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/code/news/hnskins/0.2.0.html">Read on website</a> |
  <a href="https://susam.net/tag/web.html">#web</a> |
  <a href="https://susam.net/tag/programming.html">#programming</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a>
</p>
]]>
</description>
</item>
<item>
<title>HN Skins 0.1.0</title>
<link>https://susam.net/code/news/hnskins/0.1.0.html</link>
<guid isPermaLink="false">hnsko</guid>
<pubDate>Sat, 28 Feb 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  HN Skins 0.1.0 is the initial release of HN Skins, a browser
  userscript that adds custom themes to Hacker News (HN).  It allows
  you to browse HN in style with a selection of visual skins.
</p>
<p>
  To use HN Skins, first install a userscript manager such as
  Greasemonkey, Tampermonkey or Violentmonkey in your web browser.
  Once installed, you can install HN Skins from
  <a href="https://github.com/susam/hnskins#readme">github.com/susam/hnskins</a>.
</p>
<p>
  The source code is available under the terms of the MIT licence.
  For usage instructions and screenshots, please visit
  <a href="https://github.com/susam/hnskins#readme">github.com/susam/hnskins</a>.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/code/news/hnskins/0.1.0.html">Read on website</a> |
  <a href="https://susam.net/tag/web.html">#web</a> |
  <a href="https://susam.net/tag/programming.html">#programming</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a>
</p>
]]>
</description>
</item>
<item>
<title>Soju User Delete Hash</title>
<link>https://susam.net/soju-user-delete-hash.html</link>
<guid isPermaLink="false">sudhs</guid>
<pubDate>Sat, 14 Feb 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  In <a href="from-znc-to-soju.html">my last post</a>, I talked about
  switching from ZNC to Soju as my IRC bouncer.  One thing that caught
  my attention while creating and deleting Soju users was that the
  delete command asks for a confirmation, like so:
</p>
<pre><samp>$ <kbd>sudo sojuctl user delete soju</kbd>
To confirm user deletion, send "user delete soju 4664cd"
$ <kbd>sudo sojuctl user delete soju 4664cd</kbd>
deleted user "soju"</samp></pre>
<p>
  That confirmation token for a specific user never changes, no matter
  how many times we create or delete it.  The confirmation token is
  not saved in the Soju database, as can be confirmed here:
</p>
<pre><samp>$ <kbd>sudo sqlite3 -table /var/lib/soju/main.db 'SELECT * FROM User'</kbd>
+----+----------+--------------------------------------------------------------+-------+----------+------+--------------------------+---------+--------------------------+--------------+
| id | username |                           password                           | admin | realname | nick |        created_at        | enabled | downstream_interacted_at | max_networks |
+----+----------+--------------------------------------------------------------+-------+----------+------+--------------------------+---------+--------------------------+--------------+
| 1  | soju     | $2a$10$yRj/oYlR2Zwd8YQxZPuAQuNo2j7FVJWeNdIAHF2MinYkKLmBjtf0y | 0     |          |      | 2026-02-16T13:49:46.119Z | 1       |                          | -1           |
+----+----------+--------------------------------------------------------------+-------+----------+------+--------------------------+---------+--------------------------+--------------+</samp></pre>
<p>
  Surely, then, the confirmation token is derived from the user
  definition?  Yes, indeed it is.  This can be confirmed at the
  <a href="https://codeberg.org/emersion/soju/src/commit/v0.10.1/service.go#L1185-L1203">source
  code here</a>.  Quoting the most relevant part from the source code:
</p>
<pre><code>hashBytes := sha1.Sum([]byte(username))
hash := fmt.Sprintf("%x", hashBytes[0:3])</code></pre>
<p>
  Indeed if we compute the same hash ourselves, we get the same token:
</p>
<pre><samp>$ <kbd>printf soju | sha1sum | head -c6</kbd>
4664cd</samp></pre>
<p>
  This allows us to automate the two step Soju user deletion process
  in a single command:
</p>
<pre><code>sudo sojuctl user delete soju "$(printf soju | sha1sum | head -c6)"</code></pre>
<p>
  But of course, the implementation of the confirmation token may
  change in future and Soju helpfully outputs the deletion command
  with the confirmation token when we first invoke it without the
  token, so it is perhaps more prudent to just take that output and
  feed it back to Soju, like so:
</p>
<pre><code>sudo sojuctl $(sudo sojuctl user delete soju | sed 's/.*"\(.*\)"/\1/')</code></pre>
<!-- ### -->
<p>
  <a href="https://susam.net/soju-user-delete-hash.html">Read on website</a> |
  <a href="https://susam.net/tag/shell.html">#shell</a> |
  <a href="https://susam.net/tag/irc.html">#irc</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a> |
  <a href="https://susam.net/tag/how-to.html">#how-to</a>
</p>
]]>
</description>
</item>
<item>
<title>From ZNC to Soju</title>
<link>https://susam.net/from-znc-to-soju.html</link>
<guid isPermaLink="false">fztsj</guid>
<pubDate>Thu, 12 Feb 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  I have recently switched from ZNC to Soju as my IRC bouncer and I am
  already quite pleased with it.  I usually run my bouncer on a Debian
  machine, where Soju is well packaged and runs smoothly right after
  installation.  By contrast, the ZNC package included with Debian 13
  (Trixie) and earlier fails to start after installation because of a
  missing configuration file.  As a result, I was forced to maintain
  my own configuration file along with a necessary PEM bundle, copy
  them to the Debian system and carefully set the correct file
  permissions before I could run ZNC successfully.  None of this is
  necessary with Soju, since installing it from the Debian package
  repository automatically sets up the configuration and certificate
  files.  I no longer have to manage any configuration or certificate
  files myself.
</p>
<h2 id="setup">Setup<a href="#setup"></a></h2>
<p>
  It is quite straightforward to install and set up Soju on Debian.
  The following two commands install Soju:
</p>
<pre><code>sudo apt-get update
sudo apt-get -y install soju</code></pre>
<p>
  Then setting up an IRC connection involves another two commands:
</p>
<pre><code>sudo sojuctl user create -username soju -password YOUR_SOJU_PASSWORD
sudo sojuctl user run soju network create -name bnc1 -addr irc.libera.chat -nick YOUR_NICK -pass YOUR_NICK_PASSWORD</code></pre>
<p>
  Here, <code>YOUR_SOJU_PASSWORD</code> is a placeholder for a new
  password you must choose for your Soju user.  Finally, we restart
  Soju as follows:
</p>
<pre><code>sudo systemctl restart soju</code></pre>
<h2 id="database">Database<a href="#database"></a></h2>
<p>
  What previously involved maintaining several files that had to be
  installed and configured on each machine running ZNC is now reduced
  to the two <code>sojuctl</code> commands above.  Still, the
  configuration needs to live somewhere.  In fact, the
  two <code>sojuctl</code> commands introduce earlier store the
  configuration in a SQLite database.  Here is a glimpse of what the
  database looks like:
</p>
<pre><samp>$ <kbd>sudo sqlite3 /var/lib/soju/main.db '.tables'</kbd>
Channel              MessageFTS_data      ReadReceipt
DeliveryReceipt      MessageFTS_docsize   User
Message              MessageFTS_idx       WebPushConfig
MessageFTS           MessageTarget        WebPushSubscription
MessageFTS_config    Network
$ <kbd>sudo sqlite3 /var/lib/soju/main.db 'SELECT * from User'</kbd>
1|soju|$2a$10$mM5Qcz8.OPMi9lyWDxPRh.bNxzq7jtLdxcoPl09AYTnqcmLmEqzSO|0|||2026-02-17T23:24:24.926Z|1||-1
$ <kbd>sudo sqlite3 /var/lib/soju/main.db 'SELECT * from Network'</kbd>
1|bnc1|1|irc.libera.chat|YOUR_NICK||||YOUR_NICK_PASSWORD|||||||1|1</samp></pre>
<h2 id="client">Client Configuration<a href="#client"></a></h2>
<p>
  Finally, the IRC client can be configured to connect to port 6697 on
  the system running Soju.  Here is an example of how this can be done
  in Irssi:
</p>
<pre><code>/network add -nick YOUR_NICK -user soju/bnc1 net1
/server add -tls -network bnc1 YOUR_SOJU_HOST 6697 YOUR_SOJU_PASSWORD
/connect net1
</code></pre>
<p>
  You can also set up multiple connections to IRC networks through the
  same Soju instance.  All you need to do is repeat
  the <code>sojuctl</code> commands to create additional networks such
  as <code>bnc2</code>, <code>bnc3</code> and so on, then repeat the
  configuration in your IRC client using new network names such as
  <code>net2</code>, <code>net3</code>, etc.  These network names are
  entirely user defined, so you can choose any names you like.  The
  names <code>bnc2</code>, <code>net2</code> and so on are only
  examples.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/from-znc-to-soju.html">Read on website</a> |
  <a href="https://susam.net/tag/irc.html">#irc</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a> |
  <a href="https://susam.net/tag/how-to.html">#how-to</a>
</p>
]]>
</description>
</item>
<item>
<title>Twenty Five Years of Computing</title>
<link>https://susam.net/twenty-five-years-of-computing.html</link>
<guid isPermaLink="false">tfyoc</guid>
<pubDate>Fri, 06 Feb 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  Last year, I completed 20 years in professional software
  development.  I wanted to write a post to mark the occasion back
  then, but couldn't find the time.  This post is my attempt to make
  up for that omission.  In fact, I have been involved in software
  development for a little longer than 20 years.  Although I had
  my <a href="fd-100.html">first taste</a> of computer programming as
  a child, it was only when I entered university about 25 years ago
  that I seriously got into software development.  So I'll start my
  stories from there.  These stories are less about software and more
  about people.  Unlike many posts of this kind, this one offers no
  wisdom or lessons.  It only offers a collection of stories.  I hope
  you'll like at least a few of them.
</p>
<h2 id="contents">Contents<a href="#contents"></a></h2>
<ul>
  <li><a href="#viewing-the-source">Viewing the Source</a></li>
  <li><a href="#reset-vector">The Reset Vector</a></li>
  <li><a href="#man-in-the-middle">Man in the Middle</a></li>
  <li><a href="#sphagetti-code">Sphagetti Code</a></li>
  <li><a href="#animated-television-widgets">Animated Television Widgets</a></li>
  <li><a href="#good-blessings">Good Blessings</a></li>
  <li><a href="#the-ctf-scoreboard">The CTF Scoreboard</a></li>
</ul>
<h2 id="viewing-the-source">Viewing the Source<a href="#viewing-the-source"></a></h2>
<p>
  The first story takes place in 2001, shortly after I joined
  university.  One evening, I went to the university computer
  laboratory to browse the World Wide Web.  Out of curiosity, I typed
  <code>susam.com</code> into the address bar and landed on
  <a href="https://web.archive.org/web/20010721163153/http://susam.com/">its
  home page</a>.  I remember the text and banner looking much larger
  back then.  Display resolutions were lower, so the text and banner
  covered almost half the screen.  I knew very little about the
  Internet then and I was just trying to make sense of it.  I remember
  wondering what it would take to create my own website, perhaps at
  <code>susam.com</code>.  That's when an older student who had been
  watching me browse over my shoulder approached and asked if I had
  created the website.  I told him I hadn't and that I had no idea how
  websites were made.  He asked me to move aside, took my seat and
  clicked View &gt; Source in Internet Explorer.  He then explained
  how websites are made of HTML pages and how those pages are simply
  text instructions.
</p>
<p>
  Next, he opened Notepad and wrote a simple HTML page that looked
  something like this:
</p>
<pre><code>&lt;BODY&gt;&lt;FONT COLOR="RED"&gt;HELLO&lt;/FONT&gt;&lt;/BODY&gt;</code></pre>
<p>
  Yes, we had a <code>FONT</code> tag back then and it was common
  practice to write HTML tags in uppercase.  He then opened the page
  in a web browser and showed how it rendered.  After that, he
  demonstrated a few more features such as changing the font face and
  size, centring the text and altering the page's background colour.
  Although the tutorial lasted only about ten minutes, it made the Web
  feel far less mysterious and much more fascinating.
</p>
<p>
  That person had an ulterior motive though.  After the tutorial, he
  never returned the seat to me.  He just continued browsing the Web
  and waited for me to leave.  I was too timid to ask for my seat
  back.  Seats were limited, so I returned to my dorm room both
  disappointed that I couldn't continue browsing that day and excited
  about all the websites I might create with this newfound knowledge.
  I could never register <code>susam.com</code> for myself though.
  That domain was always used by some business selling Turkish
  cuisines.  Eventually, I managed to get the next best thing:
  a <code>.net</code> domain of my own.  That brief encounter in the
  university laboratory set me on a lifelong path of creating and
  maintaining personal websites.
</p>
<h2 id="reset-vector">The Reset Vector<a href="#reset-vector"></a></h2>
<p>
  The second story also comes from my university days.  One afternoon,
  I was hanging out with my mates in the computer laboratory.  In
  front of me was an MS-DOS machine powered by an Intel 8086
  microprocessor, on which I was writing a lift control program in
  assembly.  In those days, it was considered important to
  deliberately practise solving made-up problems as a way of honing
  our programming skills.  As I worked on my program, my mind drifted
  to a small detail about the 8086 microprocessor that we had recently
  learnt in a lecture.  Our professor had explained that, when the
  8086 microprocessor is reset, execution begins with CS:IP set to
  FFFF:0000.  So I murmured to anyone who cared to listen, 'I wonder
  if the system will reboot if I jump to FFFF:0000.'  I then
  opened <code>DEBUG.EXE</code> and jumped to that address.
</p>
<pre><samp>C:\&gt;<kbd>DEBUG</kbd>
-<kbd>G =FFFF:0000</kbd></samp></pre>
<p>
  The machine rebooted instantly.  One of my friends, who topped the
  class every semester, had been watching over my shoulder.  As soon
  as the machine restarted, he exclaimed, 'How did you do that?'  I
  explained that the reset vector is located at physical address FFFF0
  and that the CS:IP value FFFF:0000 maps to that address in real
  mode.  After that, I went back to working on my lift control program
  and didn't think much more about the incident.
</p>
<p>
  About a week later, the same friend came to my dorm room.  He sat
  down with a grave look on his face and asked, 'How did you know to
  do that?  How did it occur to you to jump to the reset vector?'  I
  must have said something like, 'It just occurred to me.  I
  remembered that detail from the lecture and wanted to try it out.'
  He then said, 'I want to be able to think like that.  I come top of
  the class every semester, but I don't think the way you do.  I would
  never have thought of taking a small detail like that and testing it
  myself.'  I replied that I was just curious to see whether what we
  had learnt actually worked in practice.  He responded, 'And that's
  exactly it.  It would never occur to me to try something like that.
  I feel disappointed that I keep coming top of the class, yet I am
  not curious in the same way you are.  I've decided I don't want to
  top the class anymore.  I just want to explore and experiment with
  what we learn, the way you do.'
</p>
<p>
  That was all he said before getting up and heading back to his dorm
  room.  I didn't take it very seriously at the time.  I couldn't
  imagine why someone would willingly give up the accomplishment of
  coming first every semester.  But he kept his word.  He never topped
  the class again.  He still ranked highly, often within the top ten,
  but he kept his promise of never finishing first again.  To this
  day, I feel a mix of embarrassment and pride whenever I recall that
  incident.  With a single jump to the processor's reset entry point,
  I had somehow inspired someone to step back from academic
  competition in order to have more fun with learning.  Of course,
  there is no reason one cannot do both.  But in the end, that was his
  decision, not mine.
</p>
<h2 id="man-in-the-middle">Man in the Middle<a href="#man-in-the-middle"></a></h2>
<p>
  In my first job after university, I was assigned to a technical
  support team where part of my work involved running an installer to
  deploy a specific component of an e-banking product for customers,
  usually large banks.  As I learnt to use the installer, I realised
  how fragile it was.  The installer, written in Python, often failed
  because of incorrect assumptions about the target environment and
  almost always required some manual intervention to complete
  successfully.  During my first week on the project, I spent much of
  my time stabilising the installer and writing a step-by-step user
  guide explaining how to use it.  The result was well received by
  both my seniors and management.  To my surprise, the user guide
  received more praise than the improvements I made to the installer.
  While the first few weeks were productive, I soon realised I would
  not find the work fulfilling for long.  I wrote to management a few
  times to ask whether I could transfer to a team where I could work
  on something more substantial.
</p>
<p>
  My emails were initially met with resistance.  After several rounds
  of discussion, someone who had heard about my situation reached out
  and suggested a team whose manager might be interested in
  interviewing me.  The team was based in a different city.  I was
  young and willing to relocate wherever I could find good work, so I
  immediately agreed to the interview.
</p>
<p>
  This was in 2006, when video conferencing was not yet common.  On
  the day of the interview, the hiring manager called me on my office
  desk phone.  He began by introducing the team, which was called
  <em>Archie</em>, short for <em>architecture</em>.  The team
  developed and maintained the web framework and core architectural
  components on which the entire e-banking product was built.  The
  product had existed long before open source frameworks such as
  Spring or Django came into existence, so features such as API
  routing, authentication and authorisation layers, cookie management,
  etc. were all implemented in-house as Java Servlets and JavaServer
  Pages (JSP).  Since the software was used in banking environments,
  it also had to pass security testing and regular audits to minimise
  the risk of serious flaws.
</p>
<p>
  The interview began well.  He asked several questions related to
  software security, such as what SQL injection is, how it can be
  prevented and how one might design a web framework that mitigates
  cross-site scripting attacks.  He also asked me a few programming
  questions, most which I answered pretty well.  Towards the end,
  however, he asked how we could prevent MITM attacks.  I had never
  heard the term, so I admitted that I did not know what MITM meant.
  He then asked, 'Man in the middle?' but I still had no idea what
  that meant or whether it was even a software engineering concept.
  He replied, 'Learn everything you can about PKI and MITM.  We need
  to build a digital signatures feature for one of our corporate
  banking products.  That's the first thing we'll work on.'
</p>
<p>
  Over the next few weeks, I studied RFCs and documentation related to
  public key infrastructure, public key cryptography standards and
  related topics.  At first, the material felt intimidating, but after
  spending time each evening reading whatever relevant literature I
  could find, things gradually began to make sense.  Concepts that
  initially seemed complex and overwhelming eventually felt intuitive
  and elegant.  I relocated to the new city a few weeks later and
  delivered the digital signatures feature about a month after joining
  the team.  We used the open source Bouncy Castle library to
  implement the feature.  After that project, I worked on other parts
  of the product too.  The most rewarding part was knowing that the
  code I was writing became part of a mature product used by hundreds
  of banks and millions of users.  It was especially satisfying to see
  the work pass security testing and audits and be considered ready
  for release.
</p>
<p>
  That was my first real engineering job.  My manager also turned out
  to be an excellent mentor.  Working with him helped me develop new
  skills and his encouragement gave me confidence that stayed with me
  for years.  Nearly two decades have passed since then, yet the
  product is still in service and continues to be actively developed.
  In fact, in my current phase of life I sometimes encounter it as a
  customer.  Occasionally, I open the browser's developer tools to
  view the page source where I can still see traces of the HTML
  generated by code I wrote almost twenty years ago.
</p>
<h2 id="sphagetti-code">Sphagetti Code<a href="#sphagetti-code"></a></h2>
<p>
  Around 2007 or 2008, I began working on a proof of concept for
  developing widgets for an OpenTV set-top box.  The work involved
  writing code in a heavily trimmed-down version of C.  One afternoon,
  while making good progress on a few widgets, I noticed that they
  would occasionally crash at random.  I tried tracking down the bugs,
  but I was finding it surprisingly difficult to understand my own
  code.  I had managed to produce some truly spaghetti code full of
  dubious pointer operations that were almost certainly responsible
  for the crashes, yet I could not pinpoint where exactly things were
  going wrong.
</p>
<p>
  Ours was a small team of four people, each working on an independent
  proof of concept.  The most senior person on the team acted as our
  lead and architect.  Later that afternoon, I showed him my progress
  and explained that I was still trying to hunt down the bugs causing
  the widgets to crash.  He asked whether he could look at the code.
  After going through it briefly and probably realising that it was a
  bit of a mess, he asked me to send him the code as a tarball, which
  I promptly did.
</p>
<p>
  He then went back to his desk to study the code.  I remember
  thinking that there was no way he was going to find the problem
  anytime soon.  I had been debugging it for hours and barely
  understood what I had written myself; it was the worst spaghetti
  code I had ever produced.  With little hope of a quick solution, I
  went back to debugging on my own.
</p>
<p>
  Barely five minutes later, he came back to my desk and asked me to
  open a specific file.  He then showed me exactly where the pointer
  bug was.  It had taken him only a few minutes not only to read my
  tangled code but also to understand it well enough to identify the
  fault and point it out.  As soon as I fixed that line, the crashes
  disappeared.  I was genuinely in awe of his skill.
</p>
<p>
  I have always loved computing and programming, so I had assumed I
  was already fairly good at it.  That incident, however, made me
  realise how much further I still had to go before I could consider
  myself a good software developer.  I did improve significantly in
  the years that followed and today I am far better at managing
  software complexity than I was back then.
</p>
<h2 id="animated-television-widgets">Animated Television Widgets<a href="#animated-television-widgets"></a></h2>
<p>
  In another project from that period, we worked on another set-top
  box platform that supported Java Micro Edition (Java ME) for widget
  development.  One day, the same architect from the previous story
  asked whether I could add animations to the widgets.  I told him
  that I believed it should be possible, though I'd need to test it to
  be sure.  Before continuing with the story, I need to explain how
  the different stakeholders in the project were organised.
</p>
<p>
  Our small team effectively played the role of the software vendor.
  The final product going to market would carry the brand of a major
  telecom carrier, offering direct-to-home (DTH) television services,
  with the set-top box being one of the products sold to customers.
  The set-top box was manufactured by another company.  So the project
  was a partnership between three parties: our company as the software
  vendor, the telecom carrier and the set-top box manufacturer.  The
  telecom carrier wanted to know whether widgets could be animated on
  screen with smooth slide-in and slide-out effects.  That was why the
  architect approached me to ask whether it could be done.
</p>
<p>
  I began working on animating the widgets.  Meanwhile, the architect
  and a few senior colleagues attended a business meeting with all the
  partners present.  During the meeting, he explained that we were
  evaluating whether widget animations could be supported.  The
  set-top box manufacturer immediately dismissed the idea, saying,
  'That's impossible.  Our set-top box does not support animation.'
  When the architect returned and shared this with us, I replied, 'I
  do not understand.  If I can draw a widget, I can animate it too.
  All it takes is clearing the widget and redrawing it at slightly
  different positions repeatedly.  In fact, I already have a working
  version.'  I then showed a demo of the animated widgets running on
  the emulator.
</p>
<p>
  The following week, the architect attended another partners' meeting
  where he shared updates about our animated widgets.  I was not
  personally present, so what follows is second-hand information
  passed on by those who were there.  I learnt that the set-top box
  company reacted angrily.  For some reason, they were unhappy that we
  had managed to achieve results using their set-top box and APIs that
  they had officially described as impossible.  They demanded that we
  stop work on animation immediately, arguing that our work could not
  be allowed to contradict their official position.  At that point,
  the telecom carrier's representative intervened and bluntly told the
  set-top box representative to just shut up.  If the set-top box guy
  was furious, the telecom guy was even more so, 'You guys told us
  animation was not possible and these people are showing that it is!
  You manufacture the set-top box.  How can you not know what it is
  capable of?'
</p>
<p>
  Meanwhile, I continued working on the proof of concept.  It worked
  very well in the emulator, but I did not yet have access to the
  actual hardware.  The device was still in the process of being
  shipped to us, so all my early proof-of-concepts ran on the
  emulator.  The following week, the architect planned to travel to
  the set-top box company's office to test my widgets on the real
  hardware.
</p>
<p>
  At the time, I was quite proud of demonstrating results that even
  the hardware maker believed were impossible.  When the architect
  eventually travelled to test the widgets on the actual device, a
  problem emerged.  What looked like buttery smooth animation on the
  emulator appeared noticeably choppy on a real television.  Over the
  next few weeks, I experimented with frame rates, buffering
  strategies and optimising the computation done in the the rendering
  loop.  Each week, the architect travelled for testing and returned
  with the same report: the animation had improved somewhat, but it
  still remained choppy.  The modest embedded hardware simply could
  not keep up with the required computation and rendering.  In the
  end, the telecom carrier decided that no animation was better than
  poor animation and dropped the idea altogether.  So in the end, the
  set-top box developers turned out to be correct after all.
</p>
<h2 id="good-blessings">Good Blessings<a href="#good-blessings"></a></h2>
<p>
  Back in 2009, after completing about a year at RSA Security, I began
  looking for work that felt more intellectually stimulating,
  especially projects involving mathematics and algorithms.  I spoke
  with a few senior leaders about this, but nothing materialised for
  some time.  Then one day, Dr Burt Kaliski, Chief Scientist at RSA
  Laboratories, asked to meet me to discuss my career aspirations.  I
  have written about this in more detail in another post here:
  <a href="good-blessings.html">Good Blessings</a>.  I will summarise
  what followed.
</p>
<p>
  Dr Kaliski met me and offered a few suggestions about the kinds of
  teams I might approach to find more interesting work.  I followed
  his advice and eventually joined a team that turned out to be an
  excellent fit.  I remained with that team for the next six years.
  During that time, I worked on parser generators, formal language
  specification and implementation, as well as indexing and querying
  engines of a petabyte-scale database.  I learnt something new almost
  every day during those six years.  It remains one of the most
  enjoyable periods of my career.  I have especially fond memories of
  working on parser generators alongside remarkably skilled engineers
  from whom I learnt a lot.
</p>
<p>
  Years later, I reflected on how that brief meeting with Dr Kaliski
  had altered the trajectory of my career.  I realised I was not sure
  whether I had properly expressed my gratitude to him for the role he
  had played in shaping my path.  So I wrote to thank him and explain
  how much that single conversation had influenced my life.  A few
  days later, Dr Kaliski replied, saying he was glad to know that the
  steps I took afterwards had worked out well.  Before ending his
  message, he wrote this heart-warming note:
</p>
<blockquote>
  &lsquo;One of my goals is to be able to provide encouragement to
  others who are developing their careers, just as others have
  invested in mine, passing good blessings from one generation to
  another.&rsquo;
</blockquote>
<h2 id="the-ctf-scoreboard">The CTF Scoreboard<a href="#the-ctf-scoreboard"></a></h2>
<p>
  This story comes from 2019.  By then, I was no longer a
  twenty-something engineer just starting out.  I was now a
  middle-aged staff engineer with years of experience building both
  low-level networking systems and database systems.  Most of my work
  up to that point had been in C and C++.  I was now entering a new
  phase of my career where I would be leading the development of
  microservices written in Go and Python.  Like many people in this
  profession, computing has long been one of my favourite hobbies.  So
  although my professional work for the previous decade had focused on
  C and C++, I had plenty of hobby projects in other languages,
  including Python and Go.  As a result, switching gears from systems
  programming to application development was a smooth transition for
  me.  I cannot even say that I missed working in C and C++.  After
  all, who wants to spend their days occasionally chasing memory bugs
  in core dumps when you could be building features and delivering
  real value to customers?
</p>
<p>
  In October 2019, during Cybersecurity Awareness Month, a Capture the
  Flag (CTF) event was organised at our office.  The contest featured
  all kinds of technical puzzles, ranging from SQL injection
  challenges to insecure cryptography problems.  Some challenges also
  involved reversing binaries and exploiting stack overflow issues.
</p>
<p>
  I am usually rather intimidated by such contests.  The whole idea of
  competitive problem-solving under time pressure tends to make me
  nervous.  But one of my colleagues persuaded me to participate in
  the CTF.  And, somewhat to my surprise, I turned out to be rather
  good at it.  Within about eight hours, I had solved roughly 90% of
  the puzzles.  I finished at the top of the scoreboard.
</p>
<figure>
  <img src="files/blog/ctf-2019.png" alt="Scoreboard of a Capture the Flag (CTF) event">
  <figcaption>
    CTF Scoreboard
  </figcaption>
</figure>
<p>
  In my younger days, I was generally known to be a good problem
  solver.  I was often consulted when thorny problems needed solving
  and I usually managed to deliver results.  I also enjoyed solving
  puzzles.  I had a knack for them and happily spent hours, sometimes
  days, working through obscure mathematical or technical puzzles and
  sharing detailed write-ups with friends of the nerd variety.  Seen
  in that light, my performance at the CTF probably should not have
  surprised me.  Still, I was very pleased.  It was reassuring to know
  that I could still rely on my systems programming experience to
  solve obscure challenges.
</p>
<p>
  During the course of the contest, my performance became something of
  a talking point in the office.  Colleagues occasionally stopped by
  my desk to appreciate my progress in the CTF.  Two much younger
  colleagues, both engineers I admired for their skill and
  professionalism, were discussing the results nearby.  They were
  speaking softly, but I could still overhear parts of their
  conversation.  Curious, I leaned slightly and listened a bit more
  carefully.  I wanted to know what these two people, whom I admired a
  lot, thought about my performance.
</p>
<p>
  One of them remarked on how well I was doing in the contest.  The
  other replied, 'Of course he is doing well.  He has more than ten
  years of experience in C.'  At that moment, I realised that no
  matter how well I solved those puzzles, the result would naturally
  be credited to experience.  In my younger days, when I solved tricky
  problems like these, people would sometimes call me smart.  Now
  people simply saw it as a consequence of my experience.  Not that I
  particularly care for labels such as 'smart' anyway, but it did make
  me realise how things had changed.  I was now simply the person with
  many years of experience.  Solving technical puzzles that involved
  disassembling binaries, tracing execution paths and reconstructing
  program logic was expected rather than remarkable.
</p>
<p>
  I continue to sharpen my technical skills to this day.  While my
  technical results may now simply be attributed to experience, I hope
  I can continue to make a good impression through my professionalism,
  ethics and kindness towards the people I work with.  If those leave
  a lasting impression, that is good enough for me.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/twenty-five-years-of-computing.html">Read on website</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a> |
  <a href="https://susam.net/tag/programming.html">#programming</a>
</p>
]]>
</description>
</item>
<item>
<title>Attention Media &#x2260; Social Networks</title>
<link>https://susam.net/attention-media-vs-social-networks.html</link>
<guid isPermaLink="false">amnsm</guid>
<pubDate>Tue, 20 Jan 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  When web-based social networks started flourishing nearly two
  decades ago, they were genuinely social networks.  You would sign up
  for a popular service, follow people you knew or liked and read
  updates from them.  When you posted something, your followers would
  receive your updates as well.  Notifications were genuine.  The
  little icons in the top bar would light up because someone had sent
  you a direct message or engaged with something you had posted.
  There was also, at the beginning of this millennium, a general sense
  of hope and optimism around technology, computers and the Internet.
  Social networking platforms were one of the services that were part
  of what was called Web 2.0, a term used for websites built around
  user participation and interaction.  It felt as though the
  information superhighway was finally reaching its potential.  But
  sometime between 2012 and 2016, things took a turn for the worse.
</p>
<p>
  First came the infamous infinite scroll.  I remember feeling uneasy
  the first time a web page no longer had a bottom.  Logically, I knew
  very well that everything a browser displays is a virtual construct.
  There is no physical page.  It is just pixels pretending to be one.
  Still, my brain had learned to treat web pages as objects with a
  beginning and an end.  The sudden disappearance of that end
  disturbed my sense of ease.
</p>
<p>
  Then came the bogus notifications.  What had once been meaningful
  signals turned into arbitrary prompts.  Someone you followed had
  posted something unremarkable and the platform would surface it as a
  notification anyway.  It didn't matter whether the notification was
  relevant to me.  The notification system stopped serving me and
  started serving itself.  It felt like a violation of an unspoken
  agreement between users and services.  Despite all that, these
  platforms still remained social in some diluted sense.  Yes, the
  notifications were manipulative, but they were at least about people
  I actually knew or had chosen to follow.  That, too, would change.
</p>
<p>
  Over time, my timeline contained fewer and fewer posts from friends
  and more and more content from random strangers.  Using these
  services began to feel like standing in front of a blaring
  loudspeaker, broadcasting fragments of conversations from all over
  the world directly in my face.  That was when I gave up on these
  services.  There was nothing social about them anymore.  They had
  become <em>attention media</em>.  My attention is precious to me.  I
  cannot spend it mindlessly scrolling through videos that have
  neither relevance nor substance.
</p>
<p>
  But where one avenue disappeared, another emerged.  A few years ago,
  I stumbled upon Mastodon and it reminded me of the early days of
  Twitter.  Back in 2006, I followed a small number of folks of the
  nerd variety on Twitter and received genuinely interesting updates
  from them.  But when I log into the ruins of those older platforms
  now, all I see are random videos presented to me for reasons I can
  neither infer nor care about.  Mastodon, by contrast, still feels
  like social networking in the original sense.  I follow a small
  number of people I genuinely find interesting and I receive their
  updates and only their updates.  What I see is the result of my own
  choices rather than a system trying to capture and monetise my
  attention.  There are no bogus notifications.  The timeline feels
  calm and predictable.  If there are no new updates from people I
  follow, there is nothing to see.  It feels closer to how social
  networks used to work originally.  I hope it stays that way.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/attention-media-vs-social-networks.html">Read on website</a> |
  <a href="https://susam.net/tag/web.html">#web</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a>
</p>
]]>
</description>
</item>
<item>
<title>Nested Code Fences in Markdown</title>
<link>https://susam.net/nested-code-fences.html</link>
<guid isPermaLink="false">ncfim</guid>
<pubDate>Mon, 19 Jan 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  Today, we will meet a spiky-haired nerd named Corey Dumm, who
  normally lives within Markdown code fences.  We will get to know him
  a bit, smile with him when his fences hold and weep quietly when
  misfortune strikes.
</p>
<p>
  One of the caveats of the Markdown universe is the wide variety of
  Markdown implementations available.  In these parallel universes,
  the rules of Markdown rendering differ subtly.  In this post, we
  will focus only on the CommonMark specification.  Since GitHub
  Flavoured Markdown (GFM) is a strict superset of CommonMark,
  whatever we discuss here applies equally well to both CommonMark and
  GFM.
</p>
<h2 id="contents">Contents<a href="#contents"></a></h2>
<ul>
  <li><a href="#basic-code-fences">Basic Code Fences</a></li>
  <li><a href="#fancy-code-fences">Fancy Code Fences</a></li>
  <li><a href="#basic-code-spans">Basic Code Spans</a></li>
  <li><a href="#fancy-code-spans">Fancy Code Spans</a></li>
  <li><a href="#specification">Specification</a></li>
</ul>
<h2 id="basic-code-fences">Basic Code Fences<a href="#basic-code-fences"></a></h2>
<p>
  Corey had a knack for working with computers ever since he was a
  kid.
</p>
<!-- Markdown #1 -->
<pre><code>Corey at his computer:

```
(o_o)--.|[_]|
```</code></pre>
<p>
  Everything was perfect in Corey's world.  The CommonMark renderer
  would convert the Markdown above to the following HTML:
</p>
<!-- Rendered HTML #1 -->
<div class="box">
<p>Corey at his computer:</p>
<pre><code>(o_o)--.|[_]|
</code></pre>
</div>
<!-- Raw HTML #1 -->
<details>
<summary>View HTML</summary>
<pre><code>&lt;p&gt;Corey at his computer:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(o_o)--.|[_]|
&lt;/code&gt;&lt;/pre&gt;</code></pre>
</details>
<p>
  At this point, all was well.  Corey grew quickly.  Before long, he
  had a head full of spiky hair.  Then the fences began to matter.
</p>
<!-- Markdown #2 -->
<pre><code>Corey, all grown up:

```
 ```
(o_o)--.|[_]|
```</code></pre>
<p>
  Let us see how this renders.  I must warn you that during the
  Markdown-to-HTML translation, Corey loses his hair.  Some viewers
  may find the following scene disturbing.  Viewer discretion is
  advised.  Here is the rendered HTML:
</p>
<!-- Rendered HTML #2 -->
<div class="box">
<p>Corey, all grown up:</p>
<pre><code></code></pre>
<p>(o_o)--.|[_]|</p>
<pre><code></code></pre>
</div>
<!-- Raw HTML #2 -->
<details>
<summary>View HTML</summary>
<pre><code>&lt;p&gt;Corey, all grown up:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(o_o)--.|[_]|&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;</code></pre>
</details>
<p>
  Corey's hair is gone!  What a catastrophic accident!  Corey is
  alright, though.  He is still quite afraid of Markdown fences, but
  otherwise well and bouncing back.  Why did this happen?  The second
  set of triple backticks immediately ends the fenced code block
  started by the first set of triple backticks.  As a result, Corey's
  smiley face ends up outside the fenced code block.  The triple
  backticks that were once Corey's hair are now woven into the fabric
  of the surrounding HTML.  Fortunately, CommonMark offers a few ways
  to avoid such accidents.
</p>
<h2 id="fancy-code-fences">Fancy Code Fences<a href="#fancy-code-fences"></a></h2>
<p>
  In CommonMark, fenced code blocks are most commonly written using
  triple backticks.  However, the specification also allows tildes as
  an alternative fence.  This can be useful when the code itself
  contains backticks.  Let us see an example:
</p>
<!-- Markdown #3a -->
<pre><code>Corey, all grown up:

~~~
 ```
(o_o)--.|[_]|
~~~</code></pre>
<p>
  In fact, a code fence need not consist of exactly three backticks or
  tildes.  Any number of backticks or tildes is allowed, as long as
  that number is at least three.  The following is therefore
  equivalent:
</p>
<!-- Markdown #3b -->
<pre><code>Corey, all grown up:

~~~~~
 ```
(o_o)--.|[_]|
~~~~~</code></pre>
<p>
  And so is this:
</p>
<!-- Markdown #3c -->
<pre><code>Corey, all grown up:

`````
 ```
(o_o)--.|[_]|
`````</code></pre>
<p>
  All three examples render like this:
</p>
<!-- Rendered HTML #3 -->
<div class="box">
<p>Corey, all grown up:</p>
<pre><code> ```
(o_o)--.|[_]|
</code></pre>
</div>
<!-- Raw HTML #3 -->
<details>
<summary>View HTML</summary>
<pre><code>&lt;p&gt;Corey, all grown up:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; ```
(o_o)--.|[_]|
&lt;/code&gt;&lt;/pre&gt;</code></pre>
</details>
<p>
  No hair is lost in translation.
</p>
<h2 id="basic-code-spans">Basic Code Spans<a href="#basic-code-spans"></a></h2>
<p>
  A similar problem arises with inline code spans.  Most Markdown
  users know to use backticks to delimit inline code spans.  For
  example:
</p>
<!-- Markdown 4 -->
<pre><code>An old picture of Corey at his computer: `(o_o)--.|[_]|`</code></pre>
<p>
  This produces the following output:
</p>
<!-- Rendered HTML 4 -->
<div class="box">
<p>An old picture of Corey at his computer: <code>(o_o)--.|[_]|</code></p>
</div>
<!-- Raw HTML 4 -->
<details>
<summary>View HTML</summary>
<pre><code>&lt;p&gt;An old picture of Corey at his computer: &lt;code&gt;(o_o)--.|[_]|&lt;/code&gt;&lt;/p&gt;</code></pre>
</details>
<p>
  However, what do we do when we need to put Corey's dear friend Becky
  Trace within an inline code span?  Becky has short, straight hair
  tucked neatly on either side of her face.  Here's a picture of her:
</p>
<p class="textcenter">
  <code>`(o_o)`</code>
</p>
<p>
  I believe you can already see the difficulty here.  Inline code
  spans use backticks as delimiters.  So when we put Becky within a
  code span, the first backtick in Becky's face would terminate the
  code span immediately and then the rest of Becky would lie outside
  it.  CommonMark offers solutions for this kind of situation as well.
</p>
<h2 id="fancy-code-spans">Fancy Code Spans<a href="#fancy-code-spans"></a></h2>
<p>
  An inline code span delimiter need not consist of exactly one
  backtick.  It can consist of any number of backticks.  So
  <code>`foo`</code> and <code>``foo``</code> produce identical HTML.
  There is another important but less well-known detail.  When the
  text inside an inline code span begins and ends with spaces, one
  space is removed from each end before rendering.  So
  <code>`foo`</code> and <code>`&nbsp;foo&nbsp;`</code> are
  equivalent.  Therefore, when we need to put backticks within an
  inline code span, we can start the code span using multiple
  backticks and a space.  For example:
</p>
<!-- Markdown #5 -->
<pre><code>Meet Corey's friend Becky Trace: `` `(o_o)` ``</code></pre>
<p>
  Here is the rendered output:
</p>
<!-- Rendered HTML #5 -->
<div class="box">
<p>Meet Corey's friend Becky Trace: <code>`(o_o)`</code></p>
</div>
<!-- Raw HTML #5 -->
<details>
<summary>View HTML</summary>
<pre><code>&lt;p&gt;Meet Corey's friend Becky Trace: &lt;code&gt;`(o_o)`&lt;/code&gt;&lt;/p&gt;</code></pre>
</details>
<p>
  Becky has her hair intact too.  We have avoided the mishap that once
  caused great distress to Corey.  That, my friends, is how backticks
  survive nesting in Markdown.
</p>
<h2 id="specification">Specification<a href="#specification"></a></h2>
<p>
  Before I finish this post, let us take a look at the CommonMark
  specification to see where these details are defined.  The excerpts
  quoted below are taken from
  <a href="https://spec.commonmark.org/0.30/">CommonMark Spec Version
  0.30</a>, which is by now over four years old.
</p>
<p>
  From section
  <a href="https://spec.commonmark.org/0.30/#fenced-code-blocks">4.5
  Fenced Code Blocks</a>:
</p>
<blockquote>
  <p>
    A <a href="https://spec.commonmark.org/0.30/#code-fence">code
    fence</a> is a sequence of at least three consecutive backtick
    characters (<code>`</code>) or tildes (<code>~</code>).  (Tildes
    and backticks cannot be mixed.)
  </p>
</blockquote>
<blockquote>
  <p>
    The content of the code block consists of all subsequent lines,
    until a closing
    <a href="https://spec.commonmark.org/0.30/#code-fence">code fence</a>
    of the same type as the code block began with (backticks or tildes),
    and with at least as many backticks or tildes as the opening code
    fence.
  </p>
</blockquote>
<p>
  From section
  <a href="https://spec.commonmark.org/0.30/#code-spans">6.1 Code
  Spans</a>:
</p>
<blockquote>
  <p>
    A <a href="https://spec.commonmark.org/0.30/#backtick-string">backtick
    string</a> is a string of one or more backtick characters
    (<code>`</code>) that is neither preceded nor followed by a
    backtick.
  </p>
  <p>
    A <a href="https://spec.commonmark.org/0.30/#code-span">code
    span</a> begins with a backtick string and ends with a backtick
    string of equal length.  The contents of the code span are the
    characters between these two backtick strings, normalized in the
    following ways:
  </p>
  <ul>
    <li>
      First, <a href="https://spec.commonmark.org/0.30/#line-ending">line endings</a>
      are converted to <a href="https://spec.commonmark.org/0.30/#space">spaces</a>.
    </li>
    <li>
      If the resulting string both begins <em>and</em> ends with a
      <a href="https://spec.commonmark.org/0.30/#space">space</a>
      character, but does not consist entirely of
      <a href="https://spec.commonmark.org/0.30/#space">space</a>
      characters, a single
      <a href="https://spec.commonmark.org/0.30/#space">space</a>
      character is removed from the front and back.  This allows you
      to include code that begins or ends with backtick characters,
      which must be separated by whitespace from the opening or
      closing backtick strings.
    </li>
  </ul>
</blockquote>
<p>
  I hope these little nuggets of Markdown trivialities will one day
  prove useful in your own Markdown misfortunes.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/nested-code-fences.html">Read on website</a> |
  <a href="https://susam.net/tag/web.html">#web</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a>
</p>
]]>
</description>
</item>
<item>
<title>Minimal GitHub Workflow</title>
<link>https://susam.net/minimal-github-workflow.html</link>
<guid isPermaLink="false">mghwf</guid>
<pubDate>Thu, 15 Jan 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  This is a note where I capture the various errors we receive when we
  create GitHub workflows that are smaller than the smallest possible
  workflow.  I do not know why anyone would ever need this information
  and I doubt it will serve any purpose for me either but sometimes
  you just want to know things, no matter how useless they might be.
  This is one of the useless things I wanted to know today.
</p>
<h2 id="contents">Contents<a href="#contents"></a></h2>
<ul>
  <li><a href="#empty-workflow">Empty Workflow</a></li>
  <li><a href="#on">On</a></li>
  <li><a href="#on-push">On Push</a></li>
  <li><a href="#jobs">Jobs</a></li>
  <li><a href="#job-id">Job ID</a></li>
  <li><a href="#steps">Steps</a></li>
  <li><a href="#runs-on">Runs On</a></li>
  <li><a href="#runs-on-ubuntu-latest">Runs On Ubuntu Latest</a></li>
  <li><a href="#empty-steps">Empty Steps</a></li>
  <li><a href="#run">Run</a></li>
  <li><a href="#run-echo">Run Echo</a></li>
  <li><a href="#hello-world">Hello, World</a></li>
</ul>
<h2 id="empty-workflow">Empty Workflow<a href="#empty-workflow"></a></h2>
<p>
  For the first experiment we just create a zero byte file and push it
  to GitHub as follows, say, like this:
</p>
<pre><code>mkdir -p .github/workflows/
touch .github/workflows/hello.yml
git add .github/
git commit -m 'Empty workflow'
git push -u origin main</code></pre>
<p>
  Under the GitHub repo's <strong>Actions</strong> tab, we find this
  error:
</p>
<pre><samp>Error
No event triggers defined in `on`</samp></pre>
<h2 id="on">On<a href="#on"></a></h2>
<p>
  Then we update the workflow as follows:
</p>
<pre><code>on:</code></pre>
<p>
  Now we get this error:
</p>
<pre><samp>Invalid workflow file: .github/workflows/hello.yml#L1
(Line: 1, Col: 4): Unexpected value '', (Line: 1, Col: 1): Required property is missing: jobs</samp></pre>
<h2 id="on-push">On Push<a href="#on-push"></a></h2>
<p>
  Next update:
</p>
<pre><code>on: push</code></pre>
<p>
  Corresponding error:
</p>
<pre><samp>Invalid workflow file: .github/workflows/hello.yml#L1
(Line: 1, Col: 1): Required property is missing: jobs</samp></pre>
<h2 id="jobs">Jobs<a href="#jobs"></a></h2>
<p>
  Workflow:
</p>
<pre><code>on: push
jobs:</code></pre>
<p>
  Error:
</p>
<pre><samp>Invalid workflow file: .github/workflows/hello.yml#L1
(Line: 2, Col: 6): Unexpected value ''</samp></pre>
<h2 id="job-id">Job ID<a href="#job-id"></a></h2>
<p>
  Workflow:
</p>
<pre><code>on: push
jobs:
  world:</code></pre>
<p>
  Error:
</p>
<pre><samp>Invalid workflow file: .github/workflows/hello.yml#L1
(Line: 3, Col: 9): Unexpected value ''</samp></pre>
<h2 id="steps">Steps<a href="#steps"></a></h2>
<p>
  Workflow:
</p>
<pre><code>on: push
jobs:
  world:
    steps:</code></pre>
<p>
  Error:
</p>
<pre><samp>Invalid workflow file: .github/workflows/hello.yml#L1
(Line: 4, Col: 11): Unexpected value '', (Line: 4, Col: 5): Required property is missing: runs-on</samp></pre>
<h2 id="runs-on">Runs On<a href="#runs-on"></a></h2>
<p>
  Workflow:
</p>
<pre><code>on: push
jobs:
  world:
    runs-on:
    steps:</code></pre>
<p>
  Error:
</p>
<pre><samp>Invalid workflow file: .github/workflows/hello.yml#L1
(Line: 4, Col: 13): Unexpected value '', (Line: 5, Col: 11): Unexpected value ''</samp></pre>
<h2 id="runs-on-ubuntu-latest">Runs On Ubuntu Latest<a href="#runs-on-ubuntu-latest"></a></h2>
<p>
  Workflow:
</p>
<pre><code>on: push
jobs:
  world:
    runs-on: ubuntu-latest
    steps:</code></pre>
<p>
  Error:
</p>
<pre><samp>Invalid workflow file: .github/workflows/hello.yml#L1
(Line: 5, Col: 11): Unexpected value ''</samp></pre>
<h2 id="empty-steps">Empty Steps<a href="#empty-steps"></a></h2>
<p>
  Workflow:
</p>
<pre><code>on: push
jobs:
  world:
    runs-on: ubuntu-latest
    steps: []</code></pre>
<p>
  Error:
</p>
<pre><samp>Invalid workflow file: .github/workflows/hello.yml#L1
No steps defined in `steps` and no workflow called in `uses` for the following jobs: world</samp></pre>
<h2 id="run">Run<a href="#run"></a></h2>
<p>
  Workflow:
</p>
<pre><code>on: push
jobs:
  world:
    runs-on: ubuntu-latest
    steps:
      - run:</code></pre>
<p>
  Success:
</p>
<pre><samp>&#x25bc; Run

  shell: /usr/bin/bash -e {0}</samp></pre>
<h2 id="run-echo">Run Echo<a href="#run-echo"></a></h2>
<p>
  Workflow:
</p>
<pre><code>on: push
jobs:
  world:
    runs-on: ubuntu-latest
    steps:
      - run: echo</code></pre>
<p>
  Success:
</p>
<pre><samp>&#x25bc; Run
  echo
  shell: /usr/bin/bash -e {0}
</samp></pre>
<h2 id="hello-world">Hello, World<a href="#hello-world"></a></h2>
<p>
  Workflow:
</p>
<pre><code>on: push
jobs:
  world:
    runs-on: ubuntu-latest
    steps:
      - run: echo hello, world</code></pre>
<p>
  Success:
</p>
<pre><samp>&#x25bc; Run echo hello, world
  echo hello, world
  shell: /usr/bin/bash -e {0}
hello, world</samp></pre>
<p>
  The experiments are preserved in the commit history of
  <a href="https://github.com/spxy/minighwf">github.com/spxy/minighwf</a>.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/minimal-github-workflow.html">Read on website</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a>
</p>
]]>
</description>
</item>
<item>
<title>Writing First, Tooling Second</title>
<link>https://susam.net/writing-first-tooling-second.html</link>
<guid isPermaLink="false">wftss</guid>
<pubDate>Sat, 10 Jan 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  I am a strong proponent of running
  <a href="https://hnpwd.github.io/">independent personal websites</a>
  on your own domains and publishing your writing there.  Doing so
  keeps the web diverse and decentralised, rather than concentrating
  most writing and discussion inside a small number of large
  platforms.  It gives authors long term control over their work
  without being subject to
  <a href="attention-media-vs-social-networks.html">changing policies
  or incentives</a>.  I think that a web made up of many small,
  individually run websites is more resilient and also more
  interesting than one dominated by a handful of social media
  services.
</p>
<p>
  I often participate in discussions pertaining to authoring personal
  websites because this is an area I am passionate about.  Any
  discussion about authoring websites that I take part in seems to
  drift, sooner or later, into tooling.  Aspiring personal website
  authors worry at length about which blogging engine to use, which
  static site generator to pick, which templating language to choose
  and so on.  I think none of this is important until you have
  published at least five articles on your website.  Just write plain
  HTML and worry about tooling later.
</p>
<p>
  This very website you are reading right now began its life as a
  loose collection of HTML files typed into Notepad on a Windows 98
  machine.  I wrote some text, wrapped it in basic markup and copied
  it to my web server's document root directory.  That's it.  Tooling
  came much later, in fact many years later.  As the number of pages
  grew, I naturally wanted some consistency.  I wanted a common layout
  for my pages, navigation links, a footer that did not need to be
  edited in twenty places.  An early version of this website used HTML
  frames to accomplish these goals.  Later, I rewrote the website in
  PHP.  Now this website is generated using Common Lisp.  All of these
  additions came later, after I had been maintaining this website for
  several years.  None of this was required to begin.
</p>
<p>
  Around 2006, when blogging had become quite fashionable, I
  experimented with blogging too.  Eventually, I returned to a loose,
  chaotic collection of pages.  I do have an
  <a href="./">index page</a> and an <a href="feed.xml">RSS
  feed</a> that resemble a blog, but they are simply a list of
  selected pages arranged in reverse chronological order.  The pages
  themselves are scattered across various
  <a href="links.html">corners</a> of this website.  My point is that
  not every website needs to be a blog.  A website can just as well be
  a collection of pages, arranged in whatever way makes sense to you.
  The blog is merely one possible organising principle, not a
  requirement.  If your goal is simply to share your thoughts on your
  own web space, worrying about blogging and tooling can easily become
  counterproductive.  Just write your posts first, in plain HTML if
  need be.
</p>
<p>
  If you truly dislike writing HTML, that is fine too.  Write in
  Markdown, AsciiDoc or whatever plain text format you find pleasant
  and convert it to HTML using Pandoc or a similar tool.  Yes, I am
  slightly undermining my own point here but I think a little bit of
  tooling to make your writing process enjoyable is reasonable.
  Tooling should exist to reduce friction, not to become the main
  ceremony.  Personally, I write all my posts directly in HTML.  I use
  Emacs, which provides a number of convenient key sequences and
  functions that make writing HTML very comfortable.  For example, it
  takes only a few keystrokes in Emacs to wrap text in a
  <code>&lt;blockquote&gt;</code> element, insert a code block or
  close any open tags.  But that is just me.  I enjoy writing HTML
  documents in Emacs.  If you do not, it is easy to run your Markdown
  files through a converter and publish the result with only a little
  extra tooling overhead.
</p>
<p>
  It is easy to spend days and weeks polishing a website setup,
  selecting the perfect generator, theme and deployment pipeline, only
  to end up with a beautifully engineered website whose sole content
  is a single 'hello world' post.  That might not be very useful to
  you or anyone else, unless setting up the pipeline itself was your
  goal.  By contrast, a scrappy website made up of standalone HTML
  pages might be useful to you as well as others.  You can refer back
  to it months later.  You can send someone a link.  You can build on
  it gradually.  Even if you never turn it into a blog, never add RSS
  or never add any tooling, it still fulfills its most important
  purpose: it exists and it says something you wanted to say.
</p>
<p>
  So to summarise my post here: Create the website.  Publish
  something.  Do it in the simplest way that lets you get your words
  onto the page and onto the web.  Once you have content that you care
  about, tooling can follow.  Your thoughts, your ideas, your
  personality and quirks are the essence of your website.  Everything
  else is optional.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/writing-first-tooling-second.html">Read on website</a> |
  <a href="https://susam.net/tag/web.html">#web</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a>
</p>
]]>
</description>
</item>
<item>
<title>My Coding Adventures in 2025</title>
<link>https://susam.net/code-2025.html</link>
<guid isPermaLink="false">cdtwf</guid>
<pubDate>Wed, 24 Dec 2025 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  In this post, I return with a retrospective on my coding adventures,
  where I summarise my hobby projects and recreational programming
  activities from the current year.  I did the last such retrospective
  in <a href="code-2023.html">2023</a>.  So I think this is a good
  time to do another retrospective.
</p>
<p>
  At the outset, I should mention that I have done less hobby
  computing this year than in the past few, largely because I spent a
  substantial portion of my leisure time studying Galois theory and
  algebraic graph theory.  In case you are wondering where I am
  learning these subjects from, the books are <em>Galois Theory</em>,
  5th ed. by Ian Stewart and <em>Algebraic Graph Theory</em> by Godsil
  and Royle.  Both are absolutely fascinating subjects and the two
  books I mentioned are quite good as well.  I highly recommend them.
</p>
<p>
  Now back to the coding adventures.  Here they go:
</p>
<ul>
  <li>
    <p>
      <a href="https://github.com/susam/mathb">MathB</a>: The year
      began not with the release of a new project but with the
      opposite: discontinuing a project I had maintained for 13 years.
      MathB.in, a mathematics pastebin service, was discontinued early
      this year.  This is a project I developed in 2012 for myself and
      my friends.  Although a rather simple project, it was close to
      my heart, as I have many fond memories of exchanging
      mathematical puzzles and solutions with my friends using this
      service.  Over time, the project grew quite popular on IRC
      networks, as well as in some schools and universities, where IRC
      users, learners and students used the service to share problems
      and solutions with one another, much as my friends and I had
      done in its early days.
    </p>
    <p>
      I shut it down this year because I wanted to move on from the
      project.  Before the shutdown, a kind member of the
      <a href="https://wiki.archiveteam.org/">Archive Team</a> worked
      with me to
      <a href="https://web.archive.org/web/*/https://mathb.in/">archive</a>
      all posts from the now-defunct website.  Although shutting down
      this service was a bittersweet event for me, I feel relieved
      that I no longer have to run a live service in my spare time.
      While this was a good hobby ten years ago, it no longer is.  See
      my blog post
      <a href="mathbin-is-shutting-down.html">MathB.in Is Shutting
      Down</a> for more details on the reasons behind this decision.
      The source code of this project remains open source and
      available at
      <a href="https://github.com/susam/mathb">github.com/susam/mathb</a>.
    </p>
  </li>
  <li>
    <p>
      <a href="https://github.com/susam/quickqwerty">QuickQWERTY</a>:
      This is a touch-typing tutor that runs in a web browser.  I
      originally developed it in 2008 for myself and my friends.
      While I learnt touch typing on an actual typewriter as a child,
      those lessons did not stick with me.  Much later, while I was at
      university, I came across a Java applet-based touch-typing tutor
      that finally helped me learn touch typing properly.  I disliked
      installing Java plugins in the web browser, which is why I later
      developed this project in plain HTML and JavaScript.  This year,
      I carried out a major refactoring to collapse the entire project
      into a single standalone HTML file with no external
      dependencies.  The source code has been greatly simplified as
      well.
    </p>
    <p>
      When I was younger and more naive, inspired by the complexity
      and multiple layers of abstraction I saw in popular open source
      and professional projects, I tended to introduce similar
      abstractions and complexity into my personal projects.  Over
      time, however, I began to appreciate simplicity.  The new code
      for this project is smaller and simpler.  I am quite happy with
      the end result.  You can take a look at the code here:
      <a href="https://github.com/susam/quickqwerty/blob/main/quickqwerty.html">quickqwerty.html</a>.
      If you want to use the typing tutor, go here:
      <a href="quickqwerty.html">QuickQWERTY</a>.  Unfortunately, it
      does not support keyboard layouts other than QWERTY.  When I
      originally developed this project, my view of the computing
      world was rather limited.  I was not even aware that other
      keyboard layouts existed.  You are, however, very welcome to
      fork the project and adapt the lessons for other layouts.
    </p>
  </li>
  <li>
    <p>
      <a href="https://github.com/susam/cfrs">CFRS[]</a>: This project
      was my first contribution to the quirky world of esolangs.
      CFRS[] is an extremely minimal drawing language consisting of
      only six simple commands: <code>C</code>, <code>F</code>,
      <code>R</code>, <code>S</code>, <code>[</code> and
      <code>]</code>.  I developed it in 2023 and have since been
      maintaining it with occasional bug fixes.  This year, I
      <a href="https://github.com/susam/cfrs/commit/9bae21f">fixed an
      annoying bug</a> that caused the drawing canvas to overflow on
      some mobile web browsers.  A new demo also arrived from the
      community this year and has now been added to the community demo
      page.  See
      <a href="https://susam.github.io/cfrs/demo.html#glimmering-galaxy">Glimmering
      Galaxy</a> for the new demo.  If you want to play with CFRS[]
      now, visit <a href="cfrs.html">CFRS[]</a>.
    </p>
  </li>
  <li>
    <p>
      <a href="https://github.com/susam/fxyt">FXYT</a>: This is
      another esolang project of mine.  This too is a minimal drawing
      language, though not as minimal as CFRS[].  Instead, it is a
      stack-based, postfix canvas colouring language with only 36
      simple commands.  The canvas overflow bug described in the
      previous entry affected this project as well.  That has now been
      fixed.  Further, by popular demand, the maximum allowed code
      length has been increased from 256 bytes to 1024 bytes.  This
      means there is now more room for writing more complex FXYT
      programs.  Additionally, the maximum code length for
      distributable demo links has been increased from 64 bytes to 256
      bytes.  This allows several more impressive demos to have their
      own distributable links.  Visit <a href="fxyt.html">FXYT</a> to
      try it out now.  See also the
      <a href="https://susam.github.io/fxyt/demo.html">Community Demos</a> to
      view some fascinating artwork created by the community.
    </p>
  </li>
  <li>
    <p>
      <a href="https://github.com/susam/nq">Nerd Quiz</a>: This is a
      new project I created a couple of months ago.  It is a simple
      HTML tool that lets you test your nerdiness through short
      quizzes.  Each question is drawn from my everyday moments of
      reading, writing, thinking, learning and exploring.  The project
      is meant to serve as a repository of interesting facts I come
      across in daily life, captured in the form of quiz questions.
      Go here to try it out: <a href="nq.html">Nerd Quiz</a>.  I hope
      you will enjoy these little bits of knowledge as much as I
      enjoyed discovering them.
    </p>
  </li>
  <li>
    <p>
      <a href="primegrid.html">Prime Number Grid Explorer</a>: This is
      a simple prime number grid visualiser I wrote for fun.  It plots
      the prime numbers in a grid where you can set the number of rows
      and the number of columns.  It uses the Miller-Rabin primality
      test with bases drawn from
      <a href="https://oeis.org/A014233">oeis.org/A014233</a> to
      determine whether a number is prime.  This allows it to
      accurately test whether a number is prime up to
      3317044064679887385961980.  For example,
      <a href="primegrid.html#3317044064679887385961781-20-10">this
      grid</a> shows the upper limit of the numbers this tool can
      check.  The three circles displayed there represent the prime
      numbers 3317044064679887385961783, 3317044064679887385961801 and
      3317044064679887385961813.
    </p>
  </li>
  <li>
    <p>
      <a href="https://github.com/susam/mvs">Mark V. Shaney
      Junior</a>: Finally, I have my own Markov gibberish generator.
      Always wanted to have one.  The project is inspired by the
      legendary Usenet bot named Mark V. Shaney that used to post
      messages to various newsgroups in the 1980s.  My Markov chain
      program is written in about 30 lines of Python.  I ran it on my
      24 years of blog posts consisting of over 200 posts and about
      200,000 words and it generated some pretty interesting
      gibberish.  See my blog post
      <a href="fed-24-years-of-posts-to-markov-model.html">Fed 24
      Years of My Posts to Markov Model</a> to see the examples.
    </p>
  </li>
  <li>
    <p>
      <a href="elliptical-python-programming.html">Elliptical Python
      Programming</a>: If the previous item was not silly enough, this
      one surely is.  Earlier this year, I wrote a blog post
      describing the fine art of Python programming using copious
      amounts of ellipses.  I will not discuss it further here to
      avoid spoilers.  I'll just say that any day I'm able to do
      something pointless, whimsical and fun with computers is a good
      day for me.  And it was a good day when I wrote this post.
      Please visit the link above to read the post.  I hope you find
      it fun.
    </p>
  </li>
  <li>
    <p>
      <a href="fizz-buzz-with-cosines.html">Fizz Buzz with
      Cosines</a>: Another silly post in which I explain how to
      compute the discrete Fourier transform of the Fizz Buzz sequence
      and derive a closed-form expression that can be used to print
      the sequence.
    </p>
  </li>
  <li>
    <p>
      <a href="fizz-buzz-in-css.html">Fizz Buzz in CSS</a>: Yet
      another Fizz Buzz implementation, this time using just four
      lines of CSS.
    </p>
  </li>
</ul>
<p>
  That wraps up my coding adventures for this year.  There were fewer
  hobby projects than usual but I enjoyed spending more time learning
  new things and revisiting old ones.  One long-running project came
  to an end, another was cleaned up and a few small new ideas appeared
  along the way.  Looking forward to what the next year brings.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/code-2025.html">Read on website</a> |
  <a href="https://susam.net/tag/programming.html">#programming</a> |
  <a href="https://susam.net/tag/technology.html">#technology</a> |
  <a href="https://susam.net/tag/retrospective.html">#retrospective</a>
</p>
]]>
</description>
</item>


</channel>
</rss>
