<?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 Pal</title>
<link>https://susam.net/</link>
<atom:link rel="self" type="application/rss+xml" href="https://susam.net/feed.xml"/>
<description>Susam's Feed</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>Feb '26 Notes</title>
<link>https://susam.net/26b.html</link>
<guid isPermaLink="false">ntfts</guid>
<pubDate>Fri, 27 Feb 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  Since last month, I have been collecting brief notes on ideas and
  references that caught my attention during each month but did not
  make it into full articles.  Some of these fragments may eventually
  grow into standalone posts, though most will probably remain as they
  are.  At the very least, this approach allows me to keep a record of
  them.
</p>
<p>
  Most of <a href="26a.html">last month's notes</a> grew out of my
  reading of <em>Algebraic Graph Theory</em> by Godsil and Royle.  I
  am still exploring and learning this subject.  This month, however,
  I dove into another book with the same title but this book is
  written by Norman Biggs.  As a result, many of the notes that follow
  are drawn from Biggs's treatment of the topic.
</p>
<p>
  Since I already had a good understanding of the subject from the
  earlier book, I decided to skip the first fourteen chapters of the
  new book.  I began with Chapter 15, which discusses automorphisms of
  graphs and then moved on to the following chapters on graph
  symmetries.  My main reason for picking up Biggs's book was to
  understand Tutte's well known result that any \( s \)-arc-transitive
  finite cubic graph must satisfy \( s \le 5.  \)  While I did not
  reach that chapter this month, I made substantial progress with the
  book.  I hope to work through the proof of Tutte's theorem next
  month.
</p>
<h2 id="contents">Contents<a href="#contents"></a></h2>
<ol>
  <li><a href="#degree-of-vertices-in-an-orbit">Degree of Vertices in an Orbit</a></li>
  <li><a href="#regular-non-vertex-transitive-graphs">Regular Non-Vertex-Transitive Graphs</a></li>
  <li><a href="#vertex-transitive-but-not-edge-transitive">Vertex-Transitive But Not Edge-Transitive</a></li>
  <li><a href="#edge-transitive-but-not-vertex-transitive">Edge-Transitivex But Not Vertex-Transitive</a></li>
  <li><a href="#bipartiteness-as-a-necessary-condition">Bipartiteness as a Necessary Condition</a></li>
  <li><a href="#graph-with-an-automorphism-group">Graph with an Automorphism Group</a></li>
  <li><a href="#permutation-groups-need-not-be-automorphism-groups">Permutation Groups Need Not Be Automorphism Groups</a></li>
  <li><a href="#symmetric-graphs">Symmetric Graphs</a></li>
</ol>
<h2 id="degree-of-vertices-in-an-orbit">Degree of Vertices in an Orbit<a href="#degree-of-vertices-in-an-orbit"></a></h2>
<p>
  If two vertices of a graph belong to the same orbit, then they have
  the same degree.  In other words, for a graph \( X, \) if \( x, y
  \in V(X) \) and there is an automorphism \( \alpha \) such that \(
  \alpha(x) = y, \) then \( \deg(x) = \deg(y).  \)
