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

@bunli/tui

v0.6.0

Published

Terminal User Interface plugin for Bunli CLI framework

Readme

@bunli/tui

A React-based Terminal User Interface library for Bunli CLI framework, powered by OpenTUI's React renderer.

Features

  • React-based Components: Build TUIs using familiar React patterns and JSX
  • Component Library: Form, layout, feedback, data-display, and chart components for alternate-buffer TUIs
  • OpenTUI Integration: Full access to OpenTUI's React hooks and components
  • Type Safety: Complete TypeScript support with proper type inference
  • Animation Support: Built-in timeline system for smooth animations
  • Keyboard Handling: Easy keyboard event management with useKeyboard
  • First-Class TUI Support: TUI rendering is a first-class feature, not a plugin
  • Theme System: Preset themes with token overrides via ThemeProvider/createTheme

Installation

bun add @bunli/tui react

Quick Start

import { createCLI, defineCommand } from '@bunli/core'

const cli = await createCLI({
  name: 'my-app',
  version: '1.0.0'
})

// Define a command with TUI using the render property
const myCommand = defineCommand({
  name: 'deploy',
  description: 'Deploy application',
  render: () => (
    <box title="Deployment" style={{ border: true, padding: 2 }}>
      <text>Deploying...</text>
    </box>
  ),
  handler: async () => {
    // Non-TUI fallback when render is skipped
    console.log('Deploying application...')
  }
})

cli.command(myCommand)
await cli.run()

Bunli auto-wires the OpenTUI runtime. Runtime/context APIs now come from @bunli/runtime/app, prompt/session APIs come from @bunli/runtime/prompt, and @bunli/tui focuses on UI components and hooks.

TUI Execution Semantics

  • Commands with render run in interactive terminals.
  • Non-interactive terminals fall back to handler when present.
  • Configure fullscreen flows explicitly with bufferMode: 'alternate'.

Render Lifecycle (Runtime Exit)

Commands that use render must eventually call useRuntime().exit() (for example on submit/cancel/quit), or the command will not exit.

import { useRuntime } from '@bunli/runtime/app'
import { useKeyboard } from '@bunli/tui'

function DeployTUI() {
  const runtime = useRuntime()

  useKeyboard((key) => {
    if (key.name === 'q' || (key.ctrl && key.name === 'c')) {
      runtime.exit()
    }
  })

  return <text>Press q to quit</text>
}

Buffer Modes (Alternate vs Standard)

OpenTUI can render using either the alternate screen buffer (full-screen TUI) or the standard terminal buffer (leaves output in scrollback).

Configure this in createCLI() config:

const cli = await createCLI({
  name: 'my-app',
  version: '1.0.0',
  tui: {
    renderer: {
      bufferMode: 'alternate' // or 'standard'
    }
  }
})

Notes:

  • bufferMode: 'standard' is the default.
  • Use bufferMode: 'alternate' for fullscreen/blocking experiences.
  • Mouse tracking is disabled by default (useMouse: false) to avoid leaking raw mouse escape sequences after exit in standard-buffer workflows. Enable explicitly when needed.

Usage

Module Split

Use subpath exports depending on mode:

  • @bunli/tui/interactive: alternate-buffer interactive components.
  • @bunli/tui/charts: terminal-native chart primitives.
  • @bunli/tui: root export that re-exports shared components/hooks.
  • @bunli/runtime/app: runtime lifecycle/context APIs.
  • @bunli/runtime/prompt: prompt/session APIs.

Clack Migration Quick Map

Use this when replacing a clack mental model with Bunli-native APIs:

// clack
// import { intro, outro, confirm, select, multiselect, log } from '@clack/prompts'

// bunli
// prompt is provided via handler args by Bunli

prompt.intro('Setup')
const confirmed = await prompt.confirm('Continue?', { default: true })
const env = await prompt.select('Environment', {
  options: [
    { label: 'Development', value: 'dev' },
    { label: 'Production', value: 'prod' }
  ]
})
const features = await prompt.multiselect('Features', {
  options: [
    { label: 'Testing', value: 'testing' },
    { label: 'Docker', value: 'docker' }
  ],
  initialValues: ['testing']
})
prompt.log.success(`Selected ${env}`)
prompt.outro('Done')

