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 devOr install individual packages:
npm install @termuijs/core @termuijs/widgets @termuijs/jsxKey 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.tsxHeadless 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 changeThe Package Ecosystem
| Package | What it does |
|---|---|
@termuijs/core | Screen buffer, input parsing, event system, flexbox layout |
@termuijs/widgets | Box, Text, Table, ProgressBar, Spinner, Gauge, VirtualList, and more |
@termuijs/ui | Select, Tabs, Modal, Toast, Tree, MultiSelect, CommandPalette |
@termuijs/jsx | JSX runtime with useState, useEffect, useRef, useContext, useAsync, memo() |
@termuijs/store | Zustand-like global state with selector subscriptions |
@termuijs/testing | In-memory test renderer: render, query, fireKey, assert |
@termuijs/tss | Terminal Style Sheets with variables and pseudo-classes |
@termuijs/motion | Spring-physics and easing-based animations |
@termuijs/router | File-based screen routing with typed params and guards |
@termuijs/data | Real-time CPU, memory, disk, network, and process data |
@termuijs/dev-server | Hot-reload dev server with restart in under 200ms |
@termuijs/quick | Fluent builder API for rapid dashboard prototyping |
create-termui-app | Project scaffolding CLI |
Examples Included
Five working examples ship with the repo:
dashboard: Real-time system monitor using the quick APIjsx-dashboard: Same dashboard rebuilt with JSX componentsshowcase: Widget gallery showing every built-in componentsystem-monitor: Advanced process and resource monitortodo-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.tsxRequirements
- 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 packagesWhat 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:
dfoutput differs between macOS and Linux. The disk monitor reads column index 7 on macOS and 4 on Linux. - Effect cleanup ordering:
useEffectcleanup 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