Skip to main content

Browser Platform

Running unblessed in web browsers with XTerm.js.

Installation

npm install @unblessed/browser xterm
# or
pnpm add @unblessed/browser xterm
# or
yarn add @unblessed/browser xterm

Quick Start

HTML Setup

<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="node_modules/xterm/css/xterm.css" />
</head>
<body>
<div id="terminal"></div>
<script type="module" src="./app.js"></script>
</body>
</html>

JavaScript/TypeScript

import { Terminal } from "xterm";
import { FitAddon } from "@xterm/addon-fit";
import { Screen, Box } from "@unblessed/browser";

// Create XTerm terminal
const term = new Terminal({
cursorBlink: false,
fontSize: 14,
theme: {
background: "#1e1e1e",
foreground: "#d4d4d4",
},
});

// Add fit addon
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);

// Open terminal in DOM
term.open(document.getElementById("terminal"));
fitAddon.fit();

// Create unblessed screen with terminal
const screen = new Screen({ terminal: term });

// Create widgets
const box = new Box({
parent: screen,
top: "center",
left: "center",
width: "50%",
height: "50%",
content: "Hello from Browser!",
border: { type: "line" },
style: { border: { fg: "cyan" } },
});

// Render
screen.render();

Runtime Auto-Initialization

@unblessed/browser automatically initializes the browser runtime:

import { Screen } from "@unblessed/browser";

// Runtime is automatically initialized with polyfills
// - File system polyfills with bundled terminfo
// - Process polyfills for events
// - Buffer polyfills
// - Stream polyfills

const screen = new Screen({ terminal: term });

XTerm.js Integration

Screen Creation

The Screen automatically detects and wraps XTerm.js:

import { Terminal } from "xterm";
import { Screen } from "@unblessed/browser";

const term = new Terminal();
term.open(element);

// Screen auto-creates XTermAdapter
const screen = new Screen({
terminal: term,
smartCSR: true, // Enabled by default
fastCSR: true, // Enabled by default
fullUnicode: true, // Enabled by default
});

Terminal Options

Configure XTerm.js terminal:

const term = new Terminal({
// Display
cursorBlink: true,
cursorStyle: "block",
fontSize: 14,
fontFamily: "Monaco, monospace",

// Size
rows: 24,
cols: 80,

// Theme
theme: {
background: "#000000",
foreground: "#ffffff",
cursor: "#ffffff",
black: "#000000",
red: "#cd0000",
green: "#00cd00",
yellow: "#cdcd00",
blue: "#0000ee",
magenta: "#cd00cd",
cyan: "#00cdcd",
white: "#e5e5e5",
},

// Scrolling
scrollback: 1000,

// Selection
allowTransparency: true,
allowProposedApi: true,
});

Addons

Use XTerm.js addons:

import { FitAddon } from "@xterm/addon-fit";
import { WebLinksAddon } from "@xterm/addon-web-links";
import { SearchAddon } from "@xterm/addon-search";

const fitAddon = new FitAddon();
const webLinksAddon = new WebLinksAddon();
const searchAddon = new SearchAddon();

term.loadAddon(fitAddon);
term.loadAddon(webLinksAddon);
term.loadAddon(searchAddon);

// Fit terminal to container
fitAddon.fit();

Browser-Specific Features

Responsive Sizing

Handle window resize:

const fitAddon = new FitAddon();
term.loadAddon(fitAddon);

window.addEventListener("resize", () => {
fitAddon.fit();
screen.render();
});

Full Screen Mode

Toggle full screen:

const container = document.getElementById("terminal");

function toggleFullScreen() {
if (!document.fullscreenElement) {
container.requestFullscreen();
} else {
document.exitFullscreen();
}
}

screen.key("f", toggleFullScreen);

Copy/Paste

Enable clipboard integration:

// Browser handles copy automatically
// Paste with Ctrl+V or Cmd+V
term.onData((data) => {
// Handle pasted data
screen.program.write(data);
});

Mouse Support

Mouse events work automatically:

const box = new Box({
parent: screen,
mouse: true, // Enable mouse
});

box.on("click", (data) => {
console.log("Clicked at:", data.x, data.y);
});

box.on("wheeldown", () => {
box.scroll(1);
screen.render();
});

Polyfills

File System

Limited file system with bundled terminfo:

// Only bundled files are accessible
// Primarily used internally for terminfo

// Available files:
// - /usr/share/terminfo/x/xterm
// - /usr/share/terminfo/x/xterm-256color
// etc.

Process

Process polyfill for events:

// process.env - empty object in browser
console.log(process.env); // {}

// process.platform - always 'browser'
console.log(process.platform); // 'browser'

// process.exit() - throws error in browser
// Don't use process.exit() in browser code!

Buffer

Full Buffer API via buffer package:

import { Buffer } from "buffer";

const buf = Buffer.from("Hello");
console.log(buf.toString()); // 'Hello'

Streams

Stream polyfills via stream-browserify:

import { Readable, Writable } from "stream";

// Works like Node.js streams
const readable = new Readable();
readable.push("data");

Building for Production

Vite

Recommended bundler for browser apps:

// vite.config.ts
import { defineConfig } from "vite";

export default defineConfig({
resolve: {
alias: {
buffer: "buffer/",
stream: "stream-browserify",
util: "util/",
},
},
define: {
"process.env": {},
global: "globalThis",
},
optimizeDeps: {
include: ["buffer", "stream-browserify", "util"],
},
});

