<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>RUNAWAYDEVIL</title>
    <link>https://runawaydevil.com/</link>
    <description>A textual system: shell, virtual filesystem, BBS reader and ANSI gallery, in a fixed 80x25 grid.</description>
    <language>en</language>
    <managingEditor>Pablo Murad</managingEditor>
    <generator>RunawayDevil System / node 01</generator>
    <atom:link href="https://runawaydevil.com/feed.xml" rel="self" type="application/rss+xml" />
    <lastBuildDate>Fri, 21 Aug 2026 12:00:00 GMT</lastBuildDate>
    <item>
      <title>Eighty by Twenty-Five</title>
      <link>https://runawaydevil.com/posts/eighty-by-twenty-five</link>
      <guid isPermaLink="true">https://runawaydevil.com/posts/eighty-by-twenty-five</guid>
      <pubDate>Fri, 21 Aug 2026 12:00:00 GMT</pubDate>
      <description>The grid on this site is a specification, not a theme. Here is what enforcing it actually cost.</description>
      <content:encoded><![CDATA[<h1>Eighty by Twenty-Five</h1>
<p>This system is 80 columns by 25 lines. Not "about 80". Not "80 on desktop".
Eighty, on a phone, on a 4K panel, in a window you have dragged to the size of
a business card. What changes with your screen is how large one cell is drawn,
and nothing else.</p>
<p>That sounds like a decoration. It is closer to a constitution: almost every
awkward decision in this codebase comes from refusing to break it.</p>
<h2>What the browser wants to do instead</h2>
<p>Every terminal library for the web is built around the opposite idea. You give
it a container, it measures the container, it reports back "you have 142
columns and 38 rows", and the program on the other end reflows to match. That
is correct for a terminal that hosts <em>someone else's</em> program.</p>
<p>Here there is no other program. The system is the only thing writing to the
screen, so the size can be fixed and everything above it can be composed
against a known width. That is why the terminal in this site is written here
rather than pulled in — not because the libraries are bad, but because their
central feature is the one thing that had to go.</p>
<h2>Nothing measures text with .length</h2>
<p>The first real casualty was string length. <code>"日本語".length</code> is 3, and it
occupies 6 cells. <code>"coração"</code> measured after Unicode normalisation can be 8
code points and 7 cells. A line with colour in it —</p>
<pre><code class="language-text">ESC[91mRUNAWAY ESC[0m
</code></pre>
<p>— has fourteen characters of escape that occupy nothing at all.</p>
<p>So there is exactly one module allowed to reason about horizontal space, and
it counts <em>cells</em>: grapheme clusters, East Asian widths, combining marks, and
an ANSI scanner that skips what is invisible. Everything else calls it. A test
fails if any composed screen exceeds cell 80, which is how a typo gets caught
before it becomes a crooked box.</p>
<h2>Three defences, because one is not enough</h2>
<ol>
<li><strong>The model cannot exceed it.</strong> The screen allocates exactly 2000 cells.
Writing past column 80 wraps. There is no code path that grows the grid.</li>
<li><strong>Composition measures in cells.</strong> Every listing, every header, every piece
of art is measured before it ships. The art validator refuses to build a
piece 92 cells wide.</li>
<li><strong>The renderer fits, never reflows.</strong> Playwright asserts <code>cols === 80</code> and
<code>rows === 25</code> at 3440×1440, at 1280×720, on a tablet and on a phone.</li>
</ol>
<h2>The part that took three tries</h2>
<p>Filling the screen is where the constraint bites. An 80×25 grid has an aspect
ratio near 1.6:1; a modern monitor is closer to 1.9:1. Scale the grid until it
fills the height and the glyphs come out around 38 pixels tall — legible, and
absurd for reading prose. Cap the size and a wide screen shows a lot of black.</p>
<p>There is no arrangement that satisfies both. So the system does the honest
thing: it renders at the size an 80×25 terminal has always been — 640 by 400
pixels, cells of 8 by 16 — and lets the surround be black. The frame is a
hairline, so the emptiness reads as a terminal in a room rather than a page
that failed to load.</p>
<p>It also stopped scaling with a CSS transform, which was quietly resampling
every glyph. Setting the font size instead means the browser rasterises at the
real size. The text got noticeably sharper the day that changed.</p>
<h2>Why bother</h2>
<p>Because the limit does the editing. Seventy-six usable columns is roughly the
width prose wants anyway; the constraint just makes you notice. A pager forces
an argument to have parts. Sixteen colours force contrast to mean something.</p>
<p>None of that is nostalgia. It is a set of limits that happen to produce better
decisions than no limits at all.</p>]]></content:encoded>
      <category>runawaydevil</category>
      <category>terminal</category>
      <category>design</category>
    </item>
    <item>
      <title>A Shell That Is Not a Shell</title>
      <link>https://runawaydevil.com/posts/a-shell-that-is-not-a-shell</link>
      <guid isPermaLink="true">https://runawaydevil.com/posts/a-shell-that-is-not-a-shell</guid>
      <pubDate>Tue, 18 Aug 2026 12:00:00 GMT</pubDate>
      <description>RunawayShell has a prompt, a filesystem and a history. It has no interpreter, and that is the whole design.</description>
      <content:encoded><![CDATA[<h1>A Shell That Is Not a Shell</h1>
<p>The prompt at the bottom of this system looks like <code>bash</code>. It completes paths
with Tab, walks history with the arrows, understands <code>Ctrl+W</code>, and prints
<code>no such file or directory</code> in the right tone of voice.</p>
<p>It cannot run anything. There is no interpreter behind it at all.</p>
<h2>What actually happens when you press Enter</h2>
<p>The line is split into words, honouring quotes. The first word is looked up in
a table. If it is there, that entry's function runs with the remaining words.
If it is not there, nothing happens and you get a boxed notice.</p>
<p>That is the entire mechanism. No <code>eval</code>, no <code>Function</code> constructor, no
dynamic dispatch on a string, no fallthrough to anything. Type</p>
<pre><code class="language-text">guest@runawaydevil:~ $ rm -rf /
</code></pre>
<p>and the parser produces four words, fails to find <code>rm</code>, and says so. <code>|</code>, <code>></code>,
<code>&#x26;&#x26;</code> and <code>$(...)</code> are not operators here — they are characters that end up
inside a word and get looked up like any other.</p>
<h2>The part that is easy to get wrong</h2>
<p>The temptation, when building something like this, is a small convenience:
one command that takes an expression, or a debug hatch, or a plugin hook that
takes a callback name. Each is harmless alone and each one turns a table into
an interpreter.</p>
<p>So the rule is written as a test:</p>
<pre><code class="language-text">for (const name of ['eval','exec','sh','bash','fetch','curl','wget','sudo'])
  expect(findCommand(name)).toBeNull()
</code></pre>
<p><code>node</code> exists, but it prints node status — the BBS sense of the word. The test
asserts that too, because a name that <em>looks</em> dangerous deserves to be pinned
down rather than argued about.</p>
<h2>There is no filesystem either</h2>
<p><code>~/journal/2026-08-21-eighty-by-twenty-five.md</code> is not a path. It is a key in
a tree of objects built when the site was compiled, from Markdown files in a
repository. <code>ls</code> walks it, <code>cat</code> prints its contents, <code>grep</code> searches them.
Nothing has a writer. <code>..</code> cannot climb past the root, because there is no
root to climb past — resolution is string arithmetic over an array.</p>
<p>Which means the usual questions about a web shell do not apply. There is no
container to escape, no PTY to hijack, no socket to hold open, no host process
that exists. The whole system is static files and a program that runs in your
browser, and the worst thing a visitor can do to it is press keys.</p>
<h2>Why keep the shape then</h2>
<p>Because the shape is good. A directory listing is a better index than a
navigation bar: it has dates, sizes, and an obvious way to look inside. <code>grep</code>
is a better search box than a search box. And muscle memory is real — people
who have typed <code>ls</code> ten thousand times should get what they expect.</p>
<p>The commands, the filesystem and the pager know nothing about the DOM. They
consume key events and produce ANSI. Point that stream at a socket instead of
a renderer and the same system answers a telnet call. That is not implemented
and may never be, but the seam is real and it is marked.</p>]]></content:encoded>
      <category>runawaydevil</category>
      <category>security</category>
      <category>software</category>
    </item>
    <item>
      <title>Nothing Is Recorded</title>
      <link>https://runawaydevil.com/posts/nothing-is-recorded</link>
      <guid isPermaLink="true">https://runawaydevil.com/posts/nothing-is-recorded</guid>
      <pubDate>Tue, 11 Aug 2026 12:00:00 GMT</pubDate>
      <description>No analytics, no cookies, no backend, and a payload that stopped being shipped thirty-seven times.</description>
      <content:encoded><![CDATA[<h1>Nothing Is Recorded</h1>
<p>There is nothing on the other end of this site. No database, no session store,
no analytics pipeline, no consent platform, no log of who read what. Not
because those were disabled — because they were never built, and there is
nowhere for them to run.</p>
<p>That is easy to claim and cheap to check, so here is the actual accounting.</p>
<h2>What the browser stores</h2>
<p>Six keys, all prefixed <code>rd.</code>:</p>
<pre><code class="language-text">rd.lastVisit        the date you were last here
rd.handle           a name, if you ever set one
rd.theme            dos | amber | green | mono
rd.bootSeen         whether to play the connect sequence
rd.history          the commands you typed
rd.readerPosition   the line you left each entry on
</code></pre>
<p>All of it is in your browser and none of it is sent anywhere. A test fails if
a seventh key appears, and another asserts the cookie jar is empty.</p>
<h2>What the network does</h2>
<p>One HTML page, one stylesheet, one script, one font, one JSON file. After
that, a test watches every request the page makes while a session runs
commands and searches — and asserts the list is empty. The terminal genuinely
does not talk to anything once it has loaded.</p>
<p>The JSON file is the interesting one, because it used to be much worse.</p>
<h2>Thirty-seven copies of the same thing</h2>
<p>The shell needs the whole collection in hand whatever route you arrived on:
<code>ls</code> is not a per-page question, and neither is <code>search</code>. So the payload was
embedded in the page — which meant every one of the site's 37 routes carried
its own copy of the same 67KB.</p>
<pre><code class="language-text">before   dist 3166 KB   index.html 82 KB
after    dist  490 KB   index.html 5.4 KB
</code></pre>
<p>Two changes. The payload became one static file that every route fetches and
the browser caches once. And the plain-text field came out of it entirely —
it was 30% of the bytes and it is derivable, since stripping the escapes off
the rendered lines gives the same words. It gives them <em>better</em>, actually:
<code>grep</code> now matches exactly what is on the screen.</p>
<p>The page is now smaller than most sites' cookie banners.</p>
<h2>The feeds are the point</h2>
<p>There are four, because different people read differently:</p>
<pre><code class="language-text">/feed.xml     RSS 2.0
/feed.atom    Atom
/feed.json    JSON Feed
/sfeed.tsv    sfeed's TSV, for reading from a terminal
</code></pre>
<p>All four carry every entry in full. No account, no email, no "subscribe to
continue". The Markdown source of any entry is at <code>/posts/&#x3C;slug>/index.md</code> if
you would rather have the file than the page.</p>
<h2>And it works with the terminal off</h2>
<p>Every route renders as ordinary HTML from the same Markdown. With JavaScript
disabled you get the text, the links, the credits under the art — a plain
document. The terminal hides that fallback once it starts, and if it never
starts, the fallback is the site.</p>
<p>The terminal is the interface. It is not an excuse to break the web.</p>]]></content:encoded>
      <category>runawaydevil</category>
      <category>web</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Carta de um node solitário</title>
      <link>https://runawaydevil.com/posts/carta-de-um-node-solitario</link>
      <guid isPermaLink="true">https://runawaydevil.com/posts/carta-de-um-node-solitario</guid>
      <pubDate>Wed, 29 Jul 2026 12:00:00 GMT</pubDate>
      <description>Sobre modems, madrugadas e a ideia de um lugar que só existe enquanto alguém está conectado.</description>
      <content:encoded><![CDATA[<h1>Carta de um node solitário</h1>
<p>Havia uma hora exata em que a linha ficava livre. Depois das onze, quando
ninguém mais ia usar o telefone, o modem podia gritar à vontade. Aquele ruído
— o carrier, a negociação, o silêncio súbito quando os dois lados finalmente
concordavam — era a coisa mais próxima de uma porta se abrindo que a
informática já produziu.</p>
<h2>Um lugar com capacidade um</h2>
<p>O que mais me impressiona hoje não é a lentidão. É a <strong>exclusividade</strong>.</p>
<p>Um BBS de um node só aceitava uma pessoa por vez. Enquanto você estava lá,
ninguém mais no mundo estava. Não havia timeline, não havia outras pessoas
rolando a mesma tela em paralelo, não havia contagem de quem estava online.
Havia você, o sysop dormindo, e um computador na sala dele.</p>
<blockquote>
<p>Um lugar que só existe enquanto alguém está conectado é uma ideia
estranhamente honesta sobre o que é estar em qualquer lugar.</p>
</blockquote>
<h2>O que se fazia lá</h2>
<ol>
<li>Ler as mensagens novas desde a última visita.</li>
<li>Responder duas ou três.</li>
<li>Baixar um arquivo de 300 KB em quinze minutos.</li>
<li>Olhar a arte ANSI da tela de entrada por tempo demais.</li>
<li>Deslogar antes que a conta do telefone virasse assunto de família.</li>
</ol>
<p>Cinco coisas. Não havia uma sexta. A ausência de uma sexta coisa é
exatamente o que fazia as outras cinco valerem a pena.</p>
<h2>Coração, informação, acentuação</h2>
<p>Uma nota técnica, já que este site insiste em ser um terminal: o português
sempre foi um pequeno problema para telas de texto. Os acentos viviam na parte
alta da tabela — <code>ç</code>, <code>ã</code>, <code>õ</code>, <code>é</code> —, e cada BBS decidia por conta própria se
falava CP437, CP850 ou nada disso. Você digitava <em>coração</em> e o outro lado lia
<em>cora‡Æo</em>.</p>
<p>Aqui isso está resolvido: por dentro é tudo UTF-8, e o CP437 fica onde deve
ficar, que é no desenho.</p>
<h2>O que sobrou</h2>
<p>Sobrou o formato. Uma tela por vez, um assunto por tela, e a certeza de que
quando você desligar aquilo tudo simplesmente deixa de existir até a próxima
madrugada.</p>
<p>Não é nostalgia. É que a coisa funcionava.</p>]]></content:encoded>
      <category>bbs</category>
      <category>memoria</category>
      <category>portugues</category>
    </item>
    <item>
      <title>Sixteen Colours and a Registry</title>
      <link>https://runawaydevil.com/posts/sixteen-colours-and-a-registry</link>
      <guid isPermaLink="true">https://runawaydevil.com/posts/sixteen-colours-and-a-registry</guid>
      <pubDate>Tue, 14 Jul 2026 12:00:00 GMT</pubDate>
      <description>How the art in this system is made, measured, credited, and refused when the credit is missing.</description>
      <content:encoded><![CDATA[<h1>Sixteen Colours and a Registry</h1>
<p>The IBM PC text mode gave an artist sixteen foreground colours, eight
backgrounds, and 256 shapes. No pixels, no gradients, no partial transparency.
A cell was one character in one colour on one background, and that was the
whole medium. People made astonishing things with it for twenty years.</p>
<p>Everything drawn for this system works under the same rules. Here is what that
means in practice, and what the build does about it.</p>
<h2>The vocabulary is smaller than the palette</h2>
<pre><code class="language-text">█ full block     ▓ dark shade
▒ medium shade   ░ light shade
</code></pre>
<p>Those four are not textures — they are <em>mixing</em>. Light shade of white on black
is a grey that does not exist in the sixteen. Medium shade of bright blue on
blue is a blue that is not in the palette either. The entire tonal range of the
medium comes out of four characters.</p>
<p>The site's own pieces are held to the CP437 character set, and the validator
says so out loud when they wander:</p>
<pre><code class="language-text">WARN  ornaments   27x4   outside CP437: ▪ ▫ †
</code></pre>
<p>Which is how <code>▪▫▪</code> became <code>■·■</code> and a dagger became <code>♦</code>. Worth knowing: CP437
has no tilde vowels. It has <code>ç</code>, it has <code>á</code>, it has no <code>ã</code>. That is exactly why
a Brazilian on a 1994 board read <em>cora‡Æo</em> instead of <em>coração</em> — and why the
webfont here is the "Plus" variant rather than the pure 437 one.</p>
<h2>The art is data, and that is deliberate</h2>
<p>Original pieces live as typed data rather than as <code>.ANS</code> files on disk, so the
compiler and the tests can measure them with the same code that measures every
other line in the system. A piece 92 cells wide fails the build instead of
arriving crooked.</p>
<p>The format is not lost. <code>npm run art:export</code> writes each piece out as a real
<code>.ANS</code>: CP437 bytes, CRLF, a SAUCE record built from the registry, a
terminating SUB. They open in PabloDraw and pass through AnsiLove.</p>
<p>Getting that right took two bug fixes worth mentioning, because both destroy
art silently:</p>
<ul>
<li><code>encodeCp437(' ')</code> returned <code>0x00</code>, not <code>0x20</code>. Byte zero also decodes to a
blank, and it won the reverse lookup.</li>
<li>Escape, carriage return and line feed were being encoded as <code>←</code>, <code>♪</code> and <code>◙</code>.
In an art <em>buffer</em> the low bytes are glyphs; in a <em>stream</em> they are controls.
Confusing the two turns every colour change in a file into a arrow character.</li>
</ul>
<h2>Nobody's work goes up without their name</h2>
<p>Every piece has an entry in <code>src/art/art-registry.yml</code>: author, handle, group,
year, licence, original filename, notes. A piece with no entry does not build.
When the terms are unclear the entry says <code>reference_only: true</code>, and the
validator keeps it out of the build, out of the gallery and out of <code>dist/</code>.</p>
<p>SAUCE records — the 128 bytes the scene stapled to the end of a file with the
artist's handle, group and date — are read on import and shown by
<code>art info &#x3C;slug></code>. They are the reason we still know who drew what, thirty
years later.</p>
<p>There is one rule with no exception: an artist's signature is never removed.
Not to fit a layout, not to fit the grid. If the signature does not fit in 80
columns, the piece does not go in.</p>
<h2>Why it still reads well</h2>
<p>A constraint that severe forces an artist to think in shape and value rather
than detail. Same discipline as woodcut, as stencil, as any printing process
with a limited number of passes. Work made under those rules tends to survive
changes in display technology, because it never depended on the display being
good.</p>
<p>An ANSI drawn in 1993 on a CRT still looks correct on a 5K panel in 2026. Very
little else from 1993 can say that.</p>]]></content:encoded>
      <category>runawaydevil</category>
      <category>ansi</category>
      <category>art</category>
    </item>
  </channel>
</rss>