PromptHub
Back to Blog
Developer Tools TypeScript

Stop Wrestling with OOP TUI Libraries! Terminui's Functional Secret Revealed

B

Bright Coding

Author

12 min read 101 views
Stop Wrestling with OOP TUI Libraries! Terminui's Functional Secret Revealed

Stop Wrestling with OOP TUI Libraries! Terminui's Functional Secret Revealed

Your terminal app is stuttering again, isn't it? Every frame redraw flickers like a dying fluorescent bulb. Your class-heavy TUI framework demands inheritance gymnastics for a simple progress bar. Mutation bugs hide in this.state like landmines. And that "simple" dashboard? It's now 3,000 lines of object-oriented spaghetti that even you can't debug.

What if I told you there's a radically different path?

Meet terminui — the TypeScript library that's making veteran developers abandon their bloated terminal frameworks. No classes. No this. No mutation. Just pure functions, insane performance, and a JSX API that feels like React↗ Bright Coding Blog but renders at lightspeed thanks to double-buffered diffing.

Sound impossible? Ahmad Awais thought so too — until he built it. And now terminal UIs will never be the same.


What is Terminui?

Terminui is a fast, functional TypeScript library for building terminal user interfaces, created by Ahmad Awais, a prolific open-source developer known for pushing JavaScript↗ Bright Coding Blog tooling forward. But calling it "just another TUI library" misses the revolution entirely.

The project emerged from a simple, brutal observation: every major terminal UI framework forces object-oriented patterns onto a problem that screams for functional composition. Classes for widgets. Mutable state for rendering. Inheritance hierarchies that collapse under real-world complexity. Terminui answers with architectural heresy — zero classes, pure functions, immutable data everywhere.

Here's why it's trending now: terminal applications are experiencing a renaissance. Developers are building CLI tools, dashboards, and interactive interfaces that rival web apps in sophistication. But existing solutions — Blessed, Ink, even React-based terminals — carry baggage. Blessed is unmaintained and callback-hell incarnate. Ink forces React's reconciliation overhead into a constrained environment. Terminui cuts through this Gordian knot with a function-first, zero-overhead architecture that renders only what changes.

The secret weapon? Double-buffered rendering with cell-level diffing. Instead of clearing and redrawing entire screens, terminui tracks exactly which terminal cells changed between frames and flushes only those. The result? Buttery-smooth 60fps terminal UIs even on SSH connections and resource-constrained environments.

With TypeScript strict mode enforcement (strict: true, noUncheckedIndexedAccess: true, zero any), terminui catches bugs at compile time that other frameworks let crash at runtime. This isn't just a library — it's a statement about how terminal software should be built.


Key Features That Destroy the Competition

Terminui's feature set reads like a wishlist every terminal developer has scribbled on a frustrated sticky note:

🔥 Pure Functional Architecture — Every widget is a function. Config in, renderer out. Compose them like LEGO blocks without inheritance chains or prototype pollution. The entire pipeline flows: Backend → Terminal → Frame → Buffer → Cells, with widgets as pure functions injecting render logic.

⚡ Double-Buffered Diff Rendering — Two frame buffers swap each render cycle. Only changed cells generate ANSI escape sequences. This isn't optimization — it's architectural minimalism. Your terminal I/O drops by 90%+ compared to full-screen redraws.

📐 Constraint-Based Layout Engine — Split any rectangle with six constraint types: Length (exact cells), Percentage (proportional), Ratio (fractional), Min/Max (bounds), and Fill (weighted remainder). Vertical and horizontal directions supported. Responsive terminal layouts, finally.

🎨 Comprehensive Style System — 16 ANSI colors, 256-color indexed palette, full 24-bit RGB true color. Modifiers galore: bold, italic, underline, double underline, overline, slow blink, rapid blink, reversed, hidden, crossed out. Build aesthetic CLIs that don't look like 1987.

🌏 Wide Character Correctness — CJK characters, fullwidth symbols, and complex scripts measure and render accurately. No more broken box-drawing around Chinese text or emoji truncation.