Basic TUI Component

import { defineCommand } from '@bunli/core'

function MyTUI() {
  return (
    <box title="My App" style={{ border: true, padding: 2 }}>
      <text>Hello from My App!</text>
    </box>
  )
}

export const myCommand = defineCommand({
  name: 'my-command',
  description: 'My command with TUI',
  render: () => <MyTUI />,
  handler: async () => {
    console.log('Running my-command in CLI mode')
  }
})

Using Form Components

import { defineCommand } from '@bunli/core'
import { useRuntime } from '@bunli/runtime/app'
import { SchemaForm } from '@bunli/tui'
import { z } from 'zod'

const configSchema = z.object({
  apiUrl: z.string().url('Enter a valid URL'),
  region: z.enum(['us-east', 'us-west'])
})

function ConfigTUI() {
  const runtime = useRuntime()
  const regions = [
    { label: 'US East', value: 'us-east', hint: 'US East region' },
    { label: 'US West', value: 'us-west', hint: 'US West region' }
  ]

  return (
    <SchemaForm
      title="Configure Settings"
      schema={configSchema}
      fields={[
        {
          kind: 'text',
          name: 'apiUrl',
          label: 'API URL',
          placeholder: 'https://api.example.com',
          required: true
        },
        {
          kind: 'select',
          name: 'region',
          label: 'Region',
          options: regions
        }
      ]}
      onSubmit={(values) => {
        console.log('Validated form values:', values)
      }}
      onCancel={() => runtime.exit()}
    />
  )
}

export const configureCommand = defineCommand({
  name: 'configure',
  description: 'Configure application settings',
  render: () => <ConfigTUI />
})

Using OpenTUI Hooks

import { useRuntime } from '@bunli/runtime/app'
import { useKeyboard, useTimeline, useTerminalDimensions } from '@bunli/tui'

function InteractiveTUI({ command }) {
  const [count, setCount] = useState(0)
  const { width, height } = useTerminalDimensions()
  const runtime = useRuntime()
  
  const timeline = useTimeline({ duration: 2000 })
  
  useKeyboard((key) => {
    if (key.name === 'q') {
      runtime.exit()
    }
    if (key.name === 'space') {
      setCount(prev => prev + 1)
    }
  })
  
  useEffect(() => {
    timeline.add({ count: 0 }, {
      count: 100,
      duration: 2000,
      onUpdate: (anim) => setCount(anim.targets[0].count)
    })
  }, [])
  
  return (
    <box title="Interactive Demo" style={{ border: true, padding: 2 }}>
      <text>Count: {count}</text>
      <text>Terminal: {width}x{height}</text>
      <text>Press SPACE to increment, Q to quit</text>
    </box>
  )
}

Component Library

Interactive components are available from @bunli/tui/interactive and root exports.

Included primitives:

  • Form: Form, SchemaForm, FormField, SelectField
  • Form v2: NumberField, PasswordField, TextareaField, CheckboxField, MultiSelectField
  • Layout: Container, Stack, Grid, Panel, Card, Divider, SectionHeader
  • Navigation/flow: Tabs, Menu, CommandPalette, Modal
  • Feedback: Alert, Badge, Toast, ProgressBar, EmptyState
  • Data display: List, Table, DataTable, KeyValueList, Stat, Markdown, Diff
  • Charts: BarChart, LineChart, Sparkline from @bunli/tui/charts
  • Runtime orchestration/hooks: import from @bunli/runtime/app (DialogProvider, useDialogManager, FocusScopeProvider, useScopedKeyboard, etc.)

Keyboard Contracts

Default keyboard bindings for interactive primitives:

  • Modal: Esc / Ctrl+C close, Tab/Shift+Tab focus trap
  • Dialog confirm: Left/h/y -> confirm, Right/l/n -> cancel choice, Tab toggle, Enter submit
  • Dialog choose: Up/k previous, Down/j next, Enter submit. Disabled options are skipped for selection/navigation.
  • Menu: Up/k, Down/j, Enter
  • Tabs: Left/h previous tab, Right/l next tab
  • CommandPalette: Up/k, Down/j, Enter
  • DataTable: Left/h previous sort column, Right/l next sort column, Up/k previous row, Down/j next row, Enter select row
  • Form: Tab/Shift+Tab field navigation, Ctrl+S submit, Ctrl+R reset, F8/Shift+F8 jump error fields, Esc cancel

