` |
diff --git a/examples/kitty-images/index.html b/examples/kitty-images/index.html
new file mode 100644
index 0000000..4e76c85
--- /dev/null
+++ b/examples/kitty-images/index.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
wterm — Kitty Graphics Protocol
+
+
+
+
+
+
+
diff --git a/examples/kitty-images/package.json b/examples/kitty-images/package.json
new file mode 100644
index 0000000..c4ca369
--- /dev/null
+++ b/examples/kitty-images/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "kitty-images-example",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "predev": "command -v portless >/dev/null 2>&1 || (echo '\\nportless is required but not installed. Run: npm i -g portless\\nSee: https://github.com/vercel-labs/portless\\n' && exit 1)",
+ "dev": "portless kitty-images.wterm vite",
+ "build": "vite build",
+ "preview": "vite preview",
+ "type-check": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@wterm/dom": "workspace:*",
+ "@wterm/ghostty": "workspace:*"
+ },
+ "devDependencies": {
+ "typescript": "^6.0.2",
+ "vite": "^6.3.5"
+ }
+}
diff --git a/examples/kitty-images/src/main.ts b/examples/kitty-images/src/main.ts
new file mode 100644
index 0000000..c82c081
--- /dev/null
+++ b/examples/kitty-images/src/main.ts
@@ -0,0 +1,78 @@
+import { WTerm } from "@wterm/dom";
+import { GhosttyCore } from "@wterm/ghostty";
+import "@wterm/dom/css";
+
+const el = document.getElementById("terminal")!;
+
+const core = await GhosttyCore.load();
+const term = new WTerm(el, { core });
+await term.init();
+
+term.write(
+ "\x1b[1;36mwterm\x1b[0m — \x1b[1;35mKitty graphics protocol\x1b[0m demo\r\n\r\n",
+);
+
+term.write(
+ "PNG image transmitted via APC `\\x1b_G...` sequence and rendered\r\n" +
+ "as an absolutely-positioned
![]()
overlay above the cell grid.\r\n\r\n",
+);
+
+const pngBytes = await drawSamplePng(200, 100);
+const b64 = base64FromBytes(pngBytes);
+
+// Chunked transfer: real apps split the base64 payload across multiple
+// `m=1` chunks with a final `m=0`. Demo it here with 4 KiB chunks.
+const CHUNK = 4096;
+let offset = 0;
+let first = true;
+while (offset < b64.length) {
+ const end = Math.min(offset + CHUNK, b64.length);
+ const isLast = end >= b64.length;
+ const slice = b64.slice(offset, end);
+ const control = first
+ ? `a=T,f=100,i=1,c=25,r=6,m=${isLast ? 0 : 1}`
+ : `i=1,m=${isLast ? 0 : 1}`;
+ term.write(`\x1b_G${control};${slice}\x1b\\`);
+ offset = end;
+ first = false;
+}
+
+term.write(
+ "\r\n\x1b[2mYour PNG above. Press any key — input echoes.\x1b[0m\r\n",
+);
+
+async function drawSamplePng(w: number, h: number): Promise
{
+ const canvas = document.createElement("canvas");
+ canvas.width = w;
+ canvas.height = h;
+ const ctx = canvas.getContext("2d")!;
+
+ const grad = ctx.createLinearGradient(0, 0, w, h);
+ grad.addColorStop(0, "#ff7eb6");
+ grad.addColorStop(0.5, "#be95ff");
+ grad.addColorStop(1, "#33b1ff");
+ ctx.fillStyle = grad;
+ ctx.fillRect(0, 0, w, h);
+
+ ctx.fillStyle = "rgba(255,255,255,0.95)";
+ ctx.font = "bold 28px system-ui, sans-serif";
+ ctx.textBaseline = "middle";
+ ctx.textAlign = "center";
+ ctx.fillText("wterm 🚀", w / 2, h / 2);
+
+ const blob: Blob = await new Promise((resolve) =>
+ canvas.toBlob((b) => resolve(b!), "image/png"),
+ );
+ return new Uint8Array(await blob.arrayBuffer());
+}
+
+function base64FromBytes(bytes: Uint8Array): string {
+ let s = "";
+ const STEP = 0x8000;
+ for (let i = 0; i < bytes.length; i += STEP) {
+ s += String.fromCharCode(
+ ...bytes.subarray(i, Math.min(i + STEP, bytes.length)),
+ );
+ }
+ return btoa(s);
+}
diff --git a/examples/kitty-images/tsconfig.json b/examples/kitty-images/tsconfig.json
new file mode 100644
index 0000000..11e5462
--- /dev/null
+++ b/examples/kitty-images/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "strict": true,
+ "noEmit": true,
+ "isolatedModules": true,
+ "skipLibCheck": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"]
+ },
+ "include": ["src", "*.d.ts"]
+}
diff --git a/examples/kitty-images/vite.config.ts b/examples/kitty-images/vite.config.ts
new file mode 100644
index 0000000..87d46cd
--- /dev/null
+++ b/examples/kitty-images/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from "vite";
+
+export default defineConfig({
+ build: {
+ target: "esnext",
+ },
+});
diff --git a/examples/kitty-images/wterm-dom.d.ts b/examples/kitty-images/wterm-dom.d.ts
new file mode 100644
index 0000000..aefbc0a
--- /dev/null
+++ b/examples/kitty-images/wterm-dom.d.ts
@@ -0,0 +1 @@
+declare module "@wterm/dom/css";
diff --git a/packages/@wterm/dom/README.md b/packages/@wterm/dom/README.md
index 22098d6..94ae7d9 100644
--- a/packages/@wterm/dom/README.md
+++ b/packages/@wterm/dom/README.md
@@ -46,6 +46,7 @@ new WTerm(element: HTMLElement, options?: WTermOptions)
| `autoResize` | `boolean` | `true` | Auto-resize based on container dimensions |
| `cursorBlink` | `boolean` | `false` | Enable cursor blinking animation |
| `debug` | `boolean` | `false` | Enable debug mode. Exposes a `DebugAdapter` on the instance (`wt.debug`) for inspecting escape sequences, cell data, render performance, and unhandled CSI sequences. |
+| `images` | `boolean` | `true` | Enable inline image rendering via the [Kitty terminal graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/). When enabled, `\x1b_G…\x1b\\` APC sequences are intercepted and rendered as `
` overlays above the cell grid. Set to `false` to pass the bytes through to the core unchanged. |
| `onData` | `(data: string) => void` | — | Called when the terminal produces data (user input or host response). When omitted, input is echoed back automatically. |
| `onTitle` | `(title: string) => void` | — | Called when the terminal title changes |
| `onResize` | `(cols: number, rows: number) => void` | — | Called on resize |
@@ -79,6 +80,20 @@ ws.connect();
term.onData = (data) => ws.send(data);
```
+## Inline images (Kitty graphics protocol)
+
+When `images: true` (default), wterm intercepts Kitty graphics protocol APC sequences and renders the transmitted PNG as an absolutely-positioned `
` overlay aligned to the cell grid. Supports inline base64 transfers (`f=100`) and multi-chunk `m=1`/`m=0` payloads. Actions: `t` (transmit), `T` (transmit + display), `p` (put placement), `d` (delete).
+
+```ts
+const png = new Uint8Array(
+ await fetch("/icon.png").then((r) => r.arrayBuffer()),
+);
+const b64 = btoa(String.fromCharCode(...png));
+term.write(`\x1b_Ga=T,f=100,i=1,c=20,r=5;${b64}\x1b\\`);
+```
+
+Not yet supported: raw RGB/RGBA frames (`f=24`/`f=32`), file/shared-memory transports, virtual-placement via Unicode placeholders, animations, Sixel, and iTerm2 inline images.
+
## Themes
Import the stylesheet and apply a theme class to the terminal element:
diff --git a/packages/@wterm/dom/src/__tests__/kitty-graphics.test.ts b/packages/@wterm/dom/src/__tests__/kitty-graphics.test.ts
new file mode 100644
index 0000000..9760d3f
--- /dev/null
+++ b/packages/@wterm/dom/src/__tests__/kitty-graphics.test.ts
@@ -0,0 +1,198 @@
+import { describe, it, expect } from "vitest";
+import {
+ KittyGraphicsFilter,
+ MAX_PENDING_CHUNKS,
+ type StreamEvent,
+} from "../kitty-graphics.js";
+
+const enc = new TextEncoder();
+
+function feed(filter: KittyGraphicsFilter, s: string): StreamEvent[] {
+ return filter.push(enc.encode(s));
+}
+
+function textOf(ev: StreamEvent): string {
+ if (ev.type !== "text") throw new Error("expected text event");
+ return new TextDecoder().decode(ev.bytes);
+}
+
+function base64(bytes: ArrayLike): string {
+ let s = "";
+ for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
+ return btoa(s);
+}
+
+describe("KittyGraphicsFilter", () => {
+ it("passes through plain text untouched", () => {
+ const f = new KittyGraphicsFilter();
+ const events = feed(f, "hello world");
+ expect(events).toHaveLength(1);
+ expect(textOf(events[0])).toBe("hello world");
+ });
+
+ it("passes through normal CSI escape sequences", () => {
+ const f = new KittyGraphicsFilter();
+ const events = feed(f, "\x1b[1;31mred\x1b[0m");
+ expect(events).toHaveLength(1);
+ expect(textOf(events[0])).toBe("\x1b[1;31mred\x1b[0m");
+ });
+
+ it("passes through non-Kitty APC sequences", () => {
+ const f = new KittyGraphicsFilter();
+ const events = feed(f, "before\x1b_Xfoo\x1b\\after");
+ expect(events).toHaveLength(1);
+ expect(textOf(events[0])).toBe("before\x1b_Xfoo\x1b\\after");
+ });
+
+ it("extracts a single complete Kitty graphics APC and emits text around it", () => {
+ const f = new KittyGraphicsFilter();
+ const png = base64([1, 2, 3, 4]);
+ const events = feed(f, `prefix\x1b_Ga=T,f=100,i=1;${png}\x1b\\suffix`);
+
+ expect(events).toHaveLength(3);
+ expect(textOf(events[0])).toBe("prefix");
+ expect(events[1].type).toBe("graphics");
+ if (events[1].type === "graphics") {
+ expect(events[1].event.control.a).toBe("T");
+ expect(events[1].event.control.f).toBe(100);
+ expect(events[1].event.control.i).toBe(1);
+ expect(Array.from(events[1].event.data)).toEqual([1, 2, 3, 4]);
+ }
+ expect(textOf(events[2])).toBe("suffix");
+ });
+
+ it("accepts BEL as a terminator", () => {
+ const f = new KittyGraphicsFilter();
+ const events = feed(f, `\x1b_Ga=t,i=5,f=100;${base64([9])}\x07tail`);
+ expect(events).toHaveLength(2);
+ expect(events[0].type).toBe("graphics");
+ expect(textOf(events[1])).toBe("tail");
+ });
+
+ it("handles a chunked transfer split across writes", () => {
+ const f = new KittyGraphicsFilter();
+ // Real apps base64-encode the full payload, then split the base64 stream
+ // into fixed-size chunks. Intermediate chunks therefore have no `=` pad.
+ const full = base64([10, 11, 12, 13, 14, 15, 16, 17]);
+ const split = Math.floor(full.length / 2);
+ const chunkA = full.slice(0, split);
+ const chunkB = full.slice(split);
+
+ const first = feed(f, `\x1b_Ga=T,f=100,i=42,m=1;${chunkA}\x1b\\`);
+ expect(first).toHaveLength(0);
+
+ const second = feed(f, `\x1b_Gi=42,m=0;${chunkB}\x1b\\done`);
+ expect(second).toHaveLength(2);
+ expect(second[0].type).toBe("graphics");
+ if (second[0].type === "graphics") {
+ expect(second[0].event.control.i).toBe(42);
+ expect(Array.from(second[0].event.data)).toEqual([
+ 10, 11, 12, 13, 14, 15, 16, 17,
+ ]);
+ }
+ expect(textOf(second[1])).toBe("done");
+ });
+
+ it("handles APC bytes split across multiple push calls", () => {
+ const f = new KittyGraphicsFilter();
+ const png = base64([1, 2]);
+ const full = `\x1b_Ga=T,f=100,i=7;${png}\x1b\\`;
+ const events: StreamEvent[] = [];
+ for (const ch of full) events.push(...feed(f, ch));
+
+ expect(events).toHaveLength(1);
+ expect(events[0].type).toBe("graphics");
+ if (events[0].type === "graphics") {
+ expect(Array.from(events[0].event.data)).toEqual([1, 2]);
+ }
+ });
+
+ it("emits a delete action with no payload", () => {
+ const f = new KittyGraphicsFilter();
+ const events = feed(f, "\x1b_Ga=d,d=a\x1b\\");
+ expect(events).toHaveLength(1);
+ expect(events[0].type).toBe("graphics");
+ if (events[0].type === "graphics") {
+ expect(events[0].event.control.a).toBe("d");
+ expect(events[0].event.control.d).toBe("a");
+ expect(events[0].event.data.length).toBe(0);
+ }
+ });
+
+ it("does not consume `\\x1b` followed by something other than `_`", () => {
+ const f = new KittyGraphicsFilter();
+ const events = feed(f, "\x1b[A");
+ expect(events).toHaveLength(1);
+ expect(textOf(events[0])).toBe("\x1b[A");
+ });
+
+ it("does not consume `\\x1b_` followed by something other than `G`", () => {
+ const f = new KittyGraphicsFilter();
+ const events = feed(f, "\x1b_Q;foo\x1b\\");
+ expect(events).toHaveLength(1);
+ expect(textOf(events[0])).toBe("\x1b_Q;foo\x1b\\");
+ });
+
+ it("coalesces contiguous text runs in a single push", () => {
+ const f = new KittyGraphicsFilter();
+ const events = feed(f, "a\x1bb\x1b_c");
+ expect(events).toHaveLength(1);
+ expect(textOf(events[0])).toBe("a\x1bb\x1b_c");
+ });
+
+ it("reset clears in-flight state", () => {
+ const f = new KittyGraphicsFilter();
+ feed(f, "\x1b_Ga=T,f=100,i=99;abc"); // never terminates
+ f.reset();
+ const events = feed(f, "hi");
+ expect(events).toHaveLength(1);
+ expect(textOf(events[0])).toBe("hi");
+ });
+
+ it("silently drops an APC whose payload is invalid base64", () => {
+ const f = new KittyGraphicsFilter();
+ // "abcde" survives the strip regex but has length 5 — invalid base64
+ // (length must be a multiple of 4), so `atob` throws.
+ const events = feed(f, `before\x1b_Ga=T,f=100,i=1;abcde\x1b\\after`);
+ // No graphics event emitted; surrounding text still passes through.
+ const graphics = events.filter((e) => e.type === "graphics");
+ expect(graphics).toHaveLength(0);
+ const text = events
+ .filter((e) => e.type === "text")
+ .map(textOf)
+ .join("");
+ expect(text).toBe("beforeafter");
+ });
+
+ it("evicts the oldest pending chunk when exceeding the chunk-count cap", () => {
+ const f = new KittyGraphicsFilter();
+ // Start MAX_PENDING_CHUNKS + 1 distinct in-flight transfers (m=1, never closed).
+ for (let i = 0; i < MAX_PENDING_CHUNKS + 1; i++) {
+ const events = feed(
+ f,
+ `\x1b_Ga=T,f=100,i=${100 + i},m=1;${base64([i])}\x1b\\`,
+ );
+ // Each chunk-with-more emits nothing.
+ expect(events).toHaveLength(0);
+ }
+ // The oldest entry (i=100) was evicted. Closing it now should not produce
+ // a graphics event because no pending entry remains and m=0 is treated as
+ // a fresh standalone APC with empty payload.
+ const closeOldest = feed(f, `\x1b_Gi=100,m=0;\x1b\\`);
+ const oldestGraphics = closeOldest.filter((e) => e.type === "graphics");
+ // A fresh standalone APC still emits one graphics event (with empty data),
+ // but it must NOT contain the originally-buffered byte.
+ expect(oldestGraphics).toHaveLength(1);
+ if (oldestGraphics[0].type === "graphics") {
+ expect(Array.from(oldestGraphics[0].event.data)).toEqual([]);
+ }
+ // The newest entry (i=100 + MAX_PENDING_CHUNKS) is still pending — closing
+ // it should yield the originally-buffered byte.
+ const newestId = 100 + MAX_PENDING_CHUNKS;
+ const closeNewest = feed(f, `\x1b_Gi=${newestId},m=0;\x1b\\`);
+ expect(closeNewest).toHaveLength(1);
+ if (closeNewest[0].type === "graphics") {
+ expect(Array.from(closeNewest[0].event.data)).toEqual([MAX_PENDING_CHUNKS]);
+ }
+ });
+});
diff --git a/packages/@wterm/dom/src/__tests__/wterm.test.ts b/packages/@wterm/dom/src/__tests__/wterm.test.ts
index 5ca798f..c47f5ea 100644
--- a/packages/@wterm/dom/src/__tests__/wterm.test.ts
+++ b/packages/@wterm/dom/src/__tests__/wterm.test.ts
@@ -159,11 +159,12 @@ describe("WTerm", () => {
});
describe("write", () => {
- it("calls bridge.writeString for string data", async () => {
+ it("forwards string data as encoded bytes to bridge.writeRaw", async () => {
const term = new WTerm(element, { autoResize: false });
await term.init();
term.write("hello");
- expect(mockBridge.writeString).toHaveBeenCalledWith("hello");
+ const encoded = new TextEncoder().encode("hello");
+ expect(mockBridge.writeRaw).toHaveBeenCalledWith(encoded);
});
it("calls bridge.writeRaw for Uint8Array data", async () => {
@@ -174,10 +175,38 @@ describe("WTerm", () => {
expect(mockBridge.writeRaw).toHaveBeenCalledWith(bytes);
});
+ it("falls back to writeString when images are disabled", async () => {
+ const term = new WTerm(element, { autoResize: false, images: false });
+ await term.init();
+ term.write("hello");
+ expect(mockBridge.writeString).toHaveBeenCalledWith("hello");
+ });
+
it("is a no-op before init", () => {
const term = new WTerm(element);
term.write("hello");
expect(mockBridge.writeString).not.toHaveBeenCalled();
+ expect(mockBridge.writeRaw).not.toHaveBeenCalled();
+ });
+
+ it("intercepts Kitty APC sequences when images are enabled", async () => {
+ const term = new WTerm(element, { autoResize: false });
+ await term.init();
+ vi.mocked(mockBridge.writeRaw).mockClear();
+ const prefix = "hello ";
+ const suffix = " world";
+ // a=d delete-all APC — control-only, no payload to decode
+ const apc = "\x1b_Ga=d,d=a\x1b\\";
+ term.write(prefix + apc + suffix);
+ const encoder = new TextEncoder();
+ const allBytes = vi
+ .mocked(mockBridge.writeRaw)
+ .mock.calls.flatMap((c) => Array.from(c[0]));
+ // Every byte the bridge saw must come from prefix + suffix only.
+ const expected = Array.from(encoder.encode(prefix + suffix));
+ expect(allBytes).toEqual(expected);
+ // And ESC (0x1b) / underscore (0x5f) sequence should never reach the bridge.
+ expect(allBytes.includes(0x1b)).toBe(false);
});
});
@@ -244,7 +273,9 @@ describe("WTerm", () => {
}),
);
- expect(mockBridge.writeString).toHaveBeenCalledWith("a");
+ expect(mockBridge.writeRaw).toHaveBeenCalledWith(
+ new TextEncoder().encode("a"),
+ );
});
it("calls onData instead of write when provided", async () => {
diff --git a/packages/@wterm/dom/src/image-overlay.ts b/packages/@wterm/dom/src/image-overlay.ts
new file mode 100644
index 0000000..199706f
--- /dev/null
+++ b/packages/@wterm/dom/src/image-overlay.ts
@@ -0,0 +1,200 @@
+import type { KittyGraphicsEvent, KittyControl } from "./kitty-graphics.js";
+
+interface Stored {
+ /** PNG image bytes. */
+ data: Uint8Array;
+}
+
+interface Placement {
+ el: HTMLImageElement;
+ placementId: number;
+ imageId: string;
+ /** Cell column at top-left of the image. */
+ col: number;
+ /** Pixel offset from the top of `term-grid` to the image's top edge. */
+ topPx: number;
+ /** Object URL backing `el.src`; revoked when the placement is dropped. */
+ objectUrl: string;
+}
+
+const FMT_PNG = 100;
+
+/**
+ * Overlay layer that renders inline images (Kitty graphics protocol) on top
+ * of the terminal cell grid. Images are positioned absolutely inside the
+ * `term-grid` container so they remain aligned with their original content
+ * as new lines scroll into history.
+ */
+export class ImageOverlay {
+ private container: HTMLElement;
+ private layer: HTMLDivElement;
+ /** Source images keyed by `i:` (id) or `I:` (number). */
+ private images = new Map();
+ /** Active placements keyed by `${imageId}#${placementId}`. */
+ private placements = new Map();
+ private charWidthPx = 0;
+ private rowHeightPx = 0;
+
+ constructor(termGridContainer: HTMLElement) {
+ this.container = termGridContainer;
+ this.layer = document.createElement("div");
+ this.layer.className = "term-image-layer";
+ this.container.appendChild(this.layer);
+ }
+
+ /** Tell the overlay how many CSS pixels one cell occupies. */
+ setCellMetrics(charWidthPx: number, rowHeightPx: number): void {
+ this.charWidthPx = charWidthPx;
+ this.rowHeightPx = rowHeightPx;
+ }
+
+ /**
+ * Handle an incoming Kitty graphics event. `anchor` describes the current
+ * cursor position when the event arrives (used as the placement origin
+ * unless overridden by the event's own coordinates).
+ */
+ handle(
+ event: KittyGraphicsEvent,
+ anchor: { row: number; col: number; scrollbackCount: number },
+ ): void {
+ const action = String(event.control.a ?? "T");
+
+ if (action === "d") {
+ this._delete(event.control);
+ return;
+ }
+
+ if (action === "t" || action === "T") {
+ this._store(event);
+ }
+
+ if (action === "T" || action === "p") {
+ this._place(event.control, anchor);
+ }
+ }
+
+ /** Remove everything. Used on terminal reset / destroy. */
+ clear(): void {
+ this.images.clear();
+ for (const p of this.placements.values()) {
+ URL.revokeObjectURL(p.objectUrl);
+ p.el.remove();
+ }
+ this.placements.clear();
+ }
+
+ destroy(): void {
+ this.clear();
+ this.layer.remove();
+ }
+
+ private _store(event: KittyGraphicsEvent): void {
+ const f = event.control.f ?? FMT_PNG;
+ if (f !== FMT_PNG) {
+ // Only PNG inline transfer is supported in this MVP.
+ return;
+ }
+ const key = imageKey(event.control);
+ this.images.set(key, { data: event.data });
+ }
+
+ private _place(
+ control: KittyControl,
+ anchor: { row: number; col: number; scrollbackCount: number },
+ ): void {
+ const key = imageKey(control);
+ const stored = this.images.get(key);
+ if (!stored || stored.data.length === 0) return;
+
+ const placementId = typeof control.p === "number" ? control.p : 0;
+ const dedupeKey = `${key}#${placementId}`;
+ const existing = this.placements.get(dedupeKey);
+ if (existing) {
+ URL.revokeObjectURL(existing.objectUrl);
+ existing.el.remove();
+ }
+
+ const col = anchor.col + (typeof control.X === "number" ? control.X : 0);
+ const totalRow =
+ anchor.scrollbackCount +
+ anchor.row +
+ (typeof control.Y === "number" ? control.Y : 0);
+
+ const img = document.createElement("img");
+ img.className = "term-image";
+ img.draggable = false;
+ img.alt = "";
+ const blob = new Blob([new Uint8Array(stored.data)], { type: "image/png" });
+ const objectUrl = URL.createObjectURL(blob);
+ img.src = objectUrl;
+ const revoke = (): void => URL.revokeObjectURL(objectUrl);
+ img.addEventListener("load", revoke, { once: true });
+ img.addEventListener("error", revoke, { once: true });
+
+ const topPx = totalRow * this.rowHeightPx;
+ const leftPx = col * this.charWidthPx;
+
+ img.style.position = "absolute";
+ img.style.top = `${topPx}px`;
+ img.style.left = `${leftPx}px`;
+ if (typeof control.c === "number" && this.charWidthPx > 0) {
+ img.style.width = `${control.c * this.charWidthPx}px`;
+ }
+ if (typeof control.r === "number" && this.rowHeightPx > 0) {
+ img.style.height = `${control.r * this.rowHeightPx}px`;
+ }
+ if (typeof control.z === "number") {
+ img.style.zIndex = String(control.z);
+ }
+
+ this.layer.appendChild(img);
+ this.placements.set(dedupeKey, {
+ el: img,
+ placementId,
+ imageId: key,
+ col,
+ topPx,
+ objectUrl,
+ });
+ }
+
+ private _delete(control: KittyControl): void {
+ const specRaw = control.d;
+ const spec = typeof specRaw === "string" ? specRaw : "a";
+ const lower = spec.toLowerCase();
+
+ // Uppercase variants in the spec mean "also free image data". We treat
+ // upper and lower the same here since we don't separately track on-disk
+ // resources.
+ const removeMatching = (pred: (p: Placement) => boolean): void => {
+ for (const [k, p] of this.placements) {
+ if (pred(p)) {
+ URL.revokeObjectURL(p.objectUrl);
+ p.el.remove();
+ this.placements.delete(k);
+ }
+ }
+ };
+
+ switch (lower) {
+ case "a":
+ removeMatching(() => true);
+ break;
+ case "i": {
+ const key = imageKey(control);
+ removeMatching((p) => p.imageId === key);
+ if (spec === "I") this.images.delete(key);
+ break;
+ }
+ default:
+ // Other delete specifiers (z-index, range, etc.) not implemented.
+ break;
+ }
+ }
+}
+
+function imageKey(control: KittyControl): string {
+ if (typeof control.i === "number") return `i:${control.i}`;
+ if (typeof control.I === "number") return `I:${control.I}`;
+ return "i:0";
+}
diff --git a/packages/@wterm/dom/src/index.ts b/packages/@wterm/dom/src/index.ts
index 1ab81ec..0d3c724 100644
--- a/packages/@wterm/dom/src/index.ts
+++ b/packages/@wterm/dom/src/index.ts
@@ -10,4 +10,11 @@ export type {
PerfStats,
UnhandledEntry,
} from "./debug.js";
+export {
+ KittyGraphicsFilter,
+ type KittyGraphicsEvent,
+ type KittyControl,
+ type StreamEvent,
+} from "./kitty-graphics.js";
+export { ImageOverlay } from "./image-overlay.js";
export * from "@wterm/core";
diff --git a/packages/@wterm/dom/src/kitty-graphics.ts b/packages/@wterm/dom/src/kitty-graphics.ts
new file mode 100644
index 0000000..e1e9e72
--- /dev/null
+++ b/packages/@wterm/dom/src/kitty-graphics.ts
@@ -0,0 +1,360 @@
+/**
+ * Streaming parser for the Kitty terminal graphics protocol.
+ *
+ * Intercepts APC sequences of the form `\x1b_G;\x1b\\`
+ * (or BEL-terminated `\x1b_G;\x07`) from a byte stream,
+ * splits the input into pass-through text and graphics events, and
+ * accumulates chunked transfers (`m=1` followed by `m=0`).
+ *
+ * Spec: https://sw.kovidgoyal.net/kitty/graphics-protocol/
+ */
+
+export type KittyAction = "t" | "T" | "p" | "d" | "f" | "a" | "q";
+
+export interface KittyControl {
+ /** Action: t (transmit), T (transmit+display), p (put), d (delete), etc. */
+ a?: string;
+ /** Format: 100 = PNG, 24 = RGB, 32 = RGBA. */
+ f?: number;
+ /** Transport: d (direct base64, default), f (file), t (temp file), s (shared mem). */
+ t?: string;
+ /** Image id. */
+ i?: number;
+ /** Image number. */
+ I?: number;
+ /** More chunks follow (1) or last chunk (0). */
+ m?: number;
+ /** Quiet mode. */
+ q?: number;
+ /** Source pixel width (for raw formats). */
+ s?: number;
+ /** Source pixel height (for raw formats). */
+ v?: number;
+ /** Columns to fit. */
+ c?: number;
+ /** Rows to fit. */
+ r?: number;
+ /** Placement id. */
+ p?: number;
+ /** Z-index. */
+ z?: number;
+ /** Source rect: x, y offset. */
+ x?: number;
+ y?: number;
+ /** Source rect: width, height. */
+ w?: number;
+ h?: number;
+ /** Cell offset for placement. */
+ X?: number;
+ Y?: number;
+ /** Cursor-movement policy (0 default = move, 1 = don't move). */
+ C?: number;
+ /** Delete specifier: `a` (all), `i` (by id), etc. */
+ d?: string;
+ /** Catch-all for additional keys. */
+ [key: string]: number | string | undefined;
+}
+
+export interface KittyGraphicsEvent {
+ control: KittyControl;
+ /** Raw decoded payload bytes. Empty for control-only commands like delete. */
+ data: Uint8Array;
+}
+
+export type StreamEvent =
+ | { type: "text"; bytes: Uint8Array }
+ | { type: "graphics"; event: KittyGraphicsEvent };
+
+const ESC = 0x1b;
+const BEL = 0x07;
+const BACKSLASH = 0x5c;
+const UNDERSCORE = 0x5f;
+const G = 0x47;
+
+const enum State {
+ Idle = 0,
+ EscSeen = 1,
+ UnderscoreSeen = 2,
+ InKittyApc = 3,
+ InKittyApcEsc = 4,
+}
+
+interface PendingChunk {
+ control: KittyControl;
+ /** Concatenated base64 payload across chunks. */
+ payload: string;
+}
+
+/** Maximum number of simultaneously-buffered chunked transfers. */
+export const MAX_PENDING_CHUNKS = 8;
+/** Maximum total base64 bytes held across all in-flight chunked transfers. */
+export const MAX_PENDING_BASE64_BYTES = 32 * 1024 * 1024;
+
+/**
+ * Stateful streaming filter. Feed it raw bytes via {@link push}, receive
+ * an ordered list of pass-through and graphics events.
+ */
+export class KittyGraphicsFilter {
+ private state: State = State.Idle;
+ /** Buffered Kitty APC payload (bytes between `\x1b_G` and the terminator). */
+ private apcBuf: number[] = [];
+ /** Pending chunked transfers keyed by image id (i=) or image number (-I=). */
+ private pendingChunks = new Map();
+ /** Running total of buffered base64 bytes across all pending chunks. */
+ private pendingBytes = 0;
+
+ /**
+ * Push a chunk of bytes through the filter. Returns the ordered list of
+ * pass-through text segments and completed graphics events.
+ */
+ push(input: Uint8Array): StreamEvent[] {
+ const events: StreamEvent[] = [];
+ let textStart = -1;
+
+ const flushText = (endExclusive: number): void => {
+ if (textStart >= 0 && endExclusive > textStart) {
+ events.push({
+ type: "text",
+ bytes: input.slice(textStart, endExclusive),
+ });
+ }
+ textStart = -1;
+ };
+
+ const startText = (idx: number): void => {
+ if (textStart < 0) textStart = idx;
+ };
+
+ const emitBytesLiteral = (bytes: number[]): void => {
+ if (bytes.length === 0) return;
+ events.push({ type: "text", bytes: new Uint8Array(bytes) });
+ };
+
+ for (let i = 0; i < input.length; i++) {
+ const b = input[i];
+
+ switch (this.state) {
+ case State.Idle:
+ if (b === ESC) {
+ flushText(i);
+ this.state = State.EscSeen;
+ } else {
+ startText(i);
+ }
+ break;
+
+ case State.EscSeen:
+ if (b === UNDERSCORE) {
+ this.state = State.UnderscoreSeen;
+ } else if (b === ESC) {
+ emitBytesLiteral([ESC]);
+ // stay in EscSeen with the new ESC
+ } else {
+ emitBytesLiteral([ESC, b]);
+ this.state = State.Idle;
+ }
+ break;
+
+ case State.UnderscoreSeen:
+ if (b === G) {
+ this.state = State.InKittyApc;
+ this.apcBuf = [];
+ } else if (b === ESC) {
+ // \x1b_ — flush ESC + _ to output, reprocess ESC
+ emitBytesLiteral([ESC, UNDERSCORE]);
+ this.state = State.EscSeen;
+ } else {
+ emitBytesLiteral([ESC, UNDERSCORE, b]);
+ this.state = State.Idle;
+ }
+ break;
+
+ case State.InKittyApc:
+ if (b === ESC) {
+ this.state = State.InKittyApcEsc;
+ } else if (b === BEL) {
+ this._completeApc(events);
+ this.state = State.Idle;
+ } else {
+ this.apcBuf.push(b);
+ }
+ break;
+
+ case State.InKittyApcEsc:
+ if (b === BACKSLASH) {
+ this._completeApc(events);
+ this.state = State.Idle;
+ } else {
+ // Not ST; treat the ESC as part of the payload and reprocess `b`.
+ this.apcBuf.push(ESC);
+ this.state = State.InKittyApc;
+ i--;
+ }
+ break;
+ }
+ }
+
+ flushText(input.length);
+ return coalesce(events);
+ }
+
+ /**
+ * Discard any partially-accumulated state. Call when the upstream stream
+ * is reset (e.g. core re-init) so a half-buffered APC doesn't leak into
+ * the next session.
+ */
+ reset(): void {
+ this.state = State.Idle;
+ this.apcBuf = [];
+ this.pendingChunks.clear();
+ this.pendingBytes = 0;
+ }
+
+ private _completeApc(events: StreamEvent[]): void {
+ const buf = this.apcBuf;
+ this.apcBuf = [];
+
+ const semi = buf.indexOf(0x3b);
+ let ctrlBytes: number[];
+ let payloadBytes: number[];
+ if (semi < 0) {
+ ctrlBytes = buf;
+ payloadBytes = [];
+ } else {
+ ctrlBytes = buf.slice(0, semi);
+ payloadBytes = buf.slice(semi + 1);
+ }
+
+ const control = parseControl(decodeAscii(ctrlBytes));
+ const payloadB64 = decodeAscii(payloadBytes);
+
+ const more = control.m === 1;
+ const key = chunkKey(control);
+
+ if (more || this.pendingChunks.has(key)) {
+ const existing = this.pendingChunks.get(key);
+ if (existing) {
+ if (this.pendingBytes + payloadB64.length > MAX_PENDING_BASE64_BYTES) {
+ this.pendingBytes -= existing.payload.length;
+ this.pendingChunks.delete(key);
+ return;
+ }
+ existing.payload += payloadB64;
+ this.pendingBytes += payloadB64.length;
+ // Merge controls — later chunks may omit fields. Keep first-chunk
+ // values, but `m` reflects the latest.
+ existing.control.m = control.m;
+ } else {
+ if (payloadB64.length > MAX_PENDING_BASE64_BYTES) {
+ return;
+ }
+ if (this.pendingChunks.size >= MAX_PENDING_CHUNKS) {
+ const oldestKey = this.pendingChunks.keys().next().value;
+ if (oldestKey !== undefined) {
+ const oldest = this.pendingChunks.get(oldestKey);
+ if (oldest) this.pendingBytes -= oldest.payload.length;
+ this.pendingChunks.delete(oldestKey);
+ }
+ }
+ this.pendingChunks.set(key, {
+ control: { ...control },
+ payload: payloadB64,
+ });
+ this.pendingBytes += payloadB64.length;
+ }
+
+ if (more) return;
+
+ const completed = this.pendingChunks.get(key);
+ this.pendingChunks.delete(key);
+ if (!completed) return;
+ this.pendingBytes -= completed.payload.length;
+ let data: Uint8Array;
+ try {
+ data = decodeBase64(completed.payload);
+ } catch {
+ return;
+ }
+ events.push({
+ type: "graphics",
+ event: {
+ control: completed.control,
+ data,
+ },
+ });
+ return;
+ }
+
+ let data: Uint8Array;
+ try {
+ data = decodeBase64(payloadB64);
+ } catch {
+ return;
+ }
+ events.push({
+ type: "graphics",
+ event: {
+ control,
+ data,
+ },
+ });
+ }
+}
+
+/** Merge adjacent text events into a single chunk so callers see one writeRaw per contiguous run. */
+function coalesce(events: StreamEvent[]): StreamEvent[] {
+ if (events.length < 2) return events;
+ const out: StreamEvent[] = [];
+ for (const ev of events) {
+ const last = out[out.length - 1];
+ if (ev.type === "text" && last && last.type === "text") {
+ const merged = new Uint8Array(last.bytes.length + ev.bytes.length);
+ merged.set(last.bytes, 0);
+ merged.set(ev.bytes, last.bytes.length);
+ out[out.length - 1] = { type: "text", bytes: merged };
+ } else {
+ out.push(ev);
+ }
+ }
+ return out;
+}
+
+function chunkKey(control: KittyControl): string {
+ if (typeof control.i === "number") return `i:${control.i}`;
+ if (typeof control.I === "number") return `I:${control.I}`;
+ return "default";
+}
+
+const LATIN1 = new TextDecoder("latin1");
+
+function decodeAscii(bytes: number[]): string {
+ return LATIN1.decode(new Uint8Array(bytes));
+}
+
+function parseControl(s: string): KittyControl {
+ const out: KittyControl = {};
+ if (!s) return out;
+ for (const part of s.split(",")) {
+ const eq = part.indexOf("=");
+ if (eq < 0) continue;
+ const key = part.slice(0, eq).trim();
+ const val = part.slice(eq + 1).trim();
+ if (!key) continue;
+ const asNum = Number(val);
+ if (val !== "" && !Number.isNaN(asNum) && /^-?\d+$/.test(val)) {
+ out[key] = asNum;
+ } else {
+ out[key] = val;
+ }
+ }
+ return out;
+}
+
+function decodeBase64(b64: string): Uint8Array {
+ if (!b64) return new Uint8Array(0);
+ const cleaned = b64.replace(/[^A-Za-z0-9+/=]/g, "");
+ const binary = atob(cleaned);
+ const out = new Uint8Array(binary.length);
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
+ return out;
+}
diff --git a/packages/@wterm/dom/src/png.ts b/packages/@wterm/dom/src/png.ts
new file mode 100644
index 0000000..74f257b
--- /dev/null
+++ b/packages/@wterm/dom/src/png.ts
@@ -0,0 +1,30 @@
+/**
+ * Extract `(width, height)` in pixels from a PNG file's IHDR chunk without
+ * decoding the image. Returns `null` when the input is not a valid PNG.
+ */
+export function pngDimensions(
+ bytes: Uint8Array,
+): { width: number; height: number } | null {
+ if (bytes.length < 24) return null;
+ // PNG signature: 89 50 4E 47 0D 0A 1A 0A
+ if (
+ bytes[0] !== 0x89 ||
+ bytes[1] !== 0x50 ||
+ bytes[2] !== 0x4e ||
+ bytes[3] !== 0x47 ||
+ bytes[4] !== 0x0d ||
+ bytes[5] !== 0x0a ||
+ bytes[6] !== 0x1a ||
+ bytes[7] !== 0x0a
+ ) {
+ return null;
+ }
+ // The first chunk after the signature must be IHDR. Width/height live at
+ // bytes 16..23 as big-endian uint32.
+ const width =
+ (bytes[16] << 24) | (bytes[17] << 16) | (bytes[18] << 8) | bytes[19];
+ const height =
+ (bytes[20] << 24) | (bytes[21] << 16) | (bytes[22] << 8) | bytes[23];
+ if (width <= 0 || height <= 0) return null;
+ return { width: width >>> 0, height: height >>> 0 };
+}
diff --git a/packages/@wterm/dom/src/renderer.ts b/packages/@wterm/dom/src/renderer.ts
index a5ed237..7dda307 100644
--- a/packages/@wterm/dom/src/renderer.ts
+++ b/packages/@wterm/dom/src/renderer.ts
@@ -218,7 +218,16 @@ export class Renderer {
setup(cols: number, rows: number): void {
this.cols = cols;
this.rows = rows;
- this.container.innerHTML = "";
+ // Remove only renderer-owned row elements so overlay layers (e.g. inline
+ // image overlay) survive resize.
+ for (const child of Array.from(this.container.children)) {
+ if (
+ (child as HTMLElement).classList.contains("term-row") ||
+ (child as HTMLElement).classList.contains("term-scrollback-row")
+ ) {
+ child.remove();
+ }
+ }
this.rowEls = [];
this.prevRowBg = [];
this._scrollbackRowEls = [];
diff --git a/packages/@wterm/dom/src/terminal.css b/packages/@wterm/dom/src/terminal.css
index a88e475..50a4600 100644
--- a/packages/@wterm/dom/src/terminal.css
+++ b/packages/@wterm/dom/src/terminal.css
@@ -45,11 +45,26 @@
.term-grid {
display: block;
+ position: relative;
white-space: pre;
contain: layout paint style;
will-change: contents;
}
+.term-image-layer {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ z-index: 1;
+}
+
+.term-image {
+ position: absolute;
+ pointer-events: auto;
+ user-select: none;
+ -webkit-user-drag: none;
+}
+
.term-row {
display: block;
height: var(--term-row-height);
diff --git a/packages/@wterm/dom/src/wterm.ts b/packages/@wterm/dom/src/wterm.ts
index 0885ef9..f316b38 100644
--- a/packages/@wterm/dom/src/wterm.ts
+++ b/packages/@wterm/dom/src/wterm.ts
@@ -2,6 +2,9 @@ import { WasmBridge, type TerminalCore } from "@wterm/core";
import { Renderer } from "./renderer.js";
import { InputHandler } from "./input.js";
import { DebugAdapter } from "./debug.js";
+import { KittyGraphicsFilter } from "./kitty-graphics.js";
+import { ImageOverlay } from "./image-overlay.js";
+import { pngDimensions } from "./png.js";
export interface WTermOptions {
cols?: number;
@@ -15,6 +18,12 @@ export interface WTermOptions {
autoResize?: boolean;
cursorBlink?: boolean;
debug?: boolean;
+ /**
+ * Enable inline image rendering via the Kitty terminal graphics protocol.
+ * Defaults to `true`. Set to `false` to pass APC `_G` sequences through
+ * to the core unchanged.
+ */
+ images?: boolean;
onData?: (data: string) => void;
onTitle?: (title: string) => void;
onResize?: (cols: number, rows: number) => void;
@@ -39,7 +48,12 @@ export class WTerm {
private _destroyed = false;
private _shouldScrollToBottom = false;
private _rowHeight = 0;
+ private _charWidth = 0;
private _onClickFocus: () => void;
+ private _imagesEnabled: boolean;
+ private _kittyFilter: KittyGraphicsFilter | null = null;
+ private _imageOverlay: ImageOverlay | null = null;
+ private _textEncoder = new TextEncoder();
onData: ((data: string) => void) | null;
onTitle: ((title: string) => void) | null;
@@ -55,6 +69,7 @@ export class WTerm {
this.rows = options.rows || 24;
this.autoResize = options.autoResize !== false;
this._debugEnabled = options.debug ?? false;
+ this._imagesEnabled = options.images !== false;
this.onData = options.onData || null;
this.onTitle = options.onTitle || null;
@@ -94,6 +109,20 @@ export class WTerm {
this.renderer = new Renderer(this._container);
this.renderer.setup(this.cols, this.rows);
+ if (this._imagesEnabled) {
+ this._kittyFilter = new KittyGraphicsFilter();
+ this._imageOverlay = new ImageOverlay(this._container);
+ const cellSize = this._measureCharSize();
+ if (cellSize) {
+ this._charWidth = cellSize.charWidth;
+ this._rowHeight = cellSize.rowHeight;
+ this._imageOverlay.setCellMetrics(
+ cellSize.charWidth,
+ cellSize.rowHeight,
+ );
+ }
+ }
+
this.input = new InputHandler(
this.element,
(data) => {
@@ -145,7 +174,26 @@ export class WTerm {
if (!this.bridge) return;
if (this.debug) this.debug.traceWrite(data);
this._shouldScrollToBottom = this._isScrolledToBottom();
- if (typeof data === "string") {
+
+ if (this._kittyFilter && this._imageOverlay) {
+ const bytes =
+ typeof data === "string" ? this._textEncoder.encode(data) : data;
+ const events = this._kittyFilter.push(bytes);
+ for (const ev of events) {
+ if (ev.type === "text") {
+ this.bridge.writeRaw(ev.bytes);
+ continue;
+ }
+ const cursor = this.bridge.getCursor();
+ const scrollbackCount = this.bridge.getScrollbackCount();
+ this._imageOverlay.handle(ev.event, {
+ row: cursor.row,
+ col: cursor.col,
+ scrollbackCount,
+ });
+ this._advanceCursorForImage(ev.event);
+ }
+ } else if (typeof data === "string") {
this.bridge.writeString(data);
} else {
this.bridge.writeRaw(data);
@@ -153,6 +201,30 @@ export class WTerm {
this._scheduleRender();
}
+ private _advanceCursorForImage(event: {
+ control: Record;
+ data: Uint8Array;
+ }): void {
+ if (!this.bridge) return;
+ const action = String(event.control.a ?? "T");
+ if (action !== "T" && action !== "p") return;
+ if (event.control.C === 1) return;
+
+ let cellRows: number | null = null;
+ if (typeof event.control.r === "number" && event.control.r > 0) {
+ cellRows = event.control.r;
+ } else if (event.data.length > 0 && this._rowHeight > 0) {
+ const dims = pngDimensions(event.data);
+ if (dims) {
+ cellRows = Math.max(1, Math.ceil(dims.height / this._rowHeight));
+ }
+ }
+
+ if (cellRows == null) return;
+ const newlines = "\n".repeat(cellRows);
+ this.bridge.writeRaw(this._textEncoder.encode(newlines));
+ }
+
resize(cols: number, rows: number): void {
if (!this.bridge) return;
this._shouldScrollToBottom = this._isScrolledToBottom();
@@ -290,6 +362,9 @@ export class WTerm {
if (measured) {
charWidth = measured.charWidth;
rowHeight = measured.rowHeight;
+ this._charWidth = charWidth;
+ this._rowHeight = rowHeight;
+ this._imageOverlay?.setCellMetrics(charWidth, rowHeight);
}
for (const entry of entries) {
@@ -310,6 +385,9 @@ export class WTerm {
if (this.rafId != null) cancelAnimationFrame(this.rafId);
if (this.resizeObserver) this.resizeObserver.disconnect();
if (this.input) this.input.destroy();
+ if (this._imageOverlay) this._imageOverlay.destroy();
+ this._imageOverlay = null;
+ this._kittyFilter = null;
this.element.removeEventListener("click", this._onClickFocus);
this.element.innerHTML = "";
if (
diff --git a/packages/@wterm/react/src/Terminal.tsx b/packages/@wterm/react/src/Terminal.tsx
index f37062b..b313049 100644
--- a/packages/@wterm/react/src/Terminal.tsx
+++ b/packages/@wterm/react/src/Terminal.tsx
@@ -26,6 +26,11 @@ export interface TerminalProps extends Omit<
cursorBlink?: boolean;
/** Enable debug mode (init-only — changing after mount has no effect). */
debug?: boolean;
+ /**
+ * Enable inline image rendering via the Kitty terminal graphics protocol
+ * (init-only). Defaults to `true`.
+ */
+ images?: boolean;
onData?: (data: string) => void;
onTitle?: (title: string) => void;
onResize?: (cols: number, rows: number) => void;
@@ -50,6 +55,7 @@ const Terminal = forwardRef(function Terminal(
autoResize = false,
cursorBlink = false,
debug = false,
+ images,
onData,
onTitle,
onResize,
@@ -103,6 +109,7 @@ const Terminal = forwardRef(function Terminal(
autoResize: autoResizeRef.current,
cursorBlink,
debug,
+ images,
onData: callbacksRef.current.onData
? (data: string) => callbacksRef.current.onData?.(data)
: undefined,
diff --git a/packages/@wterm/vue/src/Terminal.ts b/packages/@wterm/vue/src/Terminal.ts
index cc6e4df..481eaf5 100644
--- a/packages/@wterm/vue/src/Terminal.ts
+++ b/packages/@wterm/vue/src/Terminal.ts
@@ -92,6 +92,12 @@ const Terminal = defineComponent({
* @defaultValue false
*/
debug: Boolean,
+ /**
+ * Enable inline image rendering via the Kitty terminal graphics protocol
+ * (init-only — changing after mount has no effect).
+ * @defaultValue true
+ */
+ images: { type: Boolean, default: true },
},
// Object form: validator signatures carry emit payload types to
@@ -140,6 +146,7 @@ const Terminal = defineComponent({
autoResize: props.autoResize,
cursorBlink: props.cursorBlink,
debug: props.debug,
+ images: props.images,
onData: hasDataListener
? (data: string) => emit("data", data)
: undefined,
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 432b5e2..a0c1e0e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -5,9 +5,7 @@ settings:
excludeLinksFromLockfile: false
patchedDependencies:
- next-themes@0.4.6:
- hash: 73478c20fb87207168b5c0fa9ccabf0c042408da2bc5a36131ed1c8bb57bf5a3
- path: patches/next-themes@0.4.6.patch
+ next-themes@0.4.6: 73478c20fb87207168b5c0fa9ccabf0c042408da2bc5a36131ed1c8bb57bf5a3
importers:
@@ -127,7 +125,7 @@ importers:
version: 9.39.4(jiti@2.6.1)
eslint-config-next:
specifier: 16.2.3
- version: 16.2.3(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2)
+ version: 16.2.3(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2)
tailwindcss:
specifier: ^4
version: 4.2.2
@@ -151,6 +149,22 @@ importers:
specifier: ^6.3.5
version: 6.4.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)
+ examples/kitty-images:
+ dependencies:
+ '@wterm/dom':
+ specifier: workspace:*
+ version: link:../../packages/@wterm/dom
+ '@wterm/ghostty':
+ specifier: workspace:*
+ version: link:../../packages/@wterm/ghostty
+ devDependencies:
+ typescript:
+ specifier: ^6.0.2
+ version: 6.0.2
+ vite:
+ specifier: ^6.3.5
+ version: 6.4.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)
+
examples/local:
dependencies:
'@tailwindcss/postcss':
@@ -210,7 +224,7 @@ importers:
version: 9.39.4(jiti@2.6.1)
eslint-config-next:
specifier: 16.2.3
- version: 16.2.3(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2)
+ version: 16.2.3(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2)
tsx:
specifier: ^4.19.4
version: 4.21.0
@@ -1495,89 +1509,105 @@ packages:
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
@@ -1756,24 +1786,28 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@next/swc-linux-arm64-musl@16.2.3':
resolution: {integrity: sha512-/YV0LgjHUmfhQpn9bVoGc4x4nan64pkhWR5wyEV8yCOfwwrH630KpvRg86olQHTwHIn1z59uh6JwKvHq1h4QEw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@next/swc-linux-x64-gnu@16.2.3':
resolution: {integrity: sha512-/HiWEcp+WMZ7VajuiMEFGZ6cg0+aYZPqCJD3YJEfpVWQsKYSjXQG06vJP6F1rdA03COD9Fef4aODs3YxKx+RDQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@next/swc-linux-x64-musl@16.2.3':
resolution: {integrity: sha512-Kt44hGJfZSefebhk/7nIdivoDr3Ugp5+oNz9VvF3GUtfxutucUIHfIO0ZYO8QlOPDQloUVQn4NVC/9JvHRk9hw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@next/swc-win32-arm64-msvc@16.2.3':
resolution: {integrity: sha512-O2NZ9ie3Tq6xj5Z5CSwBT3+aWAMW2PIZ4egUi9MaWLkwaehgtB7YZjPm+UpcNpKOme0IQuqDcor7BsW6QBiQBw==}
@@ -2568,36 +2602,42 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.15':
resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15':
resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15':
resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.15':
resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.0.0-rc.15':
resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@rolldown/binding-openharmony-arm64@1.0.0-rc.15':
resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==}
@@ -2680,66 +2720,79 @@ packages:
resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.60.1':
resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==}
cpu: [arm]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.60.1':
resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.60.1':
resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.60.1':
resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==}
cpu: [loong64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.60.1':
resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==}
cpu: [loong64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.60.1':
resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.60.1':
resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==}
cpu: [ppc64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.60.1':
resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.60.1':
resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==}
cpu: [riscv64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.60.1':
resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.60.1':
resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.60.1':
resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-openbsd-x64@4.60.1':
resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==}
@@ -2856,24 +2909,28 @@ packages:
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.2.2':
resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.2.2':
resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.2.2':
resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.2.2':
resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==}
@@ -3208,6 +3265,7 @@ packages:
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
+ deprecated: Potential CWE-502 - Update to 1.3.1 or higher
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==}
@@ -3248,41 +3306,49 @@ packages:
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
cpu: [riscv64]
os: [linux]
+ libc: [musl]
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
@@ -5382,24 +5448,28 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
lightningcss-linux-x64-gnu@1.32.0:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
+ libc: [musl]
lightningcss-win32-arm64-msvc@1.32.0:
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
@@ -11277,7 +11347,7 @@ snapshots:
'@next/eslint-plugin-next': 16.2.3
eslint: 9.39.4(jiti@2.6.1)
eslint-import-resolver-node: 0.3.10
- eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1))
+ eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1))
@@ -11320,7 +11390,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)):
+ eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3
@@ -11335,11 +11405,36 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)):
+ eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)):
+ dependencies:
+ '@nolyfill/is-core-module': 1.0.39
+ debug: 4.4.3
+ eslint: 9.39.4(jiti@2.6.1)
+ get-tsconfig: 4.13.7
+ is-bun-module: 2.0.0
+ stable-hash: 0.0.5
+ tinyglobby: 0.2.16
+ unrs-resolver: 1.11.1
+ optionalDependencies:
+ eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
+ transitivePeerDependencies:
+ - supports-color
+
+ eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2)
+ eslint: 9.39.4(jiti@2.6.1)
+ eslint-import-resolver-node: 0.3.10
+ eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))
+ transitivePeerDependencies:
+ - supports-color
+
+ eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)):
+ dependencies:
+ debug: 3.2.7
+ optionalDependencies:
eslint: 9.39.4(jiti@2.6.1)
eslint-import-resolver-node: 0.3.10
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1))
@@ -11357,7 +11452,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.39.4(jiti@2.6.1)
eslint-import-resolver-node: 0.3.10
- eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
+ eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -11386,7 +11481,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.39.4(jiti@2.6.1)
eslint-import-resolver-node: 0.3.10
- eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
+ eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3