npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

zs3-tui

v1.0.1

Published

typescript terminal framework

Readme

zs3-tui

A TypeScript framework for building interactive Terminal User Interface (TUI) applications. It provides a component model, automatic layout management, mouse support, theming, and clipboard integration — all over raw terminal escape sequences with zero runtime dependencies.

  ______  _____   _____    _____  _   _  ___
 |__  / |/ ___/  |___ /   |_   _|| | | ||_ _|
   / /   \__ \    |_ \      | |  | | | | | |
  / /__  ___) |  ___) |     | |  | |_| | | |
 /____| |____/  |____/      |_|   \___/ |___|

Features

  • Component model — TextArea, List, Toolbar, Question, TextBox, InputText, all with a shared base class for focus, mouse selection, and theme subscription
  • Flex-box layoutLayoutManager distributes terminal space between components with size / percent / flex constraints, exactly like CSS flexbox
  • Full mouse support — click, double-click, drag-select, scroll wheel, and hover events routed automatically to the right component
  • 9 built-in themes — dark, light, dracula, solarized, contrast, cyberpunk, matrix, monokai, nord; switch at runtime and all components redraw instantly
  • Streaming outputTextArea.appendDelta() redraws only the affected row, making it efficient for LLM token streaming
  • Modal dialogs — any component with modal: true blocks all input and dims the background; closing it restores focus automatically
  • Clipboard integration — drag to select text in any component, Ctrl+C to copy; right-click to paste; works on Windows, macOS, and Linux
  • Zero runtime dependencies — pure TypeScript on Node.js built-ins

Table of contents


Installation

npm install zs3-tui

Node.js ≥ 22 LTS is required. On Windows, use Node.js v22 LTS — v23+ has a ConPTY bug that prevents mouse events from reaching the process.


Quick start

import {
  terminal, hideCursor, LayoutManager,
  TextArea, List, theme,
} from "zs3-tui";

// Enter full-screen mode and enable mouse
terminal.start();
terminal.clear(theme.current.bgScreen);
terminal.mouse(true);
hideCursor();

// Create components
const log  = new TextArea({ name: "log" });
const list = new List({ name: "items", selectionMode: "single" });
list.addLine("Item A");
list.addLine("Item B");

list.onClick = (index) => log.addLine(`Clicked item ${index}`);

// Lay them out side by side, each taking half the terminal
const root = new LayoutManager({ direction: "row" });
root.add(list, { flex: 1 });
root.add(log,  { flex: 1 });

terminal.addResizeListener(size => root.resize(size));
root.resize(terminal.getSize());

// Main event loop
while (true) {
  const ev = await terminal.waitForKey();
  if (ev.key === "Escape") {
    terminal.dispose();
    process.exit(0);
  }
}

All components and utilities are exported from the single "zs3-tui" entry point.


Terminal

The Terminal class is the low-level layer: it manages stdin/stdout, dispatches keyboard and mouse events, and exposes helpers for writing colored text at absolute positions.

A pre-built singleton is exported as terminal.

import { terminal } from "zs3-tui";

Lifecycle

| Method | Description | |---|---| | start() | Enters the alternate screen buffer and starts the stdin listener. | | dispose() | Restores the previous screen, disables mouse, releases stdin. | | clear(bgColor?) | Clears the screen. Pass a background color to fill it. | | mouse(enabled) | Enables (true) or disables (false) SGR mouse reporting. | | hideCursor() | Hides the terminal cursor. Also exported as a standalone helper. | | showCursor() | Shows the terminal cursor. Also exported as a standalone helper. |

Writing

| Method / property | Description | |---|---| | write(text, pos?, colors?) | Writes text at absolute position {x, y} with optional fgColor/bgColor. | | position(pos) | Moves the cursor to {x, y} without writing. | | bgColor | Get/set the default background color applied to subsequent write() calls. | | fgColor | Get/set the default foreground color. | | setColorTransform(fn) | Applies a per-channel RGB transform to every write(). Pass null to remove. Useful for dim overlays. |

Size

const { width, height } = terminal.getSize();

Events

