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

ghostty-opentui

v1.3.12

Published

Fast ANSI/VT terminal parser powered by Ghostty's Zig terminal emulation library

Readme

ghostty-opentui

Fast ANSI/VT terminal parser powered by Ghostty's Zig terminal emulation library. Converts raw PTY logs to JSON, strips ANSI for plain text, or renders them in a TUI viewer.

Features

  • Fast - Written in Zig, processes terminal escape sequences at native speed
  • Full VT emulation - ANSI colors (16/256/RGB), styles, cursor movements, scrolling
  • TUI Viewer - Interactive terminal viewer built with opentui
  • JSON output - Compact format with merged spans for rendering
  • Plain text output - Strip ANSI codes for LLM/text processing
  • N-API - Native Node.js addon using napigen for seamless integration

Installation

bun add ghostty-opentui

For TUI rendering, you'll also need:

bun add @opentui/core @opentui/react  # For React
# or
bun add @opentui/core @opentui/solid  # For Solid.js

Usage

Basic FFI Usage

import { ptyToJson, ptyToText, type TerminalData } from "ghostty-opentui"

// Parse ANSI string or buffer to JSON with styling info
const data: TerminalData = ptyToJson("\x1b[32mHello\x1b[0m World", {
  cols: 120,
  rows: 40,
})

console.log(data.lines) // Array of lines with styled spans
console.log(data.cursor) // [col, row] cursor position

Strip ANSI for Plain Text

Use ptyToText to strip all ANSI escape codes and get plain text output. This is useful for sending terminal output to LLMs or other text processors that don't handle ANSI codes.

import { ptyToText } from "ghostty-opentui"

// Strip ANSI codes - returns plain text
const plain = ptyToText("\x1b[31mError:\x1b[0m Something went wrong")
// Returns: "Error: Something went wrong"

// Works with complex escape sequences too
const complex = ptyToText("\x1b[1;38;2;255;100;50mBold RGB\x1b[0m text")
// Returns: "Bold RGB text"

// Optional cols for line wrapping (default: 500)
const text = ptyToText(ansiBuffer, { cols: 120 })

Why use ptyToText instead of regex?

Unlike simple regex-based ANSI strippers, ptyToText uses a full terminal emulator to process escape sequences. This correctly handles:

  • Cursor movements and positioning
  • Line wrapping at terminal width
  • Scrolling regions
  • All SGR (Select Graphic Rendition) sequences
  • OSC (Operating System Command) sequences

With OpenTUI React

import { createCliRenderer } from "@opentui/core"
import { createRoot, useKeyboard, extend } from "@opentui/react"
import { GhosttyTerminalRenderable } from "ghostty-opentui/terminal-buffer"

// Register the ghostty-terminal component
extend({ "ghostty-terminal": GhosttyTerminalRenderable })

const ANSI = `\x1b[1;32muser@host\x1b[0m:\x1b[1;34m~/app\x1b[0m$ ls
\x1b[1;34msrc\x1b[0m  package.json  \x1b[1;32mbuild.sh\x1b[0m
\x1b[31mRed\x1b[0m \x1b[32mGreen\x1b[0m \x1b[33mYellow\x1b[0m \x1b[34mBlue\x1b[0m
`

function App() {
  useKeyboard((key) => {
    if (key.name === "q") process.exit(0)
  })

  return (
    <scrollbox focused style={{ flexGrow: 1 }}>
      <ghostty-terminal ansi={ANSI} cols={80} rows={24} />
    </scrollbox>
  )
}

const renderer = await createCliRenderer({ exitOnCtrlC: true })
createRoot(renderer).render(<App />)

With OpenTUI Solid.js

import { createCliRenderer } from "@opentui/core"
import { createRoot, useKeyboard, extend } from "@opentui/solid"
import { GhosttyTerminalRenderable } from "ghostty-opentui/terminal-buffer"

// Register the ghostty-terminal component
extend({ "ghostty-terminal": GhosttyTerminalRenderable })

const ANSI = `\x1b[1;32muser@host\x1b[0m:\x1b[1;34m~/app\x1b[0m$ ls
\x1b[1;34msrc\x1b[0m  package.json  \x1b[1;32mbuild.sh\x1b[0m
\x1b[31mRed\x1b[0m \x1b[32mGreen\x1b[0m \x1b[33mYellow\x1b[0m \x1b[34mBlue\x1b[0m
`

