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

terminui

v0.3.1

Published

A fast, functional TypeScript library for building terminal user interfaces

Downloads

372

Readme

terminui

terminui

A fast, functional TypeScript library for building terminal user interfaces.

Features

  • Pure functional — no classes, no this, no mutation. Plain objects in, plain objects out.
  • Double-buffered rendering — only changed cells are flushed to the terminal between frames.
  • Rich layout system — split any rect with constraints: Length, Percentage, Ratio, Min, Max, Fill.
  • Full style system — 16 ANSI colors, 256-color indexed, 24-bit RGB, modifiers (bold, italic, underline, etc.).
  • Wide character support — CJK and fullwidth characters are measured and rendered correctly.
  • 10+ built-in widgets — Block, Paragraph, List, Table, Gauge, Tabs, Sparkline, BarChart, Scrollbar, Clear.
  • Stateful widgets — List and Table selection, Scrollbar position with offset management.
  • Pluggable backends — test backend included; bring your own Node.js terminal backend.
  • TypeScript strict modestrict: true, noUncheckedIndexedAccess: true, zero any.

Install

pnpm add terminui

Quick Start

import {
  createTestBackendState,
  createTestBackend,
  testBackendToString,
  createTerminal,
  terminalDraw,
  frameRenderWidget,
  createRect,
  createLayout,
  lengthConstraint,
  fillConstraint,
  splitLayout,
  blockBordered,
  createTitle,
  createParagraph,
  renderParagraph,
} from 'terminui';

// Set up a test backend (swap for a real terminal backend in production)
const state = createTestBackendState(60, 10);
const backend = createTestBackend(state);
const terminal = createTerminal(backend);

terminalDraw(terminal, (frame) => {
  const paragraph = createParagraph('Hello, terminui!', {
    block: blockBordered({ titles: [createTitle('Greeting')] }),
  });
  frameRenderWidget(frame, renderParagraph(paragraph), frame.area);
});

console.log(testBackendToString(state));

Output:

┌Greeting──────────────────────────────────────────────┐
│Hello, terminui!                                      │
│                                                      │
│                                                      │
│                                                      │
│                                                      │
│                                                      │
│                                                      │
│                                                      │
└──────────────────────────────────────────────────────┘

JSX API (React-like)

You can keep the same terminal performance model and write UIs in a JSX style.

terminui does not use a virtual DOM reconciler here; JSX is translated into the same widget render calls (frameRenderWidget, frameRenderStatefulWidget) and still uses the existing double-buffered diff renderer.

/** @jsxImportSource terminui */
import { createTestBackendState, createTestBackend, createTerminal } from 'terminui';
import { terminalDrawJsx, Column, Row, Panel, Label, List, Gauge } from 'terminui/jsx';
import { lengthConstraint, fillConstraint } from 'terminui';

const state = createTestBackendState(60, 12);
const terminal = createTerminal(createTestBackend(state));

terminalDrawJsx(
  terminal,
  <Column constraints={[lengthConstraint(3), fillConstraint(1)]}>
    <Panel title="Header" p={1}>
      <Label text="JSX-powered terminal UI" align="center" bold />
    </Panel>
    <Row constraints={[fillConstraint(1), fillConstraint(1)]} gap={1}>
      <Panel title="Menu">
        <List items={['Overview', 'Metrics', 'Logs']} />
      </Panel>
      <Panel title="Load">
        <Gauge percent={42} />
      </Panel>
    </Row>
  </Column>,
);

Available JSX components include VStack, HStack, Box, Text, List, Table, Gauge, LineGauge, Tabs, Sparkline, BarChart, Scrollbar, Clear, and Cursor.

React-like aliases are also available:

  • Row = HStack
  • Column = VStack
  • Panel = Box with border enabled by default
  • Label = Text

Common shorthand props:

  • gap on Row / Column for spacing
  • p, px, py for panel/widget padding
  • border and title for block setup
  • fg, bg, bold for style
  • align for text alignment

Helper APIs:

  • terminalDrawJsx(terminal, <UI />)
  • terminalLoopJsx(terminal, (frame, tick) => <UI />, { maxFrames: 120 })