// Subscribe to keyboard events
terminal.addKeyListener((ev: KeyEvent) => { /* ... */ });
terminal.removeKeyListener(handler);

// Subscribe to mouse events
terminal.addMouseListener((ev: MouseEvent) => { /* ... */ });
terminal.removeMouseListener(handler);

// Subscribe to resize events
terminal.addResizeListener((size: TerminalSize) => { /* ... */ });
terminal.removeResizeListener(handler);

// Subscribe to paste events
terminal.addPasteListener((text: string) => { /* ... */ });
terminal.removePasteListener(handler);

// Async helper — resolves on the next keypress
const ev = await terminal.waitForKey();

KeyEvent

interface KeyEvent {
  key:   string;   // Logical name: 'a', 'A', 'ArrowUp', 'Enter', 'Escape', 'F1' …
  raw:   string;   // Raw bytes received from stdin
  ctrl:  boolean;
  shift: boolean;
  alt:   boolean;
  meta:  boolean;  // Always false (not detectable in a terminal)
}

MouseEvent

interface MouseEvent {
  x:      number;                                           // Absolute terminal column (0-indexed)
  y:      number;                                           // Absolute terminal row (0-indexed)
  button: 'left' | 'right' | 'middle' | 'none';
  type:   'move' | 'down' | 'up' | 'scroll-up' | 'scroll-down';
  drag:   boolean;                                          // true when button is held during a move
}

LayoutManager

LayoutManager divides a rectangular area among its children using a flex-box–inspired algorithm. Children can be Component instances or nested LayoutManager instances.

import { LayoutManager } from "zs3-tui";

const root    = new LayoutManager({ direction: 'column' }); // stack vertically
const content = new LayoutManager({ direction: 'row'    }); // split horizontally

root.add(toolbar,  { size: 5    }); // fixed 5 rows
root.add(content,  { flex: 1    }); // takes remaining rows
content.add(list,  { flex: 1    }); // half of the row width
content.add(area,  { flex: 1    }); // other half

terminal.addResizeListener(size => root.resize(size));
root.resize(terminal.getSize());

Constructor

new LayoutManager({ direction: 'row' | 'column' })

LayoutConstraints

Each child is added with a LayoutConstraints object that controls how space is distributed:

| Property | Type | Description | |---|---|---| | size | number | Fixed size in the main axis (rows for column, columns for row). | | percent | number | Percentage of the parent's main-axis size (0–100). | | flex | number | Proportion of remaining space after fixed/percent children are placed. Defaults to 1 when neither size nor percent is given. | | min | number | Minimum size (only valid for flex children). | | max | number | Maximum size (only valid for flex children). |

Only one of size, percent, or flex should be set per child.

Public methods

| Method | Description | |---|---| | add(child, constraints) | Adds a Component or nested LayoutManager with the given constraints. | | remove(child) | Removes a child and disposes it (or all nested components). | | setSize(child, size) | Changes the size constraint of a child and recomputes the layout. Useful for dynamically growing components such as InputText. | | setVisible(child, visible) | Shows or hides a child. Hidden children do not consume space. The area they occupied is erased. | | resize(terminalSize) | Recomputes the layout to fill the full terminal size. Call this from a terminal.addResizeListener handler. | | compute(x, y, width, height) | Recomputes the layout within an arbitrary rectangle. resize() calls this with (0, 0, width, height). | | invalidate() | Re-runs compute() with the last known bounds (e.g. after setSize or setVisible). | | getBounds(child) | Returns the last computed { x, y, width, height } for a child, or null. |


ComponentManager

ComponentManager is a singleton that owns the registry of all active components and routes terminal events to the correct one. It is instantiated automatically — you do not need to use it directly in most cases.

Event routing rules

  • Keyboard / paste events → focused component only.
  • Mouse events → component under the cursor (hit test). Coordinates are converted to component-local before dispatch.
  • Resize events → every registered component.
  • When a modal component is active, all input is restricted to it. Background components are re-rendered with a dark color transform so the modal appears clearly in front.

Public methods