function App() {
  useKeyboard((key) => {
    if (key.name === "q") process.exit(0)
  })

  return (
    <scrollbox focused style={{ "flex-grow": 1 }}>
      <ghostty-terminal ansi={ANSI} cols={80} rows={24} />
    </scrollbox>
  )
}

const renderer = await createCliRenderer({ exitOnCtrlC: true })
createRoot(renderer).render(<App />)

Ghostty Terminal Component

The <ghostty-terminal> component accepts raw ANSI input and renders it with full styling support.

Important: You must call extend() to register the component before using it in JSX:

import { extend } from "@opentui/react" // or "@opentui/solid"
import { GhosttyTerminalRenderable } from "ghostty-opentui/terminal-buffer"

// Register the component
extend({ "ghostty-terminal": GhosttyTerminalRenderable })

// Now you can use it with raw ANSI input
<ghostty-terminal ansi={ansiString} cols={80} rows={24} />

// cols and rows are optional (defaults: cols=120, rows=40)
<ghostty-terminal ansi={ansiString} />

Scrolling to Specific Lines

You can scroll to a specific line number in the ANSI output using refs:

import { useRef } from "react"
import type { ScrollBoxRenderable } from "@opentui/core"
import type { GhosttyTerminalRenderable } from "ghostty-opentui/terminal-buffer"

function App() {
  const scrollBoxRef = useRef<ScrollBoxRenderable>(null)
  const terminalRef = useRef<GhosttyTerminalRenderable>(null)

  const scrollToLine = (lineNumber: number) => {
    if (scrollBoxRef.current && terminalRef.current) {
      const scrollPos = terminalRef.current.getScrollPositionForLine(lineNumber)
      scrollBoxRef.current.scrollTo(scrollPos)
    }
  }

  return (
    <scrollbox ref={scrollBoxRef}>
      <ghostty-terminal ref={terminalRef} ansi={ansiString} />
    </scrollbox>
  )
}

The getScrollPositionForLine(lineNumber) method:

  • Takes a 0-based line number from the ANSI output
  • Returns the actual scrollTop position accounting for text wrapping and layout
  • Clamps out-of-bounds values automatically

Limiting Output for Performance

For large log files, use the limit parameter to only render the first N lines. Limiting happens at the Zig level before JSON serialization, making it extremely efficient:

// Only render first 100 lines of a huge log file
<ghostty-terminal 
  ansi={hugeLogFile} 
  cols={120} 
  rows={10}
  limit={100}  // Limits at Zig level (before JSON parsing!)
/>

// Quick preview: just show first 10 lines
<ghostty-terminal 
  ansi={longOutput} 
  limit={10}
/>

Benefits of using limit:

  • Maximum performance - Limits at native Zig level before JSON serialization
  • Lower memory - Doesn't process or allocate memory for skipped lines
  • Instant preview - Show first N lines of massive logs without waiting

Text Highlighting

You can highlight specific regions of text with custom background colors. This is useful for search results, error highlighting, or drawing attention to specific lines.

import { GhosttyTerminalRenderable, type HighlightRegion } from "ghostty-opentui/terminal-buffer"

const highlights: HighlightRegion[] = [
  { line: 0, start: 0, end: 5, backgroundColor: "#ffff00" },           // Yellow highlight
  { line: 2, start: 10, end: 20, backgroundColor: "#ff0000" },         // Red highlight
  { line: 5, start: 0, end: 8, backgroundColor: "#00ff00", replaceWithX: true }, // Mask with 'x'
]

<ghostty-terminal 
  ansi={ansiString} 
  cols={80} 
  rows={24}
  highlights={highlights}
/>

HighlightRegion properties:

  • line - Line number (0-based)
  • start - Start column (0-based, inclusive)
  • end - End column (0-based, exclusive)
  • backgroundColor - Hex color string like "#ff0000"
  • replaceWithX - Optional. If true, replaces highlighted text with 'x' characters (useful for testing/masking)

How highlighting works:

