Reliverse Docs
LibsRemptsPackages

@reliverse/rempts-tui

Terminal UI components and interactive forms for Rempts

@reliverse/rempts-tui

Terminal UI components and renderers for building interactive CLI experiences with Rempts.

@reliverse/rempts-tui provides React-based terminal UI components that render in the terminal using React's concurrent features and terminal-specific renderers.

Installation

bun add @reliverse/rempts-tui
npm install @reliverse/rempts-tui
pnpm add @reliverse/rempts-tui

Core Concepts

Rempts TUI enables you to build interactive terminal applications using React components that render in the terminal. Commands can use either traditional handlers or render functions that return React elements.

Render Functions

Instead of handlers, commands can define render functions:

import { defineCommand } from '@reliverse/rempts-core'
import { Box, Text } from '@reliverse/rempts-tui'

export default defineCommand({
  name: 'interactive',
  description: 'Interactive terminal UI',
  render: ({ flags, prompt, spinner }) => {
    return (
      <Box flexDirection="column" padding={1}>
        <Text color="green">Welcome to Rempts TUI!</Text>
        <Text>This is rendered in the terminal using React.</Text>
      </Box>
    )
  }
})

TUI Renderer Registration

Register the TUI renderer for commands that use render functions:

import { createCLI } from '@reliverse/rempts-core'
import { registerTuiRenderer } from '@reliverse/rempts-tui'

const cli = await createCLI()
// Register TUI renderer
registerTuiRenderer()

await cli.run()

Components

Layout Components

Box

Flexible container component for layout:

import { Box } from '@reliverse/rempts-tui'

<Box
  flexDirection="column"
  padding={1}
  borderStyle="round"
  borderColor="blue"
>
  <Text>Content</Text>
</Box>

Props:

  • flexDirection: 'row' | 'column'
  • justifyContent: 'flex-start' | 'center' | 'flex-end' | 'space-between' | 'space-around'
  • alignItems: 'flex-start' | 'center' | 'flex-end'
  • padding: number | [number, number] | [number, number, number, number]
  • margin: number | [number, number] | [number, number, number, number]
  • width: number | string
  • height: number | string
  • borderStyle: 'single' | 'double' | 'round' | 'bold' | 'singleDouble' | 'doubleSingle' | 'classic'
  • borderColor: string

Text Components

Text

Styled text component:

import { Text } from '@reliverse/rempts-tui'

<Text
  color="green"
  backgroundColor="black"
  bold
  italic
  underline
  strikethrough
>
  Styled text content
</Text>

Props:

  • color: Text color (CSS color names or hex)
  • backgroundColor: Background color
  • bold: boolean
  • italic: boolean
  • underline: boolean
  • strikethrough: boolean
  • wrap: 'wrap' | 'truncate' | 'truncate-middle'

Form Components

Form

Interactive form container:

import { Form, FormField, TextField, SelectField } from '@reliverse/rempts-tui'

const MyForm = () => {
  const [values, setValues] = useState({})

  return (
    <Form
      onSubmit={(values) => console.log('Submitted:', values)}
      onCancel={() => console.log('Cancelled')}
    >
      <FormField label="Name">
        <TextField
          placeholder="Enter your name"
          value={values.name}
          onChange={(value) => setValues({ ...values, name: value })}
        />
      </FormField>

      <FormField label="Environment">
        <SelectField
          options={[
            { label: 'Development', value: 'dev' },
            { label: 'Staging', value: 'staging' },
            { label: 'Production', value: 'prod' }
          ]}
          value={values.env}
          onChange={(value) => setValues({ ...values, env: value })}
        />
      </FormField>
    </Form>
  )
}

FormField

Form field wrapper with label:

<FormField
  label="Email"
  description="We'll never share your email"
  error="Invalid email format"
>
  <TextField value={email} onChange={setEmail} />
</FormField>

TextField

Text input component:

<TextField
  placeholder="Enter text..."
  value={value}
  onChange={setValue}
  mask={(char, index) => char.toUpperCase()} // Transform input
  validate={(value) => value.length > 0} // Validation function
/>

SelectField

Dropdown selection component:

<SelectField
  options={[
    { label: 'Option 1', value: 'opt1' },
    { label: 'Option 2', value: 'opt2' }
  ]}
  value={selectedValue}
  onChange={setSelectedValue}
  placeholder="Select an option"
/>

Progress Components

ProgressBar

Progress indicator:

import { ProgressBar } from '@reliverse/rempts-tui'

<ProgressBar
  value={75}
  max={100}
  width={40}
  color="green"
  backgroundColor="gray"
  showPercentage
/>

Props:

  • value: Current progress value
  • max: Maximum value (default: 100)
  • width: Bar width in characters
  • color: Progress bar color
  • backgroundColor: Background color
  • showPercentage: Show percentage text

Hooks

useState

React state management in terminal components:

import { useState } from 'react'

const Counter = () => {
  const [count, setCount] = useState(0)

  return (
    <Box>
      <Text>Count: {count}</Text>
      <Text color="blue" onClick={() => setCount(count + 1)}>
        Click to increment
      </Text>
    </Box>
  )
}

useEffect

Side effects in terminal components:

import { useEffect, useState } from 'react'

const Timer = () => {
  const [time, setTime] = useState(0)

  useEffect(() => {
    const interval = setInterval(() => {
      setTime(t => t + 1)
    }, 1000)

    return () => clearInterval(interval)
  }, [])

  return <Text>Time: {time}s</Text>
}