| Method | Description | |---|---| | add(name, component) | Registers a component. If component.options.modal === true, activates modal mode. | | remove(name) | Unregisters a component by name. Deactivates modal mode if it was the active modal. | | get(name) | Returns the component registered under name, or undefined. | | getAll() | Returns the full Map<string, Component> registry. | | setFocus(name) | Transfers keyboard focus to the named component programmatically. | | renderAll() | Re-renders every registered component. | | renderIntersecting(position, size) | Re-renders every component whose bounding box intersects the given rectangle. |

Note: When you use LayoutManager, components are registered and unregistered automatically. When you create components directly (without a layout manager), they self-register in their constructor.


Components

All components share a common ComponentOptions base type and inherit from the abstract Component class.

ComponentOptions

type ComponentOptions = {
  box?:       { left: boolean; right: boolean; top: boolean; bottom: boolean };
  boxColor?:  string;  // ANSI color for the border
  position?:  { x: number; y: number };
  size?:      { width: number; height: number };
  bgColor?:   string;
  fgColor?:   string;
  name?:      string;  // Unique name for the componentManager registry
  selectable?: boolean; // Whether mouse text-selection is enabled. Default: true
  modal?:     boolean;  // When true, blocks all input to other components
}

Component (base class)

Abstract base class that all components extend. It provides:

  • Theme subscription — components re-render automatically when the active theme changes.
  • Mouse text selection — drag to highlight, Ctrl+C to copy to clipboard. Works in any component that overrides getLines().
  • Right-click paste — right-clicking reads the system clipboard and calls handlePaste().
  • Event callbacks — assignable properties fired by the component manager.
  • dispose() — erases the component's footprint and removes it from the registry.

Common event callbacks

All callbacks default to null and can be assigned at any time:

| Callback | Signature | Description | |---|---|---| | onKeyDown | (ev: KeyEvent) => void | Fired on each keypress while focused. | | onMouseMove | (ev: MouseEvent) => void | Fired on mouse-move within the component. Coordinates are component-local. | | onMouseDown | (ev: MouseEvent) => void | Fired on mouse button press. | | onMouseUp | (ev: MouseEvent) => void | Fired on mouse button release. | | onScroll | (ev: MouseEvent) => void | Fired on scroll-wheel events. | | onMouseIn | () => void | Fired when the cursor enters the component area. | | onMouseOut | () => void | Fired when the cursor leaves the component area. | | onFocus | () => void | Fired when the component gains keyboard focus. | | onBlur | () => void | Fired when the component loses keyboard focus. | | onResize | (size: TerminalSize) => void | Fired when the terminal is resized. | | onPaste | (text: string) => void | Fired after text is pasted into the component. |

Common methods

| Method | Description | |---|---| | dispose() | Erases the component area, unsubscribes from themes, and removes from the component registry. | | render() | Redraws the component: content first, then the selection overlay if a drag-selection is active. | | renderContent() | Abstract. Subclasses override this to draw their specific content. |


TextArea

A read-only, scrollable log area that displays colored lines. Supports streaming output via a live-buffer API.

import { TextArea } from "zs3-tui";

const area = new TextArea({ name: "log" });
area.addLine("Hello world", { fgColor: "\x1b[32m" });  // green text

TextAreaOptions

Extends ComponentOptions with:

| Property | Type | Default | Description | |---|---|---|---| | scrollBarColor | string | theme scrollBarColor | ANSI color for the scroll bar. |

Public methods

| Method | Description | |---|---| | addLine(line, colors?) | Appends a line (splits on \n) and re-renders. | | addLines(lines, colors?) | Appends multiple lines in one call and re-renders. | | clear() | Removes all content and re-renders. | | scrollTo(position?) | Scrolls to 'top' or 'bottom' (default: 'bottom'). | | write(text, offset?, colors?) | Writes text at a local offset, bypassing the content array (direct terminal write). | | appendDelta(text, colors?) | Streaming API: appends a text fragment to the live buffer. If text contains \n, completed lines are flushed to content[]. Only the affected screen row is redrawn when there is no newline — efficient for AI/LLM token streaming. | | commitDelta() | Flushes the live buffer to content[] as a permanent line and ends streaming. | | clearDelta() | Discards the live buffer without committing it (e.g. on aborted generation). |

