diff options
Diffstat (limited to 'src/web')
| -rw-r--r-- | src/web/app.mjs | 436 | ||||
| -rw-r--r-- | src/web/index.html | 17 | ||||
| -rw-r--r-- | src/web/libc.c | 152 | ||||
| -rw-r--r-- | src/web/libc/include/assert.h | 14 | ||||
| -rw-r--r-- | src/web/libc/include/ctype.h | 7 | ||||
| -rw-r--r-- | src/web/libc/include/endian.h | 21 | ||||
| -rw-r--r-- | src/web/libc/include/inttypes.h | 8 | ||||
| -rw-r--r-- | src/web/libc/include/limits.h | 9 | ||||
| -rw-r--r-- | src/web/libc/include/stdio.h | 19 | ||||
| -rw-r--r-- | src/web/libc/include/stdlib.h | 16 | ||||
| -rw-r--r-- | src/web/libc/include/string.h | 17 | ||||
| -rw-r--r-- | src/web/libc/include/wchar.h | 9 | ||||
| -rw-r--r-- | src/web/libc/include/wctype.h | 18 | ||||
| -rw-r--r-- | src/web/pardes.css | 91 | ||||
| -rw-r--r-- | src/web/zstbi.zig | 30 |
15 files changed, 864 insertions, 0 deletions
diff --git a/src/web/app.mjs b/src/web/app.mjs new file mode 100644 index 00000000..3d32a16b --- /dev/null +++ b/src/web/app.mjs @@ -0,0 +1,436 @@ +const CELL_SIZE = 20; +const DEFAULT_FG = "#cccccc"; +const DEFAULT_BG = "#121212"; +const decoder = new TextDecoder(); +const encoder = new TextEncoder(); + +const BUTTON = Object.freeze({ left: 0, middle: 1, right: 2, wheelUp: 3, wheelDown: 4, wheelLeft: 5, wheelRight: 6, none: 7 }); +const MOUSE = Object.freeze({ press: 0, release: 1, motion: 2, drag: 3 }); +const SPECIAL_KEYS = Object.freeze({ + Enter: 0x0d, + Backspace: 0x7f, + Tab: 0x09, + Escape: 0x1b, + ArrowUp: 0xf0001, + ArrowDown: 0xf0002, + ArrowLeft: 0xf0003, + ArrowRight: 0xf0004, + Home: 0xf0005, + End: 0xf0006, + PageUp: 0xf0007, + PageDown: 0xf0008, + Delete: 0xf0009, +}); + +export class DomRenderer { + constructor(root, wasm) { + this.root = root; + this.wasm = wasm; + this.rows = []; + this.cells = []; + this.cols = 0; + this.rowCount = 0; + this.snapshot = null; + } + + render() { + const count = this.wasm.pardes_frame(); + const cols = this.wasm.pardes_frame_cols(); + const rows = this.wasm.pardes_frame_rows(); + if (count !== cols * rows) throw new Error(`bad frame: ${count} cells for ${cols}x${rows}`); + this.ensureGrid(cols, rows); + + const ptr = this.wasm.pardes_frame_ptr(); + const view = new DataView(this.wasm.memory.buffer, ptr, count * CELL_SIZE); + const bytes = new Uint8Array(this.wasm.memory.buffer); + const cursorX = this.wasm.pardes_cursor_x(); + const cursorY = this.wasm.pardes_cursor_y(); + const cursorBar = this.wasm.pardes_cursor_bar() !== 0; + const raw = new Array(count); + + for (let index = 0; index < count; index += 1) { + const off = index * CELL_SIZE; + const len = view.getUint8(off + 18); + const fg = view.getUint32(off + 8, true); + const bg = view.getUint32(off + 12, true); + const attrs = view.getUint16(off + 16, true); + const text = decoder.decode(bytes.subarray(ptr + off, ptr + off + len)) || " "; + const x = index % cols; + const y = Math.floor(index / cols); + const cursor = x === cursorX && y === cursorY; + const cell = this.cells[index]; + if (cell.textContent !== text) cell.textContent = text; + const styleKey = `${fg}:${bg}:${attrs}:${cursor ? 1 : 0}:${cursorBar ? 1 : 0}`; + if (cell.pardesStyle !== styleKey) { + applyStyle(cell, fg, bg, attrs, cursor, cursorBar); + cell.pardesStyle = styleKey; + } + raw[index] = { text, fg, bg, attrs }; + } + this.snapshot = { cols, rows, cells: raw, cursorX, cursorY, cursorBar }; + return this.snapshot; + } + + ensureGrid(cols, rows) { + if (cols === this.cols && rows === this.rowCount) return; + const document = this.root.ownerDocument; + const rowNodes = []; + const cells = []; + for (let y = 0; y < rows; y += 1) { + const row = document.createElement("div"); + row.className = "pardes-row"; + row.setAttribute("role", "row"); + row.dataset.row = String(y); + const rowCells = []; + for (let x = 0; x < cols; x += 1) { + const cell = document.createElement("span"); + cell.className = "pardes-cell"; + cell.setAttribute("role", "gridcell"); + cell.dataset.col = String(x); + cell.dataset.row = String(y); + cell.textContent = " "; + rowCells.push(cell); + cells.push(cell); + } + row.replaceChildren(...rowCells); + rowNodes.push(row); + } + this.root.replaceChildren(...rowNodes); + this.root.setAttribute("aria-colcount", String(cols)); + this.root.setAttribute("aria-rowcount", String(rows)); + this.rows = rowNodes; + this.cells = cells; + this.cols = cols; + this.rowCount = rows; + } +} + +export class PardesRuntime { + constructor(root, wasm) { + this.root = root; + this.wasm = wasm; + this.renderer = new DomRenderer(root, wasm); + this.inputPtr = wasm.pardes_input_ptr(); + this.inputCapacity = wasm.pardes_input_capacity(); + this.cellWidth = 1; + this.cellHeight = 1; + this.running = false; + this.pointer = null; + this.frameHandle = 0; + this.bindEvents(); + } + + start() { + this.measure(); + const { cols, rows } = this.gridSize(); + const error = this.wasm.pardes_init(cols, rows); + if (error) throw new Error(`Pardes wasm init failed (${error})`); + this.inputPtr = this.wasm.pardes_input_ptr(); + this.resize(); + this.renderer.render(); + this.drainEffects(); + this.running = true; + this.frameHandle = requestAnimationFrame(() => this.frame()); + this.root.focus({ preventScroll: true }); + } + + stop() { + this.running = false; + if (this.frameHandle) cancelAnimationFrame(this.frameHandle); + this.wasm.pardes_deinit(); + } + + frame() { + if (!this.running) return; + this.wasm.pardes_tick(); + this.renderer.render(); + this.drainEffects(); + this.frameHandle = requestAnimationFrame(() => this.frame()); + } + + measure() { + const probe = this.root.ownerDocument.createElement("span"); + probe.className = "pardes-probe"; + probe.textContent = "M"; + this.root.ownerDocument.body.append(probe); + const rect = probe.getBoundingClientRect(); + probe.remove(); + this.cellWidth = Math.max(1, rect.width); + this.cellHeight = Math.max(1, rect.height); + } + + gridSize() { + return { + cols: Math.max(1, Math.floor(this.root.clientWidth / this.cellWidth)), + rows: Math.max(1, Math.floor(this.root.clientHeight / this.cellHeight)), + }; + } + + resize() { + this.measure(); + const { cols, rows } = this.gridSize(); + this.wasm.pardes_resize(cols, rows); + } + + writeInput(text) { + const data = encoder.encode(text); + if (data.length > this.inputCapacity) throw new Error("browser input exceeds wasm transfer buffer"); + new Uint8Array(this.wasm.memory.buffer, this.inputPtr, data.length).set(data); + return data.length; + } + + key(event) { + const mods = modifiers(event); + let cp = SPECIAL_KEYS[event.key]; + let text = ""; + if (cp === undefined && event.key.length > 0 && [...event.key].length === 1) { + text = mods & 3 ? "" : event.key; + const shortcut = event.code?.match(/^Key([A-Z])$/); + cp = shortcut && (mods & 3) ? shortcut[1].toLowerCase().codePointAt(0) : event.key.codePointAt(0); + } + if (cp === undefined) return; + event.preventDefault(); + this.wasm.pardes_key(cp, this.writeInput(text), mods); + } + + point(event) { + const rect = this.root.getBoundingClientRect(); + return { + col: clamp(Math.floor((event.clientX - rect.left) / this.cellWidth), 0, Math.max(0, this.wasm.pardes_frame_cols() - 1)), + row: clamp(Math.floor((event.clientY - rect.top) / this.cellHeight), 0, Math.max(0, this.wasm.pardes_frame_rows() - 1)), + }; + } + + cellPoint(col, row) { + const rect = this.root.getBoundingClientRect(); + return { x: rect.left + (col + 0.5) * this.cellWidth, y: rect.top + (row + 0.5) * this.cellHeight }; + } + + pointerDown(event) { + event.preventDefault(); + this.root.focus({ preventScroll: true }); + this.root.setPointerCapture?.(event.pointerId); + const point = this.point(event); + if (event.pointerType === "touch") { + const packed = this.wasm.pardes_chrome_target(point.col, point.row) >>> 0; + const chrome = packed !== 0xffffffff; + const snapped = chrome ? { col: packed & 0xffff, row: packed >>> 16 } : point; + this.pointer = { id: event.pointerId, touch: true, chrome, startX: event.clientX, startY: event.clientY, lastX: event.clientX, lastY: event.clientY, point: snapped, scrolling: false, accum: 0 }; + if (chrome) this.wasm.pardes_mouse(BUTTON.left, MOUSE.press, snapped.col, snapped.row, modifiers(event)); + return; + } + const button = pointerButton(event.button); + this.pointer = { id: event.pointerId, touch: false, button }; + this.wasm.pardes_mouse(button, MOUSE.press, point.col, point.row, modifiers(event)); + } + + pointerMove(event) { + const point = this.point(event); + const active = this.pointer?.id === event.pointerId ? this.pointer : null; + if (!active) { + if (event.pointerType !== "touch") this.wasm.pardes_mouse(BUTTON.none, MOUSE.motion, point.col, point.row, modifiers(event)); + return; + } + event.preventDefault(); + if (!active.touch) { + this.wasm.pardes_mouse(active.button, MOUSE.drag, point.col, point.row, modifiers(event)); + return; + } + if (active.chrome) { + this.wasm.pardes_mouse(BUTTON.left, MOUSE.drag, point.col, point.row, modifiers(event)); + return; + } + const travel = Math.hypot(event.clientX - active.startX, event.clientY - active.startY); + if (travel > 8) active.scrolling = true; + if (active.scrolling) { + const dy = event.clientY - active.lastY; + active.accum += dy / this.cellHeight; + while (active.accum <= -1) { + this.wasm.pardes_mouse(BUTTON.wheelDown, MOUSE.press, point.col, point.row, 0); + active.accum += 1; + } + while (active.accum >= 1) { + this.wasm.pardes_mouse(BUTTON.wheelUp, MOUSE.press, point.col, point.row, 0); + active.accum -= 1; + } + this.wasm.pardes_touch_scroll(dy / this.cellHeight); + } + active.lastX = event.clientX; + active.lastY = event.clientY; + } + + pointerUp(event) { + const active = this.pointer?.id === event.pointerId ? this.pointer : null; + if (!active) return; + event.preventDefault(); + const point = this.point(event); + if (active.touch) { + if (active.chrome) this.wasm.pardes_mouse(BUTTON.left, MOUSE.release, point.col, point.row, modifiers(event)); + else if (!active.scrolling) { + this.wasm.pardes_mouse(BUTTON.right, MOUSE.press, point.col, point.row, 0); + this.wasm.pardes_mouse(BUTTON.right, MOUSE.release, point.col, point.row, 0); + } + } else { + this.wasm.pardes_mouse(active.button, MOUSE.release, point.col, point.row, modifiers(event)); + } + if (this.root.hasPointerCapture?.(event.pointerId)) this.root.releasePointerCapture(event.pointerId); + this.pointer = null; + } + + wheel(event) { + event.preventDefault(); + const point = this.point(event); + if (event.deltaY) this.wasm.pardes_mouse(event.deltaY < 0 ? BUTTON.wheelUp : BUTTON.wheelDown, MOUSE.press, point.col, point.row, modifiers(event)); + if (event.deltaX) this.wasm.pardes_mouse(event.deltaX < 0 ? BUTTON.wheelLeft : BUTTON.wheelRight, MOUSE.press, point.col, point.row, modifiers(event)); + } + + paste(event) { + const text = event.clipboardData?.getData("text/plain"); + if (text === undefined) return; + event.preventDefault(); + this.wasm.pardes_paste(this.writeInput(text)); + } + + bindEvents() { + this.root.addEventListener("keydown", (event) => this.key(event)); + this.root.addEventListener("paste", (event) => this.paste(event)); + this.root.addEventListener("contextmenu", (event) => event.preventDefault()); + this.root.addEventListener("pointerdown", (event) => this.pointerDown(event)); + this.root.addEventListener("pointermove", (event) => this.pointerMove(event)); + this.root.addEventListener("pointerup", (event) => this.pointerUp(event)); + this.root.addEventListener("pointercancel", (event) => this.pointerUp(event)); + this.root.addEventListener("wheel", (event) => this.wheel(event), { passive: false }); + globalThis.addEventListener?.("resize", () => this.resize()); + } + + drainEffects() { + for (;;) { + const kind = this.wasm.pardes_effect_next(); + if (!kind) return; + const ptr = this.wasm.pardes_effect_ptr(); + const len = this.wasm.pardes_effect_len(); + const data = new Uint8Array(len); + if (len) data.set(new Uint8Array(this.wasm.memory.buffer, ptr, len)); + const aux0 = this.wasm.pardes_effect_aux0(); + const aux1 = this.wasm.pardes_effect_aux1(); + this.handleEffect(kind, data, aux0, aux1); + } + } + + handleEffect(kind, data, aux0, aux1) { + if (kind === 4) { + globalThis.open?.(decoder.decode(data), "_blank", "noopener,noreferrer"); + } else if (kind === 5) { + const view = new DataView(data.buffer, data.byteOffset, data.byteLength); + const pathLength = data.length >= 4 ? view.getUint32(0, true) : 0; + const path = decoder.decode(data.subarray(4, 4 + pathLength)); + download(data.subarray(4 + pathLength), path.split("/").pop() || "pardes.txt", "text/plain;charset=utf-8"); + } else if (kind === 6) { + download(data, "pardes-dump.zon", "text/plain;charset=utf-8"); + } else if (kind === 7) { + navigator.clipboard?.writeText(decoder.decode(data)).catch(() => {}); + } else if (kind === 10) { + this.running = false; + } else { + this.root.dispatchEvent(new CustomEvent("pardes-io", { detail: { kind, data, aux0, aux1 } })); + } + } +} + +export async function instantiate(url = "./pardes.wasm") { + const response = await fetch(url); + if (!response.ok) throw new Error(`fetch ${url}: HTTP ${response.status}`); + if (WebAssembly.instantiateStreaming) { + try { + return (await WebAssembly.instantiateStreaming(response.clone(), {})).instance.exports; + } catch (error) { + if (!/mime|content-type/i.test(String(error))) throw error; + } + } + return (await WebAssembly.instantiate(await response.arrayBuffer(), {})).instance.exports; +} + +export async function boot(root, url = "./pardes.wasm") { + if (!root) throw new Error("Pardes DOM root is missing"); + const wasm = await instantiate(url); + const runtime = new PardesRuntime(root, wasm); + runtime.start(); + const test = { + grid: () => runtime.renderer.snapshot, + cellPoint: (col, row) => runtime.cellPoint(col, row), + }; + globalThis.pardes = { runtime, wasm, test }; + return runtime; +} + +function applyStyle(cell, fgCode, bgCode, attrs, cursor, cursorBar) { + let fg = color(fgCode, DEFAULT_FG); + let bg = color(bgCode, DEFAULT_BG); + if (attrs & (1 << 4)) [fg, bg] = [bg, fg]; + if (attrs & (1 << 5)) fg = bg; + if (attrs & (1 << 1)) fg = dim(fg); + cell.style.color = fg; + cell.style.backgroundColor = bg; + cell.style.fontWeight = attrs & 1 ? "700" : "400"; + cell.style.fontStyle = attrs & (1 << 2) ? "italic" : "normal"; + const underline = (attrs >>> 8) & 7; + cell.style.textDecorationLine = `${underline ? "underline " : ""}${attrs & (1 << 6) ? "line-through" : ""}`.trim() || "none"; + cell.style.textDecorationStyle = ["solid", "solid", "double", "wavy", "dotted", "dashed"][underline] || "solid"; + cell.style.setProperty("--cell-fg", fg); + cell.style.setProperty("--cell-bg", bg); + cell.classList.toggle("is-cursor", cursor); + cell.classList.toggle("is-bar", cursor && cursorBar); + cell.classList.toggle("is-blinking", Boolean(attrs & (1 << 3))); +} + +function color(encoded, fallback) { + if (encoded === 0x01000000) return fallback; + if ((encoded & 0xff000000) === 0x02000000) return indexedColor(encoded & 0xff); + return `#${(encoded & 0xffffff).toString(16).padStart(6, "0")}`; +} + +function indexedColor(index) { + const ansi = [ + "#000000", "#cc0000", "#4e9a06", "#c4a000", "#3465a4", "#75507b", "#06989a", "#d3d7cf", + "#555753", "#ef2929", "#8ae234", "#fce94f", "#729fcf", "#ad7fa8", "#34e2e2", "#eeeeec", + ]; + if (index < 16) return ansi[index]; + if (index >= 232) { + const level = 8 + (index - 232) * 10; + return rgb(level, level, level); + } + const n = index - 16; + const level = (part) => part === 0 ? 0 : 55 + part * 40; + return rgb(level(Math.floor(n / 36)), level(Math.floor(n / 6) % 6), level(n % 6)); +} + +function dim(value) { + const n = Number.parseInt(value.slice(1), 16); + return rgb(((n >>> 16) & 0xff) * 0.6, ((n >>> 8) & 0xff) * 0.6, (n & 0xff) * 0.6); +} + +function rgb(r, g, b) { + return `#${[r, g, b].map((part) => Math.round(part).toString(16).padStart(2, "0")).join("")}`; +} + +function modifiers(event) { + return (event.ctrlKey ? 1 : 0) | (event.altKey ? 2 : 0) | (event.shiftKey ? 4 : 0); +} + +function pointerButton(button) { + return button === 1 ? BUTTON.middle : button === 2 ? BUTTON.right : BUTTON.left; +} + +function clamp(value, low, high) { + return Math.max(low, Math.min(high, value)); +} + +function download(bytes, name, type) { + if (typeof document === "undefined") return; + const url = URL.createObjectURL(new Blob([bytes], { type })); + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = name; + anchor.click(); + setTimeout(() => URL.revokeObjectURL(url), 0); +} diff --git a/src/web/index.html b/src/web/index.html new file mode 100644 index 00000000..4012c313 --- /dev/null +++ b/src/web/index.html @@ -0,0 +1,17 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title>Pardes</title> + <link rel="stylesheet" href="pardes.css"> +</head> +<body> + <main id="pardes" class="pardes" role="grid" aria-label="Pardes text environment" tabindex="0"></main> + <noscript>Pardes needs JavaScript to drive its WebAssembly core.</noscript> + <script type="module"> + import { boot } from "./app.mjs"; + boot(document.getElementById("pardes")); + </script> +</body> +</html> diff --git a/src/web/libc.c b/src/web/libc.c new file mode 100644 index 00000000..c49fa03e --- /dev/null +++ b/src/web/libc.c @@ -0,0 +1,152 @@ +/* Minimal libc-shaped surface used by Tree-sitter inside the freestanding + * browser module. Allocation lives in web.zig so it shares Zig's wasm heap; + * these routines neither import browser functions nor perform IO. */ +#include <ctype.h> +#include <stdarg.h> +#include <stddef.h> +#include <stdint.h> +#include <stdio.h> +#include <string.h> +#include <wctype.h> + +_Noreturn void abort(void) { + __builtin_trap(); +} + +size_t strlen(const char *str) { + const char *end = str; + while (*end) end++; + return (size_t)(end - str); +} + +char *strchr(const char *str, int character) { + const unsigned char needle = (unsigned char)character; + do { + if ((unsigned char)*str == needle) return (char *)str; + } while (*str++); + return (char *)0; +} + +void *memchr(const void *ptr, int value, size_t count) { + const unsigned char *bytes = (const unsigned char *)ptr; + const unsigned char needle = (unsigned char)value; + for (size_t index = 0; index < count; index++) { + if (bytes[index] == needle) return (void *)(bytes + index); + } + return (void *)0; +} + +int strcmp(const char *lhs, const char *rhs) { + while (*lhs && (unsigned char)*lhs == (unsigned char)*rhs) { + lhs++; + rhs++; + } + return (int)(unsigned char)*lhs - (int)(unsigned char)*rhs; +} + +int strncmp(const char *lhs, const char *rhs, size_t count) { + while (count && *lhs && (unsigned char)*lhs == (unsigned char)*rhs) { + lhs++; + rhs++; + count--; + } + return count ? (int)(unsigned char)*lhs - (int)(unsigned char)*rhs : 0; +} + +char *strncpy(char *restrict dest, const char *restrict src, size_t count) { + char *result = dest; + while (count && *src) { + *dest++ = *src++; + count--; + } + while (count--) *dest++ = '\0'; + return result; +} + +int isprint(int character) { + return character >= 0x20 && character <= 0x7e; +} + +int isdigit(int character) { + return character >= '0' && character <= '9'; +} + +int iswspace(wint_t character) { + return character == ' ' || (character >= '\t' && character <= '\r'); +} + +int iswalnum(wint_t character) { + return iswdigit(character) || iswalpha(character); +} + +int iswalpha(wint_t character) { + return (character >= 'A' && character <= 'Z') || + (character >= 'a' && character <= 'z') || character >= 0x80; +} + +int iswblank(wint_t character) { + return character == ' ' || character == '\t'; +} + +int iswdigit(wint_t character) { + return character >= '0' && character <= '9'; +} + +int iswlower(wint_t character) { + return character >= 'a' && character <= 'z'; +} + +int iswupper(wint_t character) { + return character >= 'A' && character <= 'Z'; +} + +int iswxdigit(wint_t character) { + return iswdigit(character) || (character >= 'A' && character <= 'F') || + (character >= 'a' && character <= 'f'); +} + +wint_t towupper(wint_t character) { + return iswlower(character) ? character - ('a' - 'A') : character; +} + +int vsnprintf(char *restrict buffer, size_t size, const char *restrict format, va_list args) { + (void)format; + (void)args; + if (size) buffer[0] = '\0'; + return 0; +} + +int snprintf(char *restrict buffer, size_t size, const char *restrict format, ...) { + va_list args; + va_start(args, format); + int result = vsnprintf(buffer, size, format, args); + va_end(args); + return result; +} + +int fprintf(FILE *stream, const char *format, ...) { + (void)stream; + (void)format; + return 0; +} + +int fclose(FILE *stream) { + (void)stream; + return 0; +} + +FILE *fdopen(int descriptor, const char *mode) { + (void)descriptor; + (void)mode; + return (FILE *)0; +} + +int fputc(int character, FILE *stream) { + (void)stream; + return character; +} + +int fputs(const char *string, FILE *stream) { + (void)stream; + return (int)strlen(string); +} diff --git a/src/web/libc/include/assert.h b/src/web/libc/include/assert.h new file mode 100644 index 00000000..fd018947 --- /dev/null +++ b/src/web/libc/include/assert.h @@ -0,0 +1,14 @@ +#ifndef PARDES_WEB_ASSERT_H +#define PARDES_WEB_ASSERT_H + +#ifndef static_assert +#define static_assert _Static_assert +#endif + +#ifdef NDEBUG +#define assert(expression) ((void)0) +#else +#define assert(expression) ((expression) ? (void)0 : __builtin_trap()) +#endif + +#endif diff --git a/src/web/libc/include/ctype.h b/src/web/libc/include/ctype.h new file mode 100644 index 00000000..985c8759 --- /dev/null +++ b/src/web/libc/include/ctype.h @@ -0,0 +1,7 @@ +#ifndef PARDES_WEB_CTYPE_H +#define PARDES_WEB_CTYPE_H + +int isprint(int character); +int isdigit(int character); + +#endif diff --git a/src/web/libc/include/endian.h b/src/web/libc/include/endian.h new file mode 100644 index 00000000..a047c88f --- /dev/null +++ b/src/web/libc/include/endian.h @@ -0,0 +1,21 @@ +#ifndef PARDES_WEB_ENDIAN_H +#define PARDES_WEB_ENDIAN_H + +#define __LITTLE_ENDIAN 1234 +#define __BIG_ENDIAN 4321 +#define __BYTE_ORDER __LITTLE_ENDIAN + +#define htole16(value) (value) +#define le16toh(value) (value) +#define htole32(value) (value) +#define le32toh(value) (value) +#define htole64(value) (value) +#define le64toh(value) (value) +#define htobe16(value) __builtin_bswap16(value) +#define be16toh(value) __builtin_bswap16(value) +#define htobe32(value) __builtin_bswap32(value) +#define be32toh(value) __builtin_bswap32(value) +#define htobe64(value) __builtin_bswap64(value) +#define be64toh(value) __builtin_bswap64(value) + +#endif diff --git a/src/web/libc/include/inttypes.h b/src/web/libc/include/inttypes.h new file mode 100644 index 00000000..7e795746 --- /dev/null +++ b/src/web/libc/include/inttypes.h @@ -0,0 +1,8 @@ +#ifndef PARDES_WEB_INTTYPES_H +#define PARDES_WEB_INTTYPES_H + +#include <stdint.h> + +#define PRId32 "d" + +#endif diff --git a/src/web/libc/include/limits.h b/src/web/libc/include/limits.h new file mode 100644 index 00000000..6ba31375 --- /dev/null +++ b/src/web/libc/include/limits.h @@ -0,0 +1,9 @@ +#ifndef PARDES_WEB_LIMITS_H +#define PARDES_WEB_LIMITS_H + +#define CHAR_BIT __CHAR_BIT__ +#define INT_MAX __INT_MAX__ +#define UINT_MAX (__INT_MAX__ * 2U + 1U) +#define USHRT_MAX (__SHRT_MAX__ * 2U + 1U) + +#endif diff --git a/src/web/libc/include/stdio.h b/src/web/libc/include/stdio.h new file mode 100644 index 00000000..92895054 --- /dev/null +++ b/src/web/libc/include/stdio.h @@ -0,0 +1,19 @@ +#ifndef PARDES_WEB_STDIO_H +#define PARDES_WEB_STDIO_H + +#include <stdarg.h> +#include <stddef.h> + +typedef struct PardesWebFile FILE; + +#define stderr ((FILE *)0) + +int fprintf(FILE *stream, const char *format, ...); +int fclose(FILE *stream); +FILE *fdopen(int descriptor, const char *mode); +int fputc(int character, FILE *stream); +int fputs(const char *string, FILE *stream); +int snprintf(char *restrict buffer, size_t size, const char *restrict format, ...); +int vsnprintf(char *restrict buffer, size_t size, const char *restrict format, va_list args); + +#endif diff --git a/src/web/libc/include/stdlib.h b/src/web/libc/include/stdlib.h new file mode 100644 index 00000000..cb280904 --- /dev/null +++ b/src/web/libc/include/stdlib.h @@ -0,0 +1,16 @@ +#ifndef PARDES_WEB_STDLIB_H +#define PARDES_WEB_STDLIB_H + +#include <stddef.h> + +#ifndef NULL +#define NULL ((void *)0) +#endif + +void *malloc(size_t size); +void *calloc(size_t count, size_t size); +void *realloc(void *ptr, size_t size); +void free(void *ptr); +_Noreturn void abort(void); + +#endif diff --git a/src/web/libc/include/string.h b/src/web/libc/include/string.h new file mode 100644 index 00000000..7e5b8783 --- /dev/null +++ b/src/web/libc/include/string.h @@ -0,0 +1,17 @@ +#ifndef PARDES_WEB_STRING_H +#define PARDES_WEB_STRING_H + +#include <stddef.h> + +void *memchr(const void *ptr, int value, size_t count); +int memcmp(const void *lhs, const void *rhs, size_t count); +void *memcpy(void *restrict dest, const void *restrict src, size_t count); +void *memmove(void *dest, const void *src, size_t count); +void *memset(void *dest, int value, size_t count); +size_t strlen(const char *str); +char *strchr(const char *str, int character); +int strcmp(const char *lhs, const char *rhs); +int strncmp(const char *lhs, const char *rhs, size_t count); +char *strncpy(char *restrict dest, const char *restrict src, size_t count); + +#endif diff --git a/src/web/libc/include/wchar.h b/src/web/libc/include/wchar.h new file mode 100644 index 00000000..4888631e --- /dev/null +++ b/src/web/libc/include/wchar.h @@ -0,0 +1,9 @@ +#ifndef PARDES_WEB_WCHAR_H +#define PARDES_WEB_WCHAR_H + +#include <stddef.h> +#include <wctype.h> + +typedef __WCHAR_TYPE__ wchar_t; + +#endif diff --git a/src/web/libc/include/wctype.h b/src/web/libc/include/wctype.h new file mode 100644 index 00000000..77bbabe9 --- /dev/null +++ b/src/web/libc/include/wctype.h @@ -0,0 +1,18 @@ +#ifndef PARDES_WEB_WCTYPE_H +#define PARDES_WEB_WCTYPE_H + +#include <stddef.h> + +typedef unsigned int wint_t; + +int iswalnum(wint_t character); +int iswalpha(wint_t character); +int iswblank(wint_t character); +int iswdigit(wint_t character); +int iswlower(wint_t character); +int iswspace(wint_t character); +int iswupper(wint_t character); +int iswxdigit(wint_t character); +wint_t towupper(wint_t character); + +#endif diff --git a/src/web/pardes.css b/src/web/pardes.css new file mode 100644 index 00000000..a959e6c3 --- /dev/null +++ b/src/web/pardes.css @@ -0,0 +1,91 @@ +@font-face { + font-family: "Pardes Mono"; + src: url("./AdwaitaMono-Regular.ttf") format("truetype"); + font-display: swap; +} + +:root { + color-scheme: dark; + --pardes-bg: #121212; + --pardes-fg: #cccccc; + --pardes-font-size: 18px; + --pardes-cell-width: 9.333333px; + --pardes-cell-height: 19.459459px; +} + +html, +body { + width: 100%; + height: 100%; + margin: 0; + overflow: hidden; + background: var(--pardes-bg); +} + +.pardes { + box-sizing: border-box; + width: 100vw; + height: 100vh; + overflow: hidden; + color: var(--pardes-fg); + background: var(--pardes-bg); + font-family: "Pardes Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: var(--pardes-font-size); + font-variant-ligatures: none; + line-height: 1; + outline: none; + touch-action: none; + user-select: text; + cursor: text; +} + +.pardes-row { + display: flex; + width: max-content; + height: var(--pardes-cell-height); + line-height: var(--pardes-cell-height); + white-space: pre; +} + +.pardes-cell { + box-sizing: border-box; + display: inline-block; + flex: 0 0 var(--pardes-cell-width); + width: var(--pardes-cell-width); + height: var(--pardes-cell-height); + overflow: hidden; + text-align: left; + white-space: pre; +} + +.pardes-cell.is-cursor { + color: var(--cell-bg) !important; + background: var(--cell-fg) !important; +} + +.pardes-cell.is-cursor.is-bar { + color: var(--cell-fg) !important; + background: var(--cell-bg) !important; + box-shadow: inset 2px 0 var(--cell-fg); +} + +.pardes-cell.is-blinking { + animation: pardes-blink 1s step-end infinite; +} + +.pardes-probe { + position: fixed; + left: -10000px; + top: -10000px; + width: var(--pardes-cell-width); + height: var(--pardes-cell-height); + visibility: hidden; +} + +@keyframes pardes-blink { + 50% { opacity: 0; } +} + +@media (prefers-reduced-motion: reduce) { + .pardes-cell.is-blinking { animation: none; } +} diff --git a/src/web/zstbi.zig b/src/web/zstbi.zig new file mode 100644 index 00000000..76b144f7 --- /dev/null +++ b/src/web/zstbi.zig @@ -0,0 +1,30 @@ +//! Freestanding image-decoder shim. +//! +//! The browser core has no libc or C allocator ABI. Image bytes still survive +//! dumps, but decoding belongs in the JavaScript IO layer rather than pulling +//! stb_image and an Emscripten runtime into the state machine. + +const std = @import("std"); + +pub fn init(_: std.Io, _: std.mem.Allocator) void {} +pub fn deinit() void {} + +pub const Image = struct { + data: []u8 = &.{}, + width: u32 = 0, + height: u32 = 0, + num_components: u32 = 4, + bytes_per_component: u32 = 1, + bytes_per_row: u32 = 0, + is_hdr: bool = false, + + pub fn loadFromMemory(_: []const u8, _: u32) !Image { + return error.ImageDecodeUnavailable; + } + + pub fn resize(_: *const Image, _: u32, _: u32) Image { + return .{}; + } + + pub fn deinit(_: *Image) void {} +}; |