JSX Starter (Copy/Paste)

Minimal tsconfig.json:

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "terminui"
  }
}

Minimal hello.tsx:

/** @jsxRuntime automatic */
/** @jsxImportSource terminui */
import { createTestBackendState, createTestBackend, createTerminal, testBackendToString } from 'terminui';
import { terminalDrawJsx, Panel, Label } from 'terminui/jsx';
import { Color } from 'terminui';

const state = createTestBackendState(40, 6);
const terminal = createTerminal(createTestBackend(state));

terminalDrawJsx(
  terminal,
  <Panel title="Hello JSX" p={1}>
    <Label text="terminal UI, React-like syntax" fg={Color.Cyan} bold align="center" />
  </Panel>,
);

console.log(testBackendToString(state));

JSX Troubleshooting

If you see a TypeScript error like:

'X' cannot be used as a JSX component

check the following:

  1. Use .tsx files for JSX code.
  2. Use automatic JSX runtime with terminui as the import source.
  3. Import UI components from terminui/jsx (not from react).
  4. Make sure you are on a recent terminui version with JSX typing fixes.

tsconfig.json example:

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "terminui"
  }
}

Per-file override (optional):

/** @jsxRuntime automatic */
/** @jsxImportSource terminui */

Architecture

terminui follows a functional architecture with zero classes:

Backend → Terminal → Frame → Buffer → Cells
                       ↑
                    Widgets (pure render functions)
                       ↑
                    Layout (constraint solver)

Everything is a function. Widgets are functions that take config and return a WidgetRenderer — a function (area: Rect, buf: Buffer) => void. Compose them however you want.

Layout System

Split any rectangle using constraints:

import { createLayout, lengthConstraint, fillConstraint, percentageConstraint, splitLayout, createRect } from 'terminui';

const layout = createLayout([
  lengthConstraint(3),       // exactly 3 rows
  percentageConstraint(50),  // 50% of remaining
  fillConstraint(1),         // fill the rest
]);

const area = createRect(0, 0, 80, 24);
const [header, body, footer] = splitLayout(layout, area);

Constraint Types

| Constraint | Description | |---|---| | lengthConstraint(n) | Exactly n cells | | percentageConstraint(n) | n% of available space | | ratioConstraint(num, den) | num/den of available space | | minConstraint(n) | At least n cells | | maxConstraint(n) | At most n cells | | fillConstraint(weight) | Fill remaining space (weighted) |

Directions

// Vertical (default) — splits into rows
const vLayout = createLayout([...constraints]);

// Horizontal — splits into columns
const hLayout = createLayout([...constraints], { direction: 'horizontal' });

Style System

import { createStyle, styleFg, styleBg, styleAddModifier, Color, Modifier, patchStyle } from 'terminui';

const bold = styleAddModifier(createStyle(), Modifier.BOLD);
const warning = styleFg(styleBg(createStyle(), Color.Yellow), Color.Black);
const merged = patchStyle(bold, warning); // combines both

Colors

Color.Reset, Color.Black, Color.Red, Color.Green, Color.Yellow,
Color.Blue, Color.Magenta, Color.Cyan, Color.Gray, Color.White,
Color.DarkGray, Color.LightRed, Color.LightGreen, ...

indexedColor(42)           // 256-color palette
rgbColor(255, 128, 0)     // 24-bit true color

Modifiers

Modifier.BOLD, Modifier.DIM, Modifier.ITALIC, Modifier.UNDERLINED,
Modifier.SLOW_BLINK, Modifier.RAPID_BLINK, Modifier.REVERSED,
Modifier.HIDDEN, Modifier.CROSSED_OUT, Modifier.DOUBLE_UNDERLINED,
Modifier.OVERLINED

Widgets

Block

Container with borders, titles, and padding:

┌Header────────────────────────────────────────────────┐
│                                                      │
│  Content goes here with padding and borders         │
│                                                      │
└──────────────────────────────────────────────────────┘
import { blockBordered, createTitle, renderBlock, Borders } from 'terminui';