</p>
<p>
  The proof is quite straightforward.  Let

  \begin{align*}
    N(x) &amp;= \{ v_1, \dots, v_r \}, \\
    N(y) &amp;= \{ w_1, \dots, w_s \}
  \end{align*}

  represent the neighbours of \( x \) and \( y \) respectively.
  Therefore we have

  \[
    x \sim v_1, \; \dots, \; x \sim v_r.
  \]

  Since an automorphism preserves adjacency, we get

  \[
    \alpha(x) \sim \alpha(v_1), \; \dots, \;
    \alpha(x) \sim \alpha(v_r).
  \]

  Substituting \( \alpha(x) = y, \) we get

  \[
    y \sim \alpha(v_1), \; \dots, \; y \sim \alpha(v_r).
  \]

  Thus

  \[
    \alpha(N(x))
    = \{ \alpha(v_1), \; \dots, \; \alpha(v_r) \}
    \subseteq N(y).
  \]

  A similar argument works in reverse as well.  By the definition of
  automorphism, if \( \alpha \) is an automorphism, so is \(
  \alpha^{-1}.  \)  From the definition of \( N(y) \) above, we have

  \[
    y \sim w_1, \; \dots, \; y \sim w_s.
  \]

  Therefore

  \[
    \alpha^{-1}(y) \sim \alpha^{-1}(w_1), \; \dots, \;
    \alpha^{-1}(y) \sim \alpha^{-1}(w_s).
  \]

  This is equivalent to

  \[
    x \sim \alpha^{-1}(w_1), \; \dots, \; x \sim \alpha^{-1}(w_s).
  \]

  Thus

  \[
    \alpha^{-1}(N(y))
    = \{ \alpha^{-1}(w_1), \; \dots, \; \alpha^{-1}(w_s) \}
    \subseteq N(x)
  \]

  This can be rewritten as

  \[
    \{ \alpha^{-1}(w_1), \; \dots, \; \alpha^{-1}(w_s) \}
    \subseteq \{ v_1, \dots, v_r \}.
  \]

  Therefore

  \[
    N(y)
    = \{ w_1, \dots, w_s \}
    \subseteq \{ \alpha(v_1), \dots, \alpha(v_r) \}
    = \alpha(N(x)).
  \]

  We have shown that \( \alpha(N(x)) \subseteq N(y) \) and \( N(y)
  \subseteq \alpha(N(x)).  \)  Thus

  \[
    \alpha(N(x)) = N(y).
  \]

  Thus

  \[
    \lvert N(y) \rvert = \lvert \alpha(N(x)) \rvert = r.
  \]

  Therefore both \( x \) and \( y \) have \( r \) neighbours each.
  Hence \( \deg(x) = \deg(y).  \)
</p>
<h2 id="regular-non-vertex-transitive-graphs">Regular Non-Vertex-Transitive Graphs<a href="#regular-non-vertex-transitive-graphs"></a></h2>
<p>
  The <a href="https://en.wikipedia.org/wiki/Frucht_graph">Frucht graph</a>
  and the
  <a href="https://en.wikipedia.org/wiki/Folkman_graph">Folkman
  graph</a> are examples of graphs that are \( k \)-regular but not
  vertex-transitive.  In fact, the Folkman graph is a semi-symmetric
  graph, i.e. it is regular and edge-transitive but not
  vertex-transitive.
</p>
<h2 id="vertex-transitive-but-not-edge-transitive">Vertex-Transitive But Not Edge-Transitive<a href="#vertex-transitive-but-not-edge-transitive"></a></h2>
<p>
  The circular ladder graph \( CL_3, \) i.e. the triangular prism
  graph, is vertex-transitive but not edge-transitive.
</p>
<p>
  Every vertex has the same local structure.  Every vertex has degree
  \( 3 \) and it lies on exactly one of the two triangles and it has
  exactly one 'vertical' edge connecting it to the corresponding edge
  on the other triangle.  Any vertex can be sent to any other by an
  automorphism.
</p>
<p>
  Since triangle edges are in a triangle and vertical edges are in no
  triangle, no automorphism can send a triangle edge to a vertical
  edge or vice versa.  Therefore the graph is not edge-transitive.
</p>
<h2 id="edge-transitive-but-not-vertex-transitive">Edge-Transitivex But Not Vertex-Transitive<a href="#edge-transitive-but-not-vertex-transitive"></a></h2>
<p>
  The complete bipartite graphs \( K_{m,n} \) with \( m \ne n \) are
  edge-transitive but not vertex-transitive.
</p>
<p>
  Every edge connects one vertex from the \( m \)-part to one vertex
  from the \( n \)-part.  Any permutation of vertices inside the \( m
  \)-part preserves adjacency.  Similarly, any permutation of vertices
  inside the \( n \)-part preserves adjacency.
