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
- Node.js Platform - Running in Node.js
- Platform Differences - Key differences
- Examples - More examples