Advanced Usage

Interactive Wizards

Build multi-step interactive experiences:

import { defineCommand, useState, useEffect } from '@reliverse/rempts-tui'
import { Box, Text, Form, FormField, TextField, SelectField } from '@reliverse/rempts-tui'

const WizardCommand = defineCommand({
  name: 'wizard',
  description: 'Interactive setup wizard',
  render: ({ shell }) => {
    const [step, setStep] = useState(0)
    const [config, setConfig] = useState({})

    const steps = [
      {
        title: 'Project Setup',
        component: (
          <Form onSubmit={(values) => {
            setConfig({ ...config, ...values })
            setStep(1)
          }}>
            <FormField label="Project Name">
              <TextField placeholder="my-project" />
            </FormField>
            <FormField label="Template">
              <SelectField
                options={[
                  { label: 'Basic', value: 'basic' },
                  { label: 'Advanced', value: 'advanced' }
                ]}
              />
            </FormField>
          </Form>
        )
      },
      {
        title: 'Dependencies',
        component: (
          <Form onSubmit={(values) => {
            setConfig({ ...config, ...values })
            setStep(2)
          }}>
            <FormField label="Package Manager">
              <SelectField
                options={[
                  { label: 'Bun', value: 'bun' },
                  { label: 'npm', value: 'npm' },
                  { label: 'yarn', value: 'yarn' }
                ]}
              />
            </FormField>
          </Form>
        )
      }
    ]

    return (
      <Box flexDirection="column" padding={1}>
        <Text bold color="cyan">
          Step {step + 1} of {steps.length}: {steps[step]?.title}
        </Text>
        <Box marginTop={1}>
          {steps[step]?.component}
        </Box>
      </Box>
    )
  }
})

export default WizardCommand

Real-time Updates

Components can update in real-time:

import { defineCommand, useState, useEffect } from '@reliverse/rempts-tui'

const MonitorCommand = defineCommand({
  name: 'monitor',
  description: 'Monitor system resources',
  render: () => {
    const [cpu, setCpu] = useState(0)
    const [memory, setMemory] = useState(0)

    useEffect(() => {
      const interval = setInterval(async () => {
        // Fetch system stats
        const stats = await fetchSystemStats()
        setCpu(stats.cpu)
        setMemory(stats.memory)
      }, 1000)

      return () => clearInterval(interval)
    }, [])

    return (
      <Box flexDirection="column">
        <Text>System Monitor</Text>
        <Text>CPU: {cpu.toFixed(1)}%</Text>
        <ProgressBar value={cpu} max={100} width={30} />
        <Text>Memory: {memory.toFixed(1)}%</Text>
        <ProgressBar value={memory} max={100} width={30} />
      </Box>
    )
  }
})

Integration with Commands

Mixing Handlers and Render Functions

Commands can use both handlers and render functions:

defineCommand({
  name: 'hybrid',
  description: 'Command with both handler and render',
  options: {
    interactive: option(z.coerce.boolean().default(false))
  },
  handler: async ({ flags }) => {
    if (!flags.interactive) {
      console.log('Running in non-interactive mode')
      return
    }
    // Interactive mode will use render function
  },
  render: ({ flags }) => {
    if (!flags.interactive) return null // Handler handles this case

    return (
      <Box>
        <Text>Interactive mode!</Text>
      </Box>
    )
  }
})

Context Sharing

Share context between handler and render functions:

defineCommand({
  name: 'context-demo',
  description: 'Demonstrates context sharing',
  handler: async ({ flags, context }) => {
    // Set data for render function
    ;(global as any).handlerData = { timestamp: Date.now() }
  },
  render: () => {
    const data = (global as any).handlerData

    return (
      <Box>
        <Text>Handler ran at: {new Date(data.timestamp).toLocaleTimeString()}</Text>
      </Box>
    )
  }
})

API Reference

Renderer Functions

registerTuiRenderer(options?)

Register the TUI renderer for commands with render functions.

registerTuiRenderer({
  exitOnCtrlC: true,
  targetFps: 30,
  enableMouseMovement: false
})

clearTuiRenderer()

Clear the current TUI renderer.

clearTuiRenderer()

getTuiRenderer()

Get the current TUI renderer instance.

const renderer = getTuiRenderer()

Component Props

All components support standard React props plus terminal-specific styling props:

  • onClick: Mouse click handler
  • onKeyPress: Keyboard input handler
  • style: CSS-in-JS style object
  • className: CSS class name

Event Handling

Components support terminal-specific events:

<Text
  onClick={() => console.log('Clicked!')}
  onKeyPress={(key) => {
    if (key.name === 'return') {
      // Handle enter key
    }
  }}
>
  Interactive text
</Text>

Performance Considerations

Re-rendering

TUI components re-render efficiently using React's reconciliation. Minimize unnecessary re-renders by:

  • Using React.memo for expensive components
  • Splitting large components into smaller ones
  • Using appropriate key props for dynamic lists

Memory Management

Terminal UIs can consume memory. Clean up:

  • Clear intervals and timeouts in useEffect cleanup
  • Remove event listeners when components unmount
  • Use appropriate component lifecycle methods

Rendering Performance

For smooth animations and updates:

  • Use targetFps option to control render frequency
  • Batch state updates when possible
  • Avoid deep component trees for frequently updating components

See Also

On this page