Key bindings

| Key | Action | |---|---| | / | Scroll one line. | | PageUp / PageDown | Scroll one page. | | Drag | Select text. | | Ctrl+C | Copy selection to clipboard. | | Escape | Clear selection. |


List

A scrollable, interactive list with single or multiple selection. Built on top of TextArea.

import { List } from "zs3-tui";

const list = new List({ name: "items", selectionMode: "single" });

list.addLine("Item A");
list.addLine("Item B");

list.onClick    = (index) => console.log(`Selected: ${index}`);
list.onDblClick = (index) => console.log(`Confirmed: ${index}`);

ListOptions

Extends TextAreaOptions with:

| Property | Type | Default | Description | |---|---|---|---| | selectionMode | "single" \| "multiple" | "single" | Whether one or many items can be selected at once. | | onClick | (index: number) => void | — | Called after a single-click selection change. | | onDblClick | (index: number) => void | — | Called on double-click or Enter. |

Public methods

| Method | Description | |---|---| | addLine(line, colors?) | Appends an item to the list. (Inherited from TextArea.) | | addLines(lines, colors?) | Appends multiple items. (Inherited from TextArea.) | | clear() | Removes all items. (Inherited from TextArea.) | | getSelected() | Returns the selected index (number \| null) in single mode, or a sorted number[] in multiple mode. | | set onClick(fn) | Assigns the click handler (alternative to passing it in options). | | set onDblClick(fn) | Assigns the double-click handler. |

Key bindings

| Key | Action | |---|---| | / | Move keyboard cursor. | | PageUp / PageDown | Move cursor one page. | | Space | Toggle selection on the cursor row. | | Enter | Fire onDblClick on the cursor row. |


Toolbar

A bar of clickable buttons arranged horizontally or vertically. The toolbar grows automatically as buttons are added.

import { Toolbar, icon } from "zs3-tui";

const toolbar = new Toolbar({ direction: "horizontal", gap: 0, name: "main" });
toolbar
  .addButton({ text: `${icon("power")} Exit`,    name: "exit",    action: () => exitProgram() })
  .addButton({ text: `${icon("refresh")} Reload`, name: "refresh", action: () => refresh() });

ToolbarOptions

Extends ComponentOptions with:

| Property | Type | Default | Description | |---|---|---|---| | direction | "horizontal" \| "vertical" | "horizontal" | Layout direction of the buttons. | | gap | number | 1 | Empty cells between buttons and between buttons and the toolbar border. |

ButtonDescription

interface ButtonDescription {
  text:   string;     // Button label (may include icon strings)
  name:   string;     // Unique name within this toolbar
  action: () => void; // Called when the button is clicked
}

Public methods

| Method | Description | |---|---| | addButton(button) | Appends a button and resizes the toolbar to fit. Returns this for chaining. |


Question

A self-sizing form component with a question, a list of choices, and an optional free-text entry field.

import { Question, type QuestionResult, center } from "zs3-tui";

const size = { width: 44, height: 0 }; // height: 0 → computed automatically
const q = new Question({
  position:      center(size),
  size,
  question:      "What is your preference?",
  choices:       ["Option A", "Option B", "Option C"],
  selectionMode: "multiple",
  allowFreeText: true,
  freeTextLabel: "Other",
  modal:         true,   // blocks input to all other components
});

q.onSubmit = (result: QuestionResult) => {
  console.log(result.selected);    // sorted array of selected indices
  console.log(result.freeText);    // contents of the free-text field
  q.dispose();
};

q.onChange = (result) => { /* called on every change */ };

QuestionOptions

Extends ComponentOptions with:

| Property | Type | Default | Description | |---|---|---|---| | question | string | required | The question text. Supports word-wrap and \n. | | choices | string[] | required | The answer choices. | | selectionMode | "single" \| "multiple" | "single" | Single or multi-choice. | | allowFreeText | boolean | false | Adds a free-text input row after the choices. | | freeTextLabel | string | "Altre" | Label shown before the free-text field. | | scrollBarColor | string | theme | ANSI color for the scroll bar. |

