Spaces:
Running on Zero
Running on Zero
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>SCRYPT — session</title> | |
| <link rel="stylesheet" href="/static/app.css" /> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css" /> | |
| </head> | |
| <body> | |
| <main class="room"> | |
| <section class="term-frame"> | |
| <div class="term-bar"> | |
| <span class="dot r"></span><span class="dot y"></span><span class="dot g"></span> | |
| <span class="title">warden@scryptos — tty1</span> | |
| <a href="/">◄ leave</a> | |
| </div> | |
| <div id="terminal"></div> | |
| <div class="term-status" id="status">connecting to the machine…</div> | |
| </section> | |
| </main> | |
| <script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0.18.0/lib/addon-webgl.min.js"></script> | |
| <script> | |
| // Osaka Jade, mapped onto an xterm 16-colour palette so the game's Rich | |
| // styles land on exactly the colours the terminal build uses. | |
| const JADE = { | |
| background: "#111c18", foreground: "#c1c497", cursor: "#d7c995", | |
| cursorAccent: "#111c18", selectionBackground: "#2c4a3a", | |
| black: "#23372b", red: "#ff5345", green: "#549e6a", yellow: "#459451", | |
| blue: "#509475", magenta: "#d2689c", cyan: "#2dd5b7", white: "#f6f5dd", | |
| brightBlack: "#53685b", brightRed: "#db9f9c", brightGreen: "#9eebb3", | |
| brightYellow: "#e5c736", brightBlue: "#acd4cf", brightMagenta: "#75bbb3", | |
| brightCyan: "#8cd3cb", brightWhite: "#f6f5dd", | |
| }; | |
| // The board needs this much terminal real estate; below it, cards clip. | |
| const MIN_ROWS = 44; | |
| const MIN_COLS = 110; | |
| const MAX_FONT = 15; | |
| const MIN_FONT = 10; | |
| const term = new Terminal({ | |
| theme: JADE, | |
| fontFamily: "'IBM Plex Mono', ui-monospace, monospace", | |
| fontSize: MAX_FONT, | |
| lineHeight: 1.05, | |
| cursorBlink: true, | |
| cursorStyle: "block", | |
| allowProposedApi: true, | |
| scrollback: 2000, | |
| // Card frames, scales, and shading are box-drawing/block glyphs; let | |
| // xterm draw those itself (cell-perfect) instead of trusting the webfont, | |
| // whose substituted glyphs warp the card art. | |
| customGlyphs: true, | |
| }); | |
| const fit = new FitAddon.FitAddon(); | |
| term.loadAddon(fit); | |
| term.open(document.getElementById("terminal")); | |
| // customGlyphs needs the WebGL renderer; without it xterm falls back to the | |
| // DOM renderer, which hands the art back to the font. Fall back gracefully | |
| // on machines without WebGL. | |
| try { | |
| const webgl = new WebglAddon.WebglAddon(); | |
| webgl.onContextLoss(() => webgl.dispose()); | |
| term.loadAddon(webgl); | |
| } catch (e) { /* DOM renderer: playable, just softer card frames */ } | |
| // Fit the whole board, not just the window: start at the comfy font size and | |
| // step down until the grid clears MIN_ROWS x MIN_COLS (or we hit the floor). | |
| function fitGame() { | |
| let size = MAX_FONT; | |
| term.options.fontSize = size; | |
| fit.fit(); | |
| while ((term.rows < MIN_ROWS || term.cols < MIN_COLS) && size > MIN_FONT) { | |
| size -= 1; | |
| term.options.fontSize = size; | |
| fit.fit(); | |
| } | |
| } | |
| fitGame(); | |
| const status = document.getElementById("status"); | |
| function setStatus(html, dead = false) { | |
| status.innerHTML = html; | |
| status.classList.toggle("dead", dead); | |
| } | |
| const proto = location.protocol === "https:" ? "wss" : "ws"; | |
| const ws = new WebSocket(`${proto}://${location.host}/pty`); | |
| ws.binaryType = "arraybuffer"; | |
| function sendResize() { | |
| if (ws.readyState === WebSocket.OPEN) { | |
| // Reserve one row: the board docks its instruction prompt to the bottom | |
| // row, and FitAddon tends to report one more row than is actually | |
| // painted — without this margin that prompt renders just off-screen. | |
| const rows = Math.max(1, term.rows - 1); | |
| ws.send(JSON.stringify({ resize: [term.cols, rows] })); | |
| } | |
| } | |
| ws.onopen = () => { | |
| setStatus("the Warden is <b>watching</b> · type to play"); | |
| sendResize(); | |
| term.focus(); | |
| }; | |
| ws.onmessage = (ev) => { | |
| const bytes = typeof ev.data === "string" ? ev.data : new Uint8Array(ev.data); | |
| term.write(bytes); | |
| }; | |
| ws.onclose = () => setStatus("the machine closed the session. <b>refresh to wake it again.</b>", true); | |
| ws.onerror = () => setStatus("connection severed.", true); | |
| term.onData((data) => { | |
| if (ws.readyState === WebSocket.OPEN) ws.send(new TextEncoder().encode(data)); | |
| }); | |
| window.addEventListener("resize", () => { fitGame(); sendResize(); }); | |
| const ro = new ResizeObserver(() => { fitGame(); sendResize(); }); | |
| ro.observe(document.getElementById("terminal")); | |
| </script> | |
| </body> | |
| </html> | |