NULL(1)

User Commands — Underground Section

NAME

null — an underground zine that is also a working terminal. Issue 0x1F of NULL://TERMINAL, published by the (fictional) null collective, rendered as one green-phosphor CRT you can type at.

SYNOPSIS

null [--phosphor green|amber] [--issue 0x1F] [--no-escape]

Boots a fake POST sequence (any key skips it), then drops you at a real prompt. Everything the terminal can print is also printed below it as plain HTML — the terminal is the front door, not a wall.

DESCRIPTION

The aesthetic school is the green-phosphor CRT: P1 tubes, VT220s, BBS door screens, demoscene NFO files, and the textfile zines of the early nineties. One typeface (JetBrains Mono), one hue family per theme, scanlines from a repeating gradient, glow from stacked text-shadow, curvature suggested by a radial-gradient vignette rather than any actual distortion. Nothing on the page is an image; the masthead is figlet-style ASCII drawn by hand and verified for column alignment before shipping.

The fiction is load-bearing: five real articles (dead protocols, the 56k handshake, the 1998 iron harvest, a phosphor manifesto, a retired phreak's exit interview) are written into semantic HTML first. The terminal reads them out of the DOM at runtime — one source of truth, zero drift between the interactive and printed editions, and the whole issue survives with JavaScript disabled.

COMMANDS

help
lists the documented commands. The undocumented ones (sudo, exit, cowsay, finger, uname) are left as rumors.
ls / cat <file>
list and read the issue. TAB completes filenames; any key skips the typewriter.
banner
reprints the ASCII masthead.
theme green|amber
retunes the phosphor sitewide, and remembers.
whoami / date / clear / guide
exactly what you'd hope.

INTERNALS: THE TYPEWRITER

Output types itself with requestAnimationFrame, not setInterval — a few characters per frame, so speed scales with the display and never queues up in a background tab. A single shared skipAll flag is flipped by any keypress; every loop checks it and dumps the remainder instantly. Users with prefers-reduced-motion set skip the animation entirely and get instant text.

async function typeInto(el, text, n){
  if (REDUCED){ el.textContent += text; return; }
  let i = 0;
  while (i < text.length){
    // any key sets skipAll — dump the rest, stop animating
    if (skipAll){ el.textContent += text.slice(i); return; }
    el.textContent += text.slice(i, i + n);  // n chars per frame
    i += n;
    await new Promise(r => requestAnimationFrame(r));
  }
}

INTERNALS: THE PARSER

There is no parser, really — that's the trick. Input splits on whitespace; the first token indexes a registry object; the rest are arguments. Each command is a plain function (async if it prints slowly). Adding a command is one register() call, which is why the easter eggs cost almost nothing.

const parts = raw.split(/\s+/);
const entry = COMMANDS[parts[0].toLowerCase()];
if (entry) await entry.run(parts.slice(1));
else line('nullsh: ' + parts[0] + ': command not found', 'err');

The prompt itself is a hidden <input> mirrored into three spans: text-before-caret, the character under the caret (rendered as the block cursor), and text-after. Arrow keys, selection, and mobile keyboards all keep working because the browser still owns the real caret — the CRT cursor just rides it.

INTERNALS: HISTORY AND COMPLETION

History is an array plus one index. ArrowUp stashes your unfinished line in a draft variable before rewinding, so ArrowDown can bring it back — the detail that separates a terminal from a text box. TAB completion picks its candidate pool by position: command names for the first token, filenames after cat, handles after finger. Multiple hits extend to the longest common prefix, then print the options, bash-style.

if (e.key === 'ArrowUp'){
  if (hIdx === history.length) draft = cmd.value; // stash the draft
  if (hIdx > 0) hIdx--;
  cmd.value = history[hIdx];
}

INTERNALS: PHOSPHOR THEMING

Every color on the site — text, glow, borders, scrollbars, selection — derives from seven custom properties on :root. Switching to amber is one attribute flip; the value persists in localStorage and a two-line inline script in <head> applies it before first paint, so there is no green flash for amber loyalists.

:root[data-theme="amber"]{
  --bg:#0a0500; --ink:#ffb000; --dim:#b87d00;
  --bright:#ffdf9e; --glow:rgba(255,176,0,.4);
}
/* js: */ document.documentElement.dataset.theme = t;

HOW IT WAS MADE

Built by Claude (Fable 5) writing vanilla HTML, CSS, and JavaScript by hand — no frameworks, no build step, no images, one Google Fonts request. The ASCII masthead was generated letter-by-letter from a hand-defined figlet alphabet and programmatically checked so every row is the same width. This site is one of a 25-site showcase; the hub is at fable-25-dhb.pages.dev.

STEAL THIS

SEE ALSO

null://terminal (the issue itself), fable-showcase(7), gopher(1), finger(1) — RIP.