When size.height is 0, the height is computed automatically from the question text and number of choices.

QuestionResult

type QuestionResult = {
  selected:         number[];  // Sorted indices of selected choices
  freeText:         string;    // Contents of the free-text field
  freeTextSelected: boolean;   // Whether the free-text option is active
}

Public methods

| Method | Description | |---|---| | getResult() | Returns a QuestionResult snapshot of the current state. | | dispose() | Closes the question and restores focus to the previously focused component. |

Callbacks

| Callback | Description | |---|---| | onSubmit | Fired when the user confirms (Enter outside free-text mode). | | onChange | Fired on every selection or text change. |

Key bindings

Navigation mode:

| Key | Action | |---|---| | / | Move cursor between choices. | | Space | Toggle selection. | | Enter | Confirm (fires onSubmit). On the free-text row, enters text-editing mode. | | Any printable char on the free-text row | Activates free-text mode and inserts the character. |

Free-text editing mode:

| Key | Action | |---|---| | / | Move cursor within the field. | | Ctrl+← / Ctrl+→ | Jump word by word. | | Home / End | Beginning / end of the field. | | Backspace / Delete | Erase a character. | | / Escape | Exit free-text mode (keeps text). | | Enter | Confirm and fire onSubmit. |


TextBox

A fixed-grid widget for placing characters at arbitrary local offsets. Content is stored in a sparse internal grid and replayed on every re-render.

import { TextBox } from "zs3-tui";

const box = new TextBox({
  position: { x: 10, y: 5 },
  size:     { width: 24, height: 5 },
});

box.write("Hello, world!", { x: 2, y: 2 }, { fgColor: "\x1b[33m" });

TextBox is useful for static overlays and toast notifications. The write() offset is 1-indexed from the inside of the border: {x:1, y:1} is the top-left content cell.

Public methods

| Method | Description | |---|---| | write(text, offset?, colors?) | Writes text at offset (local, 1-indexed from inside the border) and stores it in the grid for re-renders. | | clear() | Erases all written cells and re-renders. | | dispose() | Erases the box and removes it from the registry. (Inherited from Component.) |


InputText

A multi-line text input with automatic word-wrap, scrolling, and a block cursor. The component grows vertically up to maxLines rows and then shows a scrollbar.

import { InputText } from "zs3-tui";

const input = new InputText({
  name:        "input",
  maxLines:    4,
  placeholder: "Type here… (Enter = submit, Alt+Enter = new line)",
});

input.onSubmit       = (text) => sendMessage(text);
input.onTextChange   = (text) => updateCharCount(text);
input.onHeightChange = (h)    => layout.setSize(input, h); // update layout when the field grows

InputTextOptions

Extends ComponentOptions with:

| Property | Type | Default | Description | |---|---|---|---| | maxLines | number | 4 | Maximum visual rows before the field scrolls. | | placeholder | string | "" | Muted text shown when the field is empty and unfocused. | | scrollBarColor | string | theme | ANSI color for the scroll bar. |

Public methods and properties

| Member | Description | |---|---| | value (get) | Returns the current text content. | | value (set) | Replaces the text programmatically. Cursor is clamped to the new length. | | clearText() | Clears the text, resets cursor and scroll, then re-renders. |

Callbacks

| Callback | Signature | Description | |---|---|---| | onSubmit | (text: string) => void | Fired when the user presses Enter. | | onTextChange | (text: string) => void | Fired on every content change (typing, paste, delete). | | onHeightChange | (newHeight: number) => void | Fired when the component's total height changes. Use this to call layout.setSize(input, h) so the layout adjusts. |

Key bindings

| Key | Action | |---|---| | / | Move cursor by one character. | | Ctrl+← / Ctrl+→ | Jump word by word. | | / | Move cursor to the line above/below. | | Home / End | Beginning / end of the current visual line. | | Ctrl+Home / Ctrl+End | Beginning / end of the entire text. | | Backspace / Delete | Erase one character. | | Alt+Enter | Insert a literal newline. | | Enter | Fire onSubmit. |


ThemeManager

Manages a set of named color themes and notifies components when the active theme changes. Components automatically re-render when the theme switches.