🧩 10+ Production Widgets — Block, Paragraph, List, Table, Gauge, Tabs, Sparkline, BarChart, Scrollbar, Clear. Stateful variants for List/Table selection and Scrollbar position with automatic offset management.

🔌 Pluggable Backend System — Test backend included for CI and screenshot generation. Swap in your Node.js terminal backend for production. The abstraction means terminui runs anywhere JavaScript does.

⚛️ JSX Without Virtual DOM Overhead — React-like syntax compiles directly to widget render calls. No reconciliation. No diffing virtual trees. Just declarative markup that feeds the same blazing renderer.


Use Cases Where Terminui Dominates

1. Real-Time DevOps↗ Bright Coding Blog Dashboards

Monitor Docker↗ Bright Coding Blog containers, Kubernetes pods, or CI pipelines with live-updating gauges, sparklines, and log tables. The double-buffered renderer means zero flicker even when streaming 100+ metrics per second. The constraint layout adapts gracefully to terminal resizing.

2. Interactive CLI Wizards

Build multi-step onboarding flows, configuration tools, or database migration assistants with List selections, Tab navigation, and Form-like layouts. Stateful widgets handle focus management without you writing a single useState equivalent.

3. Data Visualization in SSH Sessions

Generate bar charts, sparklines, and formatted tables for server-side analytics. The test backend enables headless screenshot generation for automated reports. Wide character support means international teams see correct output everywhere.

4. Chatbots and REPL Interfaces

The jsx-chatbot.tsx example demonstrates production-grade interactive UX: alternate screen, raw mode input, scrollable history, and clean exit handling. Build AI-powered terminal assistants that feel native, not hacked together.

5. Testing and Snapshot Generation

Use the test backend to render exact terminal output as strings. Snapshot test your entire TUI without brittle pixel comparisons. CI pipelines verify UI correctness programmatically.


Step-by-Step Installation & Setup Guide

Getting terminui running takes under two minutes. Here's the complete flow:

Install the Package

# pnpm (recommended)
pnpm add terminui

# npm works too
npm install terminui

# or yarn
yarn add terminui

Configure TypeScript (Critical for JSX)

Create or update tsconfig.json:

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "terminui",
    "strict": true,
    "noUncheckedIndexedAccess": true
  }
}

The jsxImportSource directive tells TypeScript to use terminui's JSX factory instead of React's. This is non-negotiable for the JSX API.

Verify Your First Render

Create hello.ts (or hello.tsx for JSX):

import {
  createTestBackendState,
  createTestBackend,
  testBackendToString,
  createTerminal,
  terminalDraw,
  frameRenderWidget,
  createParagraph,
  renderParagraph,
  blockBordered,
  createTitle,
} from 'terminui';

// Initialize test backend: 60 columns, 10 rows
const state = createTestBackendState(60, 10);
const backend = createTestBackend(state);
const terminal = createTerminal(backend);

// Draw a framed greeting
default terminalDraw(terminal, (frame) => {
  const paragraph = createParagraph('Hello, terminui!', {
    block: blockBordered({ titles: [createTitle('Greeting')] }),
  });
  // Render paragraph widget to the full frame area
  frameRenderWidget(frame, renderParagraph(paragraph), frame.area);
});

// Output the rendered buffer as string
console.log(testBackendToString(state));

Run with npx tsx hello.ts. You should see:

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

For Production Terminal Backends

Swap createTestBackend for a real Node.js backend that implements the Backend interface. The test backend captures output for verification; production backends write ANSI sequences directly to process.stdout.


REAL Code Examples from the Repository

Let's dissect actual code from terminui's README — no toy examples, production patterns you can deploy today.

Example 1: Constraint-Based Layout System

This is terminui's secret sauce for responsive terminal design:

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

// Define layout: header (3 rows), body (50% remainder), footer (fills rest)
const layout = createLayout([
  lengthConstraint(3),       // exactly 3 rows for header
  percentageConstraint(50),  // 50% of remaining space for body
  fillConstraint(1),         // fill whatever is left for footer
]);