Dialog Manager

Use the dialog manager to stack confirm/choose flows with consistent priority handling and dismissal semantics.

import { useDialogManager, DialogDismissedError } from '@bunli/runtime/app'

function Screen() {
  const dialogs = useDialogManager()

  async function deploy() {
    try {
      const confirmed = await dialogs.confirm({
        title: 'Deploy',
        message: 'Ship this release now?'
      })
      if (!confirmed) return

      const target = await dialogs.choose({
        title: 'Target',
        options: [
          { label: 'Staging', value: 'staging', section: 'General' },
          { label: 'Production', value: 'production', section: 'Protected', disabled: true }
        ]
      })

      console.log('Deploying to', target)
    } catch (error) {
      if (error instanceof DialogDismissedError) {
        console.log('Dialog dismissed')
      }
    }
  }

  return <box />
}

Form

A schema-driven container for controlled interactive forms.

const runtime = useRuntime()

<Form 
  title="My Form"
  schema={schema}
  onSubmit={(values) => console.log(values)}
  onCancel={() => runtime.exit()}
>
  {/* Form fields */}
</Form>

Props:

  • title: string - Form title
  • schema: StandardSchemaV1 - Validation schema (Zod and other Standard Schema adapters supported)
  • onSubmit: (values) => void | Promise<void> - Submit handler with schema-validated values
  • onCancel?: () => void - Cancel handler (optional)
  • onValidationError?: (errors: Record<string, string>) => void - Validation error callback
  • initialValues?: Partial<InferOutput<schema>> - Initial controlled values
  • validateOnChange?: boolean - Validate while typing/selecting (default true)
  • submitHint?: string - Footer hint override
  • onReset?: () => void - Reset callback
  • onDirtyChange?: (isDirty, dirtyFields) => void - Dirty-state callback
  • onSubmitStateChange?: ({ isSubmitting, isValidating }) => void - async state callback
  • scopeId?: string - keyboard scope boundary id for nested interactive flows

SchemaForm

A higher-level schema form builder that renders fields from descriptors.

<SchemaForm
  title="Deploy"
  schema={schema}
  fields={[
    { kind: 'text', name: 'service', label: 'Service' },
    { kind: 'select', name: 'env', label: 'Environment', options: envOptions },
    { kind: 'checkbox', name: 'telemetry', label: 'Enable telemetry' },
    {
      kind: 'textarea',
      name: 'notes',
      label: 'Release notes',
      visibleWhen: (values) => values.env === 'production'
    }
  ]}
  onSubmit={(values) => console.log(values)}
/>

Supported SchemaForm field kinds:

  • text
  • select
  • multiselect
  • number
  • password
  • textarea
  • checkbox

Field-level behavior:

  • visibleWhen(values) => boolean for conditional rendering.
  • deriveDefault(values) => unknown for dependent default initialization.

FormField

A controlled text field bound to form context.

<FormField
  label="Username"
  name="username"
  placeholder="Enter username"
  required
  defaultValue=""
/>

Props:

  • label: string - Field label
  • name: string - Field name
  • placeholder?: string - Placeholder text
  • required?: boolean - Whether field is required
  • description?: string - Helper text
  • defaultValue?: string - Initial value for form state
  • onChange?: (value: string) => void - Change handler
  • onSubmit?: (value: string) => void - Submit handler

SelectField

A controlled select field bound to form context.

<SelectField
  label="Environment"
  name="env"
  options={[
    { label: 'Development', value: 'dev', hint: 'Development environment' },
    { label: 'Production', value: 'prod', hint: 'Production environment' }
  ]}
  defaultValue="dev"
  onChange={setEnvironment}
/>

