/* Copyright (C) 2024 memdmp This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ export const biosSteps = 3; export const biosStepInterval = 1000; const wpmToTypingSpeed = (wpm: number, avgWordLen: number) => (1 / ((wpm * avgWordLen) / 60)) * 1000; const averageWordLength = 4.793; const typingInfo = (wpm = 80) => ({ typingSpeedAvg: wpmToTypingSpeed(wpm, averageWordLength), typingDeviation: 20, // typing speed deviation is often correlated to the typing speed - TODO: at way lower speeds, the correlation is inverse; the more deviation there is (is this also the case for fast typing? what is the inflection point if not? i forgor i went down a rabbit hole yrs ago) }); export type TypingSpeed = ReturnType; export const getDelay = (typingInfo: TypingSpeed) => (Math.random() * 2 - 1) * typingInfo.typingDeviation + typingInfo.typingSpeedAvg; export const login = { username: "lain", passwordLength: 12, ...typingInfo(100), // one usually has muscle memory for well-known frequently-typed credentials, hence higher wpm }; export const ttyTypingInfo = typingInfo(); export type RenderBlock = { value: string; colour?: string; weight?: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900; italic?: boolean; underlined?: boolean; url?: | `newtab:${string}` | `currenttab:${string}` | ((textObj: TTYText & { kind: "text" }) => void); bg?: string; raw?: boolean; dl?: string; }; export type TTYText = | { kind: "text"; renderrestriction?: "everywhere" | "js-only" | "noscript"; value: RenderBlock[]; id: string; classes: string[]; } | { kind: "removeNode"; removedId: string; removedItemClassList: string[]; } | { kind: "time"; delay: number; } | { kind: "cursorVisibility"; visible: boolean; } | { kind: "clear"; }; export type Only = { [k in Keys]: Obj[k]; }; export type Diff< T extends { [k: string]: unknown; }, U extends keyof T, > = ({ [P in keyof T]: P } & { [P in U]: never } & { [x: string]: never; })[keyof T]; export type Omit< T extends { [k: string]: unknown; }, K extends keyof T, > = Pick>; export const ttyLines: TTYText[] = (() => { const lines: TTYText[] = []; let ids: string[] = []; const byId = new Map(); let i = 69420; let defaultRenderRestriction: Only< TTYText & { kind: "text" }, "renderrestriction" >["renderrestriction"] = "everywhere"; type LimitedRenderBlock = Omit; type RenderBlockArg = [text: string, options?: LimitedRenderBlock]; /** due to hellish css constraints, each text() call creates its own line - however, each block in a text() is joined on the same line. */ const text = ( globalOptions: | Only | RenderBlockArg, ...blocks: RenderBlockArg[] ) => { const id = (++i).toString(); ids.push(id); if (Array.isArray(globalOptions)) { blocks.unshift(globalOptions); globalOptions = {}; } const txt = { kind: "text", renderrestriction: globalOptions.renderrestriction ?? defaultRenderRestriction, value: blocks.map(([t, o]) => ({ value: t, colour: "#b9b9b9", ...o, })), id, classes: [`ttytext-${id}`], } as TTYText; lines.push(txt); byId.set(id, txt); return id; }; const wait = (time: number) => lines.push({ kind: "time", delay: time, }); const del = (id: string | number) => { const removedId = typeof id === "string" ? id : id >= 0 ? ids[id] : ids[ids.length + id]; const r = byId.get(removedId); if (r?.kind === "text") { lines.push({ kind: "removeNode", removedId, removedItemClassList: r.classes, }); r.classes.push("ttytext-removed-after-done"); } ids = ids.filter((v) => v !== removedId); }; const delSince = (id: string | number) => { const removedId = typeof id === "string" ? id : id >= 0 ? ids[id] : ids[ids.length + id]; const idIndex = ids.indexOf(removedId); for (let i = idIndex; i < ids.length; i++) { const removedId = ids[i]; const r = byId.get(removedId); if (r?.kind === "text") { r.classes.push("ttytext-removed-after-done"); lines.push({ kind: "removeNode", removedId, removedItemClassList: r.classes, }); } } ids = ids.filter((_, i) => i < idIndex); }; const getLast = () => { const v = byId.get(ids[ids.length - 1]); if (v?.kind == "text") { return v.value; } else return null; }; const overwriteLast = (...newValue: RenderBlockArg[]) => { const v = byId.get(ids[ids.length - 1]); const l = v?.kind == "text" ? v.renderrestriction : "everywhere"; del(-1); return text( { renderrestriction: l, }, ...newValue, ); }; const replaceLast = ( handler: (newValue: RenderBlockArg[]) => RenderBlockArg[], ) => overwriteLast( ...handler( getLast()!.map( (v) => [ v.value, Object.fromEntries( Object.entries(v).filter(([k]) => k !== "value"), ), ] as const, ), ), ); const everyTTYClear: (() => void)[] = []; const clear = () => { if (lines.find((v) => v.kind === "clear")) { delSince(lines.findLastIndex((v) => v.kind === "clear")); } lines.push({ kind: "clear" }); everyTTYClear.forEach((v) => v()); }; everyTTYClear.push(() => { text( { renderrestriction: "noscript", }, [ "This browser does not support JS. Your experience may be degraded.", { bg: "#ff0000", colour: "#dedede", }, ], ["\n", {}], ); }); everyTTYClear.forEach((v) => v()); // wait(300); text({ renderrestriction: "js-only" }, [ ((v) => v[Math.floor(Math.random() * v.length)])([ "cool beats are stored in the balls", "found xml documents in the firmware", 'overhead: "I don\'t consent" "hey thats my line"', "uwu", "i regret making this hellhole of a codebase", ]), { colour: "#777777", }, ]); wait(1000); clear(); text( [ "lain", { colour: "#FFFF53", weight: 700, }, ], [" in "], [ "mem.estrogen.zone", { colour: "#17B117", weight: 700, }, ], [" in "], [ "~", { colour: "#53FFFF", weight: 700, }, ], ); text( ["❯ ", { colour: "#53FF53", weight: 600 }], [""], ["", { colour: "#777777" }], ); { const targetstr = "/usr/bin/env wel"; const completions = [ '/bin/sh -c "$(/usr/bin/env curl -fsSL https://blahaj.estrogen.zone/)"', "/usr/local/bin/become-estradiol", "/usr/bin/shellutil ansi cheatsheet 24bit", "/usr/bin/env sh", "/usr/bin/env wc -l src/routes/anim.css", "/usr/bin/env welcome -c ~/.local/share/welcome.toml", ]; for (const c of [...targetstr.split(""), "COMPLETE"]) { replaceLast((l) => { const prompt = l[l.length - 2]; const suggestions = l[l.length - 1]; if (c === "COMPLETE") { prompt[0] += suggestions[0]; suggestions[0] = ""; } else { prompt[0] += c; const completion = completions.find((v) => v.startsWith(prompt[0])); if (completion) { suggestions[0] = completion.substring(prompt[0].length); } else suggestions[0] = ""; } return l; }); if (c === "COMPLETE") { wait(100); } else wait(50 + Math.random() * 100); } text(["Preparing..."]); wait(200); } wait(1000); clear(); text( ["Hi! I'm a"], [""], [ " software developer.\nI do shit like the hellhole you're seeing right now, amongst other things.\n\n", ], ); wait(300); for (const c of " transfem".split("")) { replaceLast((l) => { l[1][0] += c; return l; }); wait(70); } text([ "Places you can find me:", { colour: "#7a7a7a", }, ]); wait(300); text( [ " - ", { colour: "#7a7a7a", }, ], ["estrogen.zone git: ", { colour: "#cdcdcd" }], ); wait(100); replaceLast((v) => [ ...v, [ "git.estrogen.zone", { colour: "#99aaff", underlined: true, weight: 700, url: "newtab:https://git.estrogen.zone", }, ], ]); wait(100); text( [ " - ", { colour: "#7a7a7a", }, ], ["On jsr: ", { colour: "#cdcdcd" }], ); wait(600); replaceLast((v) => [ ...v, [ "jsr.io/@memdmp", { colour: "#f7df23", underlined: true, weight: 700, url: "newtab:https://jsr.io/@memdmp", }, ], ]); wait(200); text( [ " - ", { colour: "#7a7a7a", }, ], ["In random hackerspaces", { colour: "#aaaaaa" }], ); wait(600); text([ "\nYou may find these useful:", { colour: "#7a7a7a", }, ]); wait(400); text( [ " - ", { colour: "#7a7a7a", }, ], ["GPG Root Key: ", { colour: "#cdcdcd" }], ); wait(1000); replaceLast((v) => [ ...v, [ "B546778F06BBCC8EC167DB3CD919706487B8B6DE", { colour: "#58C7F3", underlined: true, weight: 700, url: "currenttab:/keys/memdmp/primary.pgp", dl: "memdmp-primary.pgp", }, ], ]); wait(100); replaceLast((v) => [ ...v, [ " (root key)", { colour: "#999999", }, ], ]); wait(400); text([ " - ", { colour: "#7a7a7a", }, ]); wait(70); replaceLast((v) => [ ...v, ["GPG Git Key: ", { colour: "#cdcdcd" }], ]); wait(100); replaceLast((v) => [ ...v, [ "5134F05BD8D9DB8C6C0E1515A9439D346AB6DF4E", { colour: "#F0A3B3", underlined: true, weight: 700, url: "currenttab:/keys/memdmp/git.pgp", dl: "memdmp-git.pgp", }, ], ]); wait(100); replaceLast((v) => [ ...v, [ " (", { colour: "#999999", }, ], [ "signed", { colour: "#F0A3B3", underlined: true, weight: 500, url: "currenttab:/keys/memdmp/git.pgp.sig", dl: "memdmp-git.pgp.sig", }, ], [")", { colour: "#999999" }], ]); wait(45); text([ " - ", { colour: "#7a7a7a", }, ]); wait(70); replaceLast((v) => [ ...v, [ "GPG Release Signing Key: ", { colour: "#cdcdcd", }, ], ]); wait(100); replaceLast((v) => [ ...v, [ "0D93102265071798C7B65A4C9F0739B9E0C8FD60", { colour: "#ffffff", underlined: true, weight: 700, url: "currenttab:/keys/memdmp/release.pgp", dl: "memdmp-release.pgp", }, ], ]); wait(100); replaceLast((v) => [ ...v, [ " (", { colour: "#999999", }, ], [ "signed", { colour: "#ffffff", underlined: true, weight: 500, url: "currenttab:/keys/memdmp/release.pgp.sig", dl: "memdmp-release.pgp.sig", }, ], [")", { colour: "#999999" }], ]); wait(100); text([ " - ", { colour: "#7a7a7a", }, ]); wait(35); replaceLast((v) => [ ...v, [ "Source Code: ", { colour: "#cdcdcd", }, ], ]); wait(100); replaceLast((v) => [ ...v, [ "git.estrogen.zone/mem-estrogen-zone", { colour: "#F0A3B3", underlined: true, weight: 700, url: "newtab:/upstream/", }, ], ]); wait(100); text([ " - ", { colour: "#7a7a7a", }, ]); wait(35); replaceLast((v) => [ ...v, [ "Canaries: ", { colour: "#cdcdcd", }, ], ]); wait(100); replaceLast((v) => [ ...v, [ "/canaries/", { colour: "#58C7F3", underlined: true, weight: 700, url: "currenttab:/canaries/", }, ], ]); wait(5000); text([ ``, { raw: true, url: () => alert("i abused too much css for this i wanna cry now"), }, ]); wait(200); text( { renderrestriction: "js-only" }, [ "next time, you may want to ", { colour: "#fff2", }, ], [ `skip the animation`, { url: "currenttab:/skip-animation", colour: "#fff2", underlined: true, raw: true, }, ], ); // return lines; })();