diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index cbe25fea..7da72774 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -3643,7 +3643,7 @@ export const MODELS = { cacheWrite: 18.75, }, contextWindow: 200000, - maxTokens: 4096, + maxTokens: 32000, } satisfies Model<"openai-completions">, "anthropic/claude-opus-4.5": { id: "anthropic/claude-opus-4.5", @@ -3660,7 +3660,7 @@ export const MODELS = { cacheWrite: 6.25, }, contextWindow: 200000, - maxTokens: 32000, + maxTokens: 64000, } satisfies Model<"openai-completions">, "anthropic/claude-sonnet-4": { id: "anthropic/claude-sonnet-4", @@ -3977,13 +3977,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.39999999999999997, - output: 1.75, + input: 0.44999999999999996, + output: 2.1500000000000004, cacheRead: 0, cacheWrite: 0, }, - contextWindow: 163840, - maxTokens: 65536, + contextWindow: 131072, + maxTokens: 32768, } satisfies Model<"openai-completions">, "deepseek/deepseek-r1-distill-llama-70b": { id: "deepseek/deepseek-r1-distill-llama-70b", @@ -4359,23 +4359,6 @@ export const MODELS = { contextWindow: 256000, maxTokens: 128000, } satisfies Model<"openai-completions">, - "kwaipilot/kat-coder-pro:free": { - id: "kwaipilot/kat-coder-pro:free", - name: "Kwaipilot: KAT-Coder-Pro V1 (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, "meta-llama/llama-3-70b-instruct": { id: "meta-llama/llama-3-70b-instruct", name: "Meta: Llama 3 70B Instruct", @@ -4589,13 +4572,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.28, - output: 1.2, - cacheRead: 0.14, + input: 0.27, + output: 1.12, + cacheRead: 0, cacheWrite: 0, }, contextWindow: 196608, - maxTokens: 4096, + maxTokens: 65536, } satisfies Model<"openai-completions">, "mistralai/codestral-2508": { id: "mistralai/codestral-2508", diff --git a/packages/ai/test/.temp-images/small.png b/packages/ai/test/.temp-images/small.png new file mode 100644 index 00000000..91dd80f1 Binary files /dev/null and b/packages/ai/test/.temp-images/small.png differ diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 918be50d..4a5903df 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -10,7 +10,9 @@ - Session naming: `/name ` command sets a display name shown in the session selector instead of the first message. Useful for distinguishing forked sessions. Extensions can use `pi.setSessionName()` and `pi.getSessionName()`. ([#650](https://github.com/badlogic/pi-mono/pull/650) by [@scutifer](https://github.com/scutifer)) - `ctx.ui.custom()` now accepts `overlayOptions` for overlay positioning and sizing (anchor, margins, offsets, percentages, absolute positioning) -- Extension example: `overlay-qa-tests.ts` for testing all overlay positioning scenarios +- `ctx.ui.custom()` now accepts `onHandle` callback to receive the `OverlayHandle` for controlling overlay visibility +- Extension example: `overlay-qa-tests.ts` with 10 commands for testing overlay positioning, animation, and toggle scenarios +- Extension example: `doom-overlay/` - DOOM game running as an overlay at 35 FPS (auto-downloads WAD on first run) - Extension example: `notify.ts` for desktop notifications via OSC 777 escape sequence ([#658](https://github.com/badlogic/pi-mono/pull/658) by [@ferologics](https://github.com/ferologics)) - Inline hint for queued messages showing the `Alt+Up` restore shortcut ([#657](https://github.com/badlogic/pi-mono/pull/657) by [@tmustier](https://github.com/tmustier)) - Page-up/down navigation in `/resume` session selector to jump by 5 items ([#662](https://github.com/badlogic/pi-mono/pull/662) by [@aliou](https://github.com/aliou)) diff --git a/packages/coding-agent/examples/extensions/README.md b/packages/coding-agent/examples/extensions/README.md index 9d61071e..a4dfb89c 100644 --- a/packages/coding-agent/examples/extensions/README.md +++ b/packages/coding-agent/examples/extensions/README.md @@ -50,7 +50,8 @@ cp permission-gate.ts ~/.pi/agent/extensions/ | `modal-editor.ts` | Custom vim-like modal editor via `ctx.ui.setEditorComponent()` | | `notify.ts` | Desktop notifications via OSC 777 when agent finishes (Ghostty, iTerm2, WezTerm) | | `overlay-test.ts` | Test overlay compositing with inline text inputs and edge cases | -| `overlay-qa-tests.ts` | Comprehensive overlay QA tests: anchors, margins, stacking, overflow protection | +| `overlay-qa-tests.ts` | Comprehensive overlay QA tests: anchors, margins, stacking, overflow, animation | +| `doom-overlay/` | DOOM game running as an overlay at 35 FPS (demonstrates real-time game rendering) | ### Git Integration diff --git a/packages/coding-agent/examples/extensions/doom-overlay/.gitignore b/packages/coding-agent/examples/extensions/doom-overlay/.gitignore new file mode 100644 index 00000000..e3edbd8c --- /dev/null +++ b/packages/coding-agent/examples/extensions/doom-overlay/.gitignore @@ -0,0 +1,2 @@ +# Auto-downloaded on first run +doom1.wad diff --git a/packages/coding-agent/examples/extensions/doom-overlay/README.md b/packages/coding-agent/examples/extensions/doom-overlay/README.md new file mode 100644 index 00000000..420bd80b --- /dev/null +++ b/packages/coding-agent/examples/extensions/doom-overlay/README.md @@ -0,0 +1,46 @@ +# DOOM Overlay Demo + +Play DOOM as an overlay in pi. Demonstrates that the overlay system can handle real-time game rendering at 35 FPS. + +## Usage + +```bash +pi --extension ./examples/extensions/doom-overlay +``` + +Then run: +``` +/doom-overlay +``` + +The shareware WAD file (~4MB) is auto-downloaded on first run. + +## Controls + +| Action | Keys | +|--------|------| +| Move | WASD or Arrow Keys | +| Run | Shift + WASD | +| Fire | F or Ctrl | +| Use/Open | Space | +| Weapons | 1-7 | +| Map | Tab | +| Menu | Escape | +| Pause/Quit | Q | + +## How It Works + +DOOM runs as WebAssembly compiled from [doomgeneric](https://github.com/ozkl/doomgeneric). Each frame is rendered using half-block characters (▀) with 24-bit color, where the top pixel is the foreground color and the bottom pixel is the background color. + +The overlay uses: +- `width: "90%"` - 90% of terminal width +- `maxHeight: "80%"` - Maximum 80% of terminal height +- `anchor: "center"` - Centered in terminal + +Height is calculated from width to maintain DOOM's 3.2:1 aspect ratio (accounting for half-block rendering). + +## Credits + +- [id Software](https://github.com/id-Software/DOOM) for the original DOOM +- [doomgeneric](https://github.com/ozkl/doomgeneric) for the portable DOOM implementation +- [pi-doom](https://github.com/badlogic/pi-doom) for the original pi integration diff --git a/packages/coding-agent/examples/extensions/doom-overlay/doom-component.ts b/packages/coding-agent/examples/extensions/doom-overlay/doom-component.ts new file mode 100644 index 00000000..2fd7ad9d --- /dev/null +++ b/packages/coding-agent/examples/extensions/doom-overlay/doom-component.ts @@ -0,0 +1,133 @@ +/** + * DOOM Component for overlay mode + * + * Renders DOOM frames using half-block characters (▀) with 24-bit color. + * Height is calculated from width to maintain DOOM's aspect ratio. + */ + +import type { Component } from "@mariozechner/pi-tui"; +import { isKeyRelease, type TUI } from "@mariozechner/pi-tui"; +import type { DoomEngine } from "./doom-engine.js"; +import { DoomKeys, mapKeyToDoom } from "./doom-keys.js"; + +function renderHalfBlock( + rgba: Uint8Array, + width: number, + height: number, + targetCols: number, + targetRows: number, +): string[] { + const lines: string[] = []; + const scaleX = width / targetCols; + const scaleY = height / (targetRows * 2); + + for (let row = 0; row < targetRows; row++) { + let line = ""; + const srcY1 = Math.floor(row * 2 * scaleY); + const srcY2 = Math.floor((row * 2 + 1) * scaleY); + + for (let col = 0; col < targetCols; col++) { + const srcX = Math.floor(col * scaleX); + const idx1 = (srcY1 * width + srcX) * 4; + const idx2 = (srcY2 * width + srcX) * 4; + const r1 = rgba[idx1] ?? 0, + g1 = rgba[idx1 + 1] ?? 0, + b1 = rgba[idx1 + 2] ?? 0; + const r2 = rgba[idx2] ?? 0, + g2 = rgba[idx2 + 1] ?? 0, + b2 = rgba[idx2 + 2] ?? 0; + line += `\x1b[38;2;${r1};${g1};${b1}m\x1b[48;2;${r2};${g2};${b2}m▀`; + } + line += "\x1b[0m"; + lines.push(line); + } + return lines; +} + +export class DoomOverlayComponent implements Component { + private engine: DoomEngine; + private tui: TUI; + private interval: ReturnType | null = null; + private onExit: () => void; + + // Opt-in to key release events for smooth movement + wantsKeyRelease = true; + + constructor(tui: TUI, engine: DoomEngine, onExit: () => void, resume = false) { + this.tui = tui; + this.engine = engine; + this.onExit = onExit; + + // Unpause if resuming + if (resume) { + this.engine.pushKey(true, DoomKeys.KEY_PAUSE); + this.engine.pushKey(false, DoomKeys.KEY_PAUSE); + } + + this.startGameLoop(); + } + + private startGameLoop(): void { + this.interval = setInterval(() => { + try { + this.engine.tick(); + // Force full re-render to prevent bleed artifacts at high frame rates + this.tui.requestRender(true); + } catch { + // WASM error (e.g., exit via DOOM menu) - treat as quit + this.dispose(); + this.onExit(); + } + }, 1000 / 35); + } + + handleInput(data: string): void { + // Q to pause and exit (but not on release) + if (!isKeyRelease(data) && (data === "q" || data === "Q")) { + // Send DOOM's pause key before exiting + this.engine.pushKey(true, DoomKeys.KEY_PAUSE); + this.engine.pushKey(false, DoomKeys.KEY_PAUSE); + this.dispose(); + this.onExit(); + return; + } + + const doomKeys = mapKeyToDoom(data); + if (doomKeys.length === 0) return; + + const released = isKeyRelease(data); + + for (const key of doomKeys) { + this.engine.pushKey(!released, key); + } + } + + render(width: number): string[] { + // DOOM renders at 640x400 (1.6:1 ratio) + // With half-block characters, each terminal row = 2 pixels + // So effective ratio is 640:200 = 3.2:1 (width:height in terminal cells) + // Add 1 row for footer + const ASPECT_RATIO = 3.2; + const MIN_HEIGHT = 10; + const height = Math.max(MIN_HEIGHT, Math.floor(width / ASPECT_RATIO)); + + const rgba = this.engine.getFrameRGBA(); + const lines = renderHalfBlock(rgba, this.engine.width, this.engine.height, width, height); + + // Footer + const footer = " DOOM | Q=Pause | WASD=Move | Shift+WASD=Run | Space=Use | F=Fire | 1-7=Weapons"; + const truncatedFooter = footer.length > width ? footer.slice(0, width) : footer; + lines.push(`\x1b[2m${truncatedFooter}\x1b[0m`); + + return lines; + } + + invalidate(): void {} + + dispose(): void { + if (this.interval) { + clearInterval(this.interval); + this.interval = null; + } + } +} diff --git a/packages/coding-agent/examples/extensions/doom-overlay/doom-engine.ts b/packages/coding-agent/examples/extensions/doom-overlay/doom-engine.ts new file mode 100644 index 00000000..be14237c --- /dev/null +++ b/packages/coding-agent/examples/extensions/doom-overlay/doom-engine.ts @@ -0,0 +1,173 @@ +/** + * DOOM Engine - WebAssembly wrapper for doomgeneric + */ + +import { existsSync, readFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +export interface DoomModule { + _doomgeneric_Create: (argc: number, argv: number) => void; + _doomgeneric_Tick: () => void; + _DG_GetFrameBuffer: () => number; + _DG_GetScreenWidth: () => number; + _DG_GetScreenHeight: () => number; + _DG_PushKeyEvent: (pressed: number, key: number) => void; + _malloc: (size: number) => number; + _free: (ptr: number) => void; + HEAPU8: Uint8Array; + HEAPU32: Uint32Array; + FS_createDataFile: (parent: string, name: string, data: number[], canRead: boolean, canWrite: boolean) => void; + FS_createPath: (parent: string, path: string, canRead: boolean, canWrite: boolean) => string; + setValue: (ptr: number, value: number, type: string) => void; + getValue: (ptr: number, type: string) => number; +} + +export class DoomEngine { + private module: DoomModule | null = null; + private frameBufferPtr: number = 0; + private initialized = false; + private wadPath: string; + private _width = 640; + private _height = 400; + + constructor(wadPath: string) { + this.wadPath = wadPath; + } + + get width(): number { + return this._width; + } + + get height(): number { + return this._height; + } + + async init(): Promise { + // Locate WASM build + const __dirname = dirname(fileURLToPath(import.meta.url)); + const buildDir = join(__dirname, "doom", "build"); + const doomJsPath = join(buildDir, "doom.js"); + + if (!existsSync(doomJsPath)) { + throw new Error(`WASM not found at ${doomJsPath}. Run ./doom/build.sh first`); + } + + // Read WAD file + const wadData = readFileSync(this.wadPath); + const wadArray = Array.from(new Uint8Array(wadData)); + + // Load WASM module - eval to bypass jiti completely + const doomJsCode = readFileSync(doomJsPath, "utf-8"); + const moduleExports: { exports: unknown } = { exports: {} }; + const nativeRequire = createRequire(doomJsPath); + const moduleFunc = new Function("module", "exports", "__dirname", "__filename", "require", doomJsCode); + moduleFunc(moduleExports, moduleExports.exports, buildDir, doomJsPath, nativeRequire); + const createDoomModule = moduleExports.exports as (config: unknown) => Promise; + + const moduleConfig = { + locateFile: (path: string) => { + if (path.endsWith(".wasm")) { + return join(buildDir, path); + } + return path; + }, + print: () => {}, + printErr: () => {}, + preRun: [ + (module: DoomModule) => { + // Create /doom directory and add WAD + module.FS_createPath("/", "doom", true, true); + module.FS_createDataFile("/doom", "doom1.wad", wadArray, true, false); + }, + ], + }; + + this.module = await createDoomModule(moduleConfig); + if (!this.module) { + throw new Error("Failed to initialize DOOM module"); + } + + // Initialize DOOM + this.initDoom(); + + // Get framebuffer info + this.frameBufferPtr = this.module._DG_GetFrameBuffer(); + this._width = this.module._DG_GetScreenWidth(); + this._height = this.module._DG_GetScreenHeight(); + this.initialized = true; + } + + private initDoom(): void { + if (!this.module) return; + + const args = ["doom", "-iwad", "/doom/doom1.wad"]; + const argPtrs: number[] = []; + + for (const arg of args) { + const ptr = this.module._malloc(arg.length + 1); + for (let i = 0; i < arg.length; i++) { + this.module.setValue(ptr + i, arg.charCodeAt(i), "i8"); + } + this.module.setValue(ptr + arg.length, 0, "i8"); + argPtrs.push(ptr); + } + + const argvPtr = this.module._malloc(argPtrs.length * 4); + for (let i = 0; i < argPtrs.length; i++) { + this.module.setValue(argvPtr + i * 4, argPtrs[i]!, "i32"); + } + + this.module._doomgeneric_Create(args.length, argvPtr); + + for (const ptr of argPtrs) { + this.module._free(ptr); + } + this.module._free(argvPtr); + } + + /** + * Run one game tick + */ + tick(): void { + if (!this.module || !this.initialized) return; + this.module._doomgeneric_Tick(); + } + + /** + * Get current frame as RGBA pixel data + * DOOM outputs ARGB, we convert to RGBA + */ + getFrameRGBA(): Uint8Array { + if (!this.module || !this.initialized) { + return new Uint8Array(this._width * this._height * 4); + } + + const pixels = this._width * this._height; + const buffer = new Uint8Array(pixels * 4); + + for (let i = 0; i < pixels; i++) { + const argb = this.module.getValue(this.frameBufferPtr + i * 4, "i32"); + const offset = i * 4; + buffer[offset + 0] = (argb >> 16) & 0xff; // R + buffer[offset + 1] = (argb >> 8) & 0xff; // G + buffer[offset + 2] = argb & 0xff; // B + buffer[offset + 3] = 255; // A + } + + return buffer; + } + + /** + * Push a key event + */ + pushKey(pressed: boolean, key: number): void { + if (!this.module || !this.initialized) return; + this.module._DG_PushKeyEvent(pressed ? 1 : 0, key); + } + + isInitialized(): boolean { + return this.initialized; + } +} diff --git a/packages/coding-agent/examples/extensions/doom-overlay/doom-keys.ts b/packages/coding-agent/examples/extensions/doom-overlay/doom-keys.ts new file mode 100644 index 00000000..3a00eb28 --- /dev/null +++ b/packages/coding-agent/examples/extensions/doom-overlay/doom-keys.ts @@ -0,0 +1,104 @@ +/** + * DOOM key codes (from doomkeys.h) + */ +export const DoomKeys = { + KEY_RIGHTARROW: 0xae, + KEY_LEFTARROW: 0xac, + KEY_UPARROW: 0xad, + KEY_DOWNARROW: 0xaf, + KEY_STRAFE_L: 0xa0, + KEY_STRAFE_R: 0xa1, + KEY_USE: 0xa2, + KEY_FIRE: 0xa3, + KEY_ESCAPE: 27, + KEY_ENTER: 13, + KEY_TAB: 9, + KEY_F1: 0x80 + 0x3b, + KEY_F2: 0x80 + 0x3c, + KEY_F3: 0x80 + 0x3d, + KEY_F4: 0x80 + 0x3e, + KEY_F5: 0x80 + 0x3f, + KEY_F6: 0x80 + 0x40, + KEY_F7: 0x80 + 0x41, + KEY_F8: 0x80 + 0x42, + KEY_F9: 0x80 + 0x43, + KEY_F10: 0x80 + 0x44, + KEY_F11: 0x80 + 0x57, + KEY_F12: 0x80 + 0x58, + KEY_BACKSPACE: 127, + KEY_PAUSE: 0xff, + KEY_EQUALS: 0x3d, + KEY_MINUS: 0x2d, + KEY_RSHIFT: 0x80 + 0x36, + KEY_RCTRL: 0x80 + 0x1d, + KEY_RALT: 0x80 + 0x38, +} as const; + +import { Key, matchesKey, parseKey } from "@mariozechner/pi-tui"; + +/** + * Map terminal key input to DOOM key codes + * Supports both raw terminal input and Kitty protocol sequences + */ +export function mapKeyToDoom(data: string): number[] { + // Arrow keys + if (matchesKey(data, Key.up)) return [DoomKeys.KEY_UPARROW]; + if (matchesKey(data, Key.down)) return [DoomKeys.KEY_DOWNARROW]; + if (matchesKey(data, Key.right)) return [DoomKeys.KEY_RIGHTARROW]; + if (matchesKey(data, Key.left)) return [DoomKeys.KEY_LEFTARROW]; + + // WASD - check both raw char and Kitty sequences + if (data === "w" || matchesKey(data, "w")) return [DoomKeys.KEY_UPARROW]; + if (data === "W" || matchesKey(data, Key.shift("w"))) return [DoomKeys.KEY_UPARROW, DoomKeys.KEY_RSHIFT]; + if (data === "s" || matchesKey(data, "s")) return [DoomKeys.KEY_DOWNARROW]; + if (data === "S" || matchesKey(data, Key.shift("s"))) return [DoomKeys.KEY_DOWNARROW, DoomKeys.KEY_RSHIFT]; + if (data === "a" || matchesKey(data, "a")) return [DoomKeys.KEY_STRAFE_L]; + if (data === "A" || matchesKey(data, Key.shift("a"))) return [DoomKeys.KEY_STRAFE_L, DoomKeys.KEY_RSHIFT]; + if (data === "d" || matchesKey(data, "d")) return [DoomKeys.KEY_STRAFE_R]; + if (data === "D" || matchesKey(data, Key.shift("d"))) return [DoomKeys.KEY_STRAFE_R, DoomKeys.KEY_RSHIFT]; + + // Fire - F key + if (data === "f" || data === "F" || matchesKey(data, "f") || matchesKey(data, Key.shift("f"))) { + return [DoomKeys.KEY_FIRE]; + } + + // Use/Open + if (data === " " || matchesKey(data, Key.space)) return [DoomKeys.KEY_USE]; + + // Menu/UI keys + if (matchesKey(data, Key.enter)) return [DoomKeys.KEY_ENTER]; + if (matchesKey(data, Key.escape)) return [DoomKeys.KEY_ESCAPE]; + if (matchesKey(data, Key.tab)) return [DoomKeys.KEY_TAB]; + if (matchesKey(data, Key.backspace)) return [DoomKeys.KEY_BACKSPACE]; + + // Ctrl keys (except Ctrl+C) = fire (legacy support) + const parsed = parseKey(data); + if (parsed?.startsWith("ctrl+") && parsed !== "ctrl+c") { + return [DoomKeys.KEY_FIRE]; + } + if (data.length === 1 && data.charCodeAt(0) < 32 && data !== "\x03") { + return [DoomKeys.KEY_FIRE]; + } + + // Weapon selection (0-9) + if (data >= "0" && data <= "9") return [data.charCodeAt(0)]; + + // Plus/minus for screen size + if (data === "+" || data === "=") return [DoomKeys.KEY_EQUALS]; + if (data === "-") return [DoomKeys.KEY_MINUS]; + + // Y/N for prompts + if (data === "y" || data === "Y" || matchesKey(data, "y") || matchesKey(data, Key.shift("y"))) { + return ["y".charCodeAt(0)]; + } + if (data === "n" || data === "N" || matchesKey(data, "n") || matchesKey(data, Key.shift("n"))) { + return ["n".charCodeAt(0)]; + } + + // Other printable characters (for cheats) + if (data.length === 1 && data.charCodeAt(0) >= 32) { + return [data.toLowerCase().charCodeAt(0)]; + } + + return []; +} diff --git a/packages/coding-agent/examples/extensions/doom-overlay/doom/build.sh b/packages/coding-agent/examples/extensions/doom-overlay/doom/build.sh new file mode 100755 index 00000000..dd11122a --- /dev/null +++ b/packages/coding-agent/examples/extensions/doom-overlay/doom/build.sh @@ -0,0 +1,152 @@ +#!/bin/bash +# Build DOOM for pi-doom using doomgeneric and Emscripten + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +DOOM_DIR="$PROJECT_ROOT/doom" +BUILD_DIR="$PROJECT_ROOT/doom/build" + +echo "=== pi-doom Build Script ===" + +# Check for emcc +if ! command -v emcc &> /dev/null; then + echo "Error: Emscripten (emcc) not found!" + echo "" + echo "Install via Homebrew:" + echo " brew install emscripten" + echo "" + echo "Or manually:" + echo " git clone https://github.com/emscripten-core/emsdk.git ~/emsdk" + echo " cd ~/emsdk && ./emsdk install latest && ./emsdk activate latest" + echo " source ~/emsdk/emsdk_env.sh" + exit 1 +fi + +# Clone doomgeneric if not present +if [ ! -d "$DOOM_DIR/doomgeneric" ]; then + echo "Cloning doomgeneric..." + cd "$DOOM_DIR" + git clone https://github.com/ozkl/doomgeneric.git +fi + +# Create build directory +mkdir -p "$BUILD_DIR" + +# Copy our platform file +cp "$DOOM_DIR/doomgeneric_pi.c" "$DOOM_DIR/doomgeneric/doomgeneric/" + +echo "Compiling DOOM to WebAssembly..." +cd "$DOOM_DIR/doomgeneric/doomgeneric" + +# Resolution - 640x400 is doomgeneric default, good balance of speed/quality +RESX=${DOOM_RESX:-640} +RESY=${DOOM_RESY:-400} + +echo "Resolution: ${RESX}x${RESY}" + +# Compile with Emscripten (no sound) +emcc -O2 \ + -s WASM=1 \ + -s EXPORTED_FUNCTIONS="['_doomgeneric_Create','_doomgeneric_Tick','_DG_GetFrameBuffer','_DG_GetScreenWidth','_DG_GetScreenHeight','_DG_PushKeyEvent','_malloc','_free']" \ + -s EXPORTED_RUNTIME_METHODS="['ccall','cwrap','getValue','setValue','FS']" \ + -s ALLOW_MEMORY_GROWTH=1 \ + -s INITIAL_MEMORY=33554432 \ + -s MODULARIZE=1 \ + -s EXPORT_NAME="createDoomModule" \ + -s ENVIRONMENT='node' \ + -s FILESYSTEM=1 \ + -s FORCE_FILESYSTEM=1 \ + -s EXIT_RUNTIME=0 \ + -s NO_EXIT_RUNTIME=1 \ + -DDOOMGENERIC_RESX=$RESX \ + -DDOOMGENERIC_RESY=$RESY \ + -I. \ + am_map.c \ + d_event.c \ + d_items.c \ + d_iwad.c \ + d_loop.c \ + d_main.c \ + d_mode.c \ + d_net.c \ + doomdef.c \ + doomgeneric.c \ + doomgeneric_pi.c \ + doomstat.c \ + dstrings.c \ + f_finale.c \ + f_wipe.c \ + g_game.c \ + hu_lib.c \ + hu_stuff.c \ + i_cdmus.c \ + i_input.c \ + i_endoom.c \ + i_joystick.c \ + i_scale.c \ + i_sound.c \ + i_system.c \ + i_timer.c \ + i_video.c \ + icon.c \ + info.c \ + m_argv.c \ + m_bbox.c \ + m_cheat.c \ + m_config.c \ + m_controls.c \ + m_fixed.c \ + m_menu.c \ + m_misc.c \ + m_random.c \ + memio.c \ + p_ceilng.c \ + p_doors.c \ + p_enemy.c \ + p_floor.c \ + p_inter.c \ + p_lights.c \ + p_map.c \ + p_maputl.c \ + p_mobj.c \ + p_plats.c \ + p_pspr.c \ + p_saveg.c \ + p_setup.c \ + p_sight.c \ + p_spec.c \ + p_switch.c \ + p_telept.c \ + p_tick.c \ + p_user.c \ + r_bsp.c \ + r_data.c \ + r_draw.c \ + r_main.c \ + r_plane.c \ + r_segs.c \ + r_sky.c \ + r_things.c \ + s_sound.c \ + sha1.c \ + sounds.c \ + st_lib.c \ + st_stuff.c \ + statdump.c \ + tables.c \ + v_video.c \ + w_checksum.c \ + w_file.c \ + w_file_stdc.c \ + w_main.c \ + w_wad.c \ + wi_stuff.c \ + z_zone.c \ + dummy.c \ + -o "$BUILD_DIR/doom.js" + +echo "" +echo "Build complete!" +echo "Output: $BUILD_DIR/doom.js and $BUILD_DIR/doom.wasm" diff --git a/packages/coding-agent/examples/extensions/doom-overlay/doom/build/doom.js b/packages/coding-agent/examples/extensions/doom-overlay/doom/build/doom.js new file mode 100644 index 00000000..e2221dc2 --- /dev/null +++ b/packages/coding-agent/examples/extensions/doom-overlay/doom/build/doom.js @@ -0,0 +1,21 @@ +var createDoomModule = (() => { + var _scriptName = typeof document != 'undefined' ? document.currentScript?.src : undefined; + if (typeof __filename != 'undefined') _scriptName = _scriptName || __filename; + return ( +async function(moduleArg = {}) { + var moduleRtn; + +var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});var ENVIRONMENT_IS_WORKER=false;var ENVIRONMENT_IS_NODE=true;if(ENVIRONMENT_IS_NODE){}var moduleOverrides={...Module};var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("fs");var nodePath=require("path");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(!Module["thisProgram"]&&process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var wasmBinary=Module["wasmBinary"];var wasmMemory;var ABORT=false;var EXITSTATUS;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAP64,HEAPU64,HEAPF64;var runtimeInitialized=false;var isFileURI=filename=>filename.startsWith("file://");function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b);Module["HEAP64"]=HEAP64=new BigInt64Array(b);Module["HEAPU64"]=HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;if(!Module["noFSInit"]&&!FS.initialized)FS.init();TTY.init();wasmExports["__wasm_call_ctors"]();FS.ignorePermissions=false}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}var runDependencies=0;var dependenciesFulfilled=null;function getUniqueRunDependency(id){return id}function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("doom.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){return{env:wasmImports,wasi_snapshot_preview1:wasmImports}}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["memory"];updateMemoryViews();removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(mod,inst)=>{receiveInstance(mod,inst);resolve(mod.exports)})})}wasmBinaryFile??=findWasmBinary();try{var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}catch(e){readyPromiseReject(e);return Promise.reject(e)}}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.unshift(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.unshift(cb);function getValue(ptr,type="i8"){if(type.endsWith("*"))type="*";switch(type){case"i1":return HEAP8[ptr];case"i8":return HEAP8[ptr];case"i16":return HEAP16[ptr>>1];case"i32":return HEAP32[ptr>>2];case"i64":return HEAP64[ptr>>3];case"float":return HEAPF32[ptr>>2];case"double":return HEAPF64[ptr>>3];case"*":return HEAPU32[ptr>>2];default:abort(`invalid type for getValue: ${type}`)}}var noExitRuntime=Module["noExitRuntime"]||true;function setValue(ptr,value,type="i8"){if(type.endsWith("*"))type="*";switch(type){case"i1":HEAP8[ptr]=value;break;case"i8":HEAP8[ptr]=value;break;case"i16":HEAP16[ptr>>1]=value;break;case"i32":HEAP32[ptr>>2]=value;break;case"i64":HEAP64[ptr>>3]=BigInt(value);break;case"float":HEAPF32[ptr>>2]=value;break;case"double":HEAPF64[ptr>>3]=value;break;case"*":HEAPU32[ptr>>2]=value;break;default:abort(`invalid type for setValue: ${type}`)}}var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var syscallGetVarargI=()=>{var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret};var syscallGetVarargP=syscallGetVarargI;var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.slice(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.slice(0,-1)}return root+dir},basename:path=>path&&path.match(/([^\/]+|\/)\/*$/)[1],join:(...paths)=>PATH.normalize(paths.join("/")),join2:(l,r)=>PATH.normalize(l+"/"+r)};var initRandomFill=()=>{if(ENVIRONMENT_IS_NODE){var nodeCrypto=require("crypto");return view=>nodeCrypto.randomFillSync(view)}return view=>crypto.getRandomValues(view)};var randomFill=view=>{(randomFill=initRandomFill())(view)};var PATH_FS={resolve:(...args)=>{var resolvedPath="",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).slice(1);to=PATH_FS.resolve(to).slice(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var FS_stdin_getChar_buffer=[];var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx};var intArrayFromString=(stringy,dontAddNull,length)=>{var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array};var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(ENVIRONMENT_IS_NODE){var BUFSIZE=256;var buf=Buffer.alloc(BUFSIZE);var bytesRead=0;var fd=process.stdin.fd;try{bytesRead=fs.readSync(fd,buf,0,BUFSIZE)}catch(e){if(e.toString().includes("EOF"))bytesRead=0;else throw e}if(bytesRead>0){result=buf.slice(0,bytesRead).toString("utf-8")}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){out(UTF8ArrayToString(tty.output));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output?.length>0){err(UTF8ArrayToString(tty.output));tty.output=[]}}}};var mmapAlloc=size=>{abort()};var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,"/",16895,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.atime=node.mtime=node.ctime=Date.now();if(parent){parent.contents[name]=node;parent.atime=parent.mtime=parent.ctime=node.atime}return node},getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.atime);attr.mtime=new Date(node.mtime);attr.ctime=new Date(node.ctime);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){for(const key of["mode","atime","mtime","ctime"]){if(attr[key]!=null){node[key]=attr[key]}}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){throw MEMFS.doesNotExistError},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){if(FS.isDir(old_node.mode)){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}FS.hashRemoveNode(new_node)}delete old_node.parent.contents[old_node.name];new_dir.contents[new_name]=old_node;old_node.name=new_name;new_dir.ctime=new_dir.mtime=old_node.parent.ctime=old_node.parent.mtime=Date.now()},unlink(parent,name){delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},readdir(node){return[".","..",...Object.keys(node.contents)]},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length{var arrayBuffer=await readAsync(url);return new Uint8Array(arrayBuffer)};var FS_createDataFile=(parent,name,fileData,canRead,canWrite,canOwn)=>{FS.createDataFile(parent,name,fileData,canRead,canWrite,canOwn)};var preloadPlugins=Module["preloadPlugins"]||[];var FS_handledByPreloadPlugin=(byteArray,fullname,finish,onerror)=>{if(typeof Browser!="undefined")Browser.init();var handled=false;preloadPlugins.forEach(plugin=>{if(handled)return;if(plugin["canHandle"](fullname)){plugin["handle"](byteArray,fullname,finish,onerror);handled=true}});return handled};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);function processData(byteArray){function finish(byteArray){preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}onload?.();removeRunDependency(dep)}if(FS_handledByPreloadPlugin(byteArray,fullname,finish,()=>{onerror?.();removeRunDependency(dep)})){return}finish(byteArray)}addRunDependency(dep);if(typeof url=="string"){asyncLoad(url).then(processData,onerror)}else{processData(url)}};var FS_modeStringToFlags=str=>{var flagModes={r:0,"r+":2,w:512|64|1,"w+":512|64|2,a:1024|64|1,"a+":1024|64|2};var flags=flagModes[str];if(typeof flags=="undefined"){throw new Error(`Unknown file open mode: ${str}`)}return flags};var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,filesystems:null,syncFSRequests:0,readFiles:{},ErrnoError:class{name="ErrnoError";constructor(errno){this.errno=errno}},FSStream:class{shared={};get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{node_ops={};stream_ops={};readMode=292|73;writeMode=146;mounted=null;constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.rdev=rdev;this.atime=this.mtime=this.ctime=Date.now()}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){if(!path){throw new FS.ErrnoError(44)}opts.follow_mount??=true;if(!PATH.isAbs(path)){path=FS.cwd()+"/"+path}linkloop:for(var nlinks=0;nlinks<40;nlinks++){var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){if(!FS.isDir(dir.mode)){return 54}try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&(512|64)){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},checkOpExists(op,err){if(!op){throw new FS.ErrnoError(err)}return op},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},doSetAttr(stream,node,attr){var setattr=stream?.stream_ops.setattr;var arg=setattr?stream:node;setattr??=node.node_ops.setattr;FS.checkOpExists(setattr,63);setattr(arg,attr)},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}mounts.forEach(mount=>{if(!mount.type.syncfs){return done(null)}mount.type.syncfs(mount,populate,done)})},mount(type,opts,mountpoint){var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type,opts,mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);Object.keys(FS.nameTable).forEach(hash=>{var current=FS.nameTable[hash];while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}});node.mounted=null;var idx=node.mount.mounts.indexOf(mount);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name){throw new FS.ErrnoError(28)}if(name==="."||name===".."){throw new FS.ErrnoError(20)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},statfs(path){return FS.statfsNode(FS.lookupPath(path,{follow:true}).node)},statfsStream(stream){return FS.statfsNode(stream.node)},statfsNode(node){var rtn={bsize:4096,frsize:4096,blocks:1e6,bfree:5e5,bavail:5e5,files:FS.nextInode,ffree:FS.nextInode-1,fsid:42,flags:2,namelen:255};if(node.node_ops.statfs){Object.assign(rtn,node.node_ops.statfs(node.mount.opts.root))}return rtn},create(path,mode=438){mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode=511){mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var dir of dirs){if(!dir)continue;if(d||PATH.isAbs(path))d+="/";d+=dir;try{FS.mkdir(d,mode)}catch(e){if(e.errno!=20)throw e}}},mkdev(path,mode,dev){if(typeof dev=="undefined"){dev=mode;mode=438}mode|=8192;return FS.mknod(path,mode,dev)},symlink(oldpath,newpath){if(!PATH_FS.resolve(oldpath)){throw new FS.ErrnoError(44)}var lookup=FS.lookupPath(newpath,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var newname=PATH.basename(newpath);var errCode=FS.mayCreate(parent,newname);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.symlink){throw new FS.ErrnoError(63)}return parent.node_ops.symlink(parent,newname,oldpath)},rename(old_path,new_path){var old_dirname=PATH.dirname(old_path);var new_dirname=PATH.dirname(new_path);var old_name=PATH.basename(old_path);var new_name=PATH.basename(new_path);var lookup,old_dir,new_dir;lookup=FS.lookupPath(old_path,{parent:true});old_dir=lookup.node;lookup=FS.lookupPath(new_path,{parent:true});new_dir=lookup.node;if(!old_dir||!new_dir)throw new FS.ErrnoError(44);if(old_dir.mount!==new_dir.mount){throw new FS.ErrnoError(75)}var old_node=FS.lookupNode(old_dir,old_name);var relative=PATH_FS.relative(old_path,new_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(28)}relative=PATH_FS.relative(new_path,old_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(55)}var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(old_node===new_node){return}var isdir=FS.isDir(old_node.mode);var errCode=FS.mayDelete(old_dir,old_name,isdir);if(errCode){throw new FS.ErrnoError(errCode)}errCode=new_node?FS.mayDelete(new_dir,new_name,isdir):FS.mayCreate(new_dir,new_name);if(errCode){throw new FS.ErrnoError(errCode)}if(!old_dir.node_ops.rename){throw new FS.ErrnoError(63)}if(FS.isMountpoint(old_node)||new_node&&FS.isMountpoint(new_node)){throw new FS.ErrnoError(10)}if(new_dir!==old_dir){errCode=FS.nodePermissions(old_dir,"w");if(errCode){throw new FS.ErrnoError(errCode)}}FS.hashRemoveNode(old_node);try{old_dir.node_ops.rename(old_node,new_dir,new_name);old_node.parent=new_dir}catch(e){throw e}finally{FS.hashAddNode(old_node)}},rmdir(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,true);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.rmdir){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.rmdir(parent,name);FS.destroyNode(node)},readdir(path){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var readdir=FS.checkOpExists(node.node_ops.readdir,54);return readdir(node)},unlink(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,false);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.unlink){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.unlink(parent,name);FS.destroyNode(node)},readlink(path){var lookup=FS.lookupPath(path);var link=lookup.node;if(!link){throw new FS.ErrnoError(44)}if(!link.node_ops.readlink){throw new FS.ErrnoError(28)}return link.node_ops.readlink(link)},stat(path,dontFollow){var lookup=FS.lookupPath(path,{follow:!dontFollow});var node=lookup.node;var getattr=FS.checkOpExists(node.node_ops.getattr,63);return getattr(node)},fstat(fd){var stream=FS.getStreamChecked(fd);var node=stream.node;var getattr=stream.stream_ops.getattr;var arg=getattr?stream:node;getattr??=node.node_ops.getattr;FS.checkOpExists(getattr,63);return getattr(arg)},lstat(path){return FS.stat(path,true)},doChmod(stream,node,mode,dontFollow){FS.doSetAttr(stream,node,{mode:mode&4095|node.mode&~4095,ctime:Date.now(),dontFollow})},chmod(path,mode,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChmod(null,node,mode,dontFollow)},lchmod(path,mode){FS.chmod(path,mode,true)},fchmod(fd,mode){var stream=FS.getStreamChecked(fd);FS.doChmod(stream,stream.node,mode,false)},doChown(stream,node,dontFollow){FS.doSetAttr(stream,node,{timestamp:Date.now(),dontFollow})},chown(path,uid,gid,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChown(null,node,dontFollow)},lchown(path,uid,gid){FS.chown(path,uid,gid,true)},fchown(fd,uid,gid){var stream=FS.getStreamChecked(fd);FS.doChown(stream,stream.node,false)},doTruncate(stream,node,len){if(FS.isDir(node.mode)){throw new FS.ErrnoError(31)}if(!FS.isFile(node.mode)){throw new FS.ErrnoError(28)}var errCode=FS.nodePermissions(node,"w");if(errCode){throw new FS.ErrnoError(errCode)}FS.doSetAttr(stream,node,{size:len,timestamp:Date.now()})},truncate(path,len){if(len<0){throw new FS.ErrnoError(28)}var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:true});node=lookup.node}else{node=path}FS.doTruncate(null,node,len)},ftruncate(fd,len){var stream=FS.getStreamChecked(fd);if(len<0||(stream.flags&2097155)===0){throw new FS.ErrnoError(28)}FS.doTruncate(stream,stream.node,len)},utime(path,atime,mtime){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var setattr=FS.checkOpExists(node.node_ops.setattr,63);setattr(node,{atime,mtime})},open(path,flags,mode=438){if(path===""){throw new FS.ErrnoError(44)}flags=typeof flags=="string"?FS_modeStringToFlags(flags):flags;if(flags&64){mode=mode&4095|32768}else{mode=0}var node;var isDirPath;if(typeof path=="object"){node=path}else{isDirPath=path.endsWith("/");var lookup=FS.lookupPath(path,{follow:!(flags&131072),noent_okay:true});node=lookup.node;path=lookup.path}var created=false;if(flags&64){if(node){if(flags&128){throw new FS.ErrnoError(20)}}else if(isDirPath){throw new FS.ErrnoError(31)}else{node=FS.mknod(path,mode|511,0);created=true}}if(!node){throw new FS.ErrnoError(44)}if(FS.isChrdev(node.mode)){flags&=~512}if(flags&65536&&!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}if(!created){var errCode=FS.mayOpen(node,flags);if(errCode){throw new FS.ErrnoError(errCode)}}if(flags&512&&!created){FS.truncate(node,0)}flags&=~(128|512|131072);var stream=FS.createStream({node,path:FS.getPath(node),flags,seekable:true,position:0,stream_ops:node.stream_ops,ungotten:[],error:false});if(stream.stream_ops.open){stream.stream_ops.open(stream)}if(created){FS.chmod(node,mode&511)}if(Module["logReadFiles"]&&!(flags&1)){if(!(path in FS.readFiles)){FS.readFiles[path]=1}}return stream},close(stream){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(stream.getdents)stream.getdents=null;try{if(stream.stream_ops.close){stream.stream_ops.close(stream)}}catch(e){throw e}finally{FS.closeStream(stream.fd)}stream.fd=null},isClosed(stream){return stream.fd===null},llseek(stream,offset,whence){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(!stream.seekable||!stream.stream_ops.llseek){throw new FS.ErrnoError(70)}if(whence!=0&&whence!=1&&whence!=2){throw new FS.ErrnoError(28)}stream.position=stream.stream_ops.llseek(stream,offset,whence);stream.ungotten=[];return stream.position},read(stream,buffer,offset,length,position){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.read){throw new FS.ErrnoError(28)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesRead=stream.stream_ops.read(stream,buffer,offset,length,position);if(!seeking)stream.position+=bytesRead;return bytesRead},write(stream,buffer,offset,length,position,canOwn){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.write){throw new FS.ErrnoError(28)}if(stream.seekable&&stream.flags&1024){FS.llseek(stream,0,2)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesWritten=stream.stream_ops.write(stream,buffer,offset,length,position,canOwn);if(!seeking)stream.position+=bytesWritten;return bytesWritten},mmap(stream,length,position,prot,flags){if((prot&2)!==0&&(flags&2)===0&&(stream.flags&2097155)!==2){throw new FS.ErrnoError(2)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(2)}if(!stream.stream_ops.mmap){throw new FS.ErrnoError(43)}if(!length){throw new FS.ErrnoError(28)}return stream.stream_ops.mmap(stream,length,position,prot,flags)},msync(stream,buffer,offset,length,mmapFlags){if(!stream.stream_ops.msync){return 0}return stream.stream_ops.msync(stream,buffer,offset,length,mmapFlags)},ioctl(stream,cmd,arg){if(!stream.stream_ops.ioctl){throw new FS.ErrnoError(59)}return stream.stream_ops.ioctl(stream,cmd,arg)},readFile(path,opts={}){opts.flags=opts.flags||0;opts.encoding=opts.encoding||"binary";if(opts.encoding!=="utf8"&&opts.encoding!=="binary"){throw new Error(`Invalid encoding type "${opts.encoding}"`)}var ret;var stream=FS.open(path,opts.flags);var stat=FS.stat(path);var length=stat.size;var buf=new Uint8Array(length);FS.read(stream,buf,0,length,0);if(opts.encoding==="utf8"){ret=UTF8ArrayToString(buf)}else if(opts.encoding==="binary"){ret=buf}FS.close(stream);return ret},writeFile(path,data,opts={}){opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);if(typeof data=="string"){var buf=new Uint8Array(lengthBytesUTF8(data)+1);var actualNumBytes=stringToUTF8Array(data,buf,0,buf.length);FS.write(stream,buf,0,actualNumBytes,undefined,opts.canOwn)}else if(ArrayBuffer.isView(data)){FS.write(stream,data,0,data.byteLength,undefined,opts.canOwn)}else{throw new Error("Unsupported data type")}FS.close(stream)},cwd:()=>FS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices(){FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length,llseek:()=>0});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomFill(randomBuffer);randomLeft=randomBuffer.byteLength}return randomBuffer[--randomLeft]};FS.createDevice("/dev","random",randomByte);FS.createDevice("/dev","urandom",randomByte);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories(){FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount(){var node=FS.createNode(proc_self,"fd",16895,73);node.stream_ops={llseek:MEMFS.stream_ops.llseek};node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path},id:fd+1};ret.parent=ret;return ret},readdir(){return Array.from(FS.streams.entries()).filter(([k,v])=>v).map(([k,v])=>k.toString())}};return node}},{},"/proc/self/fd")},createStandardStreams(input,output,error){if(input){FS.createDevice("/dev","stdin",input)}else{FS.symlink("/dev/tty","/dev/stdin")}if(output){FS.createDevice("/dev","stdout",null,output)}else{FS.symlink("/dev/tty","/dev/stdout")}if(error){FS.createDevice("/dev","stderr",null,error)}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1)},staticInit(){FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS}},init(input,output,error){FS.initialized=true;input??=Module["stdin"];output??=Module["stdout"];error??=Module["stderr"];FS.createStandardStreams(input,output,error)},quit(){FS.initialized=false;for(var stream of FS.streams){if(stream){FS.close(stream)}}},findObject(path,dontResolveLastLink){var ret=FS.analyzePath(path,dontResolveLastLink);if(!ret.exists){return null}return ret.object},analyzePath(path,dontResolveLastLink){try{var lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});path=lookup.path}catch(e){}var ret={isRoot:false,exists:false,error:0,name:null,path:null,object:null,parentExists:false,parentPath:null,parentObject:null};try{var lookup=FS.lookupPath(path,{parent:true});ret.parentExists=true;ret.parentPath=lookup.path;ret.parentObject=lookup.node;ret.name=PATH.basename(path);lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});ret.exists=true;ret.path=lookup.path;ret.object=lookup.node;ret.name=lookup.node.name;ret.isRoot=lookup.path==="/"}catch(e){ret.error=e.errno}return ret},createPath(parent,path,canRead,canWrite){parent=typeof parent=="string"?parent:FS.getPath(parent);var parts=path.split("/").reverse();while(parts.length){var part=parts.pop();if(!part)continue;var current=PATH.join2(parent,part);try{FS.mkdir(current)}catch(e){if(e.errno!=20)throw e}parent=current}return current},createFile(parent,name,properties,canRead,canWrite){var path=PATH.join2(typeof parent=="string"?parent:FS.getPath(parent),name);var mode=FS_getMode(canRead,canWrite);return FS.create(path,mode)},createDataFile(parent,name,data,canRead,canWrite,canOwn){var path=name;if(parent){parent=typeof parent=="string"?parent:FS.getPath(parent);path=name?PATH.join2(parent,name):parent}var mode=FS_getMode(canRead,canWrite);var node=FS.create(path,mode);if(data){if(typeof data=="string"){var arr=new Array(data.length);for(var i=0,len=data.length;ithis.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)throw new Error("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)throw new Error("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")throw new Error("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(typeof XMLHttpRequest!="undefined"){if(!ENVIRONMENT_IS_WORKER)throw"Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc";var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};var keys=Object.keys(node.stream_ops);keys.forEach(key=>{var fn=node.stream_ops[key];stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}});function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr,allocated:true}};node.stream_ops=stream_ops;return node}};var UTF8ToString=(ptr,maxBytesToRead)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):"";var SYSCALLS={DEFAULT_POLLMASK:5,calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return dir+"/"+path},writeStat(buf,stat){HEAP32[buf>>2]=stat.dev;HEAP32[buf+4>>2]=stat.mode;HEAPU32[buf+8>>2]=stat.nlink;HEAP32[buf+12>>2]=stat.uid;HEAP32[buf+16>>2]=stat.gid;HEAP32[buf+20>>2]=stat.rdev;HEAP64[buf+24>>3]=BigInt(stat.size);HEAP32[buf+32>>2]=4096;HEAP32[buf+36>>2]=stat.blocks;var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();HEAP64[buf+40>>3]=BigInt(Math.floor(atime/1e3));HEAPU32[buf+48>>2]=atime%1e3*1e3*1e3;HEAP64[buf+56>>3]=BigInt(Math.floor(mtime/1e3));HEAPU32[buf+64>>2]=mtime%1e3*1e3*1e3;HEAP64[buf+72>>3]=BigInt(Math.floor(ctime/1e3));HEAPU32[buf+80>>2]=ctime%1e3*1e3*1e3;HEAP64[buf+88>>3]=BigInt(stat.ino);return 0},writeStatFs(buf,stats){HEAP32[buf+4>>2]=stats.bsize;HEAP32[buf+40>>2]=stats.bsize;HEAP32[buf+8>>2]=stats.blocks;HEAP32[buf+12>>2]=stats.bfree;HEAP32[buf+16>>2]=stats.bavail;HEAP32[buf+20>>2]=stats.files;HEAP32[buf+24>>2]=stats.ffree;HEAP32[buf+28>>2]=stats.fsid;HEAP32[buf+44>>2]=stats.flags;HEAP32[buf+36>>2]=stats.namelen},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();stream.flags|=arg;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;HEAP16[arg+offset>>1]=2;return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_ioctl(fd,op,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(op){case 21509:{if(!stream.tty)return-59;return 0}case 21505:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcgets){var termios=stream.tty.ops.ioctl_tcgets(stream);var argp=syscallGetVarargP();HEAP32[argp>>2]=termios.c_iflag||0;HEAP32[argp+4>>2]=termios.c_oflag||0;HEAP32[argp+8>>2]=termios.c_cflag||0;HEAP32[argp+12>>2]=termios.c_lflag||0;for(var i=0;i<32;i++){HEAP8[argp+i+17]=termios.c_cc[i]||0}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=HEAP32[argp>>2];var c_oflag=HEAP32[argp+4>>2];var c_cflag=HEAP32[argp+8>>2];var c_lflag=HEAP32[argp+12>>2];var c_cc=[];for(var i=0;i<32;i++){c_cc.push(HEAP8[argp+i+17])}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag,c_oflag,c_cflag,c_lflag,c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();HEAP32[argp>>2]=0;return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();HEAP16[argp>>1]=winsize[0];HEAP16[argp+2>>1]=winsize[1]}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_mkdirat(dirfd,path,mode){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);FS.mkdir(path,mode,0);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_renameat(olddirfd,oldpath,newdirfd,newpath){try{oldpath=SYSCALLS.getStr(oldpath);newpath=SYSCALLS.getStr(newpath);oldpath=SYSCALLS.calculateAt(olddirfd,oldpath);newpath=SYSCALLS.calculateAt(newdirfd,newpath);FS.rename(oldpath,newpath);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_rmdir(path){try{path=SYSCALLS.getStr(path);FS.rmdir(path);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_unlinkat(dirfd,path,flags){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);if(flags===0){FS.unlink(path)}else if(flags===512){FS.rmdir(path)}else{abort("Invalid flags passed to unlinkat")}return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __emscripten_system=command=>{if(ENVIRONMENT_IS_NODE){if(!command)return 1;var cmdstr=UTF8ToString(command);if(!cmdstr.length)return 0;var cp=require("child_process");var ret=cp.spawnSync(cmdstr,[],{shell:true,stdio:"inherit"});var _W_EXITCODE=(ret,sig)=>ret<<8|sig;if(ret.status===null){var signalToNumber=sig=>{switch(sig){case"SIGHUP":return 1;case"SIGQUIT":return 3;case"SIGFPE":return 8;case"SIGKILL":return 9;case"SIGALRM":return 14;case"SIGTERM":return 15;default:return 2}};return _W_EXITCODE(0,signalToNumber(ret.signal))}return _W_EXITCODE(ret.status,0)}if(!command)return 0;return-52};var _emscripten_get_now=()=>performance.now();var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>numINT53_MAX?NaN:Number(num);function _fd_seek(fd,offset,whence,newOffset){offset=bigintToI53Checked(offset);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);HEAP64[newOffset>>3]=BigInt(stream.position);if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};var FS_createPath=FS.createPath;var FS_unlink=path=>FS.unlink(path);var FS_createLazyFile=FS.createLazyFile;var FS_createDevice=FS.createDevice;FS.createPreloadedFile=FS_createPreloadedFile;FS.staticInit();Module["FS_createPath"]=FS.createPath;Module["FS_createDataFile"]=FS.createDataFile;Module["FS_createPreloadedFile"]=FS.createPreloadedFile;Module["FS_unlink"]=FS.unlink;Module["FS_createLazyFile"]=FS.createLazyFile;Module["FS_createDevice"]=FS.createDevice;MEMFS.doesNotExistError=new FS.ErrnoError(44);MEMFS.doesNotExistError.stack="";var wasmImports={__syscall_fcntl64:___syscall_fcntl64,__syscall_ioctl:___syscall_ioctl,__syscall_mkdirat:___syscall_mkdirat,__syscall_openat:___syscall_openat,__syscall_renameat:___syscall_renameat,__syscall_rmdir:___syscall_rmdir,__syscall_unlinkat:___syscall_unlinkat,_emscripten_system:__emscripten_system,emscripten_get_now:_emscripten_get_now,emscripten_resize_heap:_emscripten_resize_heap,exit:_exit,fd_close:_fd_close,fd_read:_fd_read,fd_seek:_fd_seek,fd_write:_fd_write};var wasmExports=await createWasm();var ___wasm_call_ctors=wasmExports["__wasm_call_ctors"];var _free=Module["_free"]=wasmExports["free"];var _malloc=Module["_malloc"]=wasmExports["malloc"];var _doomgeneric_Tick=Module["_doomgeneric_Tick"]=wasmExports["doomgeneric_Tick"];var _doomgeneric_Create=Module["_doomgeneric_Create"]=wasmExports["doomgeneric_Create"];var _DG_GetFrameBuffer=Module["_DG_GetFrameBuffer"]=wasmExports["DG_GetFrameBuffer"];var _DG_GetScreenWidth=Module["_DG_GetScreenWidth"]=wasmExports["DG_GetScreenWidth"];var _DG_GetScreenHeight=Module["_DG_GetScreenHeight"]=wasmExports["DG_GetScreenHeight"];var _DG_PushKeyEvent=Module["_DG_PushKeyEvent"]=wasmExports["DG_PushKeyEvent"];var __emscripten_stack_restore=wasmExports["_emscripten_stack_restore"];var __emscripten_stack_alloc=wasmExports["_emscripten_stack_alloc"];var _emscripten_stack_get_current=wasmExports["emscripten_stack_get_current"];Module["addRunDependency"]=addRunDependency;Module["removeRunDependency"]=removeRunDependency;Module["ccall"]=ccall;Module["cwrap"]=cwrap;Module["setValue"]=setValue;Module["getValue"]=getValue;Module["FS_createPreloadedFile"]=FS_createPreloadedFile;Module["FS_unlink"]=FS_unlink;Module["FS_createPath"]=FS_createPath;Module["FS_createDevice"]=FS_createDevice;Module["FS"]=FS;Module["FS_createDataFile"]=FS_createDataFile;Module["FS_createLazyFile"]=FS_createLazyFile;function run(){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run();moduleRtn=readyPromise; + + + return moduleRtn; +} +); +})(); +if (typeof exports === 'object' && typeof module === 'object') { + module.exports = createDoomModule; + // This default export looks redundant, but it allows TS to import this + // commonjs style module. + module.exports.default = createDoomModule; +} else if (typeof define === 'function' && define['amd']) + define([], () => createDoomModule); diff --git a/packages/coding-agent/examples/extensions/doom-overlay/doom/build/doom.wasm b/packages/coding-agent/examples/extensions/doom-overlay/doom/build/doom.wasm new file mode 100755 index 00000000..fb99ae7c Binary files /dev/null and b/packages/coding-agent/examples/extensions/doom-overlay/doom/build/doom.wasm differ diff --git a/packages/coding-agent/examples/extensions/doom-overlay/doom/doomgeneric_pi.c b/packages/coding-agent/examples/extensions/doom-overlay/doom/doomgeneric_pi.c new file mode 100644 index 00000000..bb442f45 --- /dev/null +++ b/packages/coding-agent/examples/extensions/doom-overlay/doom/doomgeneric_pi.c @@ -0,0 +1,72 @@ +/** + * pi-doom platform implementation for doomgeneric + * + * Minimal implementation - no sound, just framebuffer and input. + */ + +#include "doomgeneric.h" +#include "doomkeys.h" +#include +#include + +// Key event queue +#define KEY_QUEUE_SIZE 256 +static struct { + int pressed; + unsigned char key; +} key_queue[KEY_QUEUE_SIZE]; +static int key_queue_read = 0; +static int key_queue_write = 0; + +// Get the framebuffer pointer for JS to read +EMSCRIPTEN_KEEPALIVE +uint32_t *DG_GetFrameBuffer(void) { return DG_ScreenBuffer; } + +// Get framebuffer dimensions +EMSCRIPTEN_KEEPALIVE +int DG_GetScreenWidth(void) { return DOOMGENERIC_RESX; } + +EMSCRIPTEN_KEEPALIVE +int DG_GetScreenHeight(void) { return DOOMGENERIC_RESY; } + +// Push a key event from JavaScript +EMSCRIPTEN_KEEPALIVE +void DG_PushKeyEvent(int pressed, unsigned char key) { + int next_write = (key_queue_write + 1) % KEY_QUEUE_SIZE; + if (next_write != key_queue_read) { + key_queue[key_queue_write].pressed = pressed; + key_queue[key_queue_write].key = key; + key_queue_write = next_write; + } +} + +void DG_Init(void) { + // Nothing to initialize +} + +void DG_DrawFrame(void) { + // Frame is in DG_ScreenBuffer, JS reads via DG_GetFrameBuffer +} + +void DG_SleepMs(uint32_t ms) { + // No-op - JS handles timing + (void)ms; +} + +uint32_t DG_GetTicksMs(void) { + return (uint32_t)emscripten_get_now(); +} + +int DG_GetKey(int *pressed, unsigned char *key) { + if (key_queue_read != key_queue_write) { + *pressed = key_queue[key_queue_read].pressed; + *key = key_queue[key_queue_read].key; + key_queue_read = (key_queue_read + 1) % KEY_QUEUE_SIZE; + return 1; + } + return 0; +} + +void DG_SetWindowTitle(const char *title) { + (void)title; +} diff --git a/packages/coding-agent/examples/extensions/doom-overlay/index.ts b/packages/coding-agent/examples/extensions/doom-overlay/index.ts new file mode 100644 index 00000000..5ef08e6f --- /dev/null +++ b/packages/coding-agent/examples/extensions/doom-overlay/index.ts @@ -0,0 +1,74 @@ +/** + * DOOM Overlay Demo - Play DOOM as an overlay + * + * Usage: pi --extension ./examples/extensions/doom-overlay + * + * Commands: + * /doom-overlay - Play DOOM in an overlay (Q to pause/exit) + * + * This demonstrates that overlays can handle real-time game rendering at 35 FPS. + */ + +import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; +import { DoomOverlayComponent } from "./doom-component.js"; +import { DoomEngine } from "./doom-engine.js"; +import { ensureWadFile } from "./wad-finder.js"; + +// Persistent engine instance - survives between invocations +let activeEngine: DoomEngine | null = null; +let activeWadPath: string | null = null; + +export default function (pi: ExtensionAPI) { + pi.registerCommand("doom-overlay", { + description: "Play DOOM as an overlay. Q to pause and exit.", + + handler: async (args, ctx) => { + if (!ctx.hasUI) { + ctx.ui.notify("DOOM requires interactive mode", "error"); + return; + } + + // Auto-download WAD if not present + ctx.ui.notify("Loading DOOM...", "info"); + const wad = args?.trim() ? args.trim() : await ensureWadFile(); + + if (!wad) { + ctx.ui.notify("Failed to download DOOM WAD file. Check your internet connection.", "error"); + return; + } + + try { + // Reuse existing engine if same WAD, otherwise create new + let isResume = false; + if (activeEngine && activeWadPath === wad) { + ctx.ui.notify("Resuming DOOM...", "info"); + isResume = true; + } else { + ctx.ui.notify(`Loading DOOM from ${wad}...`, "info"); + activeEngine = new DoomEngine(wad); + await activeEngine.init(); + activeWadPath = wad; + } + + await ctx.ui.custom( + (tui, _theme, _keybindings, done) => { + return new DoomOverlayComponent(tui, activeEngine!, () => done(undefined), isResume); + }, + { + overlay: true, + overlayOptions: { + width: "75%", + maxHeight: "95%", + anchor: "center", + margin: { top: 1 }, + }, + }, + ); + } catch (error) { + ctx.ui.notify(`Failed to load DOOM: ${error}`, "error"); + activeEngine = null; + activeWadPath = null; + } + }, + }); +} diff --git a/packages/coding-agent/examples/extensions/doom-overlay/wad-finder.ts b/packages/coding-agent/examples/extensions/doom-overlay/wad-finder.ts new file mode 100644 index 00000000..002758d5 --- /dev/null +++ b/packages/coding-agent/examples/extensions/doom-overlay/wad-finder.ts @@ -0,0 +1,51 @@ +import { existsSync, writeFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +// Get the bundled WAD path (relative to this module) +const __dirname = dirname(fileURLToPath(import.meta.url)); +const BUNDLED_WAD = join(__dirname, "doom1.wad"); +const WAD_URL = "https://distro.ibiblio.org/slitaz/sources/packages/d/doom1.wad"; + +const DEFAULT_WAD_PATHS = ["./doom1.wad", "./DOOM1.WAD", "~/doom1.wad", "~/.doom/doom1.wad"]; + +export function findWadFile(customPath?: string): string | null { + if (customPath) { + const resolved = resolve(customPath.replace(/^~/, process.env.HOME || "")); + if (existsSync(resolved)) return resolved; + return null; + } + + // Check bundled WAD first + if (existsSync(BUNDLED_WAD)) { + return BUNDLED_WAD; + } + + // Fall back to default paths + for (const p of DEFAULT_WAD_PATHS) { + const resolved = resolve(p.replace(/^~/, process.env.HOME || "")); + if (existsSync(resolved)) return resolved; + } + + return null; +} + +/** Download the shareware WAD if not present. Returns path or null on failure. */ +export async function ensureWadFile(): Promise { + // Check if already exists + const existing = findWadFile(); + if (existing) return existing; + + // Download to bundled location + try { + const response = await fetch(WAD_URL); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + const buffer = await response.arrayBuffer(); + writeFileSync(BUNDLED_WAD, Buffer.from(buffer)); + return BUNDLED_WAD; + } catch { + return null; + } +} diff --git a/packages/coding-agent/examples/extensions/overlay-qa-tests.ts b/packages/coding-agent/examples/extensions/overlay-qa-tests.ts index 2771926a..cef44dc8 100644 --- a/packages/coding-agent/examples/extensions/overlay-qa-tests.ts +++ b/packages/coding-agent/examples/extensions/overlay-qa-tests.ts @@ -4,6 +4,7 @@ * Usage: pi --extension ./examples/extensions/overlay-qa-tests.ts * * Commands: + * /overlay-animation - Real-time animation demo (~30 FPS, proves DOOM-like rendering works) * /overlay-anchors - Cycle through all 9 anchor positions * /overlay-margins - Test margin and offset options * /overlay-stack - Test stacked overlays @@ -11,14 +12,30 @@ * /overlay-edge - Test overlay positioned at terminal edge * /overlay-percent - Test percentage-based positioning * /overlay-maxheight - Test maxHeight truncation + * /overlay-sidepanel - Responsive sidepanel (hides when terminal < 100 cols) + * /overlay-toggle - Toggle visibility demo (demonstrates OverlayHandle.setHidden) */ import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@mariozechner/pi-coding-agent"; -import type { OverlayAnchor, OverlayOptions, TUI } from "@mariozechner/pi-tui"; +import type { OverlayAnchor, OverlayHandle, OverlayOptions, TUI } from "@mariozechner/pi-tui"; import { matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui"; import { spawn } from "child_process"; +// Global handle for toggle demo (in real code, use a more elegant pattern) +let globalToggleHandle: OverlayHandle | null = null; + export default function (pi: ExtensionAPI) { + // Animation demo - proves overlays can handle real-time updates (like pi-doom would need) + pi.registerCommand("overlay-animation", { + description: "Test real-time animation in overlay (~30 FPS)", + handler: async (_args: string, ctx: ExtensionCommandContext) => { + await ctx.ui.custom((tui, theme, _kb, done) => new AnimationDemoComponent(tui, theme, done), { + overlay: true, + overlayOptions: { anchor: "center", width: 50, maxHeight: 20 }, + }); + }, + }); + // Test all 9 anchor positions pi.registerCommand("overlay-anchors", { description: "Cycle through all anchor positions", @@ -178,8 +195,8 @@ export default function (pi: ExtensionAPI) { overlay: true, overlayOptions: { width: 30, - rowPercent: config.row, - colPercent: config.col, + row: `${config.row}%`, + col: `${config.col}%`, }, }, ); @@ -203,6 +220,42 @@ export default function (pi: ExtensionAPI) { }); }, }); + + // Test responsive sidepanel - only shows when terminal is wide enough + pi.registerCommand("overlay-sidepanel", { + description: "Test responsive sidepanel (hides when terminal < 100 cols)", + handler: async (_args: string, ctx: ExtensionCommandContext) => { + await ctx.ui.custom((tui, theme, _kb, done) => new SidepanelComponent(tui, theme, done), { + overlay: true, + overlayOptions: { + anchor: "right-center", + width: "25%", + minWidth: 30, + margin: { right: 1 }, + // Only show when terminal is wide enough + visible: (termWidth) => termWidth >= 100, + }, + }); + }, + }); + + // Test toggle overlay - demonstrates OverlayHandle.setHidden() via onHandle callback + pi.registerCommand("overlay-toggle", { + description: "Test overlay toggle (press 't' to toggle visibility)", + handler: async (_args: string, ctx: ExtensionCommandContext) => { + await ctx.ui.custom((tui, theme, _kb, done) => new ToggleDemoComponent(tui, theme, done), { + overlay: true, + overlayOptions: { anchor: "center", width: 50 }, + // onHandle callback provides access to the OverlayHandle for visibility control + onHandle: (handle) => { + // Store handle globally so component can access it + // (In real code, you'd use a more elegant pattern like a store or event emitter) + globalToggleHandle = handle; + }, + }); + globalToggleHandle = null; + }, + }); } function sleep(ms: number): Promise { @@ -215,7 +268,7 @@ abstract class BaseOverlay { protected box(lines: string[], width: number, title?: string): string[] { const th = this.theme; - const innerW = width - 2; + const innerW = Math.max(1, width - 2); const result: string[] = []; const titleStr = title ? truncateToWidth(` ${title} `, innerW) : ""; @@ -331,7 +384,7 @@ class StackOverlayComponent extends BaseOverlay { // Use different colors for each overlay to show stacking const colors = ["error", "success", "accent"] as const; const color = colors[(this.num - 1) % colors.length]!; - const innerW = width - 2; + const innerW = Math.max(1, width - 2); const border = (char: string) => th.fg(color, char); const padLine = (s: string) => truncateToWidth(s, innerW, "...", true); const lines: string[] = []; @@ -443,7 +496,7 @@ class StreamingOverflowComponent extends BaseOverlay { render(width: number): string[] { const th = this.theme; - const innerW = width - 2; + const innerW = Math.max(1, width - 2); const padLine = (s: string) => truncateToWidth(s, innerW, "...", true); const border = (c: string) => th.fg("border", c); @@ -589,3 +642,240 @@ class MaxHeightTestComponent extends BaseOverlay { return this.box(contentLines, width, "MaxHeight Test"); } } + +// Responsive sidepanel - demonstrates percentage width and visibility callback +class SidepanelComponent extends BaseOverlay { + private items = ["Dashboard", "Messages", "Settings", "Help", "About"]; + private selectedIndex = 0; + + constructor( + private tui: TUI, + theme: Theme, + private done: () => void, + ) { + super(theme); + } + + handleInput(data: string): void { + if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { + this.done(); + } else if (matchesKey(data, "up")) { + this.selectedIndex = Math.max(0, this.selectedIndex - 1); + this.tui.requestRender(); + } else if (matchesKey(data, "down")) { + this.selectedIndex = Math.min(this.items.length - 1, this.selectedIndex + 1); + this.tui.requestRender(); + } else if (matchesKey(data, "return")) { + // Could trigger an action here + this.tui.requestRender(); + } + } + + render(width: number): string[] { + const th = this.theme; + const innerW = Math.max(1, width - 2); + const padLine = (s: string) => truncateToWidth(s, innerW, "...", true); + const border = (c: string) => th.fg("border", c); + const lines: string[] = []; + + // Header + lines.push(border(`╭${"─".repeat(innerW)}╮`)); + lines.push(border("│") + padLine(th.fg("accent", " Responsive Sidepanel")) + border("│")); + lines.push(border("├") + border("─".repeat(innerW)) + border("┤")); + + // Menu items + for (let i = 0; i < this.items.length; i++) { + const item = this.items[i]!; + const isSelected = i === this.selectedIndex; + const prefix = isSelected ? th.fg("accent", "→ ") : " "; + const text = isSelected ? th.fg("accent", item) : item; + lines.push(border("│") + padLine(`${prefix}${text}`) + border("│")); + } + + // Footer with responsive behavior info + lines.push(border("├") + border("─".repeat(innerW)) + border("┤")); + lines.push(border("│") + padLine(th.fg("warning", " ⚠ Resize terminal < 100 cols")) + border("│")); + lines.push(border("│") + padLine(th.fg("warning", " to see panel auto-hide")) + border("│")); + lines.push(border("│") + padLine(th.fg("dim", " Uses visible: (w) => w >= 100")) + border("│")); + lines.push(border("│") + padLine(th.fg("dim", " ↑↓ navigate | Esc close")) + border("│")); + lines.push(border(`╰${"─".repeat(innerW)}╯`)); + + return lines; + } +} + +// Animation demo - proves overlays can handle real-time updates like pi-doom +class AnimationDemoComponent extends BaseOverlay { + private frame = 0; + private interval: ReturnType | null = null; + private fps = 0; + private lastFpsUpdate = Date.now(); + private framesSinceLastFps = 0; + + constructor( + private tui: TUI, + theme: Theme, + private done: () => void, + ) { + super(theme); + this.startAnimation(); + } + + private startAnimation(): void { + // Run at ~30 FPS (same as DOOM target) + this.interval = setInterval(() => { + this.frame++; + this.framesSinceLastFps++; + + // Update FPS counter every second + const now = Date.now(); + if (now - this.lastFpsUpdate >= 1000) { + this.fps = this.framesSinceLastFps; + this.framesSinceLastFps = 0; + this.lastFpsUpdate = now; + } + + this.tui.requestRender(); + }, 1000 / 30); + } + + handleInput(data: string): void { + if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { + this.dispose(); + this.done(); + } + } + + render(width: number): string[] { + const th = this.theme; + const innerW = Math.max(1, width - 2); + const padLine = (s: string) => truncateToWidth(s, innerW, "...", true); + const border = (c: string) => th.fg("border", c); + + const lines: string[] = []; + lines.push(border(`╭${"─".repeat(innerW)}╮`)); + lines.push(border("│") + padLine(th.fg("accent", " Animation Demo (~30 FPS)")) + border("│")); + lines.push(border("│") + padLine(``) + border("│")); + lines.push(border("│") + padLine(` Frame: ${th.fg("accent", String(this.frame))}`) + border("│")); + lines.push(border("│") + padLine(` FPS: ${th.fg("success", String(this.fps))}`) + border("│")); + lines.push(border("│") + padLine(``) + border("│")); + + // Animated content - bouncing bar + const barWidth = Math.max(12, innerW - 4); // Ensure enough space for bar + const pos = Math.max(0, Math.floor(((Math.sin(this.frame / 10) + 1) * (barWidth - 10)) / 2)); + const bar = " ".repeat(pos) + th.fg("accent", "██████████") + " ".repeat(Math.max(0, barWidth - 10 - pos)); + lines.push(border("│") + padLine(` ${bar}`) + border("│")); + + // Spinning character + const spinChars = ["◐", "◓", "◑", "◒"]; + const spin = spinChars[this.frame % spinChars.length]; + lines.push(border("│") + padLine(` Spinner: ${th.fg("warning", spin!)}`) + border("│")); + + // Color cycling + const hue = (this.frame * 3) % 360; + const rgb = hslToRgb(hue / 360, 0.8, 0.5); + const colorBlock = `\x1b[48;2;${rgb[0]};${rgb[1]};${rgb[2]}m${" ".repeat(10)}\x1b[0m`; + lines.push(border("│") + padLine(` Color: ${colorBlock}`) + border("│")); + + lines.push(border("│") + padLine(``) + border("│")); + lines.push(border("│") + padLine(th.fg("dim", " This proves overlays can handle")) + border("│")); + lines.push(border("│") + padLine(th.fg("dim", " real-time game-like rendering.")) + border("│")); + lines.push(border("│") + padLine(th.fg("dim", " (pi-doom uses same approach)")) + border("│")); + lines.push(border("│") + padLine(``) + border("│")); + lines.push(border("│") + padLine(th.fg("dim", " Press Esc to close")) + border("│")); + lines.push(border(`╰${"─".repeat(innerW)}╯`)); + + return lines; + } + + dispose(): void { + if (this.interval) { + clearInterval(this.interval); + this.interval = null; + } + } +} + +// HSL to RGB helper for color cycling animation +function hslToRgb(h: number, s: number, l: number): [number, number, number] { + let r: number, g: number, b: number; + if (s === 0) { + r = g = b = l; + } else { + const hue2rgb = (p: number, q: number, t: number) => { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + }; + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); + } + return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; +} + +// Toggle demo - demonstrates OverlayHandle.setHidden() via onHandle callback +class ToggleDemoComponent extends BaseOverlay { + private toggleCount = 0; + private isToggling = false; + + constructor( + private tui: TUI, + theme: Theme, + private done: () => void, + ) { + super(theme); + } + + handleInput(data: string): void { + if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { + this.done(); + } else if (matchesKey(data, "t") && globalToggleHandle && !this.isToggling) { + // Demonstrate toggle by hiding for 1 second then showing again + // (In real usage, a global keybinding would control visibility) + this.isToggling = true; + this.toggleCount++; + globalToggleHandle.setHidden(true); + + // Auto-restore after 1 second to demonstrate the API + setTimeout(() => { + if (globalToggleHandle) { + globalToggleHandle.setHidden(false); + this.isToggling = false; + this.tui.requestRender(); + } + }, 1000); + } + } + + render(width: number): string[] { + const th = this.theme; + return this.box( + [ + "", + th.fg("accent", " Toggle Demo"), + "", + " This overlay demonstrates the", + " onHandle callback API.", + "", + ` Toggle count: ${th.fg("accent", String(this.toggleCount))}`, + "", + th.fg("dim", " Press 't' to hide for 1 second"), + th.fg("dim", " (demonstrates setHidden API)"), + "", + th.fg("dim", " In real usage, a global keybinding"), + th.fg("dim", " would toggle visibility externally."), + "", + th.fg("dim", " Press Esc to close"), + "", + ], + width, + "Toggle Demo", + ); + } +} diff --git a/packages/coding-agent/src/core/extensions/types.ts b/packages/coding-agent/src/core/extensions/types.ts index 517544ce..38debe33 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -15,7 +15,15 @@ import type { ThinkingLevel, } from "@mariozechner/pi-agent-core"; import type { ImageContent, Model, TextContent, ToolResultMessage } from "@mariozechner/pi-ai"; -import type { Component, EditorComponent, EditorTheme, KeyId, OverlayOptions, TUI } from "@mariozechner/pi-tui"; +import type { + Component, + EditorComponent, + EditorTheme, + KeyId, + OverlayHandle, + OverlayOptions, + TUI, +} from "@mariozechner/pi-tui"; import type { Static, TSchema } from "@sinclair/typebox"; import type { Theme } from "../../modes/interactive/theme/theme.js"; import type { BashResult } from "../bash-executor.js"; @@ -116,6 +124,8 @@ export interface ExtensionUIContext { overlay?: boolean; /** Overlay positioning/sizing options. Can be static or a function for dynamic updates. */ overlayOptions?: OverlayOptions | (() => OverlayOptions); + /** Called with the overlay handle after the overlay is shown. Use to control visibility. */ + onHandle?: (handle: OverlayHandle) => void; }, ): Promise; diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 11f5740d..d49576fb 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -21,6 +21,7 @@ import type { EditorComponent, EditorTheme, KeyId, + OverlayHandle, OverlayOptions, SlashCommand, } from "@mariozechner/pi-tui"; @@ -1260,7 +1261,11 @@ export class InteractiveMode { keybindings: KeybindingsManager, done: (result: T) => void, ) => (Component & { dispose?(): void }) | Promise, - options?: { overlay?: boolean; overlayOptions?: OverlayOptions | (() => OverlayOptions) }, + options?: { + overlay?: boolean; + overlayOptions?: OverlayOptions | (() => OverlayOptions); + onHandle?: (handle: OverlayHandle) => void; + }, ): Promise { const savedText = this.editor.getText(); const isOverlay = options?.overlay ?? false; @@ -1309,7 +1314,9 @@ export class InteractiveMode { const w = (component as { width?: number }).width; return w ? { width: w } : undefined; }; - this.ui.showOverlay(component, resolveOptions()); + const handle = this.ui.showOverlay(component, resolveOptions()); + // Expose handle to caller for visibility control + options?.onHandle?.(handle); } else { this.editorContainer.clear(); this.editorContainer.addChild(component); diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 44173e63..609eb1a4 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -6,8 +6,10 @@ - `SettingsListOptions` with `enableSearch` for fuzzy filtering in `SettingsList` ([#643](https://github.com/badlogic/pi-mono/pull/643) by [@ninlds](https://github.com/ninlds)) - `pageUp` and `pageDown` key support with `selectPageUp`/`selectPageDown` editor actions ([#662](https://github.com/badlogic/pi-mono/pull/662) by [@aliou](https://github.com/aliou)) -- `OverlayOptions` API for overlay positioning and sizing: `width`, `widthPercent`, `minWidth`, `maxHeight`, `maxHeightPercent`, `anchor`, `offsetX`, `offsetY`, `rowPercent`, `colPercent`, `row`, `col`, `margin` -- New exported types: `OverlayAnchor`, `OverlayMargin`, `OverlayOptions` +- `OverlayOptions` API for overlay positioning and sizing with CSS-like values: `width`, `maxHeight`, `row`, `col` accept numbers (absolute) or percentage strings (e.g., `"50%"`). Also supports `minWidth`, `anchor`, `offsetX`, `offsetY`, `margin`. +- `OverlayOptions.visible` callback for responsive overlays - receives terminal dimensions, return false to hide +- `showOverlay()` now returns `OverlayHandle` with `hide()`, `setHidden(boolean)`, `isHidden()` for programmatic visibility control +- New exported types: `OverlayAnchor`, `OverlayHandle`, `OverlayMargin`, `OverlayOptions`, `SizeValue` - `truncateToWidth()` now accepts optional `pad` parameter to pad result with spaces to exactly `maxWidth` ### Fixed diff --git a/packages/tui/src/index.ts b/packages/tui/src/index.ts index ba158353..4ee8de5e 100644 --- a/packages/tui/src/index.ts +++ b/packages/tui/src/index.ts @@ -76,8 +76,10 @@ export { type Component, Container, type OverlayAnchor, + type OverlayHandle, type OverlayMargin, type OverlayOptions, + type SizeValue, TUI, } from "./tui.js"; // Utilities diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts index c9e42f4f..84d0f8f4 100644 --- a/packages/tui/src/tui.ts +++ b/packages/tui/src/tui.ts @@ -65,23 +65,33 @@ export interface OverlayMargin { left?: number; } +/** Value that can be absolute (number) or percentage (string like "50%") */ +export type SizeValue = number | `${number}%`; + +/** Parse a SizeValue into absolute value given a reference size */ +function parseSizeValue(value: SizeValue | undefined, referenceSize: number): number | undefined { + if (value === undefined) return undefined; + if (typeof value === "number") return value; + // Parse percentage string like "50%" + const match = value.match(/^(\d+(?:\.\d+)?)%$/); + if (match) { + return Math.floor((referenceSize * parseFloat(match[1])) / 100); + } + return undefined; +} + /** - * Options for overlay positioning and sizing + * Options for overlay positioning and sizing. + * Values can be absolute numbers or percentage strings (e.g., "50%"). */ export interface OverlayOptions { - // === Sizing (absolute) === - /** Fixed width in columns */ - width?: number; + // === Sizing === + /** Width in columns, or percentage of terminal width (e.g., "50%") */ + width?: SizeValue; /** Minimum width in columns */ minWidth?: number; - /** Maximum height in rows */ - maxHeight?: number; - - // === Sizing (relative to terminal) === - /** Width as percentage of terminal width (0-100) */ - widthPercent?: number; - /** Maximum height as percentage of terminal height (0-100) */ - maxHeightPercent?: number; + /** Maximum height in rows, or percentage of terminal height (e.g., "50%") */ + maxHeight?: SizeValue; // === Positioning - anchor-based === /** Anchor point for positioning (default: 'center') */ @@ -91,21 +101,35 @@ export interface OverlayOptions { /** Vertical offset from anchor position (positive = down) */ offsetY?: number; - // === Positioning - percentage-based (alternative to anchor) === - /** Vertical position as percentage (0 = top, 100 = bottom) */ - rowPercent?: number; - /** Horizontal position as percentage (0 = left, 100 = right) */ - colPercent?: number; - - // === Positioning - absolute (low-level) === - /** Absolute row position (overrides anchor/percent) */ - row?: number; - /** Absolute column position (overrides anchor/percent) */ - col?: number; + // === Positioning - percentage or absolute === + /** Row position: absolute number, or percentage (e.g., "25%" = 25% from top) */ + row?: SizeValue; + /** Column position: absolute number, or percentage (e.g., "50%" = centered horizontally) */ + col?: SizeValue; // === Margin from terminal edges === /** Margin from terminal edges. Number applies to all sides. */ margin?: OverlayMargin | number; + + // === Visibility === + /** + * Control overlay visibility based on terminal dimensions. + * If provided, overlay is only rendered when this returns true. + * Called each render cycle with current terminal dimensions. + */ + visible?: (termWidth: number, termHeight: number) => boolean; +} + +/** + * Handle returned by showOverlay for controlling the overlay + */ +export interface OverlayHandle { + /** Permanently remove the overlay (cannot be shown again) */ + hide(): void; + /** Temporarily hide or show the overlay */ + setHidden(hidden: boolean): void; + /** Check if overlay is temporarily hidden */ + isHidden(): boolean; } /** @@ -165,6 +189,7 @@ export class TUI extends Container { component: Component; options?: OverlayOptions; preFocus: Component | null; + hidden: boolean; }[] = []; constructor(terminal: Terminal) { @@ -176,25 +201,90 @@ export class TUI extends Container { this.focusedComponent = component; } - /** Show an overlay component with configurable positioning and sizing. */ - showOverlay(component: Component, options?: OverlayOptions): void { - this.overlayStack.push({ component, options, preFocus: this.focusedComponent }); - this.setFocus(component); + /** + * Show an overlay component with configurable positioning and sizing. + * Returns a handle to control the overlay's visibility. + */ + showOverlay(component: Component, options?: OverlayOptions): OverlayHandle { + const entry = { component, options, preFocus: this.focusedComponent, hidden: false }; + this.overlayStack.push(entry); + // Only focus if overlay is actually visible + if (this.isOverlayVisible(entry)) { + this.setFocus(component); + } this.terminal.hideCursor(); this.requestRender(); + + // Return handle for controlling this overlay + return { + hide: () => { + const index = this.overlayStack.indexOf(entry); + if (index !== -1) { + this.overlayStack.splice(index, 1); + // Restore focus if this overlay had focus + if (this.focusedComponent === component) { + const topVisible = this.getTopmostVisibleOverlay(); + this.setFocus(topVisible?.component ?? entry.preFocus); + } + if (this.overlayStack.length === 0) this.terminal.hideCursor(); + this.requestRender(); + } + }, + setHidden: (hidden: boolean) => { + if (entry.hidden === hidden) return; + entry.hidden = hidden; + // Update focus when hiding/showing + if (hidden) { + // If this overlay had focus, move focus to next visible or preFocus + if (this.focusedComponent === component) { + const topVisible = this.getTopmostVisibleOverlay(); + this.setFocus(topVisible?.component ?? entry.preFocus); + } + } else { + // Restore focus to this overlay when showing (if it's actually visible) + if (this.isOverlayVisible(entry)) { + this.setFocus(component); + } + } + this.requestRender(); + }, + isHidden: () => entry.hidden, + }; } /** Hide the topmost overlay and restore previous focus. */ hideOverlay(): void { const overlay = this.overlayStack.pop(); if (!overlay) return; - this.setFocus(overlay.preFocus); + // Find topmost visible overlay, or fall back to preFocus + const topVisible = this.getTopmostVisibleOverlay(); + this.setFocus(topVisible?.component ?? overlay.preFocus); if (this.overlayStack.length === 0) this.terminal.hideCursor(); this.requestRender(); } + /** Check if there are any visible overlays */ hasOverlay(): boolean { - return this.overlayStack.length > 0; + return this.overlayStack.some((o) => this.isOverlayVisible(o)); + } + + /** Check if an overlay entry is currently visible */ + private isOverlayVisible(entry: (typeof this.overlayStack)[number]): boolean { + if (entry.hidden) return false; + if (entry.options?.visible) { + return entry.options.visible(this.terminal.columns, this.terminal.rows); + } + return true; + } + + /** Find the topmost visible overlay, if any */ + private getTopmostVisibleOverlay(): (typeof this.overlayStack)[number] | undefined { + for (let i = this.overlayStack.length - 1; i >= 0; i--) { + if (this.isOverlayVisible(this.overlayStack[i])) { + return this.overlayStack[i]; + } + } + return undefined; } override invalidate(): void { @@ -269,6 +359,20 @@ export class TUI extends Container { return; } + // If focused component is an overlay, verify it's still visible + // (visibility can change due to terminal resize or visible() callback) + const focusedOverlay = this.overlayStack.find((o) => o.component === this.focusedComponent); + if (focusedOverlay && !this.isOverlayVisible(focusedOverlay)) { + // Focused overlay is no longer visible, redirect to topmost visible overlay + const topVisible = this.getTopmostVisibleOverlay(); + if (topVisible) { + this.setFocus(topVisible.component); + } else { + // No visible overlays, restore to preFocus + this.setFocus(focusedOverlay.preFocus); + } + } + // Pass input to focused component (including Ctrl+C) // The focused component can decide how to handle Ctrl+C if (this.focusedComponent?.handleInput) { @@ -354,14 +458,7 @@ export class TUI extends Container { const availHeight = Math.max(1, termHeight - marginTop - marginBottom); // === Resolve width === - let width: number; - if (opt.width !== undefined) { - width = opt.width; - } else if (opt.widthPercent !== undefined) { - width = Math.floor((termWidth * opt.widthPercent) / 100); - } else { - width = Math.min(80, availWidth); - } + let width = parseSizeValue(opt.width, termWidth) ?? Math.min(80, availWidth); // Apply minWidth if (opt.minWidth !== undefined) { width = Math.max(width, opt.minWidth); @@ -370,12 +467,7 @@ export class TUI extends Container { width = Math.max(1, Math.min(width, availWidth)); // === Resolve maxHeight === - let maxHeight: number | undefined; - if (opt.maxHeight !== undefined) { - maxHeight = opt.maxHeight; - } else if (opt.maxHeightPercent !== undefined) { - maxHeight = Math.floor((termHeight * opt.maxHeightPercent) / 100); - } + let maxHeight = parseSizeValue(opt.maxHeight, termHeight); // Clamp to available space if (maxHeight !== undefined) { maxHeight = Math.max(1, Math.min(maxHeight, availHeight)); @@ -388,13 +480,22 @@ export class TUI extends Container { let row: number; let col: number; - // Absolute positioning takes precedence if (opt.row !== undefined) { - row = opt.row; - } else if (opt.rowPercent !== undefined) { - // Percentage: 0 = top, 100 = bottom - const maxRow = Math.max(0, availHeight - effectiveHeight); - row = marginTop + Math.floor((maxRow * opt.rowPercent) / 100); + if (typeof opt.row === "string") { + // Percentage: 0% = top, 100% = bottom (overlay stays within bounds) + const match = opt.row.match(/^(\d+(?:\.\d+)?)%$/); + if (match) { + const maxRow = Math.max(0, availHeight - effectiveHeight); + const percent = parseFloat(match[1]) / 100; + row = marginTop + Math.floor(maxRow * percent); + } else { + // Invalid format, fall back to center + row = this.resolveAnchorRow("center", effectiveHeight, availHeight, marginTop); + } + } else { + // Absolute row position + row = opt.row; + } } else { // Anchor-based (default: center) const anchor = opt.anchor ?? "center"; @@ -402,11 +503,21 @@ export class TUI extends Container { } if (opt.col !== undefined) { - col = opt.col; - } else if (opt.colPercent !== undefined) { - // Percentage: 0 = left, 100 = right - const maxCol = Math.max(0, availWidth - width); - col = marginLeft + Math.floor((maxCol * opt.colPercent) / 100); + if (typeof opt.col === "string") { + // Percentage: 0% = left, 100% = right (overlay stays within bounds) + const match = opt.col.match(/^(\d+(?:\.\d+)?)%$/); + if (match) { + const maxCol = Math.max(0, availWidth - width); + const percent = parseFloat(match[1]) / 100; + col = marginLeft + Math.floor(maxCol * percent); + } else { + // Invalid format, fall back to center + col = this.resolveAnchorCol("center", width, availWidth, marginLeft); + } + } else { + // Absolute column position + col = opt.col; + } } else { // Anchor-based (default: center) const anchor = opt.anchor ?? "center"; @@ -463,11 +574,16 @@ export class TUI extends Container { if (this.overlayStack.length === 0) return lines; const result = [...lines]; - // Pre-render all overlays and calculate positions + // Pre-render all visible overlays and calculate positions const rendered: { overlayLines: string[]; row: number; col: number; w: number }[] = []; let minLinesNeeded = result.length; - for (const { component, options } of this.overlayStack) { + for (const entry of this.overlayStack) { + // Skip invisible overlays (hidden or visible() returns false) + if (!this.isOverlayVisible(entry)) continue; + + const { component, options } = entry; + // Get layout with height=0 first to determine width and maxHeight // (width and maxHeight don't depend on overlay height) const { width, maxHeight } = this.resolveOverlayLayout(options, 0, termWidth, termHeight); diff --git a/packages/tui/test/overlay-options.test.ts b/packages/tui/test/overlay-options.test.ts index b5dbc5c4..2a83f4b3 100644 --- a/packages/tui/test/overlay-options.test.ts +++ b/packages/tui/test/overlay-options.test.ts @@ -167,14 +167,14 @@ describe("TUI overlay options", () => { }); }); - describe("widthPercent", () => { + describe("width percentage", () => { it("should render overlay at percentage of terminal width", async () => { const terminal = new VirtualTerminal(100, 24); const tui = new TUI(terminal); const overlay = new StaticOverlay(["test"]); tui.addChild(new EmptyContent()); - tui.showOverlay(overlay, { widthPercent: 50 }); + tui.showOverlay(overlay, { width: "50%" }); tui.start(); await renderAndFlush(tui, terminal); @@ -188,7 +188,7 @@ describe("TUI overlay options", () => { const overlay = new StaticOverlay(["test"]); tui.addChild(new EmptyContent()); - tui.showOverlay(overlay, { widthPercent: 10, minWidth: 30 }); + tui.showOverlay(overlay, { width: "10%", minWidth: 30 }); tui.start(); await renderAndFlush(tui, terminal); @@ -344,7 +344,7 @@ describe("TUI overlay options", () => { tui.addChild(new EmptyContent()); // 50% should center both ways - tui.showOverlay(overlay, { width: 10, rowPercent: 50, colPercent: 50 }); + tui.showOverlay(overlay, { width: 10, row: "50%", col: "50%" }); tui.start(); await renderAndFlush(tui, terminal); @@ -368,7 +368,7 @@ describe("TUI overlay options", () => { const overlay = new StaticOverlay(["TOP"]); tui.addChild(new EmptyContent()); - tui.showOverlay(overlay, { width: 10, rowPercent: 0 }); + tui.showOverlay(overlay, { width: 10, row: "0%" }); tui.start(); await renderAndFlush(tui, terminal); @@ -383,7 +383,7 @@ describe("TUI overlay options", () => { const overlay = new StaticOverlay(["BOTTOM"]); tui.addChild(new EmptyContent()); - tui.showOverlay(overlay, { width: 10, rowPercent: 100 }); + tui.showOverlay(overlay, { width: 10, row: "100%" }); tui.start(); await renderAndFlush(tui, terminal); @@ -421,7 +421,7 @@ describe("TUI overlay options", () => { const overlay = new StaticOverlay(["L1", "L2", "L3", "L4", "L5", "L6", "L7", "L8", "L9", "L10"]); tui.addChild(new EmptyContent()); - tui.showOverlay(overlay, { maxHeightPercent: 50 }); + tui.showOverlay(overlay, { maxHeight: "50%" }); tui.start(); await renderAndFlush(tui, terminal);