Widgets
Understanding unblessed's widget system.
Overview
Widgets are the building blocks of unblessed TUIs. They provide UI elements like boxes, lists, forms, and more, all arranged in a hierarchical tree structure.
Widget Hierarchy
All widgets inherit from the base classes:
Node (base class)
└── Element (visual base)
├── Box (rectangular area)
│ ├── List (scrollable list)
│ ├── Form (input container)
│ ├── Table (tabular data)
│ └── Text (formatted text)
├── Input (text input)
│ ├── Textbox (single-line)
│ └── Textarea (multi-line)
├── Button (clickable button)
├── ProgressBar (progress indicator)
└── ... (other widgets)
Node
The base class for all widgets:
class Node {
parent: Node | null;
children: Node[];
screen: Screen | null;
// Hierarchy methods
append(node: Node): void;
prepend(node: Node): void;
insertBefore(node: Node, ref: Node): void;
insertAfter(node: Node, ref: Node): void;
remove(node: Node): void;
detach(): void;
// Event methods
on(event: string, listener: Function): void;
emit(event: string, ...args: any[]): void;
}
Element
Adds visual properties to Node:
class Element extends Node {
// Position
top: number | string;
left: number | string;
width: number | string;
height: number | string;
// Style
style: Style;
border: BorderOptions;
padding: Padding;
// Content
content: string;
tags: boolean; // Enable tag parsing
// Methods
render(): void;
setContent(text: string): void;
focus(): void;
hide(): void;
show(): void;
}
Core Widgets
Box
The fundamental rectangular widget:
import { Box } from "@unblessed/node";
const box = new Box({
parent: screen,
top: "center",
left: "center",
width: "50%",
height: "50%",
content: "Hello {bold}World{/bold}!",
tags: true,
border: { type: "line" },
style: {
fg: "white",
bg: "blue",
border: { fg: "cyan" },
},
});
Use cases: Headers, containers, panels, status bars
List
Interactive scrollable list:
import { List } from "@unblessed/node";
const list = new List({
parent: screen,
top: 0,
left: 0,
width: "100%",
height: "100%",
items: ["Item 1", "Item 2", "Item 3"],
keys: true, // Arrow key navigation
vi: true, // Vim bindings (j/k)
mouse: true, // Mouse support
style: {
selected: {
bg: "cyan",
fg: "black",
},
},
});
list.on("select", (item, index) => {
console.log(`Selected: ${item.getText()}`);
});
Use cases: Menus, file browsers, selection lists
Table
Tabular data display:
import { Table } from "@unblessed/node";
const table = new Table({
parent: screen,
top: 0,
left: 0,
width: "100%",
height: "100%",
data: [
["Name", "Age", "City"],
["Alice", "30", "New York"],
["Bob", "25", "London"],
["Carol", "35", "Tokyo"],
],
border: { type: "line" },
style: {
header: { fg: "blue", bold: true },
cell: { fg: "white" },
},
});
Use cases: Data tables, grids, reports
Form
Container for input widgets:
import { Form, Textbox, Button } from "@unblessed/node";
const form = new Form({
parent: screen,
keys: true,
mouse: true,
});
const nameInput = new Textbox({
parent: form,
name: "name",
label: "Name:",
top: 0,
left: 0,
width: "100%",
height: 3,
});
const submitButton = new Button({
parent: form,
content: "Submit",
top: 4,
left: 0,
height: 3,
});
form.on("submit", (data) => {
console.log("Form data:", data);
});
submitButton.on("press", () => {
form.submit();
});
Use cases: User input, settings, dialogs
Textbox & Textarea
Text input widgets:
import { Textbox, Textarea } from "@unblessed/node";
// Single-line input
const textbox = new Textbox({
parent: screen,
label: "Username:",
inputOnFocus: true,
keys: true,
mouse: true,
});
// Multi-line input
const textarea = new Textarea({
parent: screen,
label: "Description:",
inputOnFocus: true,
keys: true,
mouse: true,
});
textbox.on("submit", (value) => {
console.log("Entered:", value);
});
Use cases: Text input, forms, editors
Button
Clickable button:
import { Button } from "@unblessed/node";
const button = new Button({
parent: screen,
content: "Click Me",
top: "center",
left: "center",
width: 20,
height: 3,
mouse: true,
style: {
bg: "green",
fg: "white",
focus: { bg: "cyan" },
},
});
button.on("press", () => {
console.log("Button clicked!");
});
Use cases: Actions, confirmations, navigation
ProgressBar
Progress indicator:
import { ProgressBar } from "@unblessed/node";
const progress = new ProgressBar({
parent: screen,
top: "center",
left: "center",
width: "50%",
height: 3,
filled: 0,
style: { bar: { bg: "cyan" } },
});
let value = 0;
const interval = setInterval(() => {
value += 10;
progress.setProgress(value);
screen.render();
if (value >= 100) {
clearInterval(interval);
}
}, 100);
Use cases: Loading indicators, task progress
Widget Options
Common Options
All widgets support these options:
interface WidgetOptions {
// Hierarchy
parent?: Node;
children?: Node[];
// Position (numbers or strings)
top?: number | string; // 0, '50%', 'center'
left?: number | string;
right?: number | string;
bottom?: number | string;
// Size
width?: number | string; // 50, '50%', 'shrink'
height?: number | string;
// Visual
border?: BorderOptions;
padding?: Padding;
style?: Style;
// Content
content?: string;
tags?: boolean; // Enable {bold}, {red-fg}, etc.
label?: string;
// Behavior
hidden?: boolean;
clickable?: boolean;
focusable?: boolean;
scrollable?: boolean;
draggable?: boolean;
mouse?: boolean;
keys?: boolean;
vi?: boolean;
}
Position & Sizing
Flexible positioning system:
// Absolute
{ top: 0, left: 0, width: 50, height: 10 }
// Percentage
{ top: '10%', left: '20%', width: '50%', height: '80%' }
// Center
{ top: 'center', left: 'center', width: 30, height: 10 }
// Mixed
{ top: 2, left: 'center', width: '50%', height: 10 }
// Calculated
{ top: 0, left: 0, right: 0, bottom: 0 } // Fill parent
{ width: '100%-10', height: '100%-5' } // With margin
Borders
Various border styles:
// Line border
{ border: { type: 'line' } }
// Custom characters
{ border: {
type: 'line',
ch: '#',
top: true,
bottom: true,
left: true,
right: true
}
}
// Colors
{ border: { type: 'line' },
style: { border: { fg: 'cyan', bg: 'black' } }
}
Padding
Space inside borders:
// Uniform
{ padding: 1 }
// Per side
{ padding: { left: 2, right: 2, top: 1, bottom: 1 } }
Styles
Colors and attributes:
{
style: {
fg: 'white', // Foreground color
bg: 'blue', // Background color
bold: true,
underline: true,
border: {
fg: 'cyan'
},
focus: { // When focused
bg: 'yellow',
fg: 'black'
},
selected: { // For lists
bg: 'green'
}
}
}
Colors: Named colors ('red', 'blue', etc.), RGB ('#ff0000'), or terminal codes.
Content Tags
Enable rich content formatting with tags: true:
const box = new Box({
parent: screen,
content:
"{bold}Bold text{/bold}\n" +
"{red-fg}Red text{/red-fg}\n" +
"{blue-bg}Blue background{/blue-bg}\n" +
"{center}Centered{/center}",
tags: true,
});
Available tags:
- Style:
{bold},{underline},{blink},{inverse} - Colors:
{red-fg},{blue-bg},{#ff0000-fg} - Alignment:
{left},{center},{right} - Reset:
{/bold},{/red-fg},{/}
Creating Custom Widgets
Extend existing widgets to create custom ones:
import { Box, type BoxOptions } from "@unblessed/node";
export interface StatusBoxOptions extends BoxOptions {
status?: "success" | "error" | "warning";
}
export class StatusBox extends Box {
constructor(options: StatusBoxOptions = {}) {
super(options);
this.type = "statusbox";
// Set colors based on status
if (options.status) {
this.setStatus(options.status);
}
}
setStatus(status: "success" | "error" | "warning") {
const colors = {
success: { fg: "green", border: "green" },
error: { fg: "red", border: "red" },
warning: { fg: "yellow", border: "yellow" },
};
const color = colors[status];
this.style.fg = color.fg;
if (this.border) {
this.style.border = { fg: color.border };
}
}
}
// Usage
const status = new StatusBox({
parent: screen,
status: "success",
content: "Operation completed successfully!",
border: { type: "line" },
});
See Custom Widgets for advanced examples.
Widget Lifecycle
Understanding widget lifecycle helps with proper resource management:
- Construction: Widget options processed
- Attachment: Added to parent, assigned screen
- Rendering: Visual output generated
- Events: User interactions handled
- Updates: Content/style changes
- Detachment: Removed from parent
- Destruction: Resources cleaned up
const box = new Box({
/* options */
}); // 1. Construction
box.attach(parent); // 2. Attachment
screen.render(); // 3. Rendering
box.on("click", handler); // 4. Events
box.setContent("new content"); // 5. Updates
box.detach(); // 6. Detachment
box.destroy(); // 7. Destruction
Best Practices
Use Parent Option
// ✅ Recommended
const box = new Box({ parent: screen });
// ❌ Avoid
const box = new Box();
screen.append(box);
Batch Updates
// ✅ Efficient
box.setContent("Line 1");
box.style.fg = "cyan";
screen.render(); // Single render
// ❌ Inefficient
box.setContent("Line 1");
screen.render();
box.style.fg = "cyan";
screen.render();
Clean Up Resources
// Remove event listeners
box.removeAllListeners();
// Detach from tree
box.detach();
// Destroy widget
box.destroy();
Next Steps
- Rendering - How widgets are rendered
- Events - Event handling
- Widget API Reference - Detailed API docs
- Custom Widgets - Build your own