Highlights are applied during the ANSI-to-StyledText conversion. When you set/update highlights on a GhosttyTerminalRenderable, the component re-processes the entire ANSI content to apply the new highlights. This approach:

  • Preserves all original text styling (colors, bold, etc.) while adding the highlight background
  • Handles highlights that span multiple styled spans correctly
  • Works efficiently for most use cases

For very large files with frequently changing highlights, consider using limit to reduce the rendered content.

Programmatic usage without the component:

import { ptyToJson } from "ghostty-opentui"
import { terminalDataToStyledText, type HighlightRegion } from "ghostty-opentui/terminal-buffer"

const data = ptyToJson(ansiString, { cols: 80, rows: 24 })
const highlights: HighlightRegion[] = [
  { line: 0, start: 0, end: 5, backgroundColor: "#ff0000" }
]
const styledText = terminalDataToStyledText(data, highlights)
// styledText.chunks contains TextChunk[] with highlights applied

API

Main Export

import { ptyToJson, ptyToText, type TerminalData } from "ghostty-opentui"

// Parse ANSI data to JSON with full styling info
const data = ptyToJson(input, options)

// Strip ANSI codes and return plain text (for LLMs, logging, etc.)
const plainText = ptyToText(input, options)

Ghostty Terminal Component

import { GhosttyTerminalRenderable } from "ghostty-opentui/terminal-buffer"
import { extend } from "@opentui/react" // or "@opentui/solid"

// Register component
extend({ "ghostty-terminal": GhosttyTerminalRenderable })

// Use in JSX (component calls ptyToJson internally)
<ghostty-terminal ansi={ansiString} cols={80} rows={24} />

TypeScript Types

import type { 
  TerminalData, 
  TerminalLine, 
  TerminalSpan, 
  PtyToJsonOptions,
  PtyToTextOptions
} from "ghostty-opentui"

import type { 
  GhosttyTerminalRenderable,
  GhosttyTerminalOptions,
  HighlightRegion
} from "ghostty-opentui/terminal-buffer"

interface TerminalData {
  cols: number
  rows: number
  cursor: [number, number]
  offset: number
  totalLines: number
  lines: TerminalLine[]
}

interface TerminalSpan {
  text: string
  fg: string | null   // hex color e.g. "#ff5555"
  bg: string | null
  flags: number       // StyleFlags bitmask
  width: number
}

interface PtyToTextOptions {
  cols?: number               // Terminal width for wrapping (default: 500)
  rows?: number               // Terminal height (default: 256)
}

interface GhosttyTerminalOptions {
  ansi: string | Buffer       // Raw ANSI input
  cols?: number               // Terminal width (default: 120)
  rows?: number               // Terminal height (default: 40)
  limit?: number              // Max lines to render (from start)
  highlights?: HighlightRegion[]  // Regions to highlight
}

interface HighlightRegion {
  line: number           // Line number (0-based)
  start: number          // Start column (0-based, inclusive)
  end: number            // End column (0-based, exclusive)
  backgroundColor: string // Hex color like "#ff0000"
  replaceWithX?: boolean // Replace text with 'x' (for testing)
}

// StyleFlags: bold=1, italic=2, underline=4, strikethrough=8, inverse=16, faint=32

Quick Start (Development)

# Setup (installs Zig 0.15.2, clones Ghostty, builds)
./setup.sh

# Run TUI viewer with sample
bun run dev

# Or convert a file to JSON
./zig-out/bin/pty-to-json session.log > output.json

TUI Viewer

bun run dev                      # sample ANSI demo
bun run dev testdata/session.log # view a file

Controls: up/down scroll, Page Up/Down page, Home/End jump, q/Esc quit

+-----------------------------------------+
| rootOptions (outer container)            |
|  +-----------------------------------+ ^ |
|  | viewport (visible area)           | X | <- scrollbar
|  |  +-----------------------------+  | X |
|  |  | content (padded)            |  | X |
|  |  |  +---------------------+    |  | v |
|  |  |  | terminal lines      |    |  |   |
|  |  |  +---------------------+    |  |   |
|  |  +-----------------------------+  |   |
|  +-----------------------------------+   |
|  +-----------------------------------+   |
|  | 120x40 | Cursor | Lines           |   | <- info bar
|  +-----------------------------------+   |
+-----------------------------------------+