// Create an 80x24 terminal area starting at (0,0)
const area = createRect(0, 0, 80, 24);

// Split the area into three rectangles according to constraints
const [header, body, footer] = splitLayout(layout, area);

// header:  { x: 0, y: 0, width: 80, height: 3 }
// body:    { x: 0, y: 3, width: 80, height: 10 }  (50% of 21 remaining)
// footer:  { x: 0, y: 13, width: 80, height: 11 } (fills rest)

Why this matters: Traditional TUI libraries force absolute positioning or percentage-only layouts. Terminui's constraint solver handles mixed units seamlessly — exact pixels, proportions, minimums, maximums, and weighted fills in a single layout definition. This is CSS Flexbox for terminals, implemented in pure functions.


Example 2: JSX Dashboard with Nested Layouts

The JSX API transforms complex layouts into readable, maintainable components:

/** @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,
  // Vertical stack: header row + content row
  <Column constraints={[lengthConstraint(3), fillConstraint(1)]}>
    {/* Header panel with centered title */}
    <Panel title="Header" p={1}>
      <Label text="JSX-powered terminal UI" align="center" bold />
    </Panel>
    
    {/* Horizontal split: menu + metrics */}
    <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>,
);

Critical insight: The Column/Row components map directly to createLayout with splitLayout calls. The gap={1} prop inserts spacing between siblings. Padding (p={1}), borders, and titles are shorthand for blockBordered configurations. Zero virtual DOM overhead — this compiles to immediate frameRenderWidget invocations.


Example 3: Stateful List with Selection Management

Interactive widgets require state. Terminui handles this without breaking functional purity:

import { 
  createList, 
  createListState, 
  renderStatefulList,
  createStyle,
  styleFg,
  Color,
  blockBordered,
  createTitle,
  frameRenderStatefulWidget
} from 'terminui';

// Create list configuration with 3 items
const list = createList(['Item 1', 'Item 2', 'Item 3'], {
  block: blockBordered({ titles: [createTitle('Menu')] }),
  highlightStyle: styleFg(createStyle(), Color.Yellow),  // yellow selection
  highlightSymbol: '▶ ',  // pointer prefix for selected item
});

// Mutable state object (the exception to functional purity)
const state = createListState(0); // start with first item selected

// Render with state — frameRenderStatefulWidget passes state to renderer
frameRenderStatefulWidget(frame, renderStatefulList(list), area, state);

// Navigate programmatically
state.selected = 1;  // move to 'Item 2'
// Re-render to see updated selection

Architecture note: Widget configuration (list) remains immutable. Only state mutates, and it's explicitly separated. This pattern — immutable config, mutable state — gives you predictable reasoning about what changes and when.


Example 4: Production Alternate Screen Setup

For full-screen apps like the chatbot demo:

// Pseudo-code for real terminal backend implementation
const backend = createNodeBackend(); // your Node.js backend implementation
const terminal = createTerminal(backend);

// Enter alternate screen buffer (no scrollback, clean slate)
process.stdout.write('\x1b[?1049h');

let running = true;

// Main render loop
try {
  while (running) {
    terminalDraw(terminal, (frame) => {
      // Your complete UI render here
      // Only changed cells generate output!
    });
    
    // Wait for input or next tick
    await waitForInput();
  }
} finally {
  // CRITICAL: always exit alternate screen on cleanup
  process.stdout.write('\x1b[?1049l');
}

Performance guarantee: The double-buffered diff means if only your gauge's fill character changes, terminui writes exactly those cells, not the entire 80x24 frame. On slow SSH connections, this is the difference between usable and unusable.


Advanced Usage & Best Practices

Compose, Don't Inherit — Build complex widgets by combining simpler ones. A "dashboard" is just a layout split with nested blocks. No widget base classes required.

Pre-compute StylescreateStyle() allocations are cheap but not free. Define reusable style objects at module scope:

const WARNING_STYLE = styleFg(styleBg(createStyle(), Color.Yellow), Color.Black);
const ERROR_STYLE = styleFg(createStyle(), Color.Red);

