/* Copyright (C) 2024-2026 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 Affero, Inc., at version 1. 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 Affero General Public License for more details. You should have received a copy of the 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', }, [ 'No JS. Some links may not work as intended.', { 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', `${(parseInt('0x' + (0x1312 + 0o1312).toString(16)) - 0x96cf).toString(16)}`, ]), { 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.\n`, ], ['I do'], [ ` shit like the hellhole the reader is seeing right now, amongst other things.\n\n` ] ); wait(300); for (const c of 'This one is'.split('').map((_, i, a) => a.slice(0, i + 1).join(''))) { replaceLast((l) => { l[1][0] = l[1][0].substring(c.length) + c; return l; }); wait(70); } wait(300); for (const c of 'It does'.split('').map((_, i, a) => a.slice(0, i + 1).join(''))) { replaceLast((l) => { l[3][0] = l[3][0].substring(c.length) + c; return l; }); wait(70); } text([ 'Places the reader can find it:', { 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', }, ], ['blog: ', { colour: '#cdcdcd' }], ); wait(100); replaceLast((v) => [ ...v, [ '/~mem/blog/', { colour: '#99aaff', underlined: true, weight: 700, url: 'currenttab:/blog/', }, ], [ ' (WIP)', { colour: '#999999', }, ], ]); wait(100); text( [ ' - ', { colour: '#7a7a7a', }, ], ['In random hackerspaces', { colour: '#aaaaaa' }], ); wait(600); text([ '\nThe reader may find these useful:', { colour: '#7a7a7a', }, ]); wait(400); text( [ ' - ', { colour: '#7a7a7a', }, ], ['GPG 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(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(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(100); text([ '\nFun fact: this is all pure css animation hell.', { colour: '#7a7a7a', }, ]); wait(5000); text([ ``, { raw: true, url: () => alert('it abused too much css for this it wants to cry now'), }, ]); wait(200); text( { renderrestriction: 'js-only' }, [ 'next time, the reader may want to ', { colour: '#fff2', }, ], [ `skip the animation`, { url: `currenttab:/skip-animation`, colour: '#fff2', underlined: true, raw: true, }, ], ); // return lines; })(); export const joinSimilarTTYLines = (lines: TTYText[]) => { for (const line of lines) if (line.kind === 'text') { const newvalues = [] as RenderBlock[]; let lastvalue: string = ''; for (const value of line.value) { const thisvalue_raw = { ...value } as Omit & { value?: string }; delete (thisvalue_raw as Partial)['value'] const thisvalue = Object.entries(thisvalue_raw).toSorted((a, b) => a[0] > b[0] ? 1 : a[0] === b[0] ? 0 : -1).map(v => v[1]).join('\r\n\n\r\r\n') if (thisvalue === lastvalue) { newvalues[newvalues.length - 1].value += value.value } else { lastvalue = thisvalue newvalues.push(value) } } line.value = newvalues } return lines }