Command Palette

Search for a command to run...

Back to Projects
Open SourceDeveloper ToolsCLITypeScript2026-02 — PresentActive Development

TermUI

A TypeScript framework for building terminal user interfaces with JSX, hooks, animations, and a full component ecosystem

TypeScriptNode.js

Overview

TermUI is a TypeScript framework for building terminal apps. It ships with a layout engine, JSX support, React-style hooks, global state, routing, animations, a theming system, a hot-reload dev server, and a test renderer. All 13 packages are published to npm under the @termuijs scope. No C extensions. No curses bindings. Pure TypeScript.

The Problem

Terminal UIs have always been hard to build. The existing tools fall into two camps:

  • Low-level: You manage raw ANSI codes, cursor positions, and buffer flushing yourself
  • High-level but opinionated: One widget system, one layout model, no escape hatch

Neither option works well for building rich, interactive terminal apps at scale. You end up writing the same plumbing: input parsing, layout math, state diffing, in every project.

The Solution

TermUI treats the terminal like a UI framework treats the browser. You write components. The framework handles rendering.

The architecture is strict about layers. Each package depends only on the ones below it. You install what you need.

Application Layer:    @termuijs/ui · @termuijs/quick · create-termui-app
Component Layer:      @termuijs/widgets · @termuijs/jsx · @termuijs/store
Testing:              @termuijs/testing
Feature Layer:        @termuijs/tss · @termuijs/router · @termuijs/motion
Core Layer:           @termuijs/core · @termuijs/data

Getting Started

npx create-termui-app my-app
cd my-app
npm install
npm run dev

Or install individual packages:

npm install @termuijs/core @termuijs/widgets @termuijs/jsx

Key Features

JSX with React-Style Hooks

You write components exactly as you would in React. useState, useEffect, useRef, useContext, useAsync, useInput, useInterval, useMemo, and memo() all work as expected.

import { App } from '@termuijs/core'
import { Box, Text } from '@termuijs/widgets'
import { useState, useInput } from '@termuijs/jsx'
 
function Counter() {
    const [count, setCount] = useState(0)
    useInput((key) => {
        if (key === '+') setCount((c) => c + 1)
        if (key === 'q') app.exit()
    })
    return (
        <Box border="round" padding={1}>
            <Text bold>Count: {count}</Text>
            <Text dim>Press + to increment · q to quit</Text>
        </Box>
    )
}
 
const app = new App(rootWidget)
await app.mount()

Context API

Share state across your component tree without prop drilling. The API is identical to React's context:

import { createContext, useContext } from '@termuijs/jsx'
 
const ThemeCtx = createContext({ primary: '#00ff88', bg: '#0a0a0f' })
 
function App() {
    return (
        <ThemeCtx.Provider value={theme}>
            <Dashboard />
        </ThemeCtx.Provider>
    )
}
 
function StatusBar() {
    const { primary } = useContext(ThemeCtx)
    return <Text color={primary}>Ready</Text>
}

Global State with @termuijs/store

Zustand-like state management with selector subscriptions. Components only re-render when the slice of state they read actually changes:

import { createStore } from '@termuijs/store'
 
const useAppStore = createStore((set) => ({
    count: 0,
    increment: () => set((s) => ({ count: s.count + 1 })),
}))
 
function Counter() {
    const count     = useAppStore((s) => s.count)
    const increment = useAppStore((s) => s.increment)
 
    useInput((key) => { if (key === '+') increment() })
    return <Text>Count: {count}</Text>
}

VirtualList for Large Datasets

Renders only visible rows. A list of 1,000,000 items performs identically to a list of 10:

import { VirtualList } from '@termuijs/widgets'
 
const list = new VirtualList({
    totalItems: 1_000_000,
    renderItem: (index) => `Log line ${index}: some content`,
    onSelect:   (index) => openDetail(index),
})
 
app.events.on('key', (e) => {
    if (e.key === 'up')   list.selectPrev()
    if (e.key === 'down') list.selectNext()
})

Terminal Style Sheets (@termuijs/tss)

CSS-like theming with variables, pseudo-classes, and 6 built-in themes: default, cyberpunk, nord, dracula, catppuccin, solarized. You write styles once and swap themes at runtime.

Spring Animations (@termuijs/motion)

Physics-based animation with stepSpring() and animateSpring(). Six spring presets: default, stiff, gentle, wobbly, slow, molasses. Easing functions included.

File-Based Routing (@termuijs/router)

Screen routing with typed [id] dynamic params, a history stack with configurable maxHistory, and navigation events. The router scans your file tree and wires routes automatically.