Batch State Updates — For animated UIs, collect all state mutations, then trigger a single re-render. The diff renderer makes this efficient.

Test with Snapshots — The test backend string output is deterministic. Snapshot test your entire UI:

expect(testBackendToString(state)).toMatchSnapshot();

Handle Resize Explicitly — Terminal resizes require recreating your layout with new dimensions. Listen for SIGWINCH and re-split:

process.on('SIGWINCH', () => {
  const { columns, rows } = process.stdout;
  // Re-create layout with new dimensions
});

Prefer JSX for Complex Hierarchies — The imperative API excels for dynamic content; JSX shines for static structure. Mix freely within the same codebase.


Comparison with Alternatives

Feature Terminui Ink (React) Blessed Oclif
Paradigm Pure functional React hooks OOP/events OOP/commands
Rendering Double-buffered diff React reconciliation Full redraw No TUI support
Classes Zero Components (classes or functions) Heavy inheritance Heavy inheritance
TypeScript Strict, zero any Moderate Poor Moderate
JSX Support Yes, zero VDOM Yes, full React No No
Performance Optimal (cell diff) Good (tree diff) Poor (full redraw) N/A
Bundle Size Small Large (React dep) Large Medium
Maintenance Active (2024) Active Unmaintained Active
Widget Count 10+ built-in Ecosystem Extensive None (plugins)
Backend Flexibility Pluggable, testable Ink-only Blessed-only N/A

Verdict: Choose terminui when you need maximum performance, type safety, and functional purity in terminal UIs. Choose Ink if you must share React components with web code. Avoid Blessed for new projects — it's unmaintained and architecturally obsolete.


FAQ

Q: Does terminui work with Deno or Bun? A: Yes — the pure TypeScript implementation runs anywhere TS compiles. You'll need to provide a compatible backend for terminal I/O.

Q: Can I use React hooks with terminui's JSX? A: No, and intentionally so. Terminui's JSX compiles to direct render calls without a runtime. Use plain variables and functions for state management — simpler and faster.

Q: How do I handle keyboard input? A: Terminui focuses on rendering. Use Node.js readline or process.stdin in raw mode, as shown in the jsx-chatbot.tsx example. Separation of concerns keeps both layers clean.

Q: Is the JSX API required? A: Absolutely not. The imperative API (terminalDraw, frameRenderWidget) is fully featured. JSX is syntactic sugar that compiles to the same calls.

Q: Can I create custom widgets? A: Yes — any function returning (area: Rect, buf: Buffer) => void is a valid widget. The built-in widgets demonstrate the pattern.

Q: Does terminui support mouse events? A: Not directly — handle mouse ANSI sequences in your input layer, then update widget state accordingly. The rendering layer is agnostic to input source.

Q: How does double-buffering work with streaming output? A: The backend accumulates cell changes into ANSI sequences. For test backends, this becomes a string; for live backends, it's written to stdout. The buffering happens in memory, not in terminal scrollback.


Conclusion

Terminui isn't incrementally better — it's architecturally different. By rejecting object-oriented conventions and embracing pure functions, Ahmad Awais built a terminal UI library that outperforms, out-types, and out-composes everything in its class.

The double-buffered diff renderer solves flickering once and for all. The constraint layout system brings responsive design to fixed-width terminals. The JSX API proves declarative UIs don't need virtual DOM overhead. And the TypeScript strictness catches errors that other frameworks happily ship to production.

If you're building CLI tools, devops dashboards, interactive wizards, or terminal chatbots in 2024, you're working too hard with legacy frameworks. Terminui gives you the performance of hand-rolled ANSI with the ergonomics of modern component architecture.

Your move. Install it. Build something fast. Thank yourself later.

pnpm add terminui

⭐ Star the repository, explore the examples, and join the developers who've stopped fighting their TUI framework and started shipping terminal experiences that feel like magic.

Get terminui on GitHub →

Comments (0)

Comments are moderated before appearing.

No comments yet. Be the first to share your thoughts!

Recommended Prompts

View All
All tools