Skip to main content

Platform Differences

Key differences between Node.js and Browser platforms.

Quick Comparison

FeatureNode.jsBrowser
RuntimeNative Node.js APIsPolyfills + XTerm.js
TerminalDirect TTYXTerm.js emulation
File SystemFull accessBundled terminfo only
ProcessFull process controlLimited polyfill
Exitprocess.exit()Destroy screen + terminal
StreamsNative streamsstream-browserify
BufferNative Bufferbuffer package
PerformanceFaster (native)Slower (emulation)
Bundle SizeN/A~150KB gzipped
MouseTerminal-dependentAlways available
ResizeSIGWINCH signalWindow resize events

Installation

Node.js

pnpm add @unblessed/node

Includes:

  • Node.js runtime implementation
  • All core widgets
  • Direct TTY support

Browser

pnpm add @unblessed/browser xterm

Includes:

  • Browser runtime with polyfills
  • All core widgets
  • XTerm.js integration

Initialization

Node.js

Automatic runtime initialization:

import { Screen, Box } from "@unblessed/node";

// Runtime auto-initialized with Node.js APIs
const screen = new Screen();

Browser

Requires XTerm.js terminal:

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

// Create and mount terminal
const term = new Terminal();
term.open(document.getElementById("terminal"));

// Runtime auto-initialized with polyfills
const screen = new Screen({ terminal: term });

API Differences

Screen Creation

Node.js:

const screen = new Screen({
input: process.stdin, // Node.js stream
output: process.stdout, // Node.js stream
terminal: "xterm-256color",
});

Browser:

const screen = new Screen({
terminal: term, // XTerm.js Terminal instance
});

Exiting

Node.js:

screen.key(["q", "C-c"], () => {
process.exit(0); // Works in Node.js
});

Browser:

screen.key(["q", "C-c"], () => {
screen.destroy();
term.dispose(); // Can't actually exit browser
});

File Access

Node.js:

import { readFileSync } from "fs";

const data = readFileSync("./config.json", "utf8");
box.setContent(data);

Browser:

// Use fetch API instead
const response = await fetch("/api/config.json");
const data = await response.text();
box.setContent(data);

Environment Variables

Node.js:

const apiKey = process.env.API_KEY;
const isDev = process.env.NODE_ENV === "development";

Browser:

// process.env is empty object
// Use build-time env variables instead
const apiKey = import.meta.env.VITE_API_KEY;
const isDev = import.meta.env.DEV;

Feature Availability

Available in Both

✅ All widgets (Box, List, Table, Form, etc.) ✅ Event system ✅ Keyboard input ✅ Mouse support ✅ Rendering pipeline ✅ Styling and colors ✅ Content tags ✅ Focus management

Node.js Only

✅ Direct TTY control ✅ File system access ✅ Child processes ✅ Process signals (SIGINT, SIGTERM) ✅ Native streams ✅ process.exit()

Browser Only

✅ XTerm.js integration ✅ DOM integration ✅ Full-screen mode ✅ Browser DevTools ✅ requestAnimationFrame ✅ ResizeObserver ✅ WebWorkers (for async tasks)

Performance Differences

Node.js

Advantages:

  • Direct TTY access (no emulation)
  • Faster rendering (~6.5ms empty screen)
  • Lower memory usage
  • Native Buffer operations

Use cases:

  • CLI tools
  • Server monitoring
  • Build tools
  • System utilities

Browser

Advantages:

  • Works in web browsers
  • No installation required
  • Cross-platform web apps
  • Better for demos/documentation

Trade-offs:

  • Slower rendering (~10-15ms empty screen)
  • Higher memory usage
  • Larger bundle size (~150KB)
  • XTerm.js emulation overhead

Use cases:

  • Web-based terminals
  • Interactive documentation
  • Browser DevTools
  • Remote terminals

Code Compatibility

Write Once, Run Anywhere

Most code works identically:

// Works in both Node.js and Browser!
import { Screen, Box, List } from "@unblessed/node"; // or browser

const screen = new Screen(/* platform-specific options */);

const box = new Box({
parent: screen,
top: "center",
left: "center",
width: "50%",
height: "50%",
content: "Hello World!",
border: { type: "line" },
});

screen.key("q", () => {
/* platform-specific cleanup */
});

screen.render();

Platform Detection

Detect current platform:

import { getRuntime } from "@unblessed/core/runtime-context";

const runtime = getRuntime();

