/* 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< T, Diff >; 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( ['Welcome to'], [''], [' the Estrogen Zone!\n\n'], ); wait(300); for (const c of ' my corner of'.split('')) { replaceLast((l) => { l[1][0] += c; return l; }); wait(70); } text( ['Places you can find me:', { colour: '#7a7a7a', }], ); wait(300); text( [' - ', { colour: '#7a7a7a', }], ['Estradiol SourceHut: ', { colour: '#cdcdcd' }], ); wait(100); replaceLast((v) => [...v, ['sh.estrogen.zone/~memdmp', { colour: '#99aaff', underlined: true, weight: 700, url: 'newtab:https://sh.estrogen.zone/~memdmp', }]]); 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://sh.estrogen.zone/~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.sh.estrogen.zone/~memdmp/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'), }, ]); // return lines; })();