Skip to main content

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:

  1. Construction: Widget options processed
  2. Attachment: Added to parent, assigned screen
  3. Rendering: Visual output generated
  4. Events: User interactions handled
  5. Updates: Content/style changes
  6. Detachment: Removed from parent
  7. 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​