How It Works

+----------------+     +----------------+     +----------------+
|  Raw PTY       | --> |  Zig VT        | --> |  JSON/TUI      |
|  (ANSI bytes)  |     |  Emulator      |     |  Output        |
+----------------+     +----------------+     +----------------+
  1. Input - Raw PTY bytes with ANSI escape sequences
  2. Zig Processing - Ghostty's VT parser emulates a full terminal
  3. Output - JSON with styled spans, or rendered in TUI

The Zig library is exposed via N-API for Node.js/Bun:

import { ptyToJson } from "ghostty-opentui"

const data = ptyToJson(ansiBuffer, { cols: 120, rows: 40 })
// Returns: { cols, rows, cursor, lines: [{ spans: [...] }] }

JSON Format

{
  "cols": 120,
  "rows": 40,
  "cursor": [0, 5],
  "totalLines": 42,
  "lines": [
    [["Hello ", "#5555ff", null, 1, 6], ["World", "#55ff55", null, 0, 5]]
  ]
}

Each span: [text, fg, bg, flags, width]

Flags: bold=1, italic=2, underline=4, strikethrough=8, inverse=16, faint=32

Platform Support

| Platform | Status | |----------|--------| | Linux x64 | Full support | | Linux ARM64 | Full support | | macOS ARM64 (Apple Silicon) | Full support | | macOS x64 (Intel) | Full support | | Windows | Fallback mode (plain text only) |

Windows Fallback

Windows cannot use the native Zig library due to a Zig build system bug with path handling when compiling Ghostty. Instead, Windows uses a fallback that:

  • Strips ANSI escape codes using strip-ansi
  • Returns plain text without colors or styles
  • Supports all the same API (cols, rows, limit, offset)

This means Windows users get functional output, just without syntax highlighting. For full color support on Windows, use WSL (Windows Subsystem for Linux).

Note: Persistent terminal mode (persistent: true) is not available on Windows. If you request persistent mode, the component silently falls back to stateless mode. Methods like feed(), reset(), getCursor(), and getText() will throw errors. Use hasPersistentTerminalSupport() to check availability at runtime.

Benchmarks

Performance measured on Apple Silicon (M-series). Run benchmarks with bun run bench.

ptyToJson - Terminal Parsing

| Input Size | ops/s | Latency | |------------|------:|--------:| | small (12 chars) | 4,942 | 0.20ms | | medium (30 lines) | 1,299 | 0.77ms | | 1K lines | 34 | 29ms | | 5K lines | 5.5 | 182ms | | 10K lines | 1.8 | 547ms | | 20K lines | 0.5 | 1,808ms |

Early Exit with limit Parameter

When limit is set, parsing stops early once enough lines are collected. This provides massive speedups for large inputs:

| Input Size | No Limit | With limit=100 | Speedup | |------------|----------|----------------|--------:| | 10K lines | 557ms | 3.2ms | 174x | | 20K lines | 1,869ms | 6.4ms | 292x |

This works correctly even with complex terminal output (cursor movement, clear screen, etc.) because we check the actual terminal buffer state, not just input lines.

Persistent vs Stateless Mode

For streaming scenarios (feeding data in 100 chunks):

| Mode | ops/s | Latency | Speedup | |------|------:|--------:|--------:| | Stateless (100 separate ptyToJson calls) | 5.8 | 171ms | 1x | | Persistent (100 feed() calls) | 34 | 30ms | 5.8x |

Use persistent: true for streaming/interactive terminals for significant performance gains.

Key Insights

  • Use limit for large files - 292x faster for 20K lines with limit=100
  • Persistent mode is ~6x faster for streaming use cases
  • Linear scaling without limit - 10K lines takes ~10x longer than 1K lines

Requirements

  • Zig 0.15.2 - Required by Ghostty
  • Bun - For TUI viewer and N-API
  • Ghostty - Cloned adjacent to this repo (setup.sh handles this)
  • Linux or macOS - Windows not supported (see above)

Development

zig build                        # debug build
zig build -Doptimize=ReleaseFast # release build
zig build test                   # run Zig tests
bun test                         # run TUI tests

License

MIT.