Props:

  • label: string - Field label
  • name: string - Field name
  • options: SelectOption[] - Available options
  • required?: boolean - Whether field is required
  • description?: string - Helper text
  • defaultValue?: SelectOption['value'] - Initial selected value
  • onChange?: (value: SelectOption['value']) => void - Change handler

ProgressBar

A progress bar component for showing completion status.

<ProgressBar 
  value={75} 
  label="Upload Progress" 
  color="#00ff00" 
/>

Props:

  • value: number - Progress value (0-100)
  • label?: string - Progress label
  • color?: string - Progress bar color

Chart Primitives

@bunli/tui/charts supports negative and sparse values, axis labels, and multi-series palettes.

import { BarChart, LineChart } from '@bunli/tui/charts'

<BarChart
  series={[
    { name: 'build', points: [{ label: 'Mon', value: -2 }, { label: 'Tue', value: 6 }] },
    { name: 'test', points: [{ label: 'Mon', value: null }, { label: 'Tue', value: 4 }] }
  ]}
  axis={{ yLabel: 'Jobs', xLabel: 'Day', showRange: true }}
/>

<LineChart
  series={{ name: 'latency', points: [{ value: 120 }, { value: 98 }, { value: null }, { value: 104 }] }}
  axis={{ yLabel: 'ms' }}
/>

ThemeProvider and Tokens

Use ThemeProvider to apply a built-in theme preset or token overrides.

import { ThemeProvider, createTheme } from '@bunli/tui/interactive'

const customTheme = createTheme({
  preset: 'dark',
  tokens: {
    accent: '#3ec7ff',
    textSuccess: '#3cd89b'
  }
})

function App() {
  return (
    <ThemeProvider theme={customTheme}>
      <Panel title="Deploy status">
        <Alert tone="success" message="Ready to ship" />
      </Panel>
    </ThemeProvider>
  )
}

OpenTUI Hooks

The package re-exports useful OpenTUI React hooks:

useKeyboard

Handle keyboard events.

import { useRuntime } from '@bunli/runtime/app'
import { useKeyboard } from '@bunli/tui'

const runtime = useRuntime()

useKeyboard((key) => {
  if (key.name === 'escape') {
    runtime.exit()
  }
})

useRenderer

Access the OpenTUI renderer instance.

import { useRenderer } from '@bunli/tui'

const renderer = useRenderer()
renderer.console.show()

Use useRenderer() for advanced renderer inspection/control only. Use useRuntime().exit() for normal command completion.

useTerminalDimensions

Get current terminal dimensions.

import { useTerminalDimensions } from '@bunli/tui'

const { width, height } = useTerminalDimensions()

useTimeline

Create and manage animations.

import { useTimeline } from '@bunli/tui'

const timeline = useTimeline({ duration: 2000 })

timeline.add({ x: 0 }, {
  x: 100,
  duration: 2000,
  onUpdate: (anim) => setX(anim.targets[0].x)
})

useOnResize

Handle terminal resize events.

import { useOnResize } from '@bunli/tui'

useOnResize((width, height) => {
  console.log(`Terminal resized to ${width}x${height}`)
})

Renderer Configuration

Renderer options are passed via createCLI({ tui: { renderer } }) and optional command-level overrides in command.tui.renderer.

OpenTUI Components

You can use any OpenTUI React components directly:

import { render } from '@opentui/react'

function MyComponent() {
  return (
    <box style={{ border: true, padding: 2 }}>
      <text>Hello World</text>
      <input placeholder="Type here..." />
      <select options={options} />
    </box>
  )
}

Available components:

  • <box> - Container with borders and layout
  • <text> - Text display with styling
  • <input> - Text input field
  • <select> - Dropdown selection
  • <scrollbox> - Scrollable container
  • <ascii-font> - ASCII art text
  • <tab-select> - Tab-based selection

Examples

See the examples/tui-demo directory for complete examples:

  • Deploy Command: Animated progress bar with timeline
  • Configure Command: Form with input and select fields

TypeScript Support

The package provides full TypeScript support:

import type { TuiComponent, TuiComponentProps } from '@bunli/tui'

const MyTUI: TuiComponent = ({ command, args, store }) => {
  // Fully typed props
  return <box>{command.name}</box>
}

License

MIT