Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions bench/versus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { Terminal as XTerm } from '@xterm/xterm';
import { bench, group, run } from 'mitata';
import { Ghostty, Terminal as GhosttyTerminal } from '../lib';
import '../happydom';

function generateColorText(lines: number): string {
const colors = [31, 32, 33, 34, 35, 36];
let output = '';
for (let i = 0; i < lines; i++) {
const color = colors[i % colors.length];
output += `\x1b[${color}mLine ${i}: This is some colored text with ANSI escape sequences\x1b[0m\r\n`;
}
return output;
}

function generateComplexVT(lines: number): string {
let output = '';
for (let i = 0; i < lines; i++) {
output += `\x1b[1;4;38;2;255;128;0mBold underline RGB\x1b[0m `;
output += `\x1b[48;5;236mBG 256\x1b[0m `;
output += `\x1b[7mInverse\x1b[0m\r\n`;
}
return output;
}

function generateRawBytes(size: number): Uint8Array {
const data = new Uint8Array(size);
for (let i = 0; i < size; i++) {
const mod = i % 85;
if (mod < 80) {
data[i] = 32 + (i % 95); // Printable ASCII
} else if (mod === 80) {
data[i] = 13; // \r
} else {
data[i] = 10; // \n
}
}
return data;
}

function generateCursorMovement(ops: number): string {
let output = '';
for (let i = 0; i < ops; i++) {
output += `\x1b[${(i % 24) + 1};${(i % 80) + 1}H`; // Cursor position
output += `\x1b[K`; // Clear to end of line
output += `Text at position ${i}`;
output += `\x1b[A\x1b[B\x1b[C\x1b[D`; // Up, Down, Right, Left
}
return output;
}

const withTerminals = async (fn: (term: GhosttyTerminal | XTerm) => Promise<void>) => {
const ghostty = await Ghostty.load();
bench('ghostty-web', async () => {
const container = document.createElement('div');
document.body.appendChild(container);
const term = new GhosttyTerminal({ ghostty });
await term.open(container);
await fn(term);
await term.dispose();
});
bench('xterm.js', async () => {
const xterm = new XTerm();
const container = document.createElement('div');
document.body.appendChild(container);
await xterm.open(container);
await fn(xterm);
await xterm.dispose();
});
};

const throughput = async (prefix: string, data: Record<string, Uint8Array | string>) => {
await Promise.all(
Object.entries(data).map(async ([name, data]) => {
await group(`${prefix}: ${name}`, async () => {
await withTerminals(async (term) => {
await new Promise<void>((resolve) => {
term.write(data, resolve);
});
});
});
})
);
};

await throughput('raw bytes', {
'1KB': generateRawBytes(1024),
'10KB': generateRawBytes(10 * 1024),
'100KB': generateRawBytes(100 * 1024),
'1MB': generateRawBytes(1024 * 1024),
});

await throughput('color text', {
'100 lines': generateColorText(100),
'1000 lines': generateColorText(1000),
'10000 lines': generateColorText(10000),
});

await throughput('complex VT', {
'100 lines': generateComplexVT(100),
'1000 lines': generateComplexVT(1000),
'10000 lines': generateComplexVT(10000),
});

await throughput('cursor movement', {
'1000 operations': generateCursorMovement(1000),
'10000 operations': generateCursorMovement(10000),
'100000 operations': generateCursorMovement(100000),
});

await group('read full viewport', async () => {
await withTerminals(async (term) => {
const lines = term.rows;
for (let i = 0; i < lines; i++) {
const line = term.buffer.active.getLine(i);
if (!line) {
continue;
}
line.translateToString();
}
});
});

await run();
9 changes: 9 additions & 0 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
"@biomejs/biome": "^1.9.4",
"@happy-dom/global-registrator": "^15.11.0",
"@types/bun": "^1.3.2",
"@xterm/headless": "^5.5.0",
"@xterm/xterm": "^5.5.0",
"mitata": "^1.0.34",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"vite": "^4.5.0",
Expand Down Expand Up @@ -140,6 +143,10 @@

"@vue/shared": ["@vue/[email protected]", "", {}, "sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A=="],

