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