@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-tuinpm install @reliverse/rempts-tuipnpm add @reliverse/rempts-tuiCore 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 | stringheight:number | stringborderStyle:'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 colorbold:booleanitalic:booleanunderline:booleanstrikethrough:booleanwrap:'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 valuemax: Maximum value (default: 100)width: Bar width in characterscolor: Progress bar colorbackgroundColor: Background colorshowPercentage: 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 WizardCommandReal-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 handleronKeyPress: Keyboard input handlerstyle: CSS-in-JS style objectclassName: 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.memofor 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
useEffectcleanup - Remove event listeners when components unmount
- Use appropriate component lifecycle methods
Rendering Performance
For smooth animations and updates:
- Use
targetFpsoption to control render frequency - Batch state updates when possible
- Avoid deep component trees for frequently updating components
See Also
- Core API - Command definitions and CLI setup
- Plugin System - Extending functionality
- Examples - Complete TUI examples