Hot Reload Dev Server

Your app restarts in under 200ms on every file save:

npm run dev
# or directly:
npx termui-dev --entry src/index.tsx

Headless Test Renderer

Write fast component tests without a real terminal:

import { render } from '@termuijs/testing'
 
const t = render(<Counter />)
expect(t.getByText('Count: 0')).toBeTruthy()
 
t.fireKey('+')
expect(t.getByText('Count: 1')).toBeTruthy()
 
t.unmount()

Fluent Builder API (@termuijs/quick)

Build a full system monitor dashboard in ~20 lines:

import { app, gauge, table } from '@termuijs/quick'
import { cpu, memory, processes } from '@termuijs/data'
 
app('System Monitor')
    .rows(
        app.cols(
            gauge('CPU', () => cpu.percent / 100),
            gauge('MEM', () => memory.percent / 100),
        ),
        table('Processes', {
            columns: ['Name', 'PID', 'CPU%'],
            data: () => processes.top(10).map(p => ({
                Name: p.name,
                PID: p.pid,
                'CPU%': p.cpu.toFixed(1),
            })),
        }),
    )
    .run()

memo() and Batched Updates

memo() skips re-renders when props haven't changed. Batching collapses multiple setState calls into one render pass automatically:

import { memo } from '@termuijs/jsx'
 
const ProcessRow = memo(function ProcessRow({ pid, name, cpu }) {
    return <Text>{pid} {name} {cpu}%</Text>
})
// Only re-renders when props actually change

The Package Ecosystem

PackageWhat it does
@termuijs/coreScreen buffer, input parsing, event system, flexbox layout
@termuijs/widgetsBox, Text, Table, ProgressBar, Spinner, Gauge, VirtualList, and more
@termuijs/uiSelect, Tabs, Modal, Toast, Tree, MultiSelect, CommandPalette
@termuijs/jsxJSX runtime with useState, useEffect, useRef, useContext, useAsync, memo()
@termuijs/storeZustand-like global state with selector subscriptions
@termuijs/testingIn-memory test renderer: render, query, fireKey, assert
@termuijs/tssTerminal Style Sheets with variables and pseudo-classes
@termuijs/motionSpring-physics and easing-based animations
@termuijs/routerFile-based screen routing with typed params and guards
@termuijs/dataReal-time CPU, memory, disk, network, and process data
@termuijs/dev-serverHot-reload dev server with restart in under 200ms
@termuijs/quickFluent builder API for rapid dashboard prototyping
create-termui-appProject scaffolding CLI

Examples Included

Five working examples ship with the repo:

  • dashboard: Real-time system monitor using the quick API
  • jsx-dashboard: Same dashboard rebuilt with JSX components
  • showcase: Widget gallery showing every built-in component
  • system-monitor: Advanced process and resource monitor
  • todo-app: Interactive todo list with keyboard navigation

Run any of them with:

git clone https://github.com/Karanjot786/TermUI.git
cd TermUI
pnpm install
pnpm run build
 
cd examples/dashboard
npx tsx src/index.tsx

Requirements

  • Node.js 18+
  • A terminal with TTY support (256-color or truecolor recommended)

Testing

pnpm test        # 356 tests across 44 test files
pnpm run build   # Build all 13 packages

What I Built and Learned

Building TermUI meant solving problems that don't exist in browser UI work:

  • Double-wide characters: CJK glyphs occupy two columns. Every width calculation breaks without explicit Unicode handling.
  • TTY vs pipe: When stdout is piped, raw mode fails. The framework detects TTY state and degrades cleanly.
  • Memory management in long-running processes: The JSX reconciler had a map that grew without bound. Fixed by deleting stale instance entries on every re-render.
  • Cross-platform disk stats: df output differs between macOS and Linux. The disk monitor reads column index 7 on macOS and 4 on Linux.
  • Effect cleanup ordering: useEffect cleanup had to run before the effect re-executed, not after. Getting this order right required rewriting the tracking entirely.

Each bug taught a different lesson about what the terminal actually is: a stateful, low-level I/O device that assumes nothing about your rendering model.

Roadmap

Shipped

  • 13 packages on npm
  • 356 passing tests
  • Hot-reload dev server
  • 6 built-in themes
  • 5 working examples
  • Documentation website at termui.io

In Progress

  • Full docs coverage for all 13 packages
  • Plugin API for custom renderers
  • Accessibility (screen reader passthrough mode)

Planned

  • @termuijs/forms: form validation and field binding
  • @termuijs/charts: sparklines, bar charts, time-series
  • VS Code extension for live preview during development

Key Metrics