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
| Symptom | Likely Cause | Section |
|---|---|---|
| Nothing renders | Screen not initialized / render not called | Rendering Issues |
| Keys don't work | Keys/mouse not enabled | Input Issues |
| Wrong size/position | Coordinate calculation error | Layout Issues |
| Slow performance | Too many renders / large widget tree | Performance Issues |
| Flickering | Missing Smart CSR / too frequent renders | Visual Issues |
| Memory leak | Event listeners not cleaned up | Memory Issues |
| Works in Node, not browser | Platform-specific code | Platform Issues |
| Widgets overlap | Z-index or layout misconfiguration | Layout 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:
- Check render was called:
console.log('Before render');
screen.render();
console.log('After render');
- Verify widget tree:
console.log('Children:', screen.children.length);
screen.children.forEach((child, i) => {
console.log(`Child ${i}:`, child.type, child.hidden);
});
- 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:
- Check dirty flag:
console.log('Dirty:', box.screen?.renders);
- 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:
- Test keypress event:
screen.on('keypress', (ch, key) => {
console.log('Key:', key.full);
});
- Check focus:
console.log('Focused:', screen.focused);
console.log('Widget has focus:', box === screen.focused);
- 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:
- Check terminal mouse support:
console.log('Mouse enabled:', screen.program.terminal.includes('xterm'));
- 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
- Check the console: Look for error messages and stack traces
- Enable debug mode: Use
{ debug: true, dump: true } - Simplify the problem: Create minimal reproduction
- Check documentation: Review relevant guides
- 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:
- Forgot to call
screen.render() - Widget not attached to parent
- Widget is hidden or has zero size
- Widget is positioned off-screen
Why don't my colors work?
Check:
- Terminal supports colors (
tput colorsshould be 256+) - Using valid color format
$TERMenvironment variable is set correctly
Why is rendering slow?
Common causes:
- Too many
screen.render()calls - Large widget tree (1000+ widgets)
- Smart CSR disabled
- Complex content with many tags
Why do keys not work?
Check:
keys: trueoption enabled- Widget has focus (
widget.focus()) - Key is not reserved by terminal
Why does it work in Node.js but not browser?
Common causes:
- Using Node.js-specific APIs (fs, process.exit, etc.)
- XTerm.js not properly initialized
- Missing browser polyfills
How do I debug rendering issues?
- Enable debug mode:
{ debug: true, dump: true } - Take screenshots:
screen.screenshot() - Check coordinates:
widget.lpos - Verify widget tree:
screen.children
Next Steps
- Performance Optimization - Optimize your application
- Custom Widgets - Build custom components
- API Reference - Detailed API documentation