const block = blockBordered({
  titles: [
    createTitle('Header'),
    createTitle('Footer', { position: 'bottom', alignment: 'center' }),
  ],
  borderType: 'rounded',  // 'plain' | 'rounded' | 'double' | 'thick'
  padding: uniformPadding(1),
});

frameRenderWidget(frame, renderBlock(block), area);

Paragraph

Text display with wrapping, alignment, and scrolling:

┌Paragraph─────────────────────────────────────────────┐
│  Long text that wraps automatically to fit the      │
│  available width. Supports alignment and scrolling  │
│  for content larger than the viewport.              │
└──────────────────────────────────────────────────────┘
import { createParagraph, renderParagraph } from 'terminui';

const p = createParagraph('Long text that wraps...', {
  block: blockBordered({ titles: [createTitle('Paragraph')] }),
  wrap: { trim: true },
  alignment: 'center',
  scroll: [2, 0],  // skip first 2 lines
});

frameRenderWidget(frame, renderParagraph(p), area);

List

Vertical scrollable list with selection:

┌Menu──────────────────────────────────────────────────┐
│ ▶ Item 1                                             │
│   Item 2                                             │
│   Item 3                                             │
│   Item 4                                             │
│   Item 5                                             │
└──────────────────────────────────────────────────────┘
import { createList, createListState, renderStatefulList } from 'terminui';

const list = createList(['Item 1', 'Item 2', 'Item 3'], {
  block: blockBordered({ titles: [createTitle('Menu')] }),
  highlightStyle: styleFg(createStyle(), Color.Yellow),
  highlightSymbol: '▶ ',
});

const state = createListState(0); // selected index
frameRenderStatefulWidget(frame, renderStatefulList(list), area, state);

// Navigate: state.selected = 1;

Table

Grid data with column constraints:

┌Users─────────────────────────────────────────────────┐
│ Name       Age  Role                                 │
│ Alice      42   admin                                │
│ Bob        37   user                                 │
│ Charlie    29   user                                 │
└──────────────────────────────────────────────────────┘
import { createTable, createRow, renderTable, lengthConstraint, fillConstraint } from 'terminui';

const table = createTable(
  [
    createRow(['Alice', '42', 'admin']),
    createRow(['Bob', '37', 'user']),
  ],
  [lengthConstraint(10), lengthConstraint(5), fillConstraint(1)],
  {
    header: createRow(['Name', 'Age', 'Role']),
    block: blockBordered({ titles: [createTitle('Users')] }),
  },
);

frameRenderWidget(frame, renderTable(table), area);

Gauge

Progress bars — unicode block characters or ASCII:

┌Progress──────────────────────────────────────────────┐
│ ████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
│ 67%                                                  │
└──────────────────────────────────────────────────────┘
import { gaugePercent, renderGauge } from 'terminui';

const gauge = gaugePercent(67, {
  block: blockBordered({ titles: [createTitle('Progress')] }),
  useUnicode: true,
  gaugeStyle: styleFg(createStyle(), Color.Green),
});

frameRenderWidget(frame, renderGauge(gauge), area);

Tabs

Horizontal tab selection:

 Dashboard  Logs  Settings
 ─────────────────────────────────────────────────────
 Dashboard content displayed here
import { createTabs, renderTabs } from 'terminui';

const tabs = createTabs(['Dashboard', 'Logs', 'Settings'], {
  selected: 0,
  highlightStyle: styleFg(createStyle(), Color.Yellow),
});

frameRenderWidget(frame, renderTabs(tabs), area);

Sparkline

Tiny inline data visualization:

┌CPU───────────────────────────────────────────────────┐
│ ▁▄▂█▅▃▇▆                                             │
└──────────────────────────────────────────────────────┘
import { createSparkline, renderSparkline } from 'terminui';

const spark = createSparkline([1, 4, 2, 8, 5, 3, 7, 6], {
  block: blockBordered({ titles: [createTitle('CPU')] }),
  style: styleFg(createStyle(), Color.Cyan),
});

frameRenderWidget(frame, renderSparkline(spark), area);

BarChart

Grouped bar charts (vertical or horizontal):