"@xterm/headless": ["@xterm/[email protected]", "", {}, "sha512-5xXB7kdQlFBP82ViMJTwwEc3gKCLGKR/eoxQm4zge7GPBl86tCdI0IdPJjoKd8mUSFXz5V7i/25sfsEkP4j46g=="],

"@xterm/xterm": ["@xterm/[email protected]", "", {}, "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="],

"acorn": ["[email protected]", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],

"ajv": ["[email protected]", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA=="],
Expand Down Expand Up @@ -218,6 +225,8 @@

"minimatch": ["[email protected]", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],

"mitata": ["[email protected]", "", {}, "sha512-Mc3zrtNBKIMeHSCQ0XqRLo1vbdIx1wvFV9c8NJAiyho6AjNfMY8bVhbS12bwciUdd1t4rj8099CH3N3NFahaUA=="],

"mlly": ["[email protected]", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="],

"ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
Expand Down
31 changes: 14 additions & 17 deletions demo/bin/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -446,23 +446,20 @@ wss.on('connection', (ws, req) => {
});

// Send welcome message
setTimeout(() => {
if (ws.readyState !== ws.OPEN) return;
const C = '\x1b[1;36m'; // Cyan
const G = '\x1b[1;32m'; // Green
const Y = '\x1b[1;33m'; // Yellow
const R = '\x1b[0m'; // Reset
ws.send(`${C}╔══════════════════════════════════════════════════════════════╗${R}\r\n`);
ws.send(
`${C}║${R} ${G}Welcome to ghostty-web!${R} ${C}║${R}\r\n`
);
ws.send(`${C}║${R} ${C}║${R}\r\n`);
ws.send(`${C}║${R} You have a real shell session with full PTY support. ${C}║${R}\r\n`);
ws.send(
`${C}║${R} Try: ${Y}ls${R}, ${Y}cd${R}, ${Y}top${R}, ${Y}vim${R}, or any command! ${C}║${R}\r\n`
);
ws.send(`${C}╚══════════════════════════════════════════════════════════════╝${R}\r\n\r\n`);
}, 100);
const C = '\x1b[1;36m'; // Cyan
const G = '\x1b[1;32m'; // Green
const Y = '\x1b[1;33m'; // Yellow
const R = '\x1b[0m'; // Reset
ws.send(`${C}╔══════════════════════════════════════════════════════════════╗${R}\r\n`);
ws.send(
`${C}║${R} ${G}Welcome to ghostty-web!${R} ${C}║${R}\r\n`
);
ws.send(`${C}║${R} ${C}║${R}\r\n`);
ws.send(`${C}║${R} You have a real shell session with full PTY support. ${C}║${R}\r\n`);
ws.send(
`${C}║${R} Try: ${Y}ls${R}, ${Y}cd${R}, ${Y}top${R}, ${Y}vim${R}, or any command! ${C}║${R}\r\n`
);
ws.send(`${C}╚══════════════════════════════════════════════════════════════╝${R}\r\n\r\n`);
});

// ============================================================================
Expand Down
147 changes: 147 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 48 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
description = "ghostty-web - Web terminal using Ghostty's VT100 parser via WASM";

inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
zig-overlay.url = "github:mitchellh/zig-overlay";
};

outputs = { self, nixpkgs, flake-utils, zig-overlay }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system;
overlays = [ zig-overlay.overlays.default ];
};
zig = pkgs.zigpkgs."0.15.2";
in {
devShells.default = pkgs.mkShell {
buildInputs = [
pkgs.bun
pkgs.nodejs_22
zig
];
};

packages.default = pkgs.stdenv.mkDerivation {
pname = "ghostty-web";
version = "0.3.0";

src = ./.;

nativeBuildInputs = [ pkgs.bun pkgs.nodejs_22 ];

buildPhase = ''
export HOME=$TMPDIR
bun install --frozen-lockfile
bun run build
'';

installPhase = ''
mkdir -p $out
cp -r dist/* $out/
'';
};
}
);
}
2 changes: 1 addition & 1 deletion ghostty
Submodule ghostty updated 294 files
Loading