</p>
<p>
  Take two arbitrary edges

  \[
    uv, \; u'v' \in E(K_{m,n})
  \]

  where \( u, u' \) are vertices that lie in the \( m \)-part and \(
  v, v' \) are vertices that lie in the \( n \)-part.  Permute
  vertices within the \( m \)-part to send \( u \) to \( u'.  \)
  Similarly, permute vertices within the \( n \)-part to send \( v \)
  to \( v'.  \)  This gives an automorphism that sends the edge \( uv
  \) to \( u'v'.  \)  In this manner we can find an automorphism that
  sends any edge to any other.  Therefore, \( K_{m,n} \) is
  edge-transitive.
</p>
<p>
  However, \( K_{m,n} \) is not vertex-transitive since no
  automorphism can send a vertex in the \( m \)-part to a vertex in
  the \( n \)-part since the vertices in the \( m \)-part have degree
  \( n \) and the vertices in the \( n \)-part have degree \( m.  \)
</p>
<h2 id="bipartiteness-as-a-necessary-condition">Bipartiteness as a Necessary Condition<a href="#bipartiteness-as-a-necessary-condition"></a></h2>
<p>
  If a connected graph is edge-transitive but not vertex-transitive,
  then it must be bipartite.
</p>
<h2 id="graph-with-an-automorphism-group">Graph with an Automorphism Group<a href="#graph-with-an-automorphism-group"></a></h2>
<p>
  In 1938, Frucht proved that for every finite abstract group \( G, \)
  there exists a graph whose automorphism group is isomorphic to \( G
 .  \)
</p>
<p>
  Remarkably, this result remains valid even when we restrict our
  attention to cubic graphs.  That is, for every finite abstract group
  \( G, \) there exists a cubic graph whose automorphism group is
  isomorphic to \( G.  \)  Moreover, the result has been extended to
  graphs satisfying various additional graph-theoretical properties,
  such as \( k \)-connectivity, \( k \)-regularity and prescribed
  chromatic number.
</p>
<h2 id="permutation-groups-need-not-be-automorphism-groups">Permutation Groups Need Not Be Automorphism Groups<a href="#permutation-groups-need-not-be-automorphism-groups"></a></h2>
<p>
  Consider the following specialised version of the problem discussed
  in the previous section: Given a permutation group on a set \( X, \)
  must there exist a graph with vertex set \( X \) whose automorphism
  group is precisely that permutation group?
</p>
<p>
  The answer is no.  Consider the cyclic group \( C_3 \) acting on \(
  X = \{ a, b, c \}.  \)  There is no graph \( \Gamma \) with \(
  V(\Gamma) = X \) and \( \operatorname{Aut}(\Gamma) \cong C_3.  \)  If
  we take \( \Gamma = K_3, \) then \( C_3 \subset S_3 =
  \operatorname{Aut}(K_3) \) but \( C_3 \ne \operatorname{Aut}(K_3)
 .  \)
</p>
<h2 id="symmetric-graphs">Symmetric Graphs<a href="#symmetric-graphs"></a></h2>
<p>
  It is interesting that while we study graph symmetry through
  concepts such as graph automorphisms, vertex-transitivity,
  edge-transitivity, etc. the name <em>symmetric graph</em> is
  reserved for graphs that are \( 1 \)-arc-transitive.  A
  vertex-transitive graph or an edge-transitive graph need not be
  \(1\)-arc-transitive and therefore need not be symmetric.
</p>
<p>
  However, every \( s \)-arc-transitive graph is \(1 \)-arc-transitive
  for \( s \ge 1.  \)  Consequently, every \( s \)-arc-transitive graph
  is symmetric.  Moreover, every distance-transitive graph is also \(
  1 \)-arc-transitive and hence symmetric.
</p>
<p>
  Formally, we say that a graph \( \Gamma \) is \( 1 \)-arc-transitive
  (or equivalently, symmetric) if for all \( 1 \)-arcs \( uv \) and \(
  u'v' \) of \( \Gamma, \) there is an automorphism \( \alpha \in
  \operatorname{Aut}(\Gamma) \) such that \( \alpha(uv) = u'v'.  \)
</p>
<p>
  Stated in more basic terms, we can say that \( \Gamma \) is
  symmetric if for all \( u, v, u', v' \in V(\Gamma) \) satisfying \(
  u \sim v \) and \( u' \sim v', \) there exists \( \alpha \in
  \operatorname{Aut}(\Gamma) \) such that \( \alpha(u) = u' \) and \(
  \alpha(v) = v'.  \)
</p>
<p>
  Switching gears now, we say that \( \Gamma \) is distance-transitive
  if for all \( u, v, u', v' \in V(\Gamma) \) satisfying \( d(u, v) =
  d(u', v'), \) there exists \( \alpha \in \operatorname{Aut}(\Gamma)
  \) such that \( \alpha(u) = u' \) and \( \alpha(v) = v'.  \)  Since
  all \( 1 \)-arcs \( uv \) and \( u'v' \) satisfy \( d(u, v) = d(u',
  v') = 1, \) distance-transitivity implies that there is an
  automorphism that sends \( uv \) to \( u'v'.  \)  Therefore a
  distance-transitive graph is also \( 1 \)-arc-transitive.
</p>
<p>
  To summarise, a graph must possess a certain degree of symmetry in
  order to be called symmetric.  It turns out that merely having a
  non-trivial automorphism group is not sufficient.  Even being
  vertex-transitive or edge-transitive is not enough for a graph to be
  called symmetric.  The graph needs to be at least \( 1
  \)-arc-transitive to be called symmetric.
</p>
<p>
  Another interesting aspect of this terminology is that the property
  of being asymmetric is not the exact opposite of being symmetric.
  For example, a vertex-transitive graph need not be symmetric.
  However, that does not make it asymmetric.  A graph is called
  asymmetric if it has no non-trivial automorphisms, i.e. its
  automorphism group contains only the identity permutation.  Thus, if
  a graph has at least two vertices and is vertex-transitive, it must
  admit a non-trivial automorphism that maps one vertex to another.
  So while such a vertex-transitive may not be symmetric, it isn't
  asymmetric either.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/26b.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>
</p>
]]>
</description>
</item>
<item>
<title>Nerd Quiz #4</title>
<link>https://susam.net/code/news/nq/4.0.0.html</link>
<guid isPermaLink="false">nqfou</guid>
<pubDate>Sun, 22 Feb 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  Nerd Quiz #4 is the fourth instalment of Nerd Quiz, a single page
  HTML application that challenges you to measure your inner geek with
  a brief quiz.  Each question in the quiz comes from everyday moments
  of reading, writing, thinking, learning and exploring.
</p>
<p>
  This release introduces five new questions drawn from a range of
  topics, including computing history, graph theory and Unix.
  Visit <a href="../../../nq.html#4">Nerd Quiz</a> to try the quiz.
</p>
<p>
  A community discussion page is
  <a href="../../../comments/nq.html">available here</a>.  You are
  very welcome to share your score or discuss the questions there.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/code/news/nq/4.0.0.html">Read on website</a> |
  <a href="https://susam.net/tag/web.html">#web</a> |
  <a href="https://susam.net/tag/miscellaneous.html">#miscellaneous</a> |
  <a href="https://susam.net/tag/game.html">#game</a>
</p>
]]>
</description>
</item>
<item>
<title>Deep Blue: Chess vs Programming</title>
<link>https://susam.net/deep-blue.html</link>
<guid isPermaLink="false">dblue</guid>
<pubDate>Sun, 15 Feb 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  I remember how dismayed Kasparov was after losing the 1997 match to
  IBM's Deep Blue, although his views on Deep Blue became more
  balanced with time and he accepted that we had entered a new era in
  which computers would outperform grandmasters at chess.
</p>
<p>
  Still, chess players can take comfort in the fact that chess is
  still played between humans.  Players make their name and fame by
  beating other humans because playing against computers is no longer
  interesting as a competition.
</p>
<p>
  Many software developers would like to have similar comfort.  But
  that comfort is harder to find, because unlike chess, building
  prototypes or PoCs is not seen as a sport or art form.  It is mostly
  seen as a utility.  So while brain-coding a PoC may still be
  intellectually satisfying for the programmer, to most other people
  it only matters that the thing works.  That means that programmers
  do not automatically get the same protected space that chess players
  have, where the human activity itself remains valued even after
  machines become stronger.  The activity programmers enjoy may
  continue but the recognition and economic value attached to it may
  shrink.
</p>
<p>
  So I think the big adjustment software developers have to make is
  this: The craft will still exist and we will still enjoy doing it
  but the credit and value will increasingly go to those who define
  problems well, connect systems, make good product decisions and make
  technology useful in messy real-world situations.  It has already
  been this way for a while and will only become more so as time goes
  by.
</p>
<hr>
<p>
  <em>
    This note reproduces a recent comment I posted in a Lobsters forum
    thread about LLM-assisted software development at
    at <a href="https://lobste.rs/s/qmjejh#c_4bgez9">lobste.rs/s/qmjejh</a>.
  </em>
</p>
<p>
  <em>
    See also:
    <a href="inverse-laws-of-robotics.html">Three Inverse Laws of AI and Robotics</a>.
  </em>
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/deep-blue.html">Read on website</a> |
  <a href="https://susam.net/tag/miscellaneous.html">#miscellaneous</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>Jan '26 Notes</title>
<link>https://susam.net/26a.html</link>
<guid isPermaLink="false">ntjts</guid>
<pubDate>Thu, 29 Jan 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  In these monthly notes, I jot down ideas and references I
  encountered during the month that I did not have time to expand into
  their own posts.  A few of these may later develop into independent
  posts but most of them will likely not.  In any case, this format
  ensures that I record them here.  I spent a significant part of this
  month studying the book <em>Algebraic Graph Theory</em> by Godsil
  and Royle, so many of the notes here are about it.  There are a few
  non-mathematical, technical notes towards the end.
</p>
<h2 id="contents">Contents<a href="#contents"></a></h2>
<ol>
  <li><a href="#cayley-graphs">Cayley Graphs</a></li>
  <li><a href="#vertex-transitive-graphs">Vertex-Transitive Graphs</a></li>
  <li><a href="#arc-transitive-graphs">Arc-Transitive Graphs</a></li>
  <li><a href="#bipartite-graphs-and-cycle-parity">Bipartite Graphs and Cycle Parity</a></li>
  <li><a href="#tutte-theorem">Tutte's Theorem</a></li>
  <li><a href="#tutte-8-cage">Tutte's 8-Cage</a></li>
  <li><a href="#lcg">Linear Congruential Generator</a></li>
  <li><a href="#cat-n">Numbering Lines</a></li>
</ol>
<h2 id="cayley-graphs">Cayley Graphs<a href="#cayley-graphs"></a></h2>
<p>
  Let \( G \) be a group and let \( C \subseteq G \) such that \( C \)
  is closed under taking inverses and does not contain the identity,
  i.e.

  \[
    \forall x \in C, \; x^{-1} \in C, \qquad e \notin C.
  \]

  Then the Cayley graph \( X(G, C) \) is the graph with the vertex set
  \( V(X(G, C)) \) and edge set \( E(X(G, C)) \) defined by

  \begin{align*}
    V(X(G, C)) &amp;= G, \\
    E(X(G, C)) &amp;= \{ gh : hg^{-1} \in C \}.
  \end{align*}

  The set \( C \) is known as the connection set.
</p>
<h2 id="vertex-transitive-graphs">Vertex-Transitive Graphs<a href="#vertex-transitive-graphs"></a></h2>
<p>
  A graph \( X \) is <em>vertex-transitive</em> if its automorphism
  group acts transitively on its set of vertices \( V(X).  \)
  Intuitively, this means that no vertex has a special role.  We can
  'move' the graph around so that any chosen vertex becomes any other
  vertex.  In other words, all vertices are indistinguishable.  The
  graph looks the same from each vertex.
</p>
<p>
  The \( k \)-cube \( Q_k \) is vertex-transitive.  So are the Cayley
  graphs \( X(G, C).  \)  However the path graph \( P_3 \) is not
  vertex-transitive since no automorphism can send the middle vertex
  of valency \( 2 \) to an end vertex of valency \( 1.  \)
</p>
<h2 id="arc-transitive-graphs">Arc-Transitive Graphs<a href="#arc-transitive-graphs"></a></h2>
<p>
  The cube \( Q_3 \) is \( 2 \)-arc-transitive but not \( 3
  \)-arc-transitive.  In \( Q_3, \) a \( 3 \)-arc belonging to a \( 4
  \)-cycle cannot be sent to a \( 3 \)-arc that does not belong to a
  \( 4 \)-cycle.  This is easy to explain.  The end vertices of a \( 3
  \)-arc belonging to a \( 4 \)-cycle are adjacent but the end
  vertices of a \( 3 \)-arc not belonging to a \( 4 \)-cycle are not
  adjacent.  Therefore, no automorphism can map the end vertices of
  the first \( 3 \)-arc to those of the second \( 3 \)-arc.
</p>
<p>
  For intuition, imagine that a traveller stands on a vertex and
  chooses an edge to move along.  They do this \( s \) times thereby
  walking along an arc of length \( s, \) also known as an \( s
  \)-arc.  By the definition of \( s \)-arcs, the traveller is not
  allowed to backtrack from one vertex to the previous one
  immediately.  In an \( s \)-arc-transitive graph, these arcs look
  the same no matter which vertex they start from or which edges they
  choose.  In the cube, this is indeed true for \( s = 2.  \)  All arcs
  of length \( 2 \) are indistinguishable.  No matter which arc of
  length \( 2 \) the traveller has walked along, the graph would look
  the same from their perspective at each vertex along the arc.
  However, this no longer holds good for arcs of length \( 3 \) since
  there are two distinct kinds of arcs of length \( 3.  \)  The first
  kind ends at a distance of \( 1 \) from the starting vertex of the
  arc (when the arc belongs to a \( 4 \)-cycle).  The second kind ends
  at a distance \( 3 \) from the starting vertex of the arc (when the
  arc does not belong to a \( 4 \)-cycle).  Therefore the cube is not
  \( 3 \)-arc-transitive.
</p>
<h2 id="bipartite-graphs-and-cycle-parity">Bipartite Graphs and Cycle Parity<a href="#bipartite-graphs-and-cycle-parity"></a></h2>
<p>
  A graph is bipartite if and only if it contains no cycles of odd
  length.  Equivalently, every cycle in a bipartite graph has even
  length.  Conversely, if every cycle in a graph has even length, then
  the graph is bipartite.
</p>
<h2 id="tutte-theorem">Tutte's Theorem<a href="#tutte-theorem"></a></h2>
<p>
  For any \( s \)-arc-transitive cubic graph, \( s \le 5.  \)  This was
  demonstrated by W. T. Tutte in 1947.  A proof can be found in
  Chapter 18 of <em>Algebraic Graph Theory</em> by Norman Biggs.
</p>
<p>
  In 1973, Richward Weiss established a more general theorem that
  proves that for any \( s \)-arc-transitive graph, \( s \le 7.  \)
  The bound is weaker but it applies to all graphs rather than only to
  cubic ones.
</p>
<h2 id="tutte-8-cage">Tutte's 8-Cage<a href="#tutte-8-cage"></a></h2>
<p>
  The book <em>Algebraic Graph Theory</em> by Godsil and Royle offers
  the following two descriptions of Tutte's 8-cage on 30 vertices:
</p>
<blockquote>
  Take the cube and an additional vertex \( \infty.  \)  In each set of
  four parallel edges, join the midpoint of each pair of opposite
  edges by an edge, then join the midpoint of the two new edges by an
  edge, and finally join the midpoint of this edge to \( \infty.  \)
</blockquote>
<blockquote>
  Construct a bipartite graph \( T \) with the fifteen edges as one
  colour class and the fifteen \( 1 \)-factors as the other, where
  each edge is adjacent to the three \( 1 \)-factors that contain it.
</blockquote>
<p>
  It can be shown that both descriptions construct a cubic bipartite
  graph on \( 30 \) vertices of girth \( 8.  \)  It can be further
  shown that there is a unique cubic bipartite graph on \( 30 \)
  vertices with girth \( 8.  \)  As a result both descriptions above
  construct the same graph.
</p>
<h2 id="lcg">Linear Congruential Generator<a href="#lcg"></a></h2>
<p>
  Here is a simple linear congruential generator (LCG) implementation
  in JavaScript:
</p>
<pre><code>function srand (seed) {
  let x = seed
  return function () {
    x = (1664525 * x + 1013904223) % 4294967296
    return x
  }
}</code></pre>
<p>
  Here is an example usage:
</p>
<pre><samp>&gt; <kbd>const rand = srand(0)</kbd>
undefined
&gt; <kbd>rand()</kbd>
1013904223
&gt; <kbd>rand()</kbd>
1196435762
&gt; <kbd>rand()</kbd>
3519870697</samp></pre>
<h2 id="cat-n">Numbering Lines<a href="#cat-n"></a></h2>
<p>
  Both BSD and GNU <code>cat</code> can number output lines with
  the <code>-n</code> option.  For example:
</p>
<pre><samp>$ <kbd>printf 'foo\nbar\nbaz\n' | cat -n</kbd>
     1  foo
     2  bar
     3  baz</samp></pre>
<p>
  However I have always used <code>nl</code> for this.  For example:
</p>
<pre><samp>$ <kbd>printf 'foo\nbar\nbaz\n' | nl</kbd>
     1  foo
     2  bar
     3  baz</samp></pre>
<p>
  While <code>nl</code> is
  <a href="https://pubs.opengroup.org/onlinepubs/9699919799/utilities/nl.html">specified
  in POSIX</a>, the <code>cat -n</code> option
  <a href="https://pubs.opengroup.org/onlinepubs/9699919799/utilities/cat.html">is
  not</a>.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/26a.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/programming.html">#programming</a> |
  <a href="https://susam.net/tag/javascript.html">#javascript</a> |
  <a href="https://susam.net/tag/shell.html">#shell</a>
</p>
]]>
</description>
</item>
<item>
<title>QuickQWERTY 1.2.1</title>
<link>https://susam.net/code/news/quickqwerty/1.2.1.html</link>
<guid isPermaLink="false">qqoto</guid>
<pubDate>Tue, 27 Jan 2026 00:00:00 +0000</pubDate>
<description>
<![CDATA[
<p>
  QuickQWERTY 1.2.1 is now available.  QuickQWERTY is a web-based
  touch typing tutor for QWERTY keyboards that runs directly in the
  web browser.
</p>
<p>
  This release contains a minor bug fix in Unit 4.3.  Unit 4.3 is a
  'Control' unit that lets you practise typing partial words as well
  as full words.  In one place in this unit, the following sequence of
  partial and full words occurs:
</p>
<pre><code>l li lime lime</code></pre>
<p>
  The full word <code>lime</code> was incorrectly repeated twice.
  This has been fixed to:
</p>
<pre><code>l li lim lime</code></pre>
<p>
  To try out QuickQWERTY, go to
  <a href="../../../quickqwerty.html">quickqwerty.html</a>.
</p>
<!-- ### -->
<p>
  <a href="https://susam.net/code/news/quickqwerty/1.2.1.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>
</p>
]]>
</description>
</item>


</channel>
</rss>