if (runtime.process.platform === "browser") {
// Browser-specific code
console.log("Running in browser");
} else {
// Node.js-specific code
console.log("Running in Node.js");
}

Conditional Imports

Use dynamic imports for platform-specific code:

async function initApp() {
if (typeof window !== "undefined") {
// Browser
const { Terminal } = await import("xterm");
const { Screen } = await import("@unblessed/browser");

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

return new Screen({ terminal: term });
} else {
// Node.js
const { Screen } = await import("@unblessed/node");
return new Screen();
}
}

Migration Guide

Node.js to Browser

  1. Add XTerm.js:
pnpm add xterm @xterm/addon-fit
  1. Change imports:
// Before
import { Screen } from "@unblessed/node";

// After
import { Terminal } from "xterm";
import { Screen } from "@unblessed/browser";
  1. Create terminal:
// Add this
const term = new Terminal();
term.open(document.getElementById("terminal"));

// Update screen creation
const screen = new Screen({ terminal: term });
  1. Update file access:
// Before
import { readFileSync } from "fs";
const data = readFileSync("./data.txt", "utf8");

// After
const response = await fetch("/data.txt");
const data = await response.text();
  1. Update exit handling:
// Before
screen.key("q", () => process.exit(0));

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

Browser to Node.js

  1. Remove XTerm.js:
pnpm remove xterm
  1. Change imports:
// Before
import { Terminal } from "xterm";
import { Screen } from "@unblessed/browser";

// After
import { Screen } from "@unblessed/node";
  1. Simplify screen creation:
// Before
const term = new Terminal();
term.open(element);
const screen = new Screen({ terminal: term });

// After
const screen = new Screen();
  1. Use Node.js APIs:
// Before
const response = await fetch("/data.txt");
const data = await response.text();

// After
import { readFileSync } from "fs";
const data = readFileSync("./data.txt", "utf8");

Best Practices

1. Abstract Platform Differences

Create platform adapters:

// platform.ts
export interface PlatformAdapter {
createScreen(): Screen;
readFile(path: string): Promise<string>;
exit(): void;
}

// node-platform.ts
export class NodePlatform implements PlatformAdapter {
createScreen() {
return new Screen();
}

async readFile(path: string) {
return readFileSync(path, "utf8");
}

exit() {
process.exit(0);
}
}

// browser-platform.ts
export class BrowserPlatform implements PlatformAdapter {
constructor(private term: Terminal) {}

createScreen() {
return new Screen({ terminal: this.term });
}

async readFile(path: string) {
const response = await fetch(path);
return response.text();
}

exit() {
// Can't exit browser
console.log("Application closed");
}
}

2. Feature Detection

Check for capabilities:

function hasFileSystem(): boolean {
try {
const runtime = getRuntime();
runtime.fs.existsSync("/");
return true;
} catch {
return false;
}
}

function canExit(): boolean {
const runtime = getRuntime();
return runtime.process.platform !== "browser";
}

3. Progressive Enhancement

Provide fallbacks:

async function loadData(path: string) {
const runtime = getRuntime();

if (runtime.process.platform === "browser") {
// Browser: fetch from server
const response = await fetch(path);
return response.json();
} else {
// Node.js: read from file system
const data = readFileSync(path, "utf8");
return JSON.parse(data);
}
}

4. Shared Widgets

Write platform-agnostic widgets:

export class StatusBar extends Box {
constructor(options: BoxOptions) {
super({
...options,
height: 3,
bottom: 0,
left: 0,
right: 0,
style: { bg: "blue", fg: "white" },
});
}

setStatus(message: string) {
this.setContent(` ${message}`);
this.screen?.render();
}
}

// Works in both platforms!
const status = new StatusBar({ parent: screen });
status.setStatus("Ready");

Common Pitfalls

❌ Using process.exit() in Browser

// DON'T
screen.key("q", () => process.exit(0)); // Throws error in browser

// DO
screen.key("q", () => {
screen.destroy();
if (typeof window === "undefined") {
process.exit(0);
}
});

❌ Assuming File System Exists

// DON'T
const config = readFileSync("./config.json"); // Fails in browser

// DO
async function loadConfig() {
if (hasFileSystem()) {
return readFileSync("./config.json", "utf8");
} else {
const response = await fetch("/config.json");
return response.text();
}
}

❌ Forgetting XTerm.js in Browser

// DON'T
const screen = new Screen(); // Missing terminal in browser

// DO
const term = new Terminal();
term.open(element);
const screen = new Screen({ terminal: term });

Next Steps