┌Chart─────────────────────────────────────────────────┐
│                                                      │
│   ███   ███   ███                                    │
│   ███   ███   ███                                    │
│   ███   ███   ███                                    │
│   ███   ███   ███                                    │
│  Group A Group B Group C                            │
└──────────────────────────────────────────────────────┘
import { createBar, createBarGroup, createBarChart, renderBarChart } from 'terminui';

const chart = createBarChart(
  [createBarGroup([createBar(5), createBar(8), createBar(3)], 'Group A')],
  { barWidth: 3, direction: 'vertical' },
);

frameRenderWidget(frame, renderBarChart(chart), area);

Scrollbar

Scrollbar overlay for any area:

┌Content────────────────────────────────────────────┐ █
│ Line 1                                            │ █
│ Line 2                                            │ ░
│ Line 3                                            │ ░
│ Line 4                                            │ ░
│ Line 5                                            │ ░
│ ...                                               │ ░
└────────────────────────────────────────────────────┘ █
import { createScrollbar, createScrollbarState, renderStatefulScrollbar } from 'terminui';

const sb = createScrollbar('verticalRight');
const sbState = createScrollbarState(100, 25);
sbState.viewportContentLength = 20;

frameRenderStatefulWidget(frame, renderStatefulScrollbar(sb), area, sbState);

Rendering Modes

Primary Screen

Renders directly to the terminal's primary screen. Good for one-shot output or piped commands:

const state = createTestBackendState(80, 24);
const backend = createTestBackend(state);
const terminal = createTerminal(backend);

terminalDraw(terminal, (frame) => {
  // render widgets to frame
});

console.log(testBackendToString(state));

Alternate Screen

For full-screen TUI apps (like vim, htop). In production you'd use a backend that:

  1. Enters the alternate screen (\x1b[?1049h)
  2. Renders frames in a loop with diff-based updates
  3. Exits the alternate screen on cleanup (\x1b[?1049l)
// Pseudo-code for a real alternate screen app:
const backend = createNodeBackend(); // your Node.js backend
const terminal = createTerminal(backend);

// Enter alternate screen
process.stdout.write('\x1b[?1049h');

// Main loop
while (running) {
  terminalDraw(terminal, (frame) => {
    // render your UI
  });
  await waitForInput();
}

// Exit alternate screen
process.stdout.write('\x1b[?1049l');

The double-buffered architecture ensures only changed cells are written between frames — minimal I/O for maximum performance.

Examples

Run the included examples:

# Minimal JSX starter
npx tsx examples/jsx-hello.tsx

# Interactive fake-AI terminal chat demo
npx tsx examples/jsx-chatbot.tsx

# React-like JSX API demo
npx tsx examples/jsx-dashboard.tsx

# Dashboard rendered to primary screen
npx tsx examples/primary-screen.ts

# Multi-frame alternate screen simulation
npx tsx examples/alternate-screen.ts

# Full kitchen-sink dashboard (all widgets + layout + styles + state)
npx tsx examples/kitchen-sink.ts

# Live weather dashboard (Open-Meteo)
npx tsx examples/weather-dashboard.ts --city "New York"

# One-shot weather snapshot (non-animated)
npx tsx examples/weather-dashboard.ts --city "New York" --once

examples/jsx-chatbot.tsx is the production-style reference for interactive terminal UX:

  • uses alternate screen and clears scrollback
  • uses readline keypress input in raw mode (no prompt spam)
  • uses diff-based cell updates with batched ANSI writes
  • uses a minimal, modern CLI layout (vim/blessed-like)
  • includes interactive controls:
    • Enter send message
    • / scroll conversation
    • /clear reset chat history
    • Esc or Ctrl+C quit cleanly

Dev

pnpm install
pnpm test        # vitest
pnpm typecheck   # tsc --noEmit
pnpm lint        # biome check
pnpm build       # tsup

Stack

  • TypeScript (strict mode)
  • Biome (lint + format)
  • Vitest (tests)
  • tsup (bundling)
  • pnpm (package manager)

License

Apache-2.0 - Ahmad Awais