Webpack

Configure webpack:

// webpack.config.js
module.exports = {
resolve: {
fallback: {
buffer: require.resolve("buffer/"),
stream: require.resolve("stream-browserify"),
util: require.resolve("util/"),
},
},
plugins: [
new webpack.ProvidePlugin({
Buffer: ["buffer", "Buffer"],
process: "process/browser",
}),
],
};

Bundle Size

Optimize bundle size:

// Only import what you need
import { Screen, Box, List } from "@unblessed/browser";

// Tree-shaking removes unused widgets
// Final bundle: ~150KB gzipped (including XTerm.js)

Examples

Interactive Dashboard

import { Terminal } from "xterm";
import { FitAddon } from "@xterm/addon-fit";
import { Screen, Box, List } from "@unblessed/browser";

const term = new Terminal();
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
term.open(document.getElementById("terminal"));
fitAddon.fit();

const screen = new Screen({ terminal: term });

const header = new Box({
parent: screen,
top: 0,
left: 0,
width: "100%",
height: 3,
content: "{center}{bold}Dashboard{/bold}{/center}",
tags: true,
style: { bg: "blue", fg: "white" },
});

const sidebar = new List({
parent: screen,
top: 3,
left: 0,
width: "30%",
height: "100%-3",
items: ["Overview", "Analytics", "Settings"],
keys: true,
mouse: true,
style: {
selected: { bg: "cyan", fg: "black" },
},
});

const content = new Box({
parent: screen,
top: 3,
left: "30%",
width: "70%",
height: "100%-3",
border: { type: "line" },
});

sidebar.on("select", (item) => {
content.setContent(`Selected: ${item.getText()}`);
screen.render();
});

sidebar.focus();
screen.render();

Live Code Editor

import { Terminal } from "xterm";
import { Screen, Box, Textbox } from "@unblessed/browser";

const term = new Terminal();
term.open(document.getElementById("terminal"));

const screen = new Screen({ terminal: term });

const editor = new Textbox({
parent: screen,
top: 0,
left: 0,
width: "100%",
height: "50%",
border: { type: "line" },
label: " Editor ",
inputOnFocus: true,
keys: true,
mouse: true,
});

const output = new Box({
parent: screen,
top: "50%",
left: 0,
width: "100%",
height: "50%",
border: { type: "line" },
label: " Output ",
scrollable: true,
mouse: true,
});

editor.on("submit", (value) => {
try {
const result = eval(value);
output.setContent(`> ${result}`);
} catch (error) {
output.setContent(`Error: ${error.message}`);
}
screen.render();
});

editor.focus();
screen.render();

Performance Tips

1. Throttle Renders

Limit render frequency:

let rafId: number | null = null;

function scheduleRender() {
if (rafId === null) {
rafId = requestAnimationFrame(() => {
screen.render();
rafId = null;
});
}
}

// Use instead of direct screen.render()
box.setContent("Update");
scheduleRender();

2. Virtualize Long Lists

Only render visible items:

class VirtualList extends List {
renderVisibleItems() {
const start = this.childBase;
const end = start + this.height;

// Only render items in view
return this.items.slice(start, end);
}
}

3. Debounce Resize

Prevent excessive re-renders:

let resizeTimeout: number;

window.addEventListener("resize", () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
fitAddon.fit();
screen.render();
}, 100);
});

Debugging

Console Logging

Debug to browser console:

screen.on("keypress", (ch, key) => {
console.log("Key:", key);
});

box.on("click", (data) => {
console.log("Click:", data);
});

DevTools

Use browser DevTools:

// Expose screen globally for debugging
(window as any).screen = screen;

// In console:
// > screen.children
// > screen.render()

Performance Profiling

Use Chrome DevTools Performance tab:

console.time("render");
screen.render();
console.timeEnd("render");

Limitations

No Process.exit()

Can't exit browser:

// ❌ Don't use
screen.key("q", () => process.exit(0));

// ✅ Use instead
screen.key("q", () => {
screen.destroy();
term.dispose();
});

No File System

Limited to bundled files:

// ❌ Won't work
fs.readFileSync("./myfile.txt");

// ✅ Fetch from server instead
fetch("/api/myfile.txt")
.then((res) => res.text())
.then((data) => box.setContent(data));

No Child Processes

Can't spawn processes:

// ❌ Not available
child_process.spawn("ls");

// ✅ Use web APIs instead
// - WebWorkers for background tasks
// - WebSockets for server communication

Best Practices

1. Handle Terminal Lifecycle

Clean up properly:

function cleanup() {
screen.destroy();
term.dispose();
}

window.addEventListener("beforeunload", cleanup);

2. Responsive Design

Adapt to container size:

const fitAddon = new FitAddon();
term.loadAddon(fitAddon);

const resizeObserver = new ResizeObserver(() => {
fitAddon.fit();
screen.render();
});

resizeObserver.observe(container);

3. Error Boundaries

Catch and display errors:

window.addEventListener("error", (event) => {
errorBox.setContent(`Error: ${event.message}`);
screen.render();
});

4. Progressive Enhancement

Detect browser capabilities:

const hasClipboard = !!navigator.clipboard;
const hasFullscreen = !!document.fullscreenEnabled;

if (hasFullscreen) {
screen.key("f", toggleFullScreen);
}

Next Steps