import { theme } from "zs3-tui";

// Switch to a different built-in theme
theme.selected = "dracula";  // dark | light | dracula | solarized | contrast | cyberpunk | matrix | monokai | nord

// Subscribe to theme changes (e.g. to clear the screen between renders)
const unsub = theme.subscribe(() => terminal.clear(theme.current.bgScreen));
// Call unsub() to stop receiving notifications.

// Read current colors
const { fgColor, bgColor, borderColor, focusColor, scrollBarColor, bgScreen } = theme.current;

Public API

| Member | Description | |---|---| | selected (get) | Name of the currently active theme (ThemeName). | | selected (set) | Switches the active theme and notifies all subscribers. | | current (get) | Returns the resolved Theme object for the active theme. | | getTheme(name) | Returns the resolved Theme for any built-in theme by name. | | themes (get) | Returns all themes as Record<ThemeName, Theme>. | | subscribe(cb) | Registers a callback; returns an unsubscribe function. |

Built-in themes

| Name | Style | |---|---| | dark | Dark background, cyan accent — default | | light | Light background, Windows blue accent | | dracula | Dracula palette with pink focus | | solarized | Solarized dark palette | | contrast | Pure black/white, yellow focus | | cyberpunk | Black/purple background, neon magenta focus | | matrix | Black/dark-green background, bright green focus | | monokai | Monokai palette, green focus | | nord | Nord palette, ice-blue focus |


Colors

The colors module exports ANSI escape-sequence constants and helpers for all color needs.

import { red, green, blue, bold, fg, bg, fg256, bg256 } from "zs3-tui";

Standard 16-color constants

Foreground: black red green yellow blue magenta cyan white
Bright foreground: brightBlack brightRed brightGreen brightYellow brightBlue brightMagenta brightCyan brightWhite

Background: bgBlack bgRed bgGreen bgYellow bgBlue bgMagenta bgCyan bgWhite
Bright background: bgBrightBlack bgBrightRed bgBrightGreen bgBrightYellow bgBrightBlue bgBrightMagenta bgBrightCyan bgBrightWhite

Text styles: bold dim italic underline blink inverse hidden strikethrough reset

True-color and 256-color helpers

fg(r, g, b)    // foreground RGB — e.g. fg(255, 128, 0) for orange
bg(r, g, b)    // background RGB
fg256(n)       // foreground 256-color palette index (0–255)
bg256(n)       // background 256-color palette index (0–255)

Named palette constants (foreground)

The module also exports named true-color constants that match the built-in themes:

Greys: snow offWhite silver pearl lightGray ashGray mediumGray gray slateGray darkGray dimGray charcoal almostBlack

Reds/pinks: crimson tomato coral salmon hotPink deepPink draculaPink orchid

Oranges/yellows: orange amber gold yellow255 lime

Greens: matrixGreen matrixGreenLight monokaiGreen emerald forestGreen mintGreen

Cyans: neonCyan cyanAccent iceBlue aqua turquoise skyBlue

Blues: windowsBlue solarizedBlue dodgerBlue cornflowerBlue royalBlue steelBlue purpleBlue nordSlate

Purples: neonMagenta violet deepViolet purple indigo lavender

Named palette constants (background)

bgSnow bgLightGray bgDark bgDarker bgBlackTrue bgDracula bgDraculaDark bgMatrix bgMatrixDark bgNord bgNordDark bgSolarized bgSolarizedDark bgMonokai bgMonokaiDark bgCyberpunk bgCyberpunkDark

Color conversion helpers

import { fgToBg, bgToFg } from "zs3-tui";

fgToBg(color)  // converts a foreground ANSI sequence to its background equivalent
bgToFg(color)  // converts a background ANSI sequence to its foreground equivalent

Icons

The icon() function returns a Unicode character by logical name, making it easy to use symbols in button labels, list items, or any other text.

import { icon } from "zs3-tui";

toolbar.addButton({ text: `${icon("power")} Exit`, name: "exit", action: exit });

Available icons

| Name | Char | Use | |---|---|---| | power | ⏻ | Exit / power off | | settings | ⚙ | Settings / configuration | | search | ⌕ | Search / filter | | edit | ✎ | Edit / rename | | save | ⊟ | Save to disk | | refresh | ↺ | Reload / retry | | close | ✕ | Close / discard | | ok | ✓ | Success / done | | error | ✗ | Error / invalid | | warning | ⚠ | Warning | | info | ℹ | Info | | arrow-up | ▲ | Move / scroll up | | arrow-down | ▼ | Move / scroll down | | arrow-left | ◀ | Back / previous | | arrow-right | ▶ | Forward / next | | expand | ⊞ | Open / maximize | | collapse | ⊟ | Close / minimize | | panel-off | ▭ | Panel hidden | | panel-on | ▬ | Panel visible | | folder | ▤ | Folder / directory | | file | ▢ | Regular file | | dark | ▓ | Dark theme indicator | | light | ░ | Light theme indicator | | bullet | ● | List bullet | | star | ★ | Favorite / pinned | | dot | · | Separator / muted marker | | circle | ○ | Empty state / inactive |


Utilities

center(size, relativeTo?)

Calculates the position that centers a component in the terminal (or in a given container).

import { center } from "zs3-tui";

const size = { width: 44, height: 10 };
const pos  = center(size);                    // centered in the terminal
const pos2 = center(size, { width: 80, height: 24 }); // centered in a given box
// → { x: number, y: number }

copyToClipboard(text)

Copies text to the system clipboard synchronously. Uses clip on Windows, pbcopy on macOS, and xclip on Linux.

import { copyToClipboard } from "zs3-tui";
copyToClipboard("text to copy");

readClipboard()

Reads text from the system clipboard asynchronously.

import { readClipboard } from "zs3-tui";
const text = await readClipboard();

randomId(length?)

Generates a random alphanumeric string. Defaults to 16 characters.

import { randomId } from "zs3-tui";
const id = randomId();     // e.g. "aB3kX9mPqZ1rWnYt"
const id6 = randomId(6);   // e.g. "aB3kX9"

Example: Process Monitor

The following example is a complete process monitor application. It demonstrates LayoutManager, TextArea, List, Toolbar, TextBox, and Question working together.

processes.ts

This module provides cross-platform helpers for reading the system process list.

import { execSync } from "child_process";

// A minimal process descriptor: name and PID
export interface Process {
  name: string;
  pid:  number;
}

// Detailed process information fetched on demand when a process is selected
export interface ProcessInfo {
  name:            string;
  pid:             number;
  ppid:            number | null;   // Parent PID
  user:            string | null;
  status:          string | null;
  priority:        string | null;
  cpu:             number | null;   // CPU time in seconds
  memoryRss:       number | null;   // Resident set size in bytes
  memoryVirtual:   number | null;   // Virtual memory size in bytes
  threadCount:     number | null;
  handleCount:     number | null;
  startTime:       string | null;
  executablePath:  string | null;
  commandLine:     string | null;
}

// Returns the list of running processes using tasklist (Windows) or ps (Unix/macOS)
export function getProcesses(): Process[] {
  if (process.platform === "win32") {
    const output = execSync("tasklist /fo csv /nh", { encoding: "utf8" });
    return output
      .trim()
      .split("\n")
      .map((line) => {
        const parts = line.split(",").map((p) => p.replace(/"/g, "").trim());
        return { name: parts[0], pid: parseInt(parts[1], 10) };
      })
      .filter((p) => p.name && !isNaN(p.pid));
  } else {
    const output = execSync("ps -e -o pid=,comm=", { encoding: "utf8" });
    return output
      .trim()
      .split("\n")
      .map((line) => {
        const [pid, ...nameParts] = line.trim().split(/\s+/);
        return { name: nameParts.join(" "), pid: parseInt(pid, 10) };
      })
      .filter((p) => p.name && !isNaN(p.pid));
  }
}

// Returns detailed info for a single PID, or null if the process no longer exists
export function getProcessInfo(pid: number): ProcessInfo | null {
  try {
    return process.platform === "win32"
      ? getProcessInfoWindows(pid)
      : getProcessInfoUnix(pid);
  } catch {
    return null;
  }
}

index.ts

import {
  terminal, hideCursor, LayoutManager,
  TextArea, List, Toolbar, theme,
  Question, type QuestionResult,
  icon, TextBox, center,
} from "zs3-tui";
import { getProcesses, getProcessInfo, type Process } from "./processes.js";

// ─── Exit confirmation ────────────────────────────────────────────────────────

const exitProgram = () => {
  // Compute the size without a height so the Question auto-sizes itself
  const questionSize = { width: 44, height: 0 };

  const question = new Question({
    // center() returns {x, y} that places the component in the middle of the screen
    position: center(questionSize),
    size:     questionSize,
    question: "Are you sure you want to exit?",
    choices:  ["Yes", "No"],
    selectionMode: "single",
    allowFreeText: false,
    modal: true,  // blocks all input to background components while open
  });

  // onSubmit fires when the user presses Enter on a choice
  question.onSubmit = (_result: QuestionResult) => {
    if (_result.selected[0] === 0) {
      // "Yes" was chosen: restore the terminal and exit
      terminal.dispose();
      process.exit(0);
    } else {
      // "No": just close the dialog and return to the app
      question.dispose();
    }
  };

  // Allow the user to close the dialog with Escape as well
  question.onKeyDown = (ev) => {
    if (ev.key === "Escape") question.dispose();
  };
};

// ─── Main ─────────────────────────────────────────────────────────────────────

async function main() {
  // Enter alternate screen buffer, enable mouse, hide cursor
  terminal.start();
  terminal.clear(theme.current.bgScreen);
  terminal.mouse(true);
  hideCursor();

  // Re-clear the screen whenever the theme changes so colors are consistent
  theme.subscribe(() => terminal.clear(theme.current.bgScreen));

  // Create components
  const detailsArea = new TextArea({ name: "details" });
  const list        = new List({ name: "processesList", selectionMode: "single" });

  // Populate with a loading toast
  const toastText = "loading...";
  const toastSize = { width: toastText.length + 4, height: 3 };
  const toast = new TextBox({ position: center(toastSize), size: toastSize });
  toast.write(toastText, { x: 2, y: 1 });

  const processes = getProcesses();
  processes.forEach((proc) => list.addLine(`${proc.pid} - ${proc.name}`));
  toast.dispose();

  list.onClick = (index) => {
    const proc     = processes[index];
    const procInfo = getProcessInfo(proc.pid);
    if (procInfo) {
      detailsArea.clear();
      detailsArea.addLine(`Name: ${procInfo.name}`);
      detailsArea.addLine(`PID:  ${procInfo.pid}`);
    }
  };

  const toolbar = new Toolbar({ gap: 0, direction: "horizontal", name: "actionsToolbar" });
  toolbar
    .addButton({ text: `${icon("power")} Exit`,    name: "exit",    action: () => exitProgram() })
    .addButton({ text: `${icon("refresh")} Reload`, name: "refresh", action: () => {
      list.clear();
      getProcesses().forEach(p => list.addLine(`${p.pid} - ${p.name}`));
    }});

  // ─── Layout ──────────────────────────────────────────────────────────────
  //
  //   root (column)
  //   ├── toolbar   (size: 5)       — fixed 5 rows at the top
  //   └── content (row, flex: 1)    — remaining vertical space
  //       ├── list        (flex: 1) — left half: process list
  //       └── detailsArea (flex: 1) — right half: process details
  //
  const root    = new LayoutManager({ direction: "column" });
  const content = new LayoutManager({ direction: "row" });

  root.add(toolbar,     { size: 5 });
  root.add(content,     { flex: 1 });
  content.add(list,        { flex: 1 });
  content.add(detailsArea, { flex: 1 });

  terminal.addResizeListener(size => root.resize(size));
  root.resize(terminal.getSize());

  // ─── Event loop ───────────────────────────────────────────────────────────
  while (true) {
    const ev = await terminal.waitForKey();
    if (ev.key === "Escape") {
      exitProgram();
      break;
    }
  }
}

main().catch((error) => console.error(String(error)));

License

MIT