Overview
Bunli TUI provides React hooks for building interactive terminal applications. These hooks integrate with OpenTUI’s renderer and give you access to keyboard input, terminal dimensions, rendering, and more.
Imports
import {
useKeyboard ,
useRenderer ,
useTerminalDimensions ,
useTimeline ,
useOnResize ,
useTuiTheme ,
useFormContext ,
useFormField ,
useScopedKeyboard
} from '@bunli/tui'
OpenTUI Hooks
useKeyboard
Register keyboard event handlers.
function useKeyboard (
handler : ( key : KeyEvent ) => void | boolean ,
deps ?: React . DependencyList
) : void
Parameters
handler
(key: KeyEvent) => void | boolean
required
Callback invoked on key press. Return true to mark event as handled and prevent propagation.
Dependency array for the handler
KeyEvent Interface
Key name (e.g., 'a', 'return', 'escape')
Whether Ctrl key is pressed
Whether Shift key is pressed
Whether Meta/Alt key is pressed
Example
import { useKeyboard } from '@bunli/tui'
import { useState } from 'react'
function Counter () {
const [ count , setCount ] = useState ( 0 )
useKeyboard (( key ) => {
if ( key . name === 'up' ) {
setCount ( c => c + 1 )
return true // handled
}
if ( key . name === 'down' ) {
setCount ( c => c - 1 )
return true
}
return false // not handled
}, [ count ])
return < text content = { `Count: ${ count } ` } />
}
useRenderer
Access the OpenTUI renderer instance.
function useRenderer () : CliRenderer
Example
import { useRenderer } from '@bunli/tui'
function Debug () {
const renderer = useRenderer ()
return (
< box >
< text content = { `Focused: ${ renderer . currentFocusedRenderable ?. id ?? 'none' } ` } />
</ box >
)
}
useTerminalDimensions
Get current terminal width and height.
function useTerminalDimensions () : { width : number ; height : number }
Example
import { useTerminalDimensions } from '@bunli/tui'
function ResponsiveLayout () {
const { width , height } = useTerminalDimensions ()
return (
< box >
< text content = { `Terminal: ${ width } x ${ height } ` } />
{ width > 80 ? (
< text content = "Wide layout" />
) : (
< text content = "Narrow layout" />
)}
</ box >
)
}
useOnResize
Register a handler for terminal resize events.
function useOnResize (
handler : ( dimensions : { width : number ; height : number }) => void ,
deps ?: React . DependencyList
) : void
Example
import { useOnResize } from '@bunli/tui'
import { useState } from 'react'
function ResizeLogger () {
const [ resizes , setResizes ] = useState ( 0 )
useOnResize (() => {
setResizes ( r => r + 1 )
}, [])
return < text content = { `Resized ${ resizes } times` } />
}
useTimeline
Create an animation timeline with frame-based updates.
function useTimeline (
fps ?: number
) : {
elapsed : number
frame : number
start : () => void
stop : () => void
reset : () => void
}
Parameters
Frames per second for the timeline
Example
import { useTimeline } from '@bunli/tui'
function AnimatedSpinner () {
const timeline = useTimeline ( 10 ) // 10 FPS
const frames = [ '⠋' , '⠙' , '⠹' , '⠸' , '⠼' , '⠴' , '⠦' , '⠧' , '⠇' , '⠏' ]
const spinner = frames [ timeline . frame % frames . length ]
return < text content = { ` ${ spinner } Loading...` } />
}
Theme Hooks
useTuiTheme
Access the current TUI theme.
function useTuiTheme () : TuiTheme
Returns
Example
import { useTuiTheme } from '@bunli/tui'
function ThemedText () {
const { tokens } = useTuiTheme ()
return (
< box >
< text content = "Primary" fg = {tokens. textPrimary } />
< text content = "Muted" fg = {tokens. textMuted } />
< text content = "Accent" fg = {tokens. accent } />
</ box >
)
}
useFormContext
Access form context (must be used inside a <Form> component).
function useFormContext () : FormContextValue
Returns
Validation errors by field name
Touched state by field name
Dirty state by field name
Whether any field has been modified
Whether form is currently submitting
Whether form is currently validating
setFieldValue
(name: string, value: unknown) => void
Update a field’s value
Reset form to initial values
Example
import { Form , useFormContext } from '@bunli/tui'
import { z } from 'zod'
const schema = z . object ({
name: z . string (). min ( 1 )
})
function FormDebug () {
const form = useFormContext ()
return (
< box >
< text content = { `Dirty: ${ form . isDirty } ` } />
< text content = { `Submitting: ${ form . isSubmitting } ` } />
</ box >
)
}
function App () {
return (
< Form
title = "My Form"
schema = { schema }
onSubmit = {(values) => {
console . log ( values )
}}
>
< FormDebug />
</ Form >
)
}
Register and manage a form field.
function useFormField < T = unknown >(
name : string ,
options ?: UseFormFieldOptions < T >
) : UseFormFieldResult < T >
Parameters
Field name (must match schema property)
Default value for the field
Whether pressing Enter submits the form
Returns
Whether field has been touched
Whether field value has changed
Whether field is currently focused
Example
import { Form , useFormField } from '@bunli/tui'
import { z } from 'zod'
const schema = z . object ({
email: z . string (). email ()
})
function EmailField () {
const field = useFormField < string >( 'email' , {
defaultValue: ''
})
return (
< box style = {{ flexDirection : 'column' }} >
< text content = { `Email: ${ field . value } ` } />
{ field . error && < text content = {field. error } fg = "#ff0000" /> }
{ field . active && < text content = "(focused)" /> }
</ box >
)
}
Keyboard Scope Hooks
useScopedKeyboard
Register a keyboard handler with priority and scope support.
function useScopedKeyboard (
scopeId : string ,
handler : ( key : KeyEvent ) => boolean ,
options ?: UseScopedKeyboardOptions
) : void
Parameters
Unique identifier for this keyboard scope
handler
(key: KeyEvent) => boolean
required
Handler function. Return true to mark event as handled.
Whether this handler is active
Handler priority. Higher priority handlers run first.
Example
import { useScopedKeyboard } from '@bunli/tui'
import { useState } from 'react'
function Modal ({ isOpen , onClose }) {
useScopedKeyboard (
'modal' ,
( key ) => {
if ( key . name === 'escape' ) {
onClose ()
return true
}
return false
},
{ active: isOpen , priority: 100 }
)
if ( ! isOpen ) return null
return (
< box border >
< text content = "Press Esc to close" />
</ box >
)
}
Best Practices
Dependency Arrays Always provide dependency arrays to useKeyboard and useOnResize to avoid stale closures
Keyboard Scopes Use useScopedKeyboard with priorities for modals and overlays to prevent key conflicts
Performance
Use useTimeline for animations instead of setInterval
Debounce expensive operations in useOnResize
Return true from keyboard handlers to stop propagation
Form Validation
Use useFormField for custom form inputs
Leverage schema validation instead of manual checks
Show errors only after field is touched