site generator: commands/*.md with front matter -> k8s/site/
site / generated-is-current (push) Failing after 36s

- build/build.mjs: one-file-per-command content (filename = command,
  underscores = spaces; help/suggestion/params/prefix/text/hidden in
  front matter; ${args} ${date} ${time} ${ls} ${help} resolved at runtime)
- generates index.html (banner + chips), commands.json (the single data
  source), hello.txt for curl, style.css via tailwind v4
- src/site.js is now dispatch-only over commands.json; typewriter kept
- src/telnet.py: stdlib telnet shell sharing commands.json
- k8s: python telnet sidecar in the pod + LoadBalancer service 23->2323
- .gitea/workflows/site.yml fails CI when k8s/site/ is not the built output
This commit is contained in:
momme
2026-10-14 01:14:28 +02:00
parent 5852efafd7
commit 8d33460580
47 changed files with 2773 additions and 424 deletions
+30
View File
@@ -0,0 +1,30 @@
name: site
# The cluster serves what is committed in k8s/site/ — so CI just enforces that
# k8s/site/ is exactly what the generator produces from commands/ + src/.
# If this job fails, run `npm run build` and commit the result.
on:
push:
branches: ["**"]
pull_request:
jobs:
generated-is-current:
runs-on: ubuntu-latest
container:
image: node:22-alpine
steps:
- uses: actions/checkout@v4
- run: npm ci --no-audit --no-fund
- run: npm run build
- name: generated output is committed
run: |
git add -A k8s/site
if ! git diff --staged --quiet; then
echo "k8s/site/ is not in sync with commands/ + src/ — run 'npm run build' and commit:"
git diff --staged --stat
exit 1
fi
+1
View File
@@ -0,0 +1 @@
node_modules/
+63 -59
View File
@@ -1,72 +1,76 @@
# momme.world # momme.world
Personal site at [momme.world](https://momme.world) — a terminal-style page Personal site at [momme.world](https://momme.world) — a terminal-style page
that behaves like a small shell (`whoami`, `projects`, `social`, …), plus a that behaves like a small shell, a plain-text rendering for
plain-text rendering for `curl momme.world`. `curl momme.world`, and a real telnet shell for the tailnet.
## Layout ## How it is put together
``` **`commands/*.md` is the content.** One file per command, front matter for
k8s/ metadata, body for output. The filename is the command — underscores are
kustomization.yaml the whole deployment: generated ConfigMaps + refs spaces:
deployment.yaml one unprivileged nginx, content served from ConfigMaps
service.yaml ClusterIP, :80 -> :8080
site/ the actual website, served as-is
index.html markup + noscript fallback
style.css
site.js the terminal; ALL site content lives in COMMANDS here
nginx.conf mounted as /etc/nginx/conf.d/default.conf
hello.txt what curl gets
```
There is deliberately **no build step**: no package.json, no bundler, no CI. | file | command | notes |
Edit a file under `k8s/site/`, merge to `main`, ArgoCD syncs it. The content |---|---|---|
of the site is the repo — the kustomize `configMapGenerator` hashes the files | `help.md` | `help` | |
into their ConfigMap names, which rolls the Deployment automatically. | `project_fpv.md` | `project fpv` | multi-word commands just work |
| `echo.md` | `echo hello world` | `params: <text>` + `${args}` in the body |
Front matter keys (all optional except the body):
| key | meaning |
|---|---|
| `name:` | override the filename-derived command name |
| `help:` | one-line description; shown in `help` (no help = listed by `ls`, not in `help`) |
| `params:` | argument placeholder shown in help, e.g. `<text>` |
| `prefix: true` | command swallows anything typed after its name |
| `suggestion: true` | rendered as a chip button under the terminal |
| `hidden: true` | works, but stays out of `help` |
| `text: true` | included in the plain-text page curl clients see |
Dynamic placeholders inside a body, resolved when the command runs:
`${args}` (the arguments), `${help}` (the full listing), `${ls}` (subcommand
names), `${date}`, `${time}`.
## Editing the site ## Editing the site
- New terminal command: add one object to `COMMANDS` in `k8s/site/site.js`. ```bash
It appears in `help` and in tab-completion automatically. `label -> url` npm install
lines become links. # edit commands/, src/index.html or src/input.css
- The noscript block in `index.html` mirrors the main facts by hand; keep it npm run build # regenerates k8s/site/
in sync when changing `whoami`/`contact` content. git add -A && git commit && git push
- `curl`-UA detection is the `$root_doc` map in `k8s/site/nginx.conf`.
## How it deploys
The `momme/infra` module `app-momme-world` owns the namespace, the TLS
certificate and the HTTPProxy, and creates an ArgoCD `Application` that points
at `k8s/` in this repo. Nothing in this repo talks to the cluster.
## Local preview
Any static server works, e.g.:
```
cd k8s/site && python -m http.server 8080
``` ```
`npm run build` (build/build.mjs) generates everything in `k8s/site/`:
the HTML (banner + chips injected), `commands.json` (the single data source
both the browser runtime and the telnet shell read), `hello.txt` for curl, and
`style.css` via Tailwind v4. **The generated files are committed on purpose** —
that is what ArgoCD serves — and CI fails if `k8s/site/` is out of sync with
the sources, so a forgotten build can never desync the site.
Styling is [Tailwind v4](https://tailwindcss.com): theme tokens live in
`src/input.css`, class names in the template and chips. `src/site.js` is the
browser runtime (terminal loop + typewriter), `src/telnet.py` the tailnet
shell — both are dispatch over `commands.json`, neither carries content.
## Deploy
`k8s/` is kustomize: one pod with nginx (site) and python (telnet shell), all
content as ConfigMaps, so a content change rolls the pod without any image
build. Deployed by ArgoCD from the `app-momme-world` module of the infra repo
(git.momme.cloud/momme/infra).
## Open decisions ## Open decisions
Things left deliberately open for you: * **DNS** — `momme.world` still needs an A record (Cloudflare) pointing at the
cluster's public entry `31.18.25.47`; until then no certificate and no
1. **DNS** — `momme.world` currently has *no A record*. The zone lives in public HTTPS. Also check that the `cf` ClusterIssuer's API token covers the
Cloudflare (nameservers `fred/jessica.ns.cloudflare.com`), the same place `momme.world` zone (it was created for `momme.cloud`).
`momme.cloud` lives. An `A momme.world -> 31.18.25.47` (grey cloud, the * **telnet exposure** — currently tailnet-only: `momme-world-telnet` is a
same public entry all `*.momme.cloud` services use) is all that's needed. LoadBalancer service that needs a one-line IP-pool entry in
Also check that the Cloudflare API token behind the `cf` ClusterIssuer `infra/sys-cilium/lb.tf` (pattern: `gitea-ssh`). Public port 23 would need a
(Infisical `/global`, `cloudflare_api_token`) is allowed to do DNS-01 in TCPRoute on the njalla/Envoy funnel — doable, left open on purpose.
the momme.world zone — if it's scoped to momme.cloud only, widen it. * The footer advertises `telnet momme.world 23`; if port 23 stays tailnet-only,
2. **www redirect** — not implemented. If you want `www.momme.world` to work, change it to the LB IP (or drop the hint).
add a second DNS name + HTTPProxy route (infra PR can carry it). * GitHub Pages still serves the old site at supermomme.github.io/momme.world —
3. **Analytics / visits** — none included. Self-hosted (Plausible) or none is archive or delete that repo once this one is live.
an easy later call; nothing in this repo blocks it.
4. **Blog later** — you mentioned a Markdown-driven blog could be nice
someday. The current no-build design can take it as far as "one static
page per post written in HTML"; as soon as you want real Markdown +
templates, the natural step is a generator (Lume like adb.sh, or
Hugo) plus a built image. Decision deferred, structure unaffected.
5. **Content facts** — taken from the old site's commands (work at
codeanker, projects list, socials). Say the word for anything outdated;
they all live in `COMMANDS` in `site.js`.
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env node
// momme.world site generator.
//
// commands/*.md is the single source of content. Each file describes one
// command: front matter sets metadata, the body is what it prints.
//
// filename -> command name (underscores become spaces)
// name: -> overrides the filename-derived name
// help: -> one-liner for the help listing
// params: -> argument placeholder shown in help (e.g. "<text>")
// prefix: true -> command accepts anything after its name
// suggestion: true -> chip button under the terminal + tailnet MOTD hint
// hidden: true -> works, but stays out of help
// text: true -> included in the plain-text page (curl)
// ${args} ${date} ${time} ${ls} ${help} -> resolved when the command runs
//
// Output goes to k8s/site/ and is COMMITTED: ArgoCD serves those files
// straight from the repo, so `npm run build` is part of editing the site.
import { readFileSync, writeFileSync, readdirSync, mkdirSync } from "node:fs";
import { join } from "node:path";
import { execSync } from "node:child_process";
const root = new URL("..", import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1");
const P = (p) => join(root, p);
// ---------- read commands ----------
const read = (file) => {
const raw = readFileSync(P("commands/" + file), "utf8").replace(/\r\n/g, "\n");
const m = raw.match(/^---\n([\s\S]*?)\n---\n\n?([\s\S]*)$/);
if (!m) throw new Error(`${file}: missing or malformed front matter`);
const meta = {};
for (const line of m[1].split("\n")) {
const [key, ...rest] = line.split(":");
if (key) meta[key.trim()] = rest.join(":").trim().replace(/^["']|["']$/g, "");
}
const name = meta.name || file.replace(/\.md$/, "").replace(/_/g, " ");
return {
name,
help: meta.help ?? "",
params: meta.params ?? "",
prefix: meta.prefix === "true",
suggestion: meta.suggestion === "true",
hidden: meta.hidden === "true",
text: meta.text === "true",
body: m[2].trimEnd(),
};
};
const files = readdirSync(P("commands")).filter((f) => f.endsWith(".md")).sort();
const commands = files.map(read);
for (const c of commands) {
for (const other of commands) {
if (other !== c && other.name === c.name) throw new Error(`duplicate command name: ${c.name}`);
}
}
if (new Set(commands.map((c) => c.name.split(" ")[0])).size !== new Set(commands.filter((c) => !c.name.includes(" ")).map((c) => c.name)).size) {
// multi-word commands ("project x") must not collide with first words
const firsts = new Set(commands.filter((c) => !c.name.includes(" ")).map((c) => c.name));
for (const c of commands.filter((c) => c.name.includes(" "))) {
if (firsts.has(c.name.split(" ")[0])) throw new Error(`name collision: ${c.name} vs ${c.name.split(" ")[0]}`);
}
}
// ---------- index.html ----------
const chips = commands
.filter((c) => c.suggestion && !c.hidden)
.map((c) => ` <button data-run="${c.name}" class="rounded border border-line bg-panel px-3 py-1 text-xs text-dim hover:text-accent hover:border-accent transition-colors">${c.name}</button>`)
.join("\n");
const banner = readFileSync(P("templates/banner.txt"), "utf8").replace(/\s+$/, "");
const template = readFileSync(P("src/index.html"), "utf8");
for (const [ph, val] of [["<!--@chips-->", chips], ["<!--@banner-->", banner]]) {
if (!template.includes(ph)) throw new Error(`src/index.html: ${ph} placeholder missing`);
}
writeFileSync(
P("k8s/site/index.html"),
template.replace("<!--@chips-->", chips).replace("<!--@banner-->", banner)
);
// ---------- commands.json (web runtime + telnet both read this) ----------
const json = JSON.stringify({ banner, commands }, null, 1) + "\n";
writeFileSync(P("k8s/site/commands.json"), json);
// ---------- hello.txt (what curl sees) ----------
const sections = commands
.filter((c) => c.text)
.map((c) => {
const body = c.body.replace(/\$\{help\}|\$\{ls\}/g, "");
return `${c.name}\n${"=".repeat(c.name.length)}\n${body}`.trimEnd();
});
writeFileSync(
P("k8s/site/hello.txt"),
banner + "\n\n" + sections.join("\n\n") + "\n\nThe interactive version of this page is at https://momme.world/\n"
);
// ---------- copy runtimes ----------
writeFileSync(P("k8s/site/site.js"), readFileSync(P("src/site.js"), "utf8"));
writeFileSync(P("k8s/site/telnet.py"), readFileSync(P("src/telnet.py"), "utf8"));
// ---------- tailwind ----------
try {
execSync("npx --yes @tailwindcss/cli@4 -i src/input.css -o k8s/site/style.css --minify", { cwd: root, stdio: "inherit" });
} catch {
console.error("tailwind build failed — is npm install done? (style.css stays as it was)");
process.exit(1);
}
const words = (n) => String(n).padStart(2);
console.log(`built ${words(files.length)} commands -> k8s/site/ (${chips ? commands.filter((c) => c.suggestion).length + " chips" : "no chips"})`);
+7
View File
@@ -0,0 +1,7 @@
---
help: cat README.md
prefix: true
---
cat README.md the welcome message, piped
.ssh/id_rsa nice try.
+4
View File
@@ -0,0 +1,4 @@
---
help: wipe the screen
---
+5
View File
@@ -0,0 +1,5 @@
---
help: brew coffee
---
() ~ I'm out of coffee.
+8
View File
@@ -0,0 +1,8 @@
---
help: reach me directly
suggestion: true
text: true
---
mail -> mailto:momme@juergensen.me
matrix -> https://matrix.to/#/@momme:chaos.jetzt
+6
View File
@@ -0,0 +1,6 @@
---
help: curl momme.world — you already did that
prefix: true
---
you are already reading the output :)
+5
View File
@@ -0,0 +1,5 @@
---
help: today's date
---
${date}
+7
View File
@@ -0,0 +1,7 @@
---
help: echo <text> — repeat yourself
prefix: true
params: <text>
---
${args}
+6
View File
@@ -0,0 +1,6 @@
---
help: an OS with good editing
prefix: true
---
a great OS, shame about the editor.
+5
View File
@@ -0,0 +1,5 @@
---
help: close the session
---
close the tab, or just stop reading :)
+10
View File
@@ -0,0 +1,10 @@
---
help: the welcome message
suggestion: true
text: true
---
Hey :)
I'm Momme. I build and break infrastructure, and I keep the pieces in
Kubernetes so they can be put back together again.
Type help to see what this box knows.
+6
View File
@@ -0,0 +1,6 @@
---
help: this list
params: <command>
---
${help}
+5
View File
@@ -0,0 +1,5 @@
---
help: what's in here
---
${ls}
+11
View File
@@ -0,0 +1,11 @@
---
help: project <name> — what I'm building
suggestion: true
text: true
---
nix-ha-kubernetes-cluster high-availability Kubernetes on NixOS, built to learn on
brahmsee.digital event management platform for DLRG Schleswig-Holstein
silvatica-web website of the Silvatica festival, Schwäbisch Hall
momme.world this site — no build step, GitOps all the way down
more of them at: https://github.com/supermomme
+7
View File
@@ -0,0 +1,7 @@
---
name: project brahmsee.digital
---
Event management platform for DLRG Schleswig-Holstein.
Registrations and scheduling for a state water-rescue organisation, where the
traffic arrives in waves of a few hundred people at once. -> https://brahmsee.digital
+7
View File
@@ -0,0 +1,7 @@
---
name: project fpv
help: the flying camera budget
---
FPV drones — built, flown, crashed, rebuilt.
The honest way to learn soldering, PID tuning and how fast a frame becomes parts.
+7
View File
@@ -0,0 +1,7 @@
---
name: project momme.world
---
This site. Markdown in, website out: build/build.mjs generates the commands,
the tailnet shell and this page. One nginx pod, GitOps all the way down.
-> https://git.momme.cloud/momme/momme-world
@@ -0,0 +1,7 @@
---
name: project nix-ha-kubernetes-cluster
---
An HA Kubernetes cluster on NixOS, built to learn rather than to impress.
Control plane in HA, Ceph for storage, GitOps for everything above it.
This site runs on it. -> https://github.com/supermomme/nix-ha-kubernetes-cluster
+6
View File
@@ -0,0 +1,6 @@
---
name: project silvatica-web
---
Website of the Silvatica festival in Schwäbisch Hall.
-> https://github.com/supermomme/silvatica-web
+5
View File
@@ -0,0 +1,5 @@
---
help: working directory
---
/world
+9
View File
@@ -0,0 +1,9 @@
---
help: where I hang out online
suggestion: true
text: true
---
github -> https://github.com/supermomme
mastodon -> https://cybre.town/@momme
instagram -> https://instagram.com/supermommej
+10
View File
@@ -0,0 +1,10 @@
---
help: the tools I reach for
suggestion: true
text: true
---
kubernetes self-hosted cluster, GitOps with ArgoCD, Terraform for the rest
nix reproducible configs for servers and laptops
observability Prometheus, Loki, Grafana — dashboards I actually read
fpv drones built, flown, crashed, rebuilt
+6
View File
@@ -0,0 +1,6 @@
---
help: elevate
prefix: true
---
momme is not in the sudoers file. This incident has been reported.
+5
View File
@@ -0,0 +1,5 @@
---
help: current time
---
${time}
+6
View File
@@ -0,0 +1,6 @@
---
help: system information
prefix: true
---
momme.world 1.0.0 static x86_64 GNU/freedom
+6
View File
@@ -0,0 +1,6 @@
---
help: an editor
prefix: true
---
emacs, obviously.
+9
View File
@@ -0,0 +1,9 @@
---
help: who is typing
suggestion: true
text: true
---
Momme Jürgensen — infrastructure and developer platforms.
I like work that ends up as a short command instead of a long document.
Currently at codeanker.
+38
View File
@@ -63,6 +63,41 @@ spec:
readOnlyRootFilesystem: true readOnlyRootFilesystem: true
capabilities: capabilities:
drop: ["ALL"] drop: ["ALL"]
# the telnet shell — same commands.json as the website, stdlib python
- name: telnet
image: docker.io/library/python:3.14-alpine@sha256:7f2ec713f0436c59df2362d973b6a05af9e87ba78f0e94f5245c302984a7a1fd
command: ["python", "-u", "/app/telnet.py"]
env:
- name: TELNET_PORT
value: "2323"
ports:
- name: telnet
containerPort: 2323
volumeMounts:
- name: telnet
mountPath: /app/telnet.py
subPath: telnet.py
readOnly: true
- name: site
mountPath: /app/commands.json
subPath: commands.json
readOnly: true
startupProbe:
tcpSocket:
port: telnet
failureThreshold: 30
periodSeconds: 2
resources:
requests:
cpu: 10m
memory: 48Mi
limits:
memory: 96Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumes: volumes:
- name: site - name: site
configMap: configMap:
@@ -70,5 +105,8 @@ spec:
- name: config - name: config
configMap: configMap:
name: config name: config
- name: telnet
configMap:
name: telnet
- name: tmp - name: tmp
emptyDir: {} emptyDir: {}
+4
View File
@@ -10,9 +10,13 @@ configMapGenerator:
- site/index.html - site/index.html
- site/style.css - site/style.css
- site/site.js - site/site.js
- site/commands.json
- site/hello.txt - site/hello.txt
- site/favicon.svg - site/favicon.svg
- site/robots.txt - site/robots.txt
- name: config - name: config
files: files:
- site/nginx.conf - site/nginx.conf
- name: telnet
files:
- site/telnet.py
+19
View File
@@ -11,3 +11,22 @@ spec:
- name: http - name: http
port: 80 port: 80
targetPort: http targetPort: http
---
# The telnet shell, for the tailnet. Cilium assigns the IP from the LB pool
# entry in sys-cilium/lb.tf (same pattern as gitea-ssh). Reach it with
# `telnet <ip> 23` from inside the tailnet.
apiVersion: v1
kind: Service
metadata:
name: momme-world-telnet
labels:
app: momme-world
spec:
type: LoadBalancer
externalTrafficPolicy: Local
selector:
app: momme-world
ports:
- name: telnet
port: 23
targetPort: telnet
+265
View File
@@ -0,0 +1,265 @@
{
"banner": " _ _ \n | | | |\n _ __ ___ ___ _ __ ___ _ __ ___ ___ __ _____ _ __| | __| |\n | '_ ` _ \\ / _ \\| '_ ` _ \\| '_ ` _ \\ / _ \\ \\ \\ /\\ / / _ \\| '__| |/ _` |\n | | | | | | (_) | | | | | | | | | | | __/ _ \\ V V / (_) | | | | (_| |\n |_| |_| |_|\\___/|_| |_| |_|_| |_| |_|\\___| (_) \\_/\\_/ \\___/|_| |_|\\__,_|\n\n0110110101101111011011010110110101100101001011100111011101101111011100100110110001100100",
"commands": [
{
"name": "cat",
"help": "cat README.md",
"params": "",
"prefix": true,
"suggestion": false,
"hidden": false,
"text": false,
"body": "cat README.md the welcome message, piped\n.ssh/id_rsa nice try."
},
{
"name": "clear",
"help": "wipe the screen",
"params": "",
"prefix": false,
"suggestion": false,
"hidden": false,
"text": false,
"body": ""
},
{
"name": "coffee",
"help": "brew coffee",
"params": "",
"prefix": false,
"suggestion": false,
"hidden": false,
"text": false,
"body": "() ~ I'm out of coffee."
},
{
"name": "contact",
"help": "reach me directly",
"params": "",
"prefix": false,
"suggestion": true,
"hidden": false,
"text": true,
"body": "mail -> mailto:momme@juergensen.me\nmatrix -> https://matrix.to/#/@momme:chaos.jetzt"
},
{
"name": "curl",
"help": "curl momme.world — you already did that",
"params": "",
"prefix": true,
"suggestion": false,
"hidden": false,
"text": false,
"body": "you are already reading the output :)"
},
{
"name": "date",
"help": "today's date",
"params": "",
"prefix": false,
"suggestion": false,
"hidden": false,
"text": false,
"body": "${date}"
},
{
"name": "echo",
"help": "echo <text> — repeat yourself",
"params": "<text>",
"prefix": true,
"suggestion": false,
"hidden": false,
"text": false,
"body": "${args}"
},
{
"name": "emacs",
"help": "an OS with good editing",
"params": "",
"prefix": true,
"suggestion": false,
"hidden": false,
"text": false,
"body": "a great OS, shame about the editor."
},
{
"name": "exit",
"help": "close the session",
"params": "",
"prefix": false,
"suggestion": false,
"hidden": false,
"text": false,
"body": "close the tab, or just stop reading :)"
},
{
"name": "hello",
"help": "the welcome message",
"params": "",
"prefix": false,
"suggestion": true,
"hidden": false,
"text": true,
"body": "Hey :)\nI'm Momme. I build and break infrastructure, and I keep the pieces in\nKubernetes so they can be put back together again.\nType help to see what this box knows."
},
{
"name": "help",
"help": "this list",
"params": "<command>",
"prefix": false,
"suggestion": false,
"hidden": false,
"text": false,
"body": "${help}"
},
{
"name": "ls",
"help": "what's in here",
"params": "",
"prefix": false,
"suggestion": false,
"hidden": false,
"text": false,
"body": "${ls}"
},
{
"name": "project",
"help": "project <name> — what I'm building",
"params": "",
"prefix": false,
"suggestion": true,
"hidden": false,
"text": true,
"body": "nix-ha-kubernetes-cluster high-availability Kubernetes on NixOS, built to learn on\nbrahmsee.digital event management platform for DLRG Schleswig-Holstein\nsilvatica-web website of the Silvatica festival, Schwäbisch Hall\nmomme.world this site — no build step, GitOps all the way down\n more of them at: https://github.com/supermomme"
},
{
"name": "project brahmsee.digital",
"help": "",
"params": "",
"prefix": false,
"suggestion": false,
"hidden": false,
"text": false,
"body": "Event management platform for DLRG Schleswig-Holstein.\nRegistrations and scheduling for a state water-rescue organisation, where the\ntraffic arrives in waves of a few hundred people at once. -> https://brahmsee.digital"
},
{
"name": "project fpv",
"help": "the flying camera budget",
"params": "",
"prefix": false,
"suggestion": false,
"hidden": false,
"text": false,
"body": "FPV drones — built, flown, crashed, rebuilt.\nThe honest way to learn soldering, PID tuning and how fast a frame becomes parts."
},
{
"name": "project momme.world",
"help": "",
"params": "",
"prefix": false,
"suggestion": false,
"hidden": false,
"text": false,
"body": "This site. Markdown in, website out: build/build.mjs generates the commands,\nthe tailnet shell and this page. One nginx pod, GitOps all the way down.\n-> https://git.momme.cloud/momme/momme-world"
},
{
"name": "project nix-ha-kubernetes-cluster",
"help": "",
"params": "",
"prefix": false,
"suggestion": false,
"hidden": false,
"text": false,
"body": "An HA Kubernetes cluster on NixOS, built to learn rather than to impress.\nControl plane in HA, Ceph for storage, GitOps for everything above it.\nThis site runs on it. -> https://github.com/supermomme/nix-ha-kubernetes-cluster"
},
{
"name": "project silvatica-web",
"help": "",
"params": "",
"prefix": false,
"suggestion": false,
"hidden": false,
"text": false,
"body": "Website of the Silvatica festival in Schwäbisch Hall.\n-> https://github.com/supermomme/silvatica-web"
},
{
"name": "pwd",
"help": "working directory",
"params": "",
"prefix": false,
"suggestion": false,
"hidden": false,
"text": false,
"body": "/world"
},
{
"name": "social",
"help": "where I hang out online",
"params": "",
"prefix": false,
"suggestion": true,
"hidden": false,
"text": true,
"body": "github -> https://github.com/supermomme\nmastodon -> https://cybre.town/@momme\ninstagram -> https://instagram.com/supermommej"
},
{
"name": "stack",
"help": "the tools I reach for",
"params": "",
"prefix": false,
"suggestion": true,
"hidden": false,
"text": true,
"body": "kubernetes self-hosted cluster, GitOps with ArgoCD, Terraform for the rest\nnix reproducible configs for servers and laptops\nobservability Prometheus, Loki, Grafana — dashboards I actually read\nfpv drones built, flown, crashed, rebuilt"
},
{
"name": "sudo",
"help": "elevate",
"params": "",
"prefix": true,
"suggestion": false,
"hidden": false,
"text": false,
"body": "momme is not in the sudoers file. This incident has been reported."
},
{
"name": "time",
"help": "current time",
"params": "",
"prefix": false,
"suggestion": false,
"hidden": false,
"text": false,
"body": "${time}"
},
{
"name": "uname",
"help": "system information",
"params": "",
"prefix": true,
"suggestion": false,
"hidden": false,
"text": false,
"body": "momme.world 1.0.0 static x86_64 GNU/freedom"
},
{
"name": "vim",
"help": "an editor",
"params": "",
"prefix": true,
"suggestion": false,
"hidden": false,
"text": false,
"body": "emacs, obviously."
},
{
"name": "whoami",
"help": "who is typing",
"params": "",
"prefix": false,
"suggestion": true,
"hidden": false,
"text": true,
"body": "Momme Jürgensen — infrastructure and developer platforms.\nI like work that ends up as a short command instead of a long document.\nCurrently at codeanker."
}
]
}
+27 -19
View File
@@ -7,35 +7,43 @@
0110110101101111011011010110110101100101001011100111011101101111011100100110110001100100 0110110101101111011011010110110101100101001011100111011101101111011100100110110001100100
contact
=======
mail -> mailto:momme@juergensen.me
matrix -> https://matrix.to/#/@momme:chaos.jetzt
hello
=====
Hey :) Hey :)
I'm Momme. I build and break infrastructure, and I keep the pieces in I'm Momme. I build and break infrastructure, and I keep the pieces in
Kubernetes so they can be put back together again. Kubernetes so they can be put back together again.
Type help to see what this box knows.
whoami project
------ =======
Momme Jürgensen — infrastructure and developer platforms. nix-ha-kubernetes-cluster high-availability Kubernetes on NixOS, built to learn on
I like work that ends up as a short command instead of a long document. brahmsee.digital event management platform for DLRG Schleswig-Holstein
Currently at codeanker. silvatica-web website of the Silvatica festival, Schwäbisch Hall
momme.world this site — no build step, GitOps all the way down
more of them at: https://github.com/supermomme
social
======
github -> https://github.com/supermomme
mastodon -> https://cybre.town/@momme
instagram -> https://instagram.com/supermommej
stack stack
----- =====
kubernetes self-hosted cluster, GitOps with ArgoCD, Terraform for the rest kubernetes self-hosted cluster, GitOps with ArgoCD, Terraform for the rest
nix reproducible configs for servers and laptops nix reproducible configs for servers and laptops
observability Prometheus, Loki, Grafana — dashboards I actually read observability Prometheus, Loki, Grafana — dashboards I actually read
fpv drones built, flown, crashed, rebuilt fpv drones built, flown, crashed, rebuilt
projects whoami
-------- ======
nix-ha-kubernetes-cluster high-availability Kubernetes on NixOS, built to learn on Momme Jürgensen — infrastructure and developer platforms.
brahmsee.digital event management platform for DLRG Schleswig-Holstein I like work that ends up as a short command instead of a long document.
silvatica-web website of the Silvatica festival, Schwäbisch Hall Currently at codeanker.
momme.world this site — no build step, GitOps all the way down
contact
-------
mail momme@juergensen.me
matrix @momme:juergensen.me
github https://github.com/supermomme
mastodon https://cybre.town/@momme
The interactive version of this page is at https://momme.world/ The interactive version of this page is at https://momme.world/
+28 -53
View File
@@ -13,81 +13,56 @@
<link rel="canonical" href="https://momme.world/"> <link rel="canonical" href="https://momme.world/">
<link rel="icon" href="/favicon.svg" type="image/svg+xml"> <link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/style.css"> <link rel="stylesheet" href="/style.css">
<script>document.documentElement.classList.add("js")</script>
</head> </head>
<body> <body class="min-h-dvh bg-bg text-fg font-mono antialiased flex flex-col items-center justify-center p-4 sm:p-8 selection:bg-accent/30">
<main>
<div class="term" id="term"> <main class="w-full max-w-3xl">
<div class="titlebar">
<span class="dot r"></span><span class="dot y"></span><span class="dot g"></span> <div class="rounded-lg border border-line bg-panel shadow-[0_0_40px_rgba(0,0,0,.5)] overflow-hidden">
<span class="title">momme@world: ~</span> <div class="flex items-center gap-2 border-b border-line bg-bar px-3 py-2">
<span class="size-3 rounded-full bg-[#ff5f56]"></span>
<span class="size-3 rounded-full bg-[#ffbd2e]"></span>
<span class="size-3 rounded-full bg-[#27c93f]"></span>
<span class="ml-2 text-xs text-dim select-none">momme@world: ~</span>
</div> </div>
<div class="screen" id="screen" tabindex="-1"> <div id="screen" tabindex="-1" class="h-[62dvh] overflow-y-auto px-4 py-4 text-[13.5px] sm:text-[15px] leading-[1.35] cursor-text">
<pre class="hello"> <pre class="text-dim overflow-x-auto not-prose"> _ _
_ _
| | | | | | | |
_ __ ___ ___ _ __ ___ _ __ ___ ___ __ _____ _ __| | __| | _ __ ___ ___ _ __ ___ _ __ ___ ___ __ _____ _ __| | __| |
| '_ ` _ \ / _ \| '_ ` _ \| '_ ` _ \ / _ \ \ \ /\ / / _ \| '__| |/ _` | | '_ ` _ \ / _ \| '_ ` _ \| '_ ` _ \ / _ \ \ \ /\ / / _ \| '__| |/ _` |
| | | | | | (_) | | | | | | | | | | | __/ _ \ V V / (_) | | | | (_| | | | | | | | (_) | | | | | | | | | | | __/ _ \ V V / (_) | | | | (_| |
|_| |_| |_|\___/|_| |_| |_|_| |_| |_|\___| (_) \_/\_/ \___/|_| |_|\__,_| |_| |_| |_|\___/|_| |_| |_|_| |_| |_|\___| (_) \_/\_/ \___/|_| |_|\__,_|
0110110101101111011011010110110101100101001011100111011101101111011100100110110001100100 0110110101101111011011010110110101100101001011100111011101101111011100100110110001100100</pre>
</pre>
<div id="out"></div> <div id="out" class="whitespace-pre-wrap break-words"></div>
<form id="prompt" autocomplete="off"> <form id="prompt" autocomplete="off" class="flex items-baseline gap-2 mt-1">
<span class="ps1" aria-hidden="true">momme@world:~$</span> <span aria-hidden="true" class="text-accent shrink-0">momme@world:~$</span>
<input id="cmd" aria-label="Run a command" placeholder="type a command — try `help`" <input id="cmd" aria-label="Run a command" placeholder="type a command — try `help`"
class="flex-1 min-w-0 bg-transparent outline-none border-0 p-0 text-fg placeholder:text-dim/60 caret-transparent"
autocapitalize="off" autocorrect="off" spellcheck="false"> autocapitalize="off" autocorrect="off" spellcheck="false">
<span class="caret" aria-hidden="true"></span> <span aria-hidden="true" class="cursor -ml-[0.6em]"></span>
</form> </form>
</div> </div>
</div> </div>
<nav class="chips" aria-label="Commands"> <nav class="mt-4 flex flex-wrap justify-center gap-2" aria-label="Commands">
<button data-run="whoami">whoami</button> <button data-run="contact" class="rounded border border-line bg-panel px-3 py-1 text-xs text-dim hover:text-accent hover:border-accent transition-colors">contact</button>
<button data-run="projects">projects</button> <button data-run="hello" class="rounded border border-line bg-panel px-3 py-1 text-xs text-dim hover:text-accent hover:border-accent transition-colors">hello</button>
<button data-run="stack">stack</button> <button data-run="project" class="rounded border border-line bg-panel px-3 py-1 text-xs text-dim hover:text-accent hover:border-accent transition-colors">project</button>
<button data-run="social">social</button> <button data-run="social" class="rounded border border-line bg-panel px-3 py-1 text-xs text-dim hover:text-accent hover:border-accent transition-colors">social</button>
<button data-run="contact">contact</button> <button data-run="stack" class="rounded border border-line bg-panel px-3 py-1 text-xs text-dim hover:text-accent hover:border-accent transition-colors">stack</button>
<button data-run="whoami" class="rounded border border-line bg-panel px-3 py-1 text-xs text-dim hover:text-accent hover:border-accent transition-colors">whoami</button>
</nav> </nav>
<p class="curl"> <p class="mt-4 text-center text-xs text-dim">
Terminal people: <code>curl momme.world</code> Terminal people: <code class="text-fg">curl momme.world</code>
&nbsp;·&nbsp; source: <a href="https://git.momme.cloud/momme-world/momme-world">git.momme.cloud/momme-world/momme-world</a> &nbsp;·&nbsp; tailnet: <code class="text-fg">telnet momme.world 23</code>
&nbsp;·&nbsp; source: <a class="text-accent hover:underline" href="https://git.momme.cloud/momme/momme-world">git.momme.cloud/momme/momme-world</a>
</p> </p>
<!-- Readable and copy-pasteable with JavaScript turned off. The terminal
above is the same information, delivered as a conversation. -->
<noscript>
<section class="facts">
<h2>whoami</h2>
<p>I'm <strong>Momme Jürgensen</strong>. I work on infrastructure and developer
platforms — the kind of work where the best interface is a short command.
Currently at <a href="https://codeanker.de">codeanker</a>.</p>
<h2>stack</h2>
<ul>
<li><strong>Kubernetes</strong> — self-hosted cluster, GitOps, the whole garden.</li>
<li><strong>NixOS</strong> — reproducible configs for servers and laptops.</li>
<li><strong>Observability</strong> — Prometheus, Loki, dashboards I actually read.</li>
<li><strong>FPV drones</strong> — built, flown, crashed, rebuilt.</li>
</ul>
<h2>contact</h2>
<ul>
<li>Mail: <a href="mailto:momme@juergensen.me">momme@juergensen.me</a></li>
<li>Matrix: <a href="https://matrix.to/#/@momme:chaos.jetzt">@momme:chaos.jetzt</a></li>
<li>Mastodon: <a href="https://cybre.town/@momme">@momme@cybre.town</a></li>
<li>GitHub: <a href="https://github.com/supermomme">supermomme</a></li>
<li>Instagram: <a href="https://instagram.com/supermommej">@supermommej</a></li>
</ul>
</section>
</noscript>
</main> </main>
<script src="/site.js"></script> <script src="/site.js"></script>
</body> </body>
+5
View File
@@ -32,6 +32,11 @@ server {
add_header Cache-Control "no-store"; add_header Cache-Control "no-store";
} }
# the command data the terminal fetches at boot: same freshness rule
location = /commands.json {
add_header Cache-Control "no-store";
}
location ~* \.(css|js|svg)$ { location ~* \.(css|js|svg)$ {
add_header Cache-Control "public, max-age=604800"; add_header Cache-Control "public, max-age=604800";
} }
+92 -156
View File
@@ -1,127 +1,52 @@
// Content lives in COMMANDS only: every entry renders in the terminal, and the // momme.world browser runtime. Content comes from commands.json — generated
// names double as the `help` listing. A `label -> url` line becomes a real link. // by build/build.mjs from commands/*.md; edit those files, not this one.
// Adding a section to this site means adding one object here. // This file only implements the loop, the typewriter and command dispatch.
const COMMANDS = [
{ let COMMANDS = [];
name: "hello", let INDEX = new Map();
help: "the welcome message",
body: `Hey :) const load = async () => {
I'm Momme. I build and break infrastructure, and I keep the pieces in const res = await fetch("/commands.json", { cache: "no-store" });
Kubernetes so they can be put back together again. const data = await res.json();
Type help to see what this box knows.`, COMMANDS = data.commands;
}, INDEX = new Map(COMMANDS.map((c) => [c.name, c]));
{ };
name: "whoami",
help: "who is typing", // ---------- content helpers ----------
body: `Momme Jürgensen — infrastructure and developer platforms. const visible = () => COMMANDS.filter((c) => !c.hidden);
I like work that ends up as a short command instead of a long document. const unique = () => COMMANDS.filter((c, i) => COMMANDS.findIndex((x) => x.name === c.name) === i);
Currently at codeanker.`, const commandNames = () => unique().map((c) => c.name);
}, const usages = () =>
{ visible().filter((c) => c.help).map((c) => ` ${c.name.padEnd(22)} ${c.help}`);
name: "projects", const fail = (message, ...more) => ({ error: message, more: more.filter(Boolean) });
help: "projects <name> — what I'm building", const split = (value) => String(value).split("\n");
body: `nix-ha-kubernetes-cluster high-availability Kubernetes on NixOS, built to learn on
brahmsee.digital event management platform for DLRG Schleswig-Holstein
silvatica-web website of the Silvatica festival, Schwäbisch Hall
momme.world this site — no build step, GitOps all the way down
more of them at: https://github.com/supermomme`,
detail: {
"nix-ha-kubernetes-cluster": `An HA Kubernetes cluster on NixOS, built to learn rather than to impress.
Control plane in HA, Ceph for storage, GitOps for everything above it.
This site runs on it. -> https://github.com/supermomme/nix-ha-kubernetes-cluster`,
"brahmsee.digital": `Event management platform for DLRG Schleswig-Holstein.
Registrations and scheduling for a state water-rescue organisation, where the
traffic arrives in waves of a few hundred people at once. -> https://brahmsee.digital`,
"silvatica-web": `Website of the Silvatica festival in Schwäbisch Hall.
-> https://github.com/supermomme/silvatica-web`,
"momme.world": `This site. Plain files, a stock nginx image, three manifests, no build
step. ArgoCD points at the repo, so a merged commit is a deployed site.`,
},
},
{
name: "stack",
help: "the tools I reach for",
body: `kubernetes self-hosted cluster, GitOps with ArgoCD, Terraform for the rest
nix reproducible configs for servers and laptops
observability Prometheus, Loki, Grafana — dashboards I actually read
fpv drones built, flown, crashed, rebuilt`,
},
{
name: "social",
help: "where I hang out online",
body: `github -> https://github.com/supermomme
mastodon -> https://cybre.town/@momme
instagram -> https://instagram.com/supermommej`,
},
{
name: "contact",
help: "reach me directly",
body: `mail -> mailto:momme@juergensen.me
matrix -> https://matrix.to/#/@momme:chaos.jetzt`,
},
{
name: "ls",
help: "what's in here",
body: () => names("projects").join(" "),
},
{
name: "cat",
prefix: true,
help: "cat README.md",
run: (arg) => {
const f = arg.toLowerCase();
if (f === "readme.md" || f === "readme") return body("hello");
if (f === ".ssh/id_rsa") return fail("nice try.");
return fail(`cat: ${arg}: No such file or directory`, "try: cat README.md");
},
},
{
name: "help",
help: "this list",
body: () => ["usage: <command>", "", ...usages()],
run: (arg) => {
if (!arg) return body("help");
const row = usages().find((line) => line.trim().startsWith(arg + " "));
return row ? row : fail(`no command called "${arg}"`, "see: help");
},
},
{ name: "clear", help: "wipe the screen", body: "" },
{ name: "date", help: "today's date", body: () => new Date().toDateString() },
{ name: "time", help: "current time", body: () => new Date().toLocaleTimeString("de-DE") },
{ name: "echo", prefix: true, help: "echo <text> — repeat yourself", body: (arg) => arg },
{ name: "pwd", help: "working directory", body: "/world" },
{ name: "uname", prefix: true, help: "system information", body: "momme.world 1.0.0 static x86_64 GNU/freedom" },
{ name: "curl", prefix: true, help: "curl momme.world — you already did that", body: "you are already reading the output :)" },
{ name: "sudo", prefix: true, help: "elevate", body: "momme is not in the sudoers file. This incident has been reported." },
{ name: "vim", prefix: true, help: "an editor", body: "emacs, obviously." },
{ name: "emacs", prefix: true, help: "an OS with good editing", body: "a great OS, shame about the editor." },
{ name: "coffee", help: "brew coffee", body: "() ~ I'm out of coffee." },
{ name: "exit", help: "close the session", body: "close the tab, or just stop reading :)" },
];
const ALIASES = { const ALIASES = {
whois: "whoami", about: "whoami", openme: "whoami", whois: "whoami", about: "whoami", openme: "whoami",
tech: "stack", tools: "stack", tech: "stack", tools: "stack",
links: "social", socials: "social", links: "social", socials: "social",
mail: "contact", email: "contact", mail: "contact", email: "contact",
repos: "projects", rm: "sudo", repos: "project", projects: "project", rm: "sudo",
}; };
// ---------- content helpers ---------- // ${...} placeholders a command body may contain; resolved at run time.
const byName = (name) => COMMANDS.filter((c) => c.name === name && !c.prefix)[0]; const substitutions = (arg) => ({
const body = (name) => render(byName(name).body, ""); args: arg,
const render = (value, arg) => { date: () => new Date().toDateString(),
const v = typeof value === "function" ? value(arg) : value; time: () => new Date().toLocaleTimeString("de-DE"),
return Array.isArray(v) ? v.join("\n") : String(v); help: () => ["usage: <command>", "", ...usages()].join("\n"),
}; ls: () =>
const names = (name) => commandNames()
split(body(name)).map((line) => line.trim().split(/\s{2,}/)[0]).filter(Boolean); .filter((n) => n.includes(" "))
const commandNames = () => unique().map((c) => c.name); .map((n) => n.split(" ").slice(1).join(" "))
const unique = () => .join(" "),
COMMANDS.filter((c, i) => COMMANDS.findIndex((x) => x.name === c.name) === i); });
const usages = () => unique().map((c) => ` ${c.name.padEnd(9)} ${c.help}`);
const fail = (message, ...more) => ({ error: message, more: more.filter(Boolean) }); const resolve = (body, arg) =>
const split = (value) => value.split("\n"); String(body).replace(/\$\{(\w+)\}/g, (_, key) => {
const sub = substitutions(arg)[key];
return sub === undefined ? "" : typeof sub === "function" ? sub() : sub;
});
// ---------- dom ---------- // ---------- dom ----------
const out = document.getElementById("out"); const out = document.getElementById("out");
@@ -149,7 +74,7 @@ let fast = false;
let gen = 0; let gen = 0;
let dumpNow = null; let dumpNow = null;
// finish every pending line synchronously, then let the queue drain instantly // finish the line being typed synchronously; the queue then drains instantly
const flatten = (node) => const flatten = (node) =>
node.segs.forEach((seg) => node.append(seg.href ? anchor(seg) : seg.text)); node.segs.forEach((seg) => node.append(seg.href ? anchor(seg) : seg.text));
@@ -170,13 +95,12 @@ const kill = () => {
}; };
function print(value, arg) { function print(value, arg) {
const resolved = typeof value === "function" ? value(arg) : value;
const lines = []; const lines = [];
if (resolved && resolved.error) { if (value && value.error) {
lines.push(build(resolved.error, "line err")); lines.push(build(value.error, "text-[#ff7b72]"));
resolved.more.forEach((line) => lines.push(build(line, "line dim"))); value.more.forEach((line) => lines.push(build(line, "text-[#5c6f8a]")));
} else { } else {
split(render(resolved, arg)).forEach((line) => lines.push(build(line, "line out"))); split(resolve(value, arg)).forEach((line) => lines.push(build(line, "")));
} }
queue.push(lines); queue.push(lines);
pump(); pump();
@@ -186,7 +110,7 @@ function print(value, arg) {
const LINK = /(https?:\/\/\S+|mailto:\S+)/; const LINK = /(https?:\/\/\S+|mailto:\S+)/;
function build(line, cls) { function build(line, cls) {
const node = make("span", cls); const node = make("span", "block min-h-[1.35em] " + (cls || ""));
const segs = []; const segs = [];
if (!LINK.test(line)) { if (!LINK.test(line)) {
segs.push({ text: line || "\u200b" }); segs.push({ text: line || "\u200b" });
@@ -201,10 +125,10 @@ function build(line, cls) {
} }
node.segs = segs; node.segs = segs;
return node; return node;
}; }
const anchor = (seg) => { const anchor = (seg) => {
const a = make("a", null, seg.text); const a = make("a", "text-accent hover:underline", seg.text);
a.href = seg.href; a.href = seg.href;
if (!seg.href.startsWith("mailto:")) a.rel = "noopener me"; if (!seg.href.startsWith("mailto:")) a.rel = "noopener me";
return a; return a;
@@ -301,35 +225,43 @@ function recall(step) {
input.value = history.items[history.at]; input.value = history.items[history.at];
} }
// Resolve input words to [command, argument]. Longest multi-word name wins so
// "project fpv" beats any one-word command; a command with params swallows
// the rest.
function lookup(words) {
for (let len = words.length; len > 0; len--) {
const joined = words.slice(0, len).join(" ");
const cmd = INDEX.get(ALIASES[joined] || joined);
if (cmd) return [cmd, words.slice(len).join(" ")];
}
return [null, ""];
}
function execute(raw) { function execute(raw) {
const [word = "", ...rest] = raw.trim().split(/\s+/);
const arg = rest.join(" ");
const name = word.toLowerCase();
flush(); flush();
out.append(make("div", "line cmd", raw)); out.append(make("div", "block text-fg mt-4", raw));
if (!name) return scrollDown(); const words = raw.trim().toLowerCase().split(/\s+/).filter(Boolean);
if (name === "clear") return kill(), (out.textContent = ""); if (!words.length) return scrollDown();
if (words[0] === "clear") return kill(), (out.textContent = "");
const wanted = ALIASES[name] || name; const [cmd, arg] = lookup(words);
const found = if (!cmd) {
COMMANDS.find((c) => c.name === wanted && !c.prefix) || const near = commandNames().filter((n) => n !== words[0] && n.startsWith(words[0])).slice(0, 3);
COMMANDS.find((c) => c.name === wanted && c.prefix); return print(fail(`command not found: ${words[0]}`,
near.length ? `did you mean: ${near.join(", ")}` : "see: help"), "");
if (!found) {
const near = commandNames().filter((n) => n !== name && n[0] === name[0]).slice(0, 3);
return print(fail(`command not found: ${name}`,
near.length ? `did you mean: ${near.join(", ")}` : "see: help"));
} }
if (found.name === "projects" && arg) { // argument on a parent: either a known subcommand or a name error
const key = arg.toLowerCase(); if (arg && !cmd.params && !cmd.prefix) {
if (key === "list") return print(body("projects")); const kids = commandNames().filter((n) => n.startsWith(cmd.name + " "));
return print(found.detail[key] || if (kids.length) {
fail(`no project called "${arg}"`, "names:", ...names("projects"))); const kid = INDEX.get(cmd.name + " " + arg);
if (kid) return print(resolve(kid.body, ""), "");
return print(fail(`no ${cmd.name} called "${arg}"`,
"names: " + kids.map((k) => k.slice(cmd.name.length + 1)).join(", ")), "");
}
} }
return print(resolve(cmd.body, arg), arg);
return print(found.run ? found.run(arg) : found.body, arg);
} }
form.addEventListener("submit", (event) => { form.addEventListener("submit", (event) => {
@@ -346,7 +278,7 @@ input.addEventListener("keydown", (event) => {
if (event.key === "ArrowUp") { event.preventDefault(); recall(-1); } if (event.key === "ArrowUp") { event.preventDefault(); recall(-1); }
if (event.key === "ArrowDown") { event.preventDefault(); recall(1); } if (event.key === "ArrowDown") { event.preventDefault(); recall(1); }
if (event.key !== "Tab") return; if (event.key !== "Tab") return;
const word = input.value.trim().split(/\s+/)[0]; const word = input.value.trim().toLowerCase().split(/\s+/)[0];
const hit = commandNames().filter((n) => word && n.startsWith(word)); const hit = commandNames().filter((n) => word && n.startsWith(word));
if (hit.length === 1) { event.preventDefault(); input.value = hit[0] + " "; } if (hit.length === 1) { event.preventDefault(); input.value = hit[0] + " "; }
}); });
@@ -356,12 +288,16 @@ screen.addEventListener("click", () => {
if (!getSelection().toString()) input.focus(); if (!getSelection().toString()) input.focus();
}); });
document.querySelectorAll("[data-run]").forEach((button) => load().then(
button.addEventListener("click", () => { () => {
execute(button.dataset.run); document.querySelectorAll("[data-run]").forEach((button) =>
button.addEventListener("click", () => {
execute(button.dataset.run);
input.focus();
})
);
print("hello — type help for the rest of it.", "");
input.focus(); input.focus();
}) },
() => print({ error: "commands.json did not load — did the site generator run?", more: [] }, "")
); );
print(`hello — type help for the rest of it.\n`);
input.focus();
+2 -136
View File
File diff suppressed because one or more lines are too long
+165
View File
@@ -0,0 +1,165 @@
#!/usr/bin/env python3
"""momme.world telnet shell.
Serves the same commands as the website, to real terminals:
telnet momme.world 23
Content lives in commands.json next to this file (generated from commands/*.md
by build/build.mjs) — this server is only a REPL. Standard library only. It
answers enough telnet negotiation to keep classic clients happy; it is not a
login shell, there is nothing here to log into.
Inside the cluster it listens on TELNET_PORT (2323); the public port 23
arrives via Envoy Gateway's TCPRoute on the tailscale funnel.
"""
import json
import os
import re
import select
import socketserver
import sys
import time
HERE = os.path.dirname(os.path.abspath(__file__))
COMMANDS_FILE = os.environ.get("COMMANDS_FILE", os.path.join(HERE, "commands.json"))
PORT = int(os.environ.get("TELNET_PORT", "2323"))
IDLE_TIMEOUT = 300 # seconds; five idle minutes is a long enough hello
# telnet negotiation: answer WILL/WONT so clients stop asking
IAC, WONT, ECHO, SGA = 255, 252, 1, 3
ALIASES = {
"whois": "whoami", "about": "whoami", "openme": "whoami",
"tech": "stack", "tools": "stack",
"links": "social", "socials": "social",
"mail": "contact", "email": "contact",
"repos": "project", "projects": "project", "rm": "sudo", "quit": "exit",
}
with open(COMMANDS_FILE, encoding="utf-8") as fh:
SITE = json.load(fh)
COMMANDS = {c["name"]: c for c in SITE["commands"]}
BANNER = SITE["banner"]
def usages():
return [
f" {c['name']:<22} {c['help']}"
for c in SITE["commands"]
if not c.get("hidden") and c.get("help")
]
def substitute(body, arg):
def rep(match):
key = match.group(1)
if key == "args":
return arg
if key == "date":
return time.strftime("%a %b %d %Y")
if key == "time":
return time.strftime("%H:%M:%S")
if key == "help":
return "\n".join(["usage: <command>", ""] + usages())
if key == "ls":
return " ".join(n.split(" ", 1)[1] for n in COMMANDS if " " in n)
return ""
return re.sub(r"\$\{(\w+)\}", rep, body)
def lookup(words):
for length in range(len(words), 0, -1):
joined = " ".join(words[:length])
cmd = COMMANDS.get(ALIASES.get(joined, joined))
if cmd:
return cmd, " ".join(words[length:])
return None, ""
def run(line):
"""Return (text, quit_after). Empty text means print nothing."""
words = [w for w in line.strip().lower().split() if w]
if not words:
return "", False
cmd, arg = lookup(words)
if not cmd:
near = [n for n in COMMANDS if n != words[0] and n.startswith(words[0])][:3]
hint = "did you mean: " + ", ".join(near) if near else "see: help"
return f"command not found: {words[0]}\n{hint}", False
if cmd["name"] == "clear":
return "\033[2J\033[H", False
if cmd["name"] == "exit":
return "bye — https://momme.world/", True
if arg and not cmd.get("params") and not cmd.get("prefix"):
kids = [n[len(cmd["name"]) + 1:] for n in COMMANDS if n.startswith(cmd["name"] + " ")]
if kids:
kid = COMMANDS.get(cmd["name"] + " " + arg)
if kid:
return substitute(kid["body"], ""), False
names = ", ".join(kids)
return f'no {cmd["name"]} called "{arg}"\nnames: {names}', False
return substitute(cmd["body"], arg), cmd["name"] == "quit"
class Shell(socketserver.BaseRequestHandler):
def send(self, text):
try:
self.request.sendall(text.replace("\n", "\r\n").encode("utf-8"))
except OSError:
raise ConnectionError
def setup(self):
try:
self.request.sendall(bytes([IAC, WONT, ECHO, IAC, WONT, SGA]))
self.send(BANNER + "\n\nhello — type help for the rest of it.\n")
self.loop()
except (ConnectionError, ConnectionResetError, BrokenPipeError):
pass
def loop(self):
line = ""
self.send("momme@world:~$ ")
while True:
ready, _, _ = select.select([self.request], [], [], IDLE_TIMEOUT)
if not ready:
return
data = self.request.recv(1024)
if not data:
return
i = 0
while i < len(data):
byte = data[i]
if byte == IAC:
i += 3 # skip a negotiation triple wholesale
continue
i += 1
if byte in (13, 10):
self.send("\n")
text, goodbye = run(line)
if text:
self.send(text + "\n")
if goodbye:
self.send("closing connection\n")
return
line = ""
self.send("momme@world:~$ ")
elif byte in (127, 8):
if line:
line = line[:-1]
self.send("\b \b")
elif 32 <= byte < 127:
line += chr(byte)
self.send(chr(byte))
class Server(socketserver.ThreadingTCPServer):
allow_reuse_address = True
daemon_threads = True
if __name__ == "__main__":
with Server(("0.0.0.0", PORT), Shell) as srv:
print(f"momme.world telnet shell on :{PORT}", file=sys.stderr, flush=True)
srv.serve_forever()
+1170
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
{
"name": "momme-world",
"private": true,
"type": "module",
"description": "momme.world — commands/*.md in, website out",
"scripts": {
"build": "node build/build.mjs"
},
"devDependencies": {
"tailwindcss": "^4.1.0",
"@tailwindcss/cli": "^4.1.0"
}
}
+57
View File
@@ -0,0 +1,57 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>momme.world — Momme Jürgensen</title>
<meta name="description" content="Hi, I'm Momme. I build and break infrastructure: Kubernetes, NixOS, self-hosting, and FPV drones. Say hello on the terminal.">
<meta name="color-scheme" content="dark">
<meta property="og:type" content="profile">
<meta property="og:title" content="momme.world">
<meta property="og:description" content="Hi, I'm Momme. I build and break infrastructure: Kubernetes, NixOS, self-hosting, and FPV drones.">
<meta property="og:url" content="https://momme.world/">
<link rel="canonical" href="https://momme.world/">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/style.css">
</head>
<body class="min-h-dvh bg-bg text-fg font-mono antialiased flex flex-col items-center justify-center p-4 sm:p-8 selection:bg-accent/30">
<main class="w-full max-w-3xl">
<div class="rounded-lg border border-line bg-panel shadow-[0_0_40px_rgba(0,0,0,.5)] overflow-hidden">
<div class="flex items-center gap-2 border-b border-line bg-bar px-3 py-2">
<span class="size-3 rounded-full bg-[#ff5f56]"></span>
<span class="size-3 rounded-full bg-[#ffbd2e]"></span>
<span class="size-3 rounded-full bg-[#27c93f]"></span>
<span class="ml-2 text-xs text-dim select-none">momme@world: ~</span>
</div>
<div id="screen" tabindex="-1" class="h-[62dvh] overflow-y-auto px-4 py-4 text-[13.5px] sm:text-[15px] leading-[1.35] cursor-text">
<pre class="text-dim overflow-x-auto not-prose"><!--@banner--></pre>
<div id="out" class="whitespace-pre-wrap break-words"></div>
<form id="prompt" autocomplete="off" class="flex items-baseline gap-2 mt-1">
<span aria-hidden="true" class="text-accent shrink-0">momme@world:~$</span>
<input id="cmd" aria-label="Run a command" placeholder="type a command — try `help`"
class="flex-1 min-w-0 bg-transparent outline-none border-0 p-0 text-fg placeholder:text-dim/60 caret-transparent"
autocapitalize="off" autocorrect="off" spellcheck="false">
<span aria-hidden="true" class="cursor -ml-[0.6em]"></span>
</form>
</div>
</div>
<nav class="mt-4 flex flex-wrap justify-center gap-2" aria-label="Commands">
<!--@chips-->
</nav>
<p class="mt-4 text-center text-xs text-dim">
Terminal people: <code class="text-fg">curl momme.world</code>
&nbsp;·&nbsp; tailnet: <code class="text-fg">telnet momme.world 23</code>
&nbsp;·&nbsp; source: <a class="text-accent hover:underline" href="https://git.momme.cloud/momme/momme-world">git.momme.cloud/momme/momme-world</a>
</p>
</main>
<script src="/site.js"></script>
</body>
</html>
+31
View File
@@ -0,0 +1,31 @@
@import "tailwindcss";
/* Source scanning: the generated output (template + chips + runtime) carries
the class names; build order guarantees it exists before this runs. */
@source "../k8s/site";
@theme {
--color-bg: #0a0e14;
--color-panel: #10161f;
--color-bar: #161d29;
--color-line: #1f2937;
--color-fg: #d7e3d7;
--color-dim: #5c6f8a;
--color-accent: #7dd3a0;
--font-mono: ui-monospace, "Cascadia Mono", "JetBrains Mono", Menlo, Consolas, monospace;
}
/* block caret: the blinking square, reused by the typewriter */
.cursor {
display: inline-block;
inline-size: 0.6em;
block-size: 1.1em;
translate: 0 0.2em;
background: var(--color-accent);
animation: blink 1.1s steps(1) infinite;
}
@keyframes blink { 50% { opacity: 0 } }
@media (prefers-reduced-motion: reduce) {
.cursor { animation: none; }
}
+303
View File
@@ -0,0 +1,303 @@
// momme.world browser runtime. Content comes from commands.json — generated
// by build/build.mjs from commands/*.md; edit those files, not this one.
// This file only implements the loop, the typewriter and command dispatch.
let COMMANDS = [];
let INDEX = new Map();
const load = async () => {
const res = await fetch("/commands.json", { cache: "no-store" });
const data = await res.json();
COMMANDS = data.commands;
INDEX = new Map(COMMANDS.map((c) => [c.name, c]));
};
// ---------- content helpers ----------
const visible = () => COMMANDS.filter((c) => !c.hidden);
const unique = () => COMMANDS.filter((c, i) => COMMANDS.findIndex((x) => x.name === c.name) === i);
const commandNames = () => unique().map((c) => c.name);
const usages = () =>
visible().filter((c) => c.help).map((c) => ` ${c.name.padEnd(22)} ${c.help}`);
const fail = (message, ...more) => ({ error: message, more: more.filter(Boolean) });
const split = (value) => String(value).split("\n");
const ALIASES = {
whois: "whoami", about: "whoami", openme: "whoami",
tech: "stack", tools: "stack",
links: "social", socials: "social",
mail: "contact", email: "contact",
repos: "project", projects: "project", rm: "sudo",
};
// ${...} placeholders a command body may contain; resolved at run time.
const substitutions = (arg) => ({
args: arg,
date: () => new Date().toDateString(),
time: () => new Date().toLocaleTimeString("de-DE"),
help: () => ["usage: <command>", "", ...usages()].join("\n"),
ls: () =>
commandNames()
.filter((n) => n.includes(" "))
.map((n) => n.split(" ").slice(1).join(" "))
.join(" "),
});
const resolve = (body, arg) =>
String(body).replace(/\$\{(\w+)\}/g, (_, key) => {
const sub = substitutions(arg)[key];
return sub === undefined ? "" : typeof sub === "function" ? sub() : sub;
});
// ---------- dom ----------
const out = document.getElementById("out");
const screen = document.getElementById("screen");
const form = document.getElementById("prompt");
const input = document.getElementById("cmd");
const make = (tag, cls, value) => {
const node = document.createElement(tag);
if (cls) node.className = cls;
if (value != null) node.textContent = value;
return node;
};
const scrollDown = () => (screen.scrollTop = screen.scrollHeight);
// ---------- typewriter ----------
// print() never renders directly: it queues fully-built line nodes and this
// machine writes them character by character. A keypress or click while
// typing is active dumps the pending output instantly; `clear` drops it.
const TYPE_MS = matchMedia("(prefers-reduced-motion: reduce)").matches ? 0 : 5;
let queue = [];
let typing = null;
let fast = false;
let gen = 0;
let dumpNow = null;
// finish the line being typed synchronously; the queue then drains instantly
const flatten = (node) =>
node.segs.forEach((seg) => node.append(seg.href ? anchor(seg) : seg.text));
const flush = () => {
if (!typing && !queue.length) return;
fast = true;
if (dumpNow) dumpNow();
else if (typing) write();
else pump();
};
const kill = () => {
gen++;
queue.length = 0;
typing = null;
dumpNow = null;
fast = false;
};
function print(value, arg) {
const lines = [];
if (value && value.error) {
lines.push(build(value.error, "text-[#ff7b72]"));
value.more.forEach((line) => lines.push(build(line, "text-[#5c6f8a]")));
} else {
split(resolve(value, arg)).forEach((line) => lines.push(build(line, "")));
}
queue.push(lines);
pump();
}
// Split a line into typed fragments: plain text, or a link that lands whole.
const LINK = /(https?:\/\/\S+|mailto:\S+)/;
function build(line, cls) {
const node = make("span", "block min-h-[1.35em] " + (cls || ""));
const segs = [];
if (!LINK.test(line)) {
segs.push({ text: line || "\u200b" });
} else {
const arrow = line.indexOf("->");
if (arrow > 0) segs.push({ text: line.slice(0, arrow) });
(arrow > 0 ? line.slice(arrow + 2) : line).split(LINK).forEach((part) => {
if (!part) return;
if (!LINK.test(part)) return segs.push({ text: part });
segs.push({ href: part, text: part });
});
}
node.segs = segs;
return node;
}
const anchor = (seg) => {
const a = make("a", "text-accent hover:underline", seg.text);
a.href = seg.href;
if (!seg.href.startsWith("mailto:")) a.rel = "noopener me";
return a;
};
function pump() {
if (typing || !queue.length) return;
typing = { g: gen, lines: queue.shift(), i: 0 };
write();
}
function write() {
if (!typing) return;
if (typing.i >= typing.lines.length) {
typing = null;
if (!queue.length) fast = false;
scrollDown();
return pump();
}
const node = typing.lines[typing.i];
out.append(node);
if (fast) {
flatten(node);
typing.i++;
scrollDown();
return write();
}
const g = typing.g;
typeSegs(node, g, () => {
if (typing && typing.g === g) typing.i++;
write();
});
}
function typeSegs(node, g, ready) {
const segs = node.segs;
if (!TYPE_MS) return dump(segs, null);
const caret = make("span", "cursor");
let si = 0, ci = 0, tn = null;
node.append(caret);
dumpNow = () => { dumpNow = null; dump(segs, caret); };
(function tick() {
if (g !== gen) return;
if (fast) return dump(segs, caret);
if (si >= segs.length) return dump(segs, caret);
const seg = segs[si];
if (seg.href) {
node.insertBefore(anchor(seg), caret);
si++;
return setTimeout(tick, TYPE_MS * 6);
}
if (!tn) {
tn = document.createTextNode("");
node.insertBefore(tn, caret);
}
if (ci < seg.text.length) {
tn.appendData(seg.text[ci++]);
if (ci % 8 === 0) scrollDown();
return setTimeout(tick, TYPE_MS);
}
si++, (ci = 0), (tn = null);
tick();
})();
function dump(segs, caret) {
dumpNow = null;
if (tn) {
const seg = segs[si];
if (seg && !seg.href) tn.appendData(seg.text.slice(ci));
si++, (ci = 0);
}
caret && caret.remove();
for (; si < segs.length; si++) {
const seg = segs[si];
seg.href ? node.append(anchor(seg)) : node.append(seg.text);
}
scrollDown();
ready();
}
}
// ---------- the loop ----------
const history = { items: [], at: 0 };
function recall(step) {
history.at += step;
if (history.at < 0) history.at = 0;
if (history.at >= history.items.length) {
history.at = history.items.length;
input.value = "";
return;
}
input.value = history.items[history.at];
}
// Resolve input words to [command, argument]. Longest multi-word name wins so
// "project fpv" beats any one-word command; a command with params swallows
// the rest.
function lookup(words) {
for (let len = words.length; len > 0; len--) {
const joined = words.slice(0, len).join(" ");
const cmd = INDEX.get(ALIASES[joined] || joined);
if (cmd) return [cmd, words.slice(len).join(" ")];
}
return [null, ""];
}
function execute(raw) {
flush();
out.append(make("div", "block text-fg mt-4", raw));
const words = raw.trim().toLowerCase().split(/\s+/).filter(Boolean);
if (!words.length) return scrollDown();
if (words[0] === "clear") return kill(), (out.textContent = "");
const [cmd, arg] = lookup(words);
if (!cmd) {
const near = commandNames().filter((n) => n !== words[0] && n.startsWith(words[0])).slice(0, 3);
return print(fail(`command not found: ${words[0]}`,
near.length ? `did you mean: ${near.join(", ")}` : "see: help"), "");
}
// argument on a parent: either a known subcommand or a name error
if (arg && !cmd.params && !cmd.prefix) {
const kids = commandNames().filter((n) => n.startsWith(cmd.name + " "));
if (kids.length) {
const kid = INDEX.get(cmd.name + " " + arg);
if (kid) return print(resolve(kid.body, ""), "");
return print(fail(`no ${cmd.name} called "${arg}"`,
"names: " + kids.map((k) => k.slice(cmd.name.length + 1)).join(", ")), "");
}
}
return print(resolve(cmd.body, arg), arg);
}
form.addEventListener("submit", (event) => {
event.preventDefault();
const raw = input.value;
input.value = "";
if (raw.trim()) history.items.push(raw.trim());
history.at = history.items.length;
execute(raw);
});
input.addEventListener("keydown", (event) => {
flush();
if (event.key === "ArrowUp") { event.preventDefault(); recall(-1); }
if (event.key === "ArrowDown") { event.preventDefault(); recall(1); }
if (event.key !== "Tab") return;
const word = input.value.trim().toLowerCase().split(/\s+/)[0];
const hit = commandNames().filter((n) => word && n.startsWith(word));
if (hit.length === 1) { event.preventDefault(); input.value = hit[0] + " "; }
});
screen.addEventListener("click", () => {
flush();
if (!getSelection().toString()) input.focus();
});
load().then(
() => {
document.querySelectorAll("[data-run]").forEach((button) =>
button.addEventListener("click", () => {
execute(button.dataset.run);
input.focus();
})
);
print("hello — type help for the rest of it.", "");
input.focus();
},
() => print({ error: "commands.json did not load — did the site generator run?", more: [] }, "")
);
+165
View File
@@ -0,0 +1,165 @@
#!/usr/bin/env python3
"""momme.world telnet shell.
Serves the same commands as the website, to real terminals:
telnet momme.world 23
Content lives in commands.json next to this file (generated from commands/*.md
by build/build.mjs) — this server is only a REPL. Standard library only. It
answers enough telnet negotiation to keep classic clients happy; it is not a
login shell, there is nothing here to log into.
Inside the cluster it listens on TELNET_PORT (2323); the public port 23
arrives via Envoy Gateway's TCPRoute on the tailscale funnel.
"""
import json
import os
import re
import select
import socketserver
import sys
import time
HERE = os.path.dirname(os.path.abspath(__file__))
COMMANDS_FILE = os.environ.get("COMMANDS_FILE", os.path.join(HERE, "commands.json"))
PORT = int(os.environ.get("TELNET_PORT", "2323"))
IDLE_TIMEOUT = 300 # seconds; five idle minutes is a long enough hello
# telnet negotiation: answer WILL/WONT so clients stop asking
IAC, WONT, ECHO, SGA = 255, 252, 1, 3
ALIASES = {
"whois": "whoami", "about": "whoami", "openme": "whoami",
"tech": "stack", "tools": "stack",
"links": "social", "socials": "social",
"mail": "contact", "email": "contact",
"repos": "project", "projects": "project", "rm": "sudo", "quit": "exit",
}
with open(COMMANDS_FILE, encoding="utf-8") as fh:
SITE = json.load(fh)
COMMANDS = {c["name"]: c for c in SITE["commands"]}
BANNER = SITE["banner"]
def usages():
return [
f" {c['name']:<22} {c['help']}"
for c in SITE["commands"]
if not c.get("hidden") and c.get("help")
]
def substitute(body, arg):
def rep(match):
key = match.group(1)
if key == "args":
return arg
if key == "date":
return time.strftime("%a %b %d %Y")
if key == "time":
return time.strftime("%H:%M:%S")
if key == "help":
return "\n".join(["usage: <command>", ""] + usages())
if key == "ls":
return " ".join(n.split(" ", 1)[1] for n in COMMANDS if " " in n)
return ""
return re.sub(r"\$\{(\w+)\}", rep, body)
def lookup(words):
for length in range(len(words), 0, -1):
joined = " ".join(words[:length])
cmd = COMMANDS.get(ALIASES.get(joined, joined))
if cmd:
return cmd, " ".join(words[length:])
return None, ""
def run(line):
"""Return (text, quit_after). Empty text means print nothing."""
words = [w for w in line.strip().lower().split() if w]
if not words:
return "", False
cmd, arg = lookup(words)
if not cmd:
near = [n for n in COMMANDS if n != words[0] and n.startswith(words[0])][:3]
hint = "did you mean: " + ", ".join(near) if near else "see: help"
return f"command not found: {words[0]}\n{hint}", False
if cmd["name"] == "clear":
return "\033[2J\033[H", False
if cmd["name"] == "exit":
return "bye — https://momme.world/", True
if arg and not cmd.get("params") and not cmd.get("prefix"):
kids = [n[len(cmd["name"]) + 1:] for n in COMMANDS if n.startswith(cmd["name"] + " ")]
if kids:
kid = COMMANDS.get(cmd["name"] + " " + arg)
if kid:
return substitute(kid["body"], ""), False
names = ", ".join(kids)
return f'no {cmd["name"]} called "{arg}"\nnames: {names}', False
return substitute(cmd["body"], arg), cmd["name"] == "quit"
class Shell(socketserver.BaseRequestHandler):
def send(self, text):
try:
self.request.sendall(text.replace("\n", "\r\n").encode("utf-8"))
except OSError:
raise ConnectionError
def setup(self):
try:
self.request.sendall(bytes([IAC, WONT, ECHO, IAC, WONT, SGA]))
self.send(BANNER + "\n\nhello — type help for the rest of it.\n")
self.loop()
except (ConnectionError, ConnectionResetError, BrokenPipeError):
pass
def loop(self):
line = ""
self.send("momme@world:~$ ")
while True:
ready, _, _ = select.select([self.request], [], [], IDLE_TIMEOUT)
if not ready:
return
data = self.request.recv(1024)
if not data:
return
i = 0
while i < len(data):
byte = data[i]
if byte == IAC:
i += 3 # skip a negotiation triple wholesale
continue
i += 1
if byte in (13, 10):
self.send("\n")
text, goodbye = run(line)
if text:
self.send(text + "\n")
if goodbye:
self.send("closing connection\n")
return
line = ""
self.send("momme@world:~$ ")
elif byte in (127, 8):
if line:
line = line[:-1]
self.send("\b \b")
elif 32 <= byte < 127:
line += chr(byte)
self.send(chr(byte))
class Server(socketserver.ThreadingTCPServer):
allow_reuse_address = True
daemon_threads = True
if __name__ == "__main__":
with Server(("0.0.0.0", PORT), Shell) as srv:
print(f"momme.world telnet shell on :{PORT}", file=sys.stderr, flush=True)
srv.serve_forever()
+8
View File
@@ -0,0 +1,8 @@
_ _
| | | |
_ __ ___ ___ _ __ ___ _ __ ___ ___ __ _____ _ __| | __| |
| '_ ` _ \ / _ \| '_ ` _ \| '_ ` _ \ / _ \ \ \ /\ / / _ \| '__| |/ _` |
| | | | | | (_) | | | | | | | | | | | __/ _ \ V V / (_) | | | | (_| |
|_| |_| |_|\___/|_| |_| |_|_| |_| |_|\___| (_) \_/\_/ \___/|_| |_|\__,_|
0110110101101111011011010110110101100101001011100111011101101111011100100110110001100100