Skip to main content

Troubleshooting

Common issues and how to resolve them.

Overview​

This guide covers common problems you might encounter when using unblessed, organized by category with detailed solutions and debugging strategies.

Quick Diagnostic​

Problem Categories​

SymptomLikely CauseSection
Nothing rendersScreen not initialized / render not calledRendering Issues
Keys don't workKeys/mouse not enabledInput Issues
Wrong size/positionCoordinate calculation errorLayout Issues
Slow performanceToo many renders / large widget treePerformance Issues
FlickeringMissing Smart CSR / too frequent rendersVisual Issues
Memory leakEvent listeners not cleaned upPerformance Issues
Works in Node, not browserPlatform-specific codePlatform Issues
Widgets overlapZ-index or layout misconfigurationLayout Issues

Rendering Issues​

Nothing Renders​

Problem: Screen is blank, widgets don't appear.

Causes and Solutions:

// ❌ Problem 1: Forgot to call render()
const screen = new Screen();
const box = new Box({ parent: screen });
// Nothing appears!

// ✅ Solution: Always call render()
screen.render();
// ❌ Problem 2: No parent attached
const box = new Box({
// Missing: parent: screen
content: "Hello",
});
screen.render();

// ✅ Solution: Attach to parent
const box = new Box({
parent: screen, // Must have parent!
content: "Hello",
});
screen.render();
// ❌ Problem 3: Hidden or zero size
const box = new Box({
parent: screen,
width: 0, // Zero width!
height: 0, // Zero height!
hidden: true, // Hidden!
});

// ✅ Solution: Provide valid size
const box = new Box({
parent: screen,
width: 50,
height: 10,
hidden: false,
});

Debug Steps:

  1. Check render was called:
console.log("Before render");
screen.render();
console.log("After render");
  1. Verify widget tree:
console.log("Children:", screen.children.length);
screen.children.forEach((child, i) => {
console.log(`Child ${i}:`, child.type, child.hidden);
});
  1. Check coordinates:
const coords = box.lpos;
console.log("Coordinates:", coords);
// Should have valid xi, yi, xl, yl

Widgets Don't Update​

Problem: Changes to widgets don't appear on screen.

Causes and Solutions:

// ❌ Problem: Forgot to render after change
box.setContent("New content");
// Nothing happens

// ✅ Solution: Render after changes
box.setContent("New content");
screen.render();
// ❌ Problem: Widget not marked dirty
box.content = "Direct assignment"; // Bypasses dirty tracking

// ✅ Solution: Use setter methods
box.setContent("Use setter method");
screen.render();

Debug Steps:

  1. Check dirty flag:
console.log("Dirty:", box.screen?.renders);
  1. Force re-render:
box.screen?.draw(0, box.screen.lines.length - 1);

Partial Rendering​

Problem: Only some widgets render, others missing.

Causes and Solutions:

// ❌ Problem: Overlapping widgets
const box1 = new Box({
parent: screen,
top: 0,
left: 0,
width: 100,
height: 100, // Very large!
});

const box2 = new Box({
parent: screen,
top: 5,
left: 5,
width: 50,
height: 50, // Hidden behind box1!
});

// ✅ Solution: Check z-index or order
box2.setIndex(screen.children.length - 1); // Bring to front

Debug: Check rendering order:

screen.children.forEach((child, i) => {
const coords = child.lpos;
console.log(`Widget ${i}:`, child.type, coords);
});

Input Issues​

Keys Not Working​

Problem: Keyboard input not captured.

Causes and Solutions:

// ❌ Problem 1: Keys not enabled
const box = new Box({
parent: screen,
// Missing: keys: true
});

box.key("enter", () => {
// Never called!
});

// ✅ Solution: Enable keys
const box = new Box({
parent: screen,
keys: true, // Must enable!
});
// ❌ Problem 2: Widget not focused
box.key("enter", handler); // Won't work without focus

// ✅ Solution: Focus widget
box.focus();
// ❌ Problem 3: Screen not grabbing keys
const screen = new Screen({
// Missing: grabKeys: true
});

// ✅ Solution: Enable grabKeys
const screen = new Screen({
grabKeys: true, // For global keys
});

Debug Steps:

  1. Test keypress event:
screen.on("keypress", (ch, key) => {
console.log("Key:", key.full);
});
  1. Check focus:
console.log("Focused:", screen.focused);
console.log("Widget has focus:", box === screen.focused);
  1. Verify key binding:
console.log("Key bindings:", Object.keys(box._listenedKeys || {}));

Mouse Not Working​

Problem: Mouse events not firing.

Causes and Solutions:

// ❌ Problem 1: Mouse not enabled
const box = new Box({
parent: screen,
// Missing: mouse: true
});

box.on("click", () => {
// Never called!
});

// ✅ Solution: Enable mouse
const box = new Box({
parent: screen,
mouse: true,
});
// ❌ Problem 2: Terminal doesn't support mouse
// Some terminals don't have mouse support

// ✅ Solution: Test mouse capability
const screen = new Screen({
mouse: true,
sendFocus: true,
});

screen.program.on("mouse", (data) => {
console.log("Mouse supported!", data);
});

Debug Steps:

  1. Check terminal mouse support:
console.log("Mouse enabled:", screen.program.terminal.includes("xterm"));
  1. Test raw mouse events:
screen.program.enableMouse();
screen.program.on("mouse", (data) => {
console.log("Mouse event:", data);
});

Focus Issues​

Problem: Wrong widget has focus.

Causes and Solutions:

// ❌ Problem: Multiple focus() calls conflict
widget1.focus();
widget2.focus(); // widget1 loses focus
widget3.focus(); // widget2 loses focus

// ✅ Solution: Track focus changes
screen.on("element focus", (el) => {
console.log("Focused:", el.type);
});

// ✅ Solution: Use focus groups
const form = new Form({ parent: screen });
const input1 = new Textbox({ parent: form });
const input2 = new Textbox({ parent: form });
// Tab key will cycle focus within form

Debug:

console.log("Current focus:", screen.focused?.type);
console.log(
"Focusable widgets:",
screen.children.filter((c) => c.keyable).map((c) => c.type),
);

Layout Issues​

Wrong Size​

Problem: Widget is wrong size.

Causes and Solutions:

// ❌ Problem 1: Percentage calculation wrong
const box = new Box({
parent: screen,
width: "50%", // 50% of what?
height: "100%", // May not be what you expect
});

// ✅ Solution: Check parent size
console.log("Parent size:", {
width: screen.width,
height: screen.height,
});

// ✅ Solution: Use absolute sizing
const box = new Box({
parent: screen,
width: 40,
height: 20,
});
// ❌ Problem 2: Border/padding not accounted for
const box = new Box({
parent: screen,
width: 50,
height: 10,
border: { type: "line" }, // Takes 2 cells!
padding: 2, // Takes 4 cells!
});
// Content area is actually 50-4=46 x 10-4=6

// ✅ Solution: Account for decorations
const innerWidth = 50 - 2 - 2 * 2; // width - border - padding
const innerHeight = 10 - 2 - 2 * 2; // height - border - padding

Debug:

const coords = box.lpos;
console.log("Box dimensions:", {
width: coords.xl - coords.xi,
height: coords.yl - coords.yi,
inner: {
width: box.iwidth,
height: box.iheight,
},
});

Wrong Position​

Problem: Widget appears in wrong location.

Causes and Solutions:

// ❌ Problem: Relative positioning misunderstood
const box = new Box({
parent: screen,
top: "50%", // 50% of screen height
left: "center", // Centered
});
// May not be where you expect!

// ✅ Solution: Use absolute positioning
const box = new Box({
parent: screen,
top: 10,
left: 20,
});
// ❌ Problem: Parent offset not considered
const container = new Box({
parent: screen,
top: 5,
left: 5,
});

const child = new Box({
parent: container,
top: 0, // Relative to container!
left: 0, // Not screen
});
// Child is at screen coordinates (5, 5), not (0, 0)

// ✅ Solution: Understand coordinate systems
console.log("Child absolute position:", child.lpos);

Debug:

function debugPosition(widget: Element) {
const coords = widget.lpos!;
console.log(`${widget.type}:`, {
relative: { top: widget.top, left: widget.left },
absolute: { xi: coords.xi, yi: coords.yi },
});
}

debugPosition(container);
debugPosition(child);

Overlapping Widgets​

Problem: Widgets render on top of each other.

Causes and Solutions:

// ❌ Problem: Same coordinates
const box1 = new Box({ parent: screen, top: 0, left: 0 });
const box2 = new Box({ parent: screen, top: 0, left: 0 });
// They overlap!

// ✅ Solution 1: Different positions
const box1 = new Box({ parent: screen, top: 0, left: 0 });
const box2 = new Box({ parent: screen, top: 5, left: 0 });

// ✅ Solution 2: Control z-index
box1.setIndex(0); // Behind
box2.setIndex(1); // In front

Debug:

screen.children.forEach((child, index) => {
console.log(`Z-index ${index}:`, child.type, child.lpos);
});

Performance Issues​

Slow Rendering​

Problem: Screen updates are slow.

Causes and Solutions:

// ❌ Problem 1: Rendering inside loop
for (let i = 0; i < 100; i++) {
box.setContent(`Item ${i}`);
screen.render(); // 100 renders!
}

// ✅ Solution: Batch renders
for (let i = 0; i < 100; i++) {
box.setContent(`Item ${i}`);
}
screen.render(); // 1 render
// ❌ Problem 2: Too many widgets
for (let i = 0; i < 1000; i++) {
new Box({ parent: screen, content: `Item ${i}` });
}
// Very slow!

// ✅ Solution: Virtualize list
class VirtualList extends List {
renderVisibleOnly() {
const start = this.childBase;
const end = start + this.height;
return this.items.slice(start, end);
}
}

Debug:

console.time("render");
screen.render();
console.timeEnd("render");
// Should be < 20ms for most screens

// Profile widget count
console.log("Total widgets:", screen.children.length);

function countAllWidgets(el: Node): number {
let count = 1;
el.children.forEach((child) => {
count += countAllWidgets(child);
});
return count;
}

console.log("Total in tree:", countAllWidgets(screen));

High CPU Usage​

Problem: Process uses too much CPU.

Causes and Solutions:

// ❌ Problem: Rendering in tight loop
setInterval(() => {
box.setContent(new Date().toString());
screen.render();
}, 1); // Every millisecond! Too fast!

// ✅ Solution: Throttle updates
let rafId: number | null = null;

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

setInterval(() => {
box.setContent(new Date().toString());
scheduleRender(); // Max ~60 FPS
}, 16);

Debug:

let renderCount = 0;
const origRender = screen.render.bind(screen);

screen.render = function () {
renderCount++;
return origRender();
};

setInterval(() => {
console.log("Renders per second:", renderCount);
renderCount = 0;
}, 1000);

Memory Leak​

Problem: Memory usage grows over time.

Causes and Solutions:

// ❌ Problem: Listeners not removed
function createWidget() {
const box = new Box({ parent: screen });

box.on("focus", () => {
// Handler stays in memory even after box destroyed!
});

box.destroy(); // Listeners still attached!
}

// ✅ Solution: Clean up listeners
function createWidget() {
const box = new Box({ parent: screen });

const handler = () => {
/* ... */
};
box.on("focus", handler);

// Later
box.off("focus", handler);
box.removeAllListeners();
box.destroy();
}
// ❌ Problem: Circular references
class MyWidget extends Box {
private data: any[] = [];

addData(item: any) {
item.widget = this; // Circular reference!
this.data.push(item);
}
}

// ✅ Solution: Use WeakMap
class MyWidget extends Box {
private data: WeakMap<any, any> = new WeakMap();

addData(item: any, value: any) {
this.data.set(item, value); // Can be garbage collected
}
}

Debug:

const used = process.memoryUsage();
console.log({
rss: `${(used.rss / 1024 / 1024).toFixed(2)} MB`,
heapUsed: `${(used.heapUsed / 1024 / 1024).toFixed(2)} MB`,
});

// Track listener count
function countListeners(el: Node): number {
let count = Object.keys(el._events || {}).length;
el.children.forEach((child) => {
count += countListeners(child);
});
return count;
}

console.log("Total listeners:", countListeners(screen));

Visual Issues​

Flickering​

Problem: Screen flickers during updates.

Causes and Solutions:

// ❌ Problem 1: Smart CSR disabled
const screen = new Screen({
smartCSR: false, // Full screen redraws
});

// ✅ Solution: Enable Smart CSR
const screen = new Screen({
smartCSR: true, // Incremental updates
fastCSR: true, // Even faster
});
// ❌ Problem 2: Too frequent renders
setInterval(() => {
screen.render();
}, 1); // Causes flickering

// ✅ Solution: Limit render rate
setInterval(() => {
screen.render();
}, 16); // ~60 FPS max

Debug:

console.log("Smart CSR:", screen.smartCSR);
console.log("Fast CSR:", screen.fastCSR);

Wrong Colors​

Problem: Colors don't appear as expected.

Causes and Solutions:

// ❌ Problem 1: Terminal doesn't support color
const screen = new Screen({
term: "dumb", // No color support!
});

// ✅ Solution: Use color-capable terminal
const screen = new Screen({
term: "xterm-256color",
});

// Check color support
console.log("Terminal:", screen.tput.terminal);
console.log("Colors:", screen.tput.colors);
// ❌ Problem 2: Invalid color format
box.style.fg = "#xyz"; // Invalid hex

// ✅ Solution: Use valid colors
box.style.fg = "red"; // Named color
box.style.fg = "#ff0000"; // Valid hex
box.style.fg = 196; // Color index

Debug:

import colors from "@unblessed/core/lib/colors";

const converted = colors.convert("#ff0000");
console.log("Color value:", converted);

const reduced = colors.reduce(converted, 256);
console.log("Reduced to 256:", reduced);

Garbled Characters​

Problem: Unicode characters appear wrong.

Causes and Solutions:

// ❌ Problem: Terminal encoding wrong
process.env.LANG = "C"; // No UTF-8

// ✅ Solution: Set UTF-8 encoding
process.env.LANG = "en_US.UTF-8";

const screen = new Screen({
fullUnicode: true, // Enable full Unicode support
});
// ❌ Problem: Wide characters misaligned
box.setContent("中文字符"); // Chinese characters

// ✅ Solution: Enable east asian width
const screen = new Screen({
fullUnicode: true,
forceUnicode: true,
});

Platform Issues​

Browser vs Node.js​

Problem: Code works in Node.js but not browser (or vice versa).

Causes and Solutions:

// ❌ Problem: Using process.exit() in browser
screen.key("q", () => {
process.exit(0); // Throws error in browser!
});

// ✅ Solution: Platform-specific code
import { getRuntime } from "@unblessed/core/runtime-context";

screen.key("q", () => {
const runtime = getRuntime();
if (runtime.process.platform === "browser") {
screen.destroy();
terminal.dispose();
} else {
process.exit(0);
}
});
// ❌ Problem: File system access in browser
import { readFileSync } from "fs";
const data = readFileSync("./file.txt"); // Fails in browser!

// ✅ Solution: Use fetch in browser
async function loadFile(path: string) {
if (typeof window !== "undefined") {
const response = await fetch(path);
return response.text();
} else {
return readFileSync(path, "utf8");
}
}

Debug:

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

const runtime = getRuntime();
console.log("Platform:", runtime.process.platform);
console.log("Node.js:", typeof process !== "undefined" && process.version);
console.log("Browser:", typeof window !== "undefined");

XTerm.js Issues (Browser)​

Problem: XTerm.js terminal not working correctly.

Causes and Solutions:

// ❌ Problem 1: Terminal not attached to DOM
const term = new Terminal();
const screen = new Screen({ terminal: term });
// Terminal not visible!

// ✅ Solution: Attach to DOM first
const term = new Terminal();
term.open(document.getElementById("terminal"));
const screen = new Screen({ terminal: term });
// ❌ Problem 2: FitAddon not loaded
const term = new Terminal();
term.open(element);
// Terminal has default size, doesn't fit container

// ✅ Solution: Use FitAddon
import { FitAddon } from "@xterm/addon-fit";

const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
term.open(element);
fitAddon.fit();

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

Debug:

console.log("Terminal rows:", term.rows);
console.log("Terminal cols:", term.cols);
console.log("Container size:", element.offsetWidth, element.offsetHeight);

Runtime Errors​

Common Error Messages​

"Cannot read property 'render' of undefined"​

Cause: Widget not attached to screen.

Solution:

// ❌ Wrong
const box = new Box({ content: "Hello" });
box.screen.render(); // screen is undefined!

// ✅ Correct
const box = new Box({ parent: screen, content: "Hello" });
screen.render();

"Maximum call stack size exceeded"​

Cause: Circular reference or infinite recursion.

Solution:

// ❌ Problem: Event causes another event
box.on("focus", () => {
box.focus(); // Infinite recursion!
});

// ✅ Solution: Add guard
let isFocusing = false;
box.on("focus", () => {
if (isFocusing) return;
isFocusing = true;
// ... do work
isFocusing = false;
});

"Cannot attach to destroyed widget"​

Cause: Trying to use widget after destroy().

Solution:

// ❌ Wrong
box.destroy();
box.setContent("Hello"); // Error!

// ✅ Correct: Check before use
if (!box.destroyed) {
box.setContent("Hello");
}

"process.exit is not a function"​

Cause: Using Node.js APIs in browser.

Solution: See Browser vs Node.js above.

Debugging Techniques​

Enable Debug Mode​

const screen = new Screen({
debug: true,
dump: true, // Dump output to string instead of terminal
log: "./debug.log", // Log to file
});

// Check debug output
console.log(screen.screenshot());

Visual Tree Inspection​

function printTree(node: Node, indent = 0) {
const spaces = " ".repeat(indent * 2);
console.log(`${spaces}${node.type} (${node.children.length} children)`);

if ("lpos" in node) {
const el = node as Element;
const coords = el.lpos;
console.log(`${spaces} Position: (${coords.xi}, ${coords.yi})`);
console.log(
`${spaces} Size: ${coords.xl - coords.xi} x ${coords.yl - coords.yi}`,
);
}

node.children.forEach((child) => printTree(child, indent + 1));
}

printTree(screen);

Screenshot Comparison​

// Take before/after screenshots
const before = screen.screenshot();

box.setContent("Changed!");
screen.render();

const after = screen.screenshot();

if (before === after) {
console.log("❌ Nothing changed!");
} else {
console.log("✅ Screen updated");
}

Event Logging​

// Log all events on screen
const originalEmit = screen.emit.bind(screen);
screen.emit = function (event: string, ...args: any[]) {
console.log("Event:", event, args);
return originalEmit(event, ...args);
};

// Log specific widget events
["focus", "blur", "click", "keypress"].forEach((event) => {
widget.on(event, (...args) => {
console.log(`${widget.type}.${event}:`, args);
});
});

Performance Profiling​

// Profile render time
function profileRender() {
const start = process.hrtime.bigint();
screen.render();
const end = process.hrtime.bigint();
const ms = Number(end - start) / 1_000_000;

console.log(`Render: ${ms.toFixed(3)}ms`);

if (ms > 20) {
console.warn("⚠️ Slow render detected!");
}
}

// Profile continuously
setInterval(profileRender, 1000);

Memory Profiling​

function profileMemory() {
const used = process.memoryUsage();

console.log({
rss: `${(used.rss / 1024 / 1024).toFixed(2)} MB`,
heapUsed: `${(used.heapUsed / 1024 / 1024).toFixed(2)} MB`,
heapTotal: `${(used.heapTotal / 1024 / 1024).toFixed(2)} MB`,
external: `${(used.external / 1024 / 1024).toFixed(2)} MB`,
});

// Trigger garbage collection (requires --expose-gc flag)
if (global.gc) {
global.gc();
console.log("GC triggered");
}
}

setInterval(profileMemory, 5000);

Getting Help​

Before Asking for Help​

  1. Check the console: Look for error messages and stack traces
  2. Enable debug mode: Use { debug: true, dump: true }
  3. Simplify the problem: Create minimal reproduction
  4. Check documentation: Review relevant guides
  5. Search issues: Look for similar problems on GitHub

Creating a Minimal Reproduction​

// Minimal example that reproduces the issue
import { Screen, Box } from "@unblessed/node";

const screen = new Screen({ debug: true, dump: true });

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

screen.render();

// Expected: Box appears centered
// Actual: Box appears in wrong position

console.log("Screenshot:", screen.screenshot());
console.log("Coordinates:", box.lpos);

Information to Include​

When reporting issues, include:

  • unblessed version: npm list @unblessed/core
  • Node.js version: node --version
  • Platform: macOS / Linux / Windows
  • Terminal: iTerm2, gnome-terminal, Windows Terminal, etc.
  • Minimal reproduction: Code that demonstrates the issue
  • Expected behavior: What should happen
  • Actual behavior: What actually happens
  • Screenshots: If visual issue
  • Error messages: Full stack trace

Useful Commands​

# Check versions
npm list @unblessed/core
node --version

# Enable debug output
DEBUG=* node app.js

# Save output to file
node app.js > output.txt 2>&1

# Check terminal type
echo $TERM

# Test terminal colors
tput colors

# Check locale
echo $LANG

FAQ​

Why is my screen blank?​

Most common causes:

  1. Forgot to call screen.render()
  2. Widget not attached to parent
  3. Widget is hidden or has zero size
  4. Widget is positioned off-screen

Why don't my colors work?​

Check:

  1. Terminal supports colors (tput colors should be 256+)
  2. Using valid color format
  3. $TERM environment variable is set correctly

Why is rendering slow?​

Common causes:

  1. Too many screen.render() calls
  2. Large widget tree (1000+ widgets)
  3. Smart CSR disabled
  4. Complex content with many tags

Why do keys not work?​

Check:

  1. keys: true option enabled
  2. Widget has focus (widget.focus())
  3. Key is not reserved by terminal

Why does it work in Node.js but not browser?​

Common causes:

  1. Using Node.js-specific APIs (fs, process.exit, etc.)
  2. XTerm.js not properly initialized
  3. Missing browser polyfills

How do I debug rendering issues?​

  1. Enable debug mode: { debug: true, dump: true }
  2. Take screenshots: screen.screenshot()
  3. Check coordinates: widget.lpos
  4. Verify widget tree